diff --git a/core/src/main/java/org/apache/calcite/tools/RelBuilder.java b/core/src/main/java/org/apache/calcite/tools/RelBuilder.java index 92c5a414158..d1ed73441ea 100644 --- a/core/src/main/java/org/apache/calcite/tools/RelBuilder.java +++ b/core/src/main/java/org/apache/calcite/tools/RelBuilder.java @@ -5114,12 +5114,73 @@ private OverCall orderBy_(ImmutableList sortKeys) { } }; final RelDataType type = op.inferReturnType(bind); + final ImmutableList newPartitionKeys = + simplifyPartitionKeys(partitionKeys); + final ImmutableList newSortKeys = + simplifySortKeys(newPartitionKeys, sortKeys); final RexNode over = getRexBuilder() - .makeOver(pos, type, op, operands, partitionKeys, sortKeys, + .makeOver(pos, type, op, operands, newPartitionKeys, newSortKeys, lowerBound, upperBound, exclude, rows, allowPartial, nullWhenCountZero, distinct, ignoreNulls); return aliasMaybe(over, alias); } + + /** Removes constant keys from a window's {@code PARTITION BY}. A constant + * partition key places every row in the same partition, so it does not + * partition the data and can be dropped. */ + private ImmutableList simplifyPartitionKeys( + List partitionKeys) { + final ImmutableList.Builder newKeys = ImmutableList.builder(); + for (RexNode key : partitionKeys) { + if (!RexUtil.isConstant(key)) { + newKeys.add(key); + } + } + return newKeys.build(); + } + + /** Removes redundant keys from a window's {@code ORDER BY}. A sort key is + * redundant if it is constant, or if it is functionally determined by the + * partition keys and earlier sort keys (those columns are fixed within a + * partition, so the key cannot affect the ordering). For example, with + * {@code PARTITION BY x, y ORDER BY x + y, z} the key {@code x + y} only + * references fixed columns and is dropped, leaving {@code ORDER BY z}. */ + private ImmutableList simplifySortKeys( + List partitionKeys, List sortKeys) { + // A RANGE frame with a value offset (e.g. RANGE BETWEEN 5 PRECEDING) + // derives its bounds from the sort key values, so its keys must be kept. + if (!rows + && (lowerBound.getOffset() != null || upperBound.getOffset() != null)) { + return ImmutableList.copyOf(sortKeys); + } + // Columns whose value is fixed within a partition: partition keys plus + // columns pinned by an earlier single-column sort keys. + ImmutableBitSet fixedColumns = ImmutableBitSet.of(); + for (RexNode key : partitionKeys) { + if (key instanceof RexInputRef) { + fixedColumns = fixedColumns.set(((RexInputRef) key).getIndex()); + } + } + final ImmutableList.Builder newSortKeys = + ImmutableList.builder(); + for (RexFieldCollation collation : sortKeys) { + final RexNode key = collation.left; + if (RexUtil.isConstant(key)) { + continue; + } + final ImmutableBitSet keyColumns = RelOptUtil.InputFinder.bits(key); + if (!keyColumns.isEmpty() + && RexUtil.isDeterministic(key) + && fixedColumns.contains(keyColumns)) { + continue; + } + newSortKeys.add(collation); + if (key instanceof RexInputRef) { + fixedColumns = fixedColumns.set(((RexInputRef) key).getIndex()); + } + } + return newSortKeys.build(); + } } /** Collects the extra expressions needed for {@link #aggregate}. diff --git a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java index 9877bd5779e..b1ece9bf88e 100644 --- a/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java +++ b/core/src/test/java/org/apache/calcite/rel/rel2sql/RelToSqlConverterTest.java @@ -2858,8 +2858,9 @@ private SqlDialect nonOrdinalDialect() { @Test void testNoNeedRewriteOrderByConstantsForOver() { final String query = "select row_number() over " + "(order by 1 nulls last) from \"employee\""; - // Default dialect keep numeric constant keys in the over of order-by. - sql(query).ok("SELECT ROW_NUMBER() OVER (ORDER BY 1)\n" + // A constant ORDER BY key places every row in the same peer group, so it + // is removed when the window is built, leaving an empty OVER clause. + sql(query).ok("SELECT ROW_NUMBER() OVER ()\n" + "FROM \"foodmart\".\"employee\""); } diff --git a/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java b/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java index cce1aea3095..ebe8990c1dd 100644 --- a/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java +++ b/core/src/test/java/org/apache/calcite/test/RelBuilderTest.java @@ -1186,6 +1186,108 @@ private RexNode caseCall(RelBuilder b, RexNode ref, RexNode... nodes) { assertThat(f.apply(createBuilder()), hasTree(expected)); } + /** Tests that RelBuilder removes a constant key from a window's + * {@code PARTITION BY}, since a constant partition key places every row in + * the same partition. */ + @Test void testProjectOverConstantPartitionKey() { + final Function f = b -> b.scan("EMP") + .project(b.field("DEPTNO"), + b.aggregateCall(SqlStdOperatorTable.ROW_NUMBER) + .over() + .partitionBy(b.literal(1)) + .orderBy(b.field("EMPNO")) + .rowsUnbounded() + .as("x")) + .build(); + final String expected = "" + + "LogicalProject(DEPTNO=[$7], x=[ROW_NUMBER() OVER (ORDER BY $0)])\n" + + " LogicalTableScan(table=[[scott, EMP]])\n"; + assertThat(f.apply(createBuilder()), hasTree(expected)); + } + + /** Tests that RelBuilder keeps non-constant partition keys and drops only the + * constant one. */ + @Test void testProjectOverPartialConstantPartitionKey() { + final Function f = b -> b.scan("EMP") + .project(b.field("DEPTNO"), + b.aggregateCall(SqlStdOperatorTable.SUM, b.field("SAL")) + .over() + .partitionBy(b.field("DEPTNO"), b.literal(1)) + .orderBy(b.field("EMPNO")) + .rowsUnbounded() + .as("x")) + .build(); + final String expected = "" + + "LogicalProject(DEPTNO=[$7], " + + "x=[SUM($5) OVER (PARTITION BY $7 ORDER BY $0 RANGE BETWEEN " + + "UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)])\n" + + " LogicalTableScan(table=[[scott, EMP]])\n"; + assertThat(f.apply(createBuilder()), hasTree(expected)); + } + + /** Tests that RelBuilder removes a constant key from a window's + * {@code ORDER BY}. */ + @Test void testProjectOverConstantSortKey() { + final Function f = b -> b.scan("EMP") + .project(b.field("DEPTNO"), + b.aggregateCall(SqlStdOperatorTable.ROW_NUMBER) + .over() + .partitionBy() + .orderBy(b.literal(1), b.field("EMPNO")) + .rowsUnbounded() + .as("x")) + .build(); + final String expected = "" + + "LogicalProject(DEPTNO=[$7], x=[ROW_NUMBER() OVER (ORDER BY $0)])\n" + + " LogicalTableScan(table=[[scott, EMP]])\n"; + assertThat(f.apply(createBuilder()), hasTree(expected)); + } + + /** Tests that RelBuilder removes a sort key that is functionally determined + * by the partition keys: with {@code PARTITION BY DEPTNO, SAL ORDER BY + * DEPTNO + SAL, EMPNO} the key {@code DEPTNO + SAL} references only fixed + * columns and is dropped, leaving {@code ORDER BY EMPNO}. */ + @Test void testProjectOverFunctionallyDependentSortKey() { + final Function f = b -> b.scan("EMP") + .project(b.field("DEPTNO"), + b.aggregateCall(SqlStdOperatorTable.ROW_NUMBER) + .over() + .partitionBy(b.field("DEPTNO"), b.field("SAL")) + .orderBy( + b.call(SqlStdOperatorTable.PLUS, b.field("DEPTNO"), + b.field("SAL")), + b.field("EMPNO")) + .rowsUnbounded() + .as("x")) + .build(); + final String expected = "" + + "LogicalProject(DEPTNO=[$7], " + + "x=[ROW_NUMBER() OVER (PARTITION BY $7, $5 ORDER BY $0)])\n" + + " LogicalTableScan(table=[[scott, EMP]])\n"; + assertThat(f.apply(createBuilder()), hasTree(expected)); + } + + /** Tests that RelBuilder keeps a sort key that would otherwise be dropped + * (here {@code DEPTNO}, which equals the partition key) when the frame is a + * RANGE with a value offset, because such a frame derives its bounds from the + * sort key values. */ + @Test void testProjectOverRangeOffsetKeepsSortKey() { + final Function f = b -> b.scan("EMP") + .project(b.field("DEPTNO"), + b.aggregateCall(SqlStdOperatorTable.SUM, b.field("SAL")) + .over() + .partitionBy(b.field("DEPTNO")) + .orderBy(b.field("DEPTNO")) + .rangeBetween(b.preceding(b.literal(5)), b.currentRow()) + .as("x")) + .build(); + final String expected = "" + + "LogicalProject(DEPTNO=[$7], " + + "x=[SUM($5) OVER (PARTITION BY $7 ORDER BY $7 RANGE 5 PRECEDING)])\n" + + " LogicalTableScan(table=[[scott, EMP]])\n"; + assertThat(f.apply(createBuilder()), hasTree(expected)); + } + @Test void testRename() { final RelBuilder builder = RelBuilder.create(config().build()); diff --git a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml index ba303c062bd..bbf4331b694 100644 --- a/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml +++ b/core/src/test/resources/org/apache/calcite/test/RelOptRulesTest.xml @@ -17330,7 +17330,7 @@ from ( @@ -17355,7 +17355,7 @@ from ( @@ -17363,7 +17363,7 @@ LogicalProject(COL1=[SUM(100) OVER (ORDER BY $7, $0 RANGE BETWEEN CURRENT ROW AN diff --git a/core/src/test/resources/sql/sub-query.iq b/core/src/test/resources/sql/sub-query.iq index e1cf4060814..08bab096649 100644 --- a/core/src/test/resources/sql/sub-query.iq +++ b/core/src/test/resources/sql/sub-query.iq @@ -8622,7 +8622,7 @@ EnumerableCalc(expr#0..1=[{inputs}], T1B=[$t1]) EnumerableValues(tuples=[[{ 'val1a', 6 }, { 'val1b', 8 }, { 'val1a', 16 }, { 'val1a', 16 }, { 'val1c', 8 }, { 'val1d', null }, { 'val1d', null }, { 'val1e', 10 }, { 'val1e', 10 }, { 'val1d', 10 }, { 'val1a', 6 }, { 'val1e', 10 }]]) EnumerableSort(sort0=[$0], dir0=[ASC]) EnumerableAggregate(group=[{0}], EXPR$0=[MAX($4)]) - EnumerableWindow(window#0=[window(partition {0, 1, 3} order by [3] aggs [RANK()])]) + EnumerableWindow(window#0=[window(partition {0, 1, 3} aggs [RANK()])]) EnumerableMergeJoin(condition=[=($2, $3)], joinType=[inner]) EnumerableSort(sort0=[$2], dir0=[ASC]) EnumerableValues(tuples=[[{ 'val2a', 6, 12 }, { 'val1b', 10, 12 }, { 'val1b', 8, 16 }, { 'val1c', 12, 16 }, { 'val1b', null, 16 }, { 'val2e', 8, null }, { 'val1f', 19, null }, { 'val1b', 10, 12 }, { 'val1b', 8, 16 }, { 'val1c', 12, 16 }, { 'val1e', 8, null }, { 'val1f', 19, null }, { 'val1b', null, 16 }]]) @@ -8663,7 +8663,7 @@ EnumerableSort(sort0=[$0], dir0=[ASC]) EnumerableNestedLoopJoin(condition=[>(CAST($0):BIGINT, $1)], joinType=[inner]) EnumerableValues(tuples=[[{ 6 }, { 8 }, { 16 }, { 16 }, { 8 }, { null }, { null }, { 10 }, { 10 }, { 10 }, { 6 }, { 10 }]]) EnumerableAggregate(group=[{}], EXPR$0=[MAX($3)]) - EnumerableWindow(window#0=[window(partition {1, 2} order by [1] aggs [RANK()])]) + EnumerableWindow(window#0=[window(partition {1, 2} aggs [RANK()])]) EnumerableAggregate(group=[{0, 1}], T3D=[MAX($2)]) EnumerableValues(tuples=[[{ 6, 12, 110 }, { 6, 12, 10 }, { 10, 12, 219 }, { 10, 12, 19 }, { 8, 16, 319 }, { 8, 16, 19 }, { 17, 16, 519 }, { 17, 16, 19 }, { null, 16, 419 }, { null, 16, 19 }, { 8, null, 719 }, { 8, null, 19 }]]) !plan