Skip to content

[core] Sort provably all-null splits last in TopN pushdown with NULLS LAST - #10020

Open
LuciferYang wants to merge 1 commit into
apache:masterfrom
LuciferYang:fix/topn-nulls-last-split-order
Open

LuciferYang wants to merge 1 commit into
apache:masterfrom
LuciferYang:fix/topn-nulls-last-split-order

Conversation

@LuciferYang

Copy link
Copy Markdown
Contributor

Purpose

close #10019

TopNDataSplitEvaluator supports Spark single-key TopN pushdown (ORDER BY col [ASC|DESC] [NULLS FIRST|LAST] LIMIT n on an append-only table, a min/max-comparable sort type, limit ≤ 100). It orders the splits by the sort column's aggregate min/max and keeps the first limit. In the NULLS LAST branches the comparison falls to ascCompare / descCompare, which treat a null min/max as the smallest value, so a split whose sort column is entirely NULL sorts to the front, takes a kept slot, and displaces the split holding the real top value. With LIMIT 1 and two or more splits the reader keeps only the all-null split and emits a NULL row instead of the true top-N.

This keeps a provably-all-null split (its tracked null count equals its row count) last under NULLS LAST. RichSplit gains an allNull flag, and the NULLS LAST comparators order by it first, then fall back to the existing min/max and null-count comparison. A split whose bound is merely unknown (null min/max but not provably all-null, for example a stats.mode=counts file) is not all-null, so it still sorts first and is read conservatively. NULLS FIRST ordering is unchanged.

It also fixes a Comparator contract violation in the same file: ascCompare / descCompare / nullsFirstCompare / nullsLastCompare returned -1 when the left operand was null without checking the right, so two nulls compared as -1 in both directions. They now return 0 when both operands are null.

Tests

TableScanTest.testPushDownTopNNullsLastSortsAllNullSplitLast: with an all-null split and a real split under ASC and DESC NULLS LAST at LIMIT 1, asserts the real split is kept; without the fix the all-null split takes the slot and the query returns a NULL row. It also asserts NULLS FIRST still treats the null-containing split as the best candidate, and that two tied all-null splits are both kept.

TableScanTest.testPushDownTopNNullsLastKeepsStatsUnknownSplitFirst: a split with unknown bounds (null min/max, null count below row count) stays first under NULLS LAST, so an unknown-stats split is read rather than dropped.

… LAST

TopNDataSplitEvaluator picks the splits to read for a TopN query by
ordering splits by their best row under the query's sort order. In the
NULLS LAST branches the asc/desc comparators returned -1 for a null
min/max, so a split whose sort column is entirely null — the worst
candidate under NULLS LAST — sorted first and took one of the limit
slots, displacing the split holding the real top values. For example
ORDER BY col DESC LIMIT 1 (DESC defaults to NULLS LAST in both Spark
and Flink) could return a null row instead of the true maximum.

Sort such a split last only when it is provably all-null: the null
count equals the row count. A null min/max that is not provably
all-null means the bound is unknown rather than null-valued — for
example files written with stats.mode=counts — and those splits keep
ordering first so they are still read, conservatively.

Also report 0 from the null-count and bound tie-break comparators when
both sides are null, restoring the Comparator contract
(compare(x,x) must be 0); the previous -1 could make TimSort throw
"Comparison method violates its general contract".

Assisted-by: GLM-5.3
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.

[Bug] TopN pushdown returns a NULL row when a split's sort column is all NULL under NULLS LAST

1 participant