With null handling enabled, a selection query whose first ORDER BY expression is a column and that has a LIMIT
can return wrong rows without any error, or fail with a NullPointerException. Both come from
MinMaxValueBasedSelectionOrderByCombineOperator (COMBINE_SELECT_ORDERBY_MINMAX in EXPLAIN), which skips segments
based on the column min/max value. That value cannot bound null rows: a null is stored as the column's default null
value, and the metadata has no null semantics.
Found while testing for regressions in #19120.
Cases
All examples use two segments on one server and SET enableNullHandling=true.
1. Wrong rows when nulls sort first (default for DESC, or explicit NULLS FIRST). Reachable since 1.0.0, when
DESC started defaulting to NULLS FIRST (#10805, #10817).
| Segment |
intCol values |
| A |
100, 101, 102 |
| B |
5, null, null |
| Query |
Expected |
Actual |
SELECT intCol FROM t ORDER BY intCol DESC LIMIT 3 |
null, null, 102 |
102, 101, 100 |
SELECT intCol FROM t ORDER BY intCol DESC NULLS FIRST LIMIT 3 |
null, null, 102 |
102, 101, 100 |
Segment A sets the boundary to 100. Segment B's max value is 5, so it is skipped together with its null rows, which
should have come first. This also happens with column-based null handling on a nullable column.
2. Wrong rows with ASC NULLS FIRST and a custom default null value. The null rows are stored as the default
null value, which raises the segment's min value. Column intCol with defaultNullValue 1000:
| Segment |
intCol values |
| A |
1, 2, 3 |
| B |
500, null, null |
| Query |
Expected |
Actual |
SELECT intCol FROM t ORDER BY intCol ASC NULLS FIRST LIMIT 3 |
null, null, 1 |
1, 2, 3 |
With the usual default for an INT dimension (Integer.MIN_VALUE), the null rows lower the min value instead, and the
result is correct.
3. NullPointerException when a segment's kept rows end in a null. This also happens when nulls sort last.
Reachable since 0.11.0, when selection rows started carrying nulls (#8927).
| Segment |
intCol values |
| A |
100, 90, 80 |
| B |
95, null, null |
SELECT intCol FROM t ORDER BY intCol DESC NULLS LAST LIMIT 3 should return 100, 95, 90 but fails with (error code
200):
Cannot invoke "java.lang.Comparable.compareTo(Object)" because "segmentBoundaryValue" is null
Segment A sets the boundary to 80. Segment B's max value is 95, so it is processed, and its last kept row (null)
becomes the boundary for the next compareTo.
Not affected: apart from the exception in case 3, DESC NULLS LAST, ASC and ASC NULLS LAST return the
correct rows. Queries without null handling, and queries whose first ORDER BY expression is not a plain column (they
use SelectionOrderByCombineOperator), are not affected.
Why existing tests miss it
The null-handling query tests (NullHandlingEnabledQueriesTest, NullEnabledQueriesTest, AllNullQueriesTest,
BooleanNullEnabledQueriesTest) register one segment twice, so both segments have the same min/max value and nothing
is ever skipped. Most of their selection ORDER BY queries also have no LIMIT.
Unit tests that reproduce it
These tests are added in the fix PR #19724; on current master they fail as shown.
NullQueriesFluentTest (two segments on one instance, maxExecutionThreads=1):
| Test |
Case |
Failure on master |
testMinMaxCombineOrderByDescKeepsNullsFirst |
1 (DESC, DESC NULLS FIRST) |
expected [null] but found [102] |
testMinMaxCombineOrderByDescKeepsNullsFirstColumnBasedNullHandling |
1, column-based null handling |
expected [null] but found [102] |
testMinMaxCombineOrderByAscNullsFirstWithCustomDefaultNullValue |
2 |
expected [null] but found [1] |
testMinMaxCombineNullBoundaryValue |
3 |
Cannot invoke "java.lang.Comparable.compareTo(Object)" because "segmentBoundaryValue" is null |
testMinMaxCombineNullsLast |
control: nulls last |
passes |
For example, case 1:
@Test
public void testMinMaxCombineOrderByDescKeepsNullsFirst() {
FluentQueryTest.withBaseDir(_baseDir)
.withExtraQueryOptions(Map.of("maxExecutionThreads", "1"))
.withNullHandling(true)
.givenTable(INT_SCHEMA, NULL_HANDLING_TABLE_CONFIG)
.onFirstInstance(new Object[]{100}, new Object[]{101}, new Object[]{102})
.andSegment(new Object[]{5}, new Object[]{null}, new Object[]{null})
.whenQuery("select intCol from testTable order by intCol desc limit 3")
// The harness serves the instance as two servers, so the broker merges two copies of [null, null, 102]
.thenResultIs(new Object[]{null}, new Object[]{null}, new Object[]{null});
}
SelectionCombineOperatorTest runs the combine operator directly with one thread:
| Test |
Checks |
Result on master |
selectionOrderByMinMaxProcessesSegmentWithNullsWhenNullsSortFirst |
case 1: rows [null, null, 102], both segments processed |
fails: Lists differ at element [0]: null != 102 |
selectionOrderByMinMaxSkipsSegmentWithoutNullsUnderNullHandling |
a segment without nulls is still skipped under null handling |
passes |
selectionOrderByMinMaxSkipsSegmentWithNullsWhenNullsSortLast |
a segment with nulls is still skipped when nulls sort last |
passes |
selectionOrderByMinMaxSkipsSegmentForNonNullableColumn |
a non-nullable column is still skipped |
passes |
The last three guard that a fix keeps segment skipping where it is safe (numSegmentsMatched == 1).
Run:
./mvnw -pl pinot-core test -Dtest='NullQueriesFluentTest,SelectionCombineOperatorTest'
Workaround
Order by an expression instead of the plain column (for example ORDER BY intCol + 0 DESC), which uses
SelectionOrderByCombineOperator and returns the correct rows.
With null handling enabled, a selection query whose first
ORDER BYexpression is a column and that has aLIMITcan return wrong rows without any error, or fail with a
NullPointerException. Both come fromMinMaxValueBasedSelectionOrderByCombineOperator(COMBINE_SELECT_ORDERBY_MINMAXinEXPLAIN), which skips segmentsbased on the column min/max value. That value cannot bound null rows: a null is stored as the column's default null
value, and the metadata has no null semantics.
Found while testing for regressions in #19120.
Cases
All examples use two segments on one server and
SET enableNullHandling=true.1. Wrong rows when nulls sort first (default for
DESC, or explicitNULLS FIRST). Reachable since 1.0.0, whenDESCstarted defaulting toNULLS FIRST(#10805, #10817).intColvaluesSELECT intCol FROM t ORDER BY intCol DESC LIMIT 3SELECT intCol FROM t ORDER BY intCol DESC NULLS FIRST LIMIT 3Segment A sets the boundary to 100. Segment B's max value is 5, so it is skipped together with its null rows, which
should have come first. This also happens with column-based null handling on a nullable column.
2. Wrong rows with
ASC NULLS FIRSTand a custom default null value. The null rows are stored as the defaultnull value, which raises the segment's min value. Column
intColwithdefaultNullValue1000:intColvaluesSELECT intCol FROM t ORDER BY intCol ASC NULLS FIRST LIMIT 3With the usual default for an INT dimension (
Integer.MIN_VALUE), the null rows lower the min value instead, and theresult is correct.
3.
NullPointerExceptionwhen a segment's kept rows end in a null. This also happens when nulls sort last.Reachable since 0.11.0, when selection rows started carrying nulls (#8927).
intColvaluesSELECT intCol FROM t ORDER BY intCol DESC NULLS LAST LIMIT 3should return100, 95, 90but fails with (error code200):
Segment A sets the boundary to 80. Segment B's max value is 95, so it is processed, and its last kept row (null)
becomes the boundary for the next
compareTo.Not affected: apart from the exception in case 3,
DESC NULLS LAST,ASCandASC NULLS LASTreturn thecorrect rows. Queries without null handling, and queries whose first
ORDER BYexpression is not a plain column (theyuse
SelectionOrderByCombineOperator), are not affected.Why existing tests miss it
The null-handling query tests (
NullHandlingEnabledQueriesTest,NullEnabledQueriesTest,AllNullQueriesTest,BooleanNullEnabledQueriesTest) register one segment twice, so both segments have the same min/max value and nothingis ever skipped. Most of their selection
ORDER BYqueries also have noLIMIT.Unit tests that reproduce it
These tests are added in the fix PR #19724; on current master they fail as shown.
NullQueriesFluentTest(two segments on one instance,maxExecutionThreads=1):testMinMaxCombineOrderByDescKeepsNullsFirstDESC,DESC NULLS FIRST)expected [null] but found [102]testMinMaxCombineOrderByDescKeepsNullsFirstColumnBasedNullHandlingexpected [null] but found [102]testMinMaxCombineOrderByAscNullsFirstWithCustomDefaultNullValueexpected [null] but found [1]testMinMaxCombineNullBoundaryValueCannot invoke "java.lang.Comparable.compareTo(Object)" because "segmentBoundaryValue" is nulltestMinMaxCombineNullsLastFor example, case 1:
SelectionCombineOperatorTestruns the combine operator directly with one thread:selectionOrderByMinMaxProcessesSegmentWithNullsWhenNullsSortFirst[null, null, 102], both segments processedLists differ at element [0]: null != 102selectionOrderByMinMaxSkipsSegmentWithoutNullsUnderNullHandlingselectionOrderByMinMaxSkipsSegmentWithNullsWhenNullsSortLastselectionOrderByMinMaxSkipsSegmentForNonNullableColumnThe last three guard that a fix keeps segment skipping where it is safe (
numSegmentsMatched == 1).Run:
Workaround
Order by an expression instead of the plain column (for example
ORDER BY intCol + 0 DESC), which usesSelectionOrderByCombineOperatorand returns the correct rows.