This is an automated email from the ASF dual-hosted git repository.
FrankChen021 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/druid.git
The following commit(s) were added to refs/heads/master by this push:
new 11065c14a14 perf(sql): reduce SQL planning allocations for large
literal arrays (#20367)
11065c14a14 is described below
commit 11065c14a14499616733ef6c56e1c99cfda3ef5f
Author: Frank Chen <[email protected]>
AuthorDate: Sat Oct 17 21:11:11 2026 +0800
perf(sql): reduce SQL planning allocations for large literal arrays (#20367)
* perf(sql): reduce planning allocation for literal string arrays
* perf(sql): extend literal array reduction to integers
* perf(sql): reuse normalized literal arrays and verify IN semantics
* Update
* perf(sql): restrict literal array fast path to normalized operands
---
.../sql/calcite/planner/DruidRexExecutor.java | 51 +++++
.../apache/druid/sql/calcite/CalciteQueryTest.java | 57 +++++
.../sql/calcite/planner/DruidRexExecutorTest.java | 241 +++++++++++++++++++++
3 files changed, 349 insertions(+)
diff --git
a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidRexExecutor.java
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidRexExecutor.java
index 378a89f25b8..13991455f56 100644
---
a/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidRexExecutor.java
+++
b/sql/src/main/java/org/apache/druid/sql/calcite/planner/DruidRexExecutor.java
@@ -19,13 +19,16 @@
package org.apache.druid.sql.calcite.planner;
+import com.google.common.annotations.VisibleForTesting;
import org.apache.calcite.rex.RexBuilder;
import org.apache.calcite.rex.RexCall;
import org.apache.calcite.rex.RexExecutor;
import org.apache.calcite.rex.RexLiteral;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.sql.SqlKind;
+import org.apache.calcite.sql.fun.SqlStdOperatorTable;
import org.apache.calcite.sql.type.SqlTypeName;
+import org.apache.calcite.sql.type.SqlTypeUtil;
import org.apache.druid.error.InvalidSqlInput;
import org.apache.druid.java.util.common.DateTimes;
import org.apache.druid.math.expr.Evals;
@@ -39,6 +42,7 @@ import org.apache.druid.segment.column.RowSignature;
import org.apache.druid.sql.calcite.expression.DruidExpression;
import org.apache.druid.sql.calcite.expression.Expressions;
+import javax.annotation.Nullable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.Arrays;
@@ -67,6 +71,12 @@ public class DruidRexExecutor implements RexExecutor
)
{
for (RexNode constExp : constExps) {
+ final RexNode reducedArray = tryReduceLiteralArray(rexBuilder, constExp);
+ if (reducedArray != null) {
+ reducedValues.add(reducedArray);
+ continue;
+ }
+
final DruidExpression druidExpression = Expressions.toDruidExpression(
plannerContext,
EMPTY_ROW_SIGNATURE,
@@ -246,4 +256,45 @@ public class DruidRexExecutor implements RexExecutor
}
return constExp.toString();
}
+
+ ///
+ /// Reduces literal string and integer arrays without formatting, parsing,
or evaluating a Druid expression.
+ ///
+ /// Reuses the original array call when every literal's type matches the
array component type, ignoring nullability.
+ /// These literals already have the required types and values, so rebuilding
them would only add allocations.
+ /// Nullability differences do not require conversion: a non-null literal
may belong to an array with nullable elements,
+ /// and reusing the call preserves both null values and the original array
type.
+ ///
+ /// Returns null for mismatched element types, unsupported types, or
non-literal operands (including CAST calls).
+ /// These retain the existing Druid expression evaluation path, including
any required type normalization.
+ ///
+ @Nullable
+ @VisibleForTesting
+ static RexNode tryReduceLiteralArray(final RexBuilder rexBuilder, final
RexNode expression)
+ {
+ if (!(expression instanceof RexCall)) {
+ return null;
+ }
+ if (!((RexCall)
expression).getOperator().equals(SqlStdOperatorTable.ARRAY_VALUE_CONSTRUCTOR)) {
+ return null;
+ }
+
+ final SqlTypeName elementType =
expression.getType().getComponentType().getSqlTypeName();
+ final boolean isString = SqlTypeName.CHAR_TYPES.contains(elementType);
+ if (!isString && !SqlTypeName.INT_TYPES.contains(elementType)) {
+ return null;
+ }
+
+ final List<RexNode> operands = ((RexCall) expression).getOperands();
+ for (final RexNode operand : operands) {
+ if (!(operand instanceof RexLiteral)
+ || !SqlTypeUtil.equalSansNullability(
+ rexBuilder.getTypeFactory(),
expression.getType().getComponentType(), operand.getType()
+ )) {
+ return null;
+ }
+ }
+
+ return expression;
+ }
}
diff --git
a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java
b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java
index 5858747d379..3320b06296f 100644
--- a/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java
+++ b/sql/src/test/java/org/apache/druid/sql/calcite/CalciteQueryTest.java
@@ -5907,6 +5907,63 @@ public class CalciteQueryTest extends
BaseCalciteQueryTest
);
}
+ @Test
+ public void testStringInLiteralArraySemantics()
+ {
+ final String values = "('', 'abc', 'abc', 'a''b', '中文', 'abc ',
'nonmatching')";
+ for (final int threshold : new int[]{2, 1000}) {
+ final Map<String, Object> context = QueryContexts.override(
+ QUERY_CONTEXT_DEFAULT,
+ QueryContexts.IN_FUNCTION_THRESHOLD,
+ threshold
+ );
+ // Exercise the array conversion and ordinary IN planning with the same
SQL and expected results.
+ for (final Object[] testCase : new Object[][]{
+ {"dim1 IN " + values, 2L},
+ {"dim1 NOT IN " + values, 4L},
+ {"dim1 IN ('', 'abc', NULL)", 2L},
+ {"dim1 NOT IN ('', 'abc', NULL)", 0L},
+ {"NULLIF(dim1, 'abc') IN ('abc', 'def', 'nonmatching')", 1L},
+ {"NULLIF(dim1, 'abc') NOT IN ('abc', 'def', 'nonmatching')", 4L},
+ {"dim1 IN (CAST('abc' AS VARCHAR(30)), CONCAT('d', 'ef'),
'nonmatching')", 2L}
+ }) {
+ testBuilder()
+ .sql("SELECT COUNT(*) FROM druid.foo WHERE " + testCase[0])
+ .queryContext(context)
+ .expectedResults(ImmutableList.of(new Object[]{testCase[1]}))
+ .run();
+ }
+ }
+ }
+
+ @Test
+ public void testLongInLiteralArraySemantics()
+ {
+ for (final int threshold : new int[]{2, 1000}) {
+ final Map<String, Object> context = QueryContexts.override(
+ QUERY_CONTEXT_DEFAULT,
+ QueryContexts.IN_FUNCTION_THRESHOLD,
+ threshold
+ );
+ for (final Object[] testCase : new Object[][]{
+ {"cnt IN (1, 1, -9223372036854775808, 9223372036854775807)", 6L},
+ {"cnt NOT IN (1, 1, -9223372036854775808, 9223372036854775807)", 0L},
+ {"cnt IN (-9223372036854775808, 0, 9223372036854775807)", 0L},
+ {"cnt IN (1, 2, NULL)", 6L},
+ {"cnt NOT IN (0, 2, NULL)", 0L},
+ {"NULLIF(cnt, 1) IN (1, 2, 3)", 0L},
+ {"NULLIF(cnt, 1) NOT IN (1, 2, 3)", 0L},
+ {"cnt IN (CAST(1 AS BIGINT), CAST(2 AS INTEGER), 3 + 1)", 6L}
+ }) {
+ testBuilder()
+ .sql("SELECT COUNT(*) FROM druid.foo WHERE " + testCase[0])
+ .queryContext(context)
+ .expectedResults(ImmutableList.of(new Object[]{testCase[1]}))
+ .run();
+ }
+ }
+ }
+
@Test
public void
testInFilterWith23Elements_overBothScalarInArrayAndInSubQueryThresholds()
{
diff --git
a/sql/src/test/java/org/apache/druid/sql/calcite/planner/DruidRexExecutorTest.java
b/sql/src/test/java/org/apache/druid/sql/calcite/planner/DruidRexExecutorTest.java
index 3e271f0d3f7..6ab7bb5e9e1 100644
---
a/sql/src/test/java/org/apache/druid/sql/calcite/planner/DruidRexExecutorTest.java
+++
b/sql/src/test/java/org/apache/druid/sql/calcite/planner/DruidRexExecutorTest.java
@@ -23,11 +23,14 @@ import com.google.common.collect.ImmutableList;
import com.google.common.collect.ImmutableMap;
import com.google.common.collect.ImmutableSet;
import org.apache.calcite.jdbc.JavaTypeFactoryImpl;
+import org.apache.calcite.rel.type.RelDataType;
import org.apache.calcite.rel.type.RelDataTypeFactory;
import org.apache.calcite.rex.RexBuilder;
+import org.apache.calcite.rex.RexCall;
import org.apache.calcite.rex.RexLiteral;
import org.apache.calcite.rex.RexNode;
import org.apache.calcite.schema.SchemaPlus;
+import org.apache.calcite.sql.SqlCollation;
import org.apache.calcite.sql.SqlFunctionCategory;
import org.apache.calcite.sql.SqlKind;
import org.apache.calcite.sql.SqlOperator;
@@ -66,9 +69,13 @@ import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.math.BigDecimal;
+import java.nio.charset.Charset;
+import java.nio.charset.StandardCharsets;
import java.util.ArrayList;
+import java.util.Arrays;
import java.util.Collections;
import java.util.List;
+import java.util.Locale;
public class DruidRexExecutorTest extends InitializedNullHandlingTest
{
@@ -293,6 +300,240 @@ public class DruidRexExecutorTest extends
InitializedNullHandlingTest
);
}
+ @Test
+ public void testArrayOfStringLiteralsReduction()
+ {
+ for (final SqlTypeName typeName : SqlTypeName.CHAR_TYPES) {
+ assertLiteralArrayReduction(typeName, Arrays.asList("", "a'b",
"back\\slash", "中文", null));
+ assertLiteralArrayReduction(typeName, Collections.singletonList(null));
+ }
+ }
+
+ @Test
+ public void testStringArrayExpressionUsesEvaluator()
+ {
+ final RexNode array = rexBuilder.makeCall(
+ SqlStdOperatorTable.ARRAY_VALUE_CONSTRUCTOR,
+ rexBuilder.makeCall(SqlStdOperatorTable.CONCAT,
rexBuilder.makeLiteral("a"), rexBuilder.makeLiteral("b"))
+ );
+ // CONCAT requires evaluation rather than literal-array reuse.
+ Assertions.assertNull(DruidRexExecutor.tryReduceLiteralArray(rexBuilder,
array));
+ final RexCall reduced = reduceArrayWithEvaluator(array);
+ Assertions.assertEquals(1, reduced.getOperands().size());
+ Assertions.assertEquals("ab",
RexLiteral.stringValue(reduced.getOperands().get(0)));
+ Assertions.assertEquals(SqlTypeName.CHAR,
reduced.getOperands().get(0).getType().getSqlTypeName());
+ Assertions.assertEquals(2,
reduced.getOperands().get(0).getType().getPrecision());
+ }
+
+ @Test
+ public void testIntegerLiteralArraysReduction()
+ {
+ for (final SqlTypeName typeName : SqlTypeName.INT_TYPES) {
+ assertLiteralArrayReduction(
+ typeName,
+ Arrays.asList(BigDecimal.valueOf(-1), BigDecimal.ZERO, null,
BigDecimal.TEN)
+ );
+ }
+ }
+
+ @Test
+ public void testLongLiteralArrayBoundaries()
+ {
+ for (final List<BigDecimal> values : Arrays.<List<BigDecimal>>asList(
+ Arrays.asList(BigDecimal.valueOf(Long.MIN_VALUE), null,
BigDecimal.valueOf(Long.MAX_VALUE)),
+ Collections.singletonList(null)
+ )) {
+ assertLiteralArrayReduction(SqlTypeName.BIGINT, values);
+ }
+ }
+
+ @Test
+ public void testIntegerArrayExpressionUsesEvaluator()
+ {
+ final RexNode array = rexBuilder.makeCall(
+ SqlStdOperatorTable.ARRAY_VALUE_CONSTRUCTOR,
+ rexBuilder.makeCall(SqlStdOperatorTable.PLUS,
rexBuilder.makeBigintLiteral(BigDecimal.ONE),
+ rexBuilder.makeBigintLiteral(BigDecimal.TEN))
+ );
+ // Addition requires evaluation rather than literal-array reuse.
+ Assertions.assertNull(DruidRexExecutor.tryReduceLiteralArray(rexBuilder,
array));
+ final RexCall reduced = reduceArrayWithEvaluator(array);
+ Assertions.assertEquals(1, reduced.getOperands().size());
+ Assertions.assertEquals(BigDecimal.valueOf(11),
RexLiteral.value(reduced.getOperands().get(0)));
+ Assertions.assertEquals(SqlTypeName.BIGINT,
reduced.getOperands().get(0).getType().getSqlTypeName());
+ }
+
+ @Test
+ public void testLiteralArrayReductionRejectsUnsupportedTypes()
+ {
+ for (final SqlTypeName typeName : Arrays.asList(
+ SqlTypeName.DECIMAL,
+ SqlTypeName.DOUBLE,
+ SqlTypeName.BOOLEAN,
+ SqlTypeName.DATE
+ )) {
+ final RelDataType elementType = typeFactory.createSqlType(typeName);
+ final RexNode array = rexBuilder.makeCall(
+ typeFactory.createArrayType(elementType, -1),
SqlStdOperatorTable.ARRAY_VALUE_CONSTRUCTOR,
+ ImmutableList.of(rexBuilder.makeNullLiteral(elementType))
+ );
+ // Unsupported component types retain evaluator fallback, even when
their only element is null.
+ Assertions.assertNull(DruidRexExecutor.tryReduceLiteralArray(rexBuilder,
array), typeName.toString());
+ }
+ // A scalar literal is not an array constructor.
+ Assertions.assertNull(DruidRexExecutor.tryReduceLiteralArray(rexBuilder,
rexBuilder.makeLiteral("a")));
+ // A non-array call is not eligible for this fast path.
+ Assertions.assertNull(DruidRexExecutor.tryReduceLiteralArray(
+ rexBuilder,
+ rexBuilder.makeCall(SqlStdOperatorTable.CONCAT,
rexBuilder.makeLiteral("a"), rexBuilder.makeLiteral("b"))
+ ));
+ }
+
+ @Test
+ public void testLiteralArrayReductionRejectsCastsAndMismatchedOperands()
+ {
+ final RelDataType stringType =
typeFactory.createSqlType(SqlTypeName.VARCHAR, 30);
+ final RelDataType arrayType = typeFactory.createArrayType(stringType, -1);
+ for (final RexNode operand : ImmutableList.of(
+ rexBuilder.makeAbstractCast(stringType, rexBuilder.makeLiteral("a"),
false),
+ rexBuilder.makeBigintLiteral(BigDecimal.ONE)
+ )) {
+ final RexNode array = rexBuilder.makeCall(
+ arrayType,
+ SqlStdOperatorTable.ARRAY_VALUE_CONSTRUCTOR,
+ ImmutableList.of(operand)
+ );
+ // CAST calls and integer operands in a string array cannot be reused as
matching string literals.
+ Assertions.assertNull(DruidRexExecutor.tryReduceLiteralArray(rexBuilder,
array));
+ }
+ }
+
+ @Test
+ public void testLiteralArrayReductionRejectsDifferentElementTypes()
+ {
+ final RelDataType charType = typeFactory.createSqlType(SqlTypeName.CHAR,
3);
+ final RelDataType arrayType = typeFactory.createArrayType(charType, -1);
+ final RexNode array = rexBuilder.makeCall(
+ arrayType,
+ SqlStdOperatorTable.ARRAY_VALUE_CONSTRUCTOR,
+ ImmutableList.of(rexBuilder.makeLiteral("a"),
rexBuilder.makeLiteral("abc"))
+ );
+ // CHAR(1) does not match CHAR(3); the evaluator must perform the required
padding.
+ Assertions.assertNull(DruidRexExecutor.tryReduceLiteralArray(rexBuilder,
array));
+ final RexCall reduced = reduceArrayWithEvaluator(array);
+ Assertions.assertEquals(2, reduced.getOperands().size());
+ Assertions.assertEquals("a ",
RexLiteral.stringValue(reduced.getOperands().get(0)));
+ Assertions.assertEquals("abc",
RexLiteral.stringValue(reduced.getOperands().get(1)));
+ for (final RexNode operand : reduced.getOperands()) {
+ Assertions.assertEquals(SqlTypeName.CHAR,
operand.getType().getSqlTypeName());
+ Assertions.assertEquals(3, operand.getType().getPrecision());
+ }
+ }
+
+ @Test
+ public void testLiteralArrayReductionRejectsMixedIntegerTypes()
+ {
+ final RelDataType arrayType =
typeFactory.createArrayType(typeFactory.createSqlType(SqlTypeName.BIGINT), -1);
+ final RexNode array = rexBuilder.makeCall(
+ arrayType,
+ SqlStdOperatorTable.ARRAY_VALUE_CONSTRUCTOR,
+ ImmutableList.of(
+ rexBuilder.makeLiteral(BigDecimal.ONE,
typeFactory.createSqlType(SqlTypeName.INTEGER), false),
+ rexBuilder.makeBigintLiteral(BigDecimal.TEN)
+ )
+ );
+ // INTEGER and BIGINT operands require normalization to the BIGINT
component type.
+ Assertions.assertNull(DruidRexExecutor.tryReduceLiteralArray(rexBuilder,
array));
+ final RexCall reduced = reduceArrayWithEvaluator(array);
+ Assertions.assertEquals(2, reduced.getOperands().size());
+ Assertions.assertEquals(BigDecimal.ONE,
RexLiteral.value(reduced.getOperands().get(0)));
+ Assertions.assertEquals(BigDecimal.TEN,
RexLiteral.value(reduced.getOperands().get(1)));
+ for (final RexNode operand : reduced.getOperands()) {
+ Assertions.assertEquals(SqlTypeName.BIGINT,
operand.getType().getSqlTypeName());
+ }
+ }
+
+ @Test
+ public void testLiteralArrayReductionRejectsDifferentCharsets()
+ {
+ assertCharacterTypeMismatch(StandardCharsets.ISO_8859_1, "primary");
+ }
+
+ @Test
+ public void testLiteralArrayReductionRejectsDifferentCollations()
+ {
+ assertCharacterTypeMismatch(StandardCharsets.UTF_8, "tertiary");
+ }
+
+ private void assertCharacterTypeMismatch(final Charset operandCharset, final
String operandStrength)
+ {
+ final RelDataType charType = typeFactory.createSqlType(SqlTypeName.CHAR,
1);
+ final RelDataType componentType =
typeFactory.createTypeWithCharsetAndCollation(
+ charType,
+ StandardCharsets.UTF_8,
+ new SqlCollation(SqlCollation.Coercibility.IMPLICIT, Locale.US,
StandardCharsets.UTF_8, "primary")
+ );
+ final RelDataType operandType =
typeFactory.createTypeWithCharsetAndCollation(
+ charType,
+ operandCharset,
+ new SqlCollation(SqlCollation.Coercibility.IMPLICIT, Locale.US,
operandCharset, operandStrength)
+ );
+ final RexNode operand = rexBuilder.makeLiteral("a", operandType, false);
+ Assertions.assertInstanceOf(RexLiteral.class, operand);
+ Assertions.assertEquals(operandType, operand.getType());
+ Assertions.assertNotEquals(componentType, operand.getType());
+ final RelDataType arrayType = typeFactory.createArrayType(componentType,
-1);
+ final RexNode array = rexBuilder.makeCall(
+ arrayType,
+ SqlStdOperatorTable.ARRAY_VALUE_CONSTRUCTOR,
+ ImmutableList.of(operand)
+ );
+ // Matching CHAR lengths are insufficient when the charset or collation
differs.
+ Assertions.assertNull(DruidRexExecutor.tryReduceLiteralArray(rexBuilder,
array));
+ final RexCall reduced = reduceArrayWithEvaluator(array);
+ Assertions.assertEquals(1, reduced.getOperands().size());
+ final RexNode reducedOperand = reduced.getOperands().get(0);
+ Assertions.assertEquals("a", RexLiteral.stringValue(reducedOperand));
+ Assertions.assertEquals(componentType, reducedOperand.getType());
+ }
+
+ private void assertLiteralArrayReduction(final SqlTypeName typeName, final
List<?> values)
+ {
+ final RelDataType elementType = SqlTypeName.CHAR_TYPES.contains(typeName)
+ ? typeFactory.createSqlType(typeName, 30)
+ : typeFactory.createSqlType(typeName);
+ final RelDataType arrayType = typeFactory.createArrayType(
+ typeFactory.createTypeWithNullability(elementType, true), -1
+ );
+ final List<RexNode> operands = new ArrayList<>();
+ for (final Object value : values) {
+ final RexNode operand = value == null
+ ? rexBuilder.makeNullLiteral(elementType)
+ : rexBuilder.makeLiteral(value, elementType,
false);
+ Assertions.assertInstanceOf(RexLiteral.class, operand);
+ operands.add(operand);
+ }
+ final RexNode array = rexBuilder.makeCall(arrayType,
SqlStdOperatorTable.ARRAY_VALUE_CONSTRUCTOR, operands);
+ final RexNode reduced = DruidRexExecutor.tryReduceLiteralArray(rexBuilder,
array);
+ if (typeName == SqlTypeName.VARCHAR && values.stream().anyMatch(value ->
value != null)) {
+ // makeLiteral(..., false) creates CHAR literals here, which do not
match the VARCHAR component type.
+ Assertions.assertNull(reduced);
+ } else {
+ // Matching literals, including typed nulls, reuse the exact array
despite nullability differences.
+ Assertions.assertSame(array, reduced);
+ }
+ }
+
+ private RexCall reduceArrayWithEvaluator(final RexNode array)
+ {
+ final List<RexNode> reduced = new ArrayList<>();
+ new DruidRexExecutor(PLANNER_CONTEXT).reduce(rexBuilder,
ImmutableList.of(array), reduced);
+ Assertions.assertEquals(1, reduced.size());
+ final RexCall reducedArray = Assertions.assertInstanceOf(RexCall.class,
reduced.get(0));
+ Assertions.assertEquals(SqlKind.ARRAY_VALUE_CONSTRUCTOR,
reducedArray.getKind());
+ return reducedArray;
+ }
+
@Test
public void testMultiValueStringNotReduced()
{
---------------------------------------------------------------------
To unsubscribe, e-mail: [email protected]
For additional commands, e-mail: [email protected]