Skip to content

[CALCITE-7680] PruneEmptyRules should prune Sort when OFFSET is greater than or equal to max input rows - #5132

Open
xuzifu666 wants to merge 6 commits into
apache:mainfrom
xuzifu666:sort_offset_fix
Open

[CALCITE-7680] PruneEmptyRules should prune Sort when OFFSET is greater than or equal to max input rows#5132
xuzifu666 wants to merge 6 commits into
apache:mainfrom
xuzifu666:sort_offset_fix

Conversation

@xuzifu666

Copy link
Copy Markdown
Member

* <li>Sort[offset=5](input with at most 2 rows) becomes Empty
* </ul>
*
* <p>It relies on {@link org.apache.calcite.rel.metadata.RelMdMaxRowCount}

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.

How the rule is implemented should not be documented here, but in the rule body.

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.

Thanks for the reminder; this part doesn't actually need a comment, and the existing rules already cover it, so I'll remove that comment for now.

// no rows. RelMdMaxRowCount#getMaxRowCount(Sort) already subtracts the
// offset from the input row count, so the Sort is definitely empty.
return sort.offset instanceof RexLiteral
&& RelMdUtil.isRelDefinitelyEmpty(call.getMetadataQuery(), sort);

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.

If something is definitely empty, why does it matter if it's a sort or some other operation? Why does the offset matter?

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.

The rule targets Sort specifically because PruneEmptyRules traditionally separates structural pruning (child is empty Values) from metadata-based pruning. This rule fills the metadata-based gap for Sort: an OFFSET that skips all rows.
The offset check is there for clarity and to avoid invoking the metadata provider on Sorts without an OFFSET, where emptiness is already covered by SORT_INSTANCE or SORT_FETCH_ZERO_INSTANCE. Maybe we could consider unifying the three Sort rules into one metadata-based rule in the future.

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.

If you can merge two rules without increasing complexity you should.

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.

Yes, I merged SORT_FETCH_ZERO_INSTANCE and SORT_OFFSET_INSTANCE into a single SORT_EMPTY_INSTANCE rule that simply checks RelMdUtil.isRelDefinitelyEmpty. This covers LIMIT 0, OFFSET >= maxRowCount, and empty input with one rule and no extra complexity. SORT_FETCH_ZERO_INSTANCE is kept as a deprecated alias for backward compatibility. This impact should be minimal.

return sort.fetch != null
&& !(sort.fetch instanceof RexDynamicParam)
&& RexLiteral.bigDecimalValue(sort.fetch).equals(BigDecimal.ZERO);
final Sort sort = call.rel(0);

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.

I think this is an improvement, but I still don't understand why this cannot be applied to any Rel node.

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.

You're right that isRelDefinitelyEmpty itself is generic.
The reason this rule is specific to Sort is that the replacement logic is not generic: RemoveEmptySingleRule can safely turn any SingleRel into an empty Values, but Join, Union, Intersect, Minus, and Aggregate each need their own logic when they are empty. This rule reuses RemoveEmptySingleRule because Sort is a SingleRel. A fully generic prune anything that is definitely empty rule would need a way to know how to rewrite each operator type correctly. The existing EMPTY_TABLE_INSTANCE follows the same pattern: it uses isRelDefinitelyEmpty but only for TableScan.

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.

What matters is that the operator has a single OUTPUT, and they all do.
Maybe you can find a way to do it, even if it means writing a bit more code to handle operators with more than 1 input.

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.

OK, I generalized ZeroMaxRowsRuleConfig (EMPTY_TABLE_INSTANCE) to match any RelNode that is definitely empty, not just TableScan. It excludes Values and TableModify (the latter may have side effects). This removes the need for a separate Sort-specific rule, so SORT_EMPTY_INSTANCE was deleted and SORT_FETCH_ZERO_INSTANCE is now a deprecated alias. The rule still relies on RelMdUtil.isRelDefinitelyEmpty, so the logic remains simple and covers LIMIT 0, large OFFSET, and any other case where Sort's max row count is zero.

@sonarqubecloud

sonarqubecloud Bot commented Aug 1, 2026

Copy link
Copy Markdown

@mihaibudiu mihaibudiu left a comment

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.

Please see @julianhyde's comment in Jira

@xuzifu666

Copy link
Copy Markdown
Member Author

Please see @julianhyde's comment in Jira

Thanks for the reminder!I’ve outlined my views in Jira; please take a look when you have a moment to see if there are any issues. @mihaibudiu

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants