Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
63 changes: 62 additions & 1 deletion core/src/main/java/org/apache/calcite/tools/RelBuilder.java
Original file line number Diff line number Diff line change
Expand Up @@ -5114,12 +5114,73 @@
}
};
final RelDataType type = op.inferReturnType(bind);
final ImmutableList<RexNode> newPartitionKeys =
simplifyPartitionKeys(partitionKeys);
final ImmutableList<RexFieldCollation> 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<RexNode> simplifyPartitionKeys(
List<RexNode> partitionKeys) {
final ImmutableList.Builder<RexNode> 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<RexFieldCollation> simplifySortKeys(
List<RexNode> partitionKeys, List<RexFieldCollation> sortKeys) {
// A RANGE frame with a value offset (e.g. RANGE BETWEEN 5 PRECEDING)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

which test covers this case?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thank you for pointing out, I had added a test testProjectOverRangeOffsetKeepsSortKey to cover it. The new test would fail without the following modification.

// 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<RexFieldCollation> newSortKeys =
ImmutableList.builder();
for (RexFieldCollation collation : sortKeys) {

Check warning on line 5166 in core/src/main/java/org/apache/calcite/tools/RelBuilder.java

View check run for this annotation

SonarQubeCloud / SonarCloud Code Analysis

Reduce the total number of break and continue statements in this loop to use at most one.

See more on https://sonarcloud.io/project/issues?id=apache_calcite&issues=AZ_gU6qaC-mCpXWPwuly&open=AZ_gU6qaC-mCpXWPwuly&pullRequest=5165
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}.
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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\"");
}

Expand Down
102 changes: 102 additions & 0 deletions core/src/test/java/org/apache/calcite/test/RelBuilderTest.java
Original file line number Diff line number Diff line change
Expand Up @@ -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<RelBuilder, RelNode> 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<RelBuilder, RelNode> 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<RelBuilder, RelNode> 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<RelBuilder, RelNode> 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<RelBuilder, RelNode> 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());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -17330,15 +17330,15 @@ from (
<Resource name="planAfter">
<![CDATA[
LogicalProject(COL1=[$2], COL2=[$3], COL3=[$4])
LogicalWindow(window#0=[window(partition {1} range between UNBOUNDED PRECEDING and CURRENT ROW aggs [SUM($2)])], window#1=[window(order by [1] aggs [SUM($2)])], window#2=[window(partition {1} range between UNBOUNDED PRECEDING and CURRENT ROW aggs [SUM(5000)])], constants=[[100]])
LogicalWindow(window#0=[window(partition {1} aggs [SUM($2)])], window#1=[window(order by [1] aggs [SUM($2)])], window#2=[window(partition {1} range between UNBOUNDED PRECEDING and CURRENT ROW aggs [SUM(5000)])], constants=[[100]])
LogicalProject(SAL=[$5], DEPTNO=[$7])
LogicalFilter(condition=[=($5, 5000)])
LogicalTableScan(table=[[CATALOG, SALES, EMP]])
]]>
</Resource>
<Resource name="planBefore">
<![CDATA[
LogicalProject(COL1=[SUM(100) OVER (PARTITION BY $7, $5 ORDER BY $5)], COL2=[SUM(100) OVER (PARTITION BY $5 ORDER BY $7)], COL3=[SUM($5) OVER (PARTITION BY $7 ORDER BY $5)])
LogicalProject(COL1=[SUM(100) OVER (PARTITION BY $7, $5)], COL2=[SUM(100) OVER (PARTITION BY $5 ORDER BY $7)], COL3=[SUM($5) OVER (PARTITION BY $7 ORDER BY $5)])
LogicalFilter(condition=[=($5, 5000)])
LogicalTableScan(table=[[CATALOG, SALES, EMP]])
]]>
Expand All @@ -17355,15 +17355,15 @@ from (
</Resource>
<Resource name="planBefore">
<![CDATA[
LogicalProject(COL1=[SUM(100) OVER (ORDER BY $7, $0 RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)], COL2=[SUM(100) OVER (PARTITION BY $5, $7 ORDER BY $7, $0 RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)])
LogicalProject(COL1=[SUM(100) OVER (ORDER BY $7, $0 RANGE BETWEEN CURRENT ROW AND UNBOUNDED FOLLOWING)], COL2=[SUM(100) OVER (PARTITION BY $5, $7 ORDER BY $0 RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING)])
LogicalFilter(condition=[=($5, 5000)])
LogicalTableScan(table=[[CATALOG, SALES, EMP]])
]]>
</Resource>
<Resource name="planAfter">
<![CDATA[
LogicalProject(COL1=[$3], COL2=[$4])
LogicalWindow(window#0=[window(order by [2, 0] range between CURRENT ROW and UNBOUNDED FOLLOWING aggs [SUM($3)])], window#1=[window(partition {2} order by [2, 0] range between UNBOUNDED PRECEDING and UNBOUNDED FOLLOWING aggs [SUM($3)])], constants=[[100]])
LogicalWindow(window#0=[window(order by [2, 0] range between CURRENT ROW and UNBOUNDED FOLLOWING aggs [SUM($3)])], window#1=[window(partition {2} order by [0] range between UNBOUNDED PRECEDING and UNBOUNDED FOLLOWING aggs [SUM($3)])], constants=[[100]])
LogicalProject(EMPNO=[$0], SAL=[$5], DEPTNO=[$7])
LogicalFilter(condition=[=($5, 5000)])
LogicalTableScan(table=[[CATALOG, SALES, EMP]])
Expand Down Expand Up @@ -23394,13 +23394,13 @@ window w as (partition by empno order by empno)]]>
<Resource name="planAfter">
<![CDATA[
LogicalProject(EXPR$0=[$9], EXPR$1=[$9])
LogicalWindow(window#0=[window(partition {0} order by [0] aggs [COUNT()])])
LogicalWindow(window#0=[window(partition {0} aggs [COUNT()])])
LogicalTableScan(table=[[CATALOG, SALES, EMP]])
]]>
</Resource>
<Resource name="planBefore">
<![CDATA[
LogicalProject(EXPR$0=[COUNT() OVER (PARTITION BY $0 ORDER BY $0)], EXPR$1=[COUNT() OVER (PARTITION BY $0 ORDER BY $0)])
LogicalProject(EXPR$0=[COUNT() OVER (PARTITION BY $0)], EXPR$1=[COUNT() OVER (PARTITION BY $0)])
LogicalTableScan(table=[[CATALOG, SALES, EMP]])
]]>
</Resource>
Expand Down
4 changes: 2 additions & 2 deletions core/src/test/resources/sql/sub-query.iq
Original file line number Diff line number Diff line change
Expand Up @@ -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 }]])
Expand Down Expand Up @@ -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
Expand Down
Loading