Skip to content

Selection ORDER BY ... LIMIT returns wrong rows or throws NPE with null handling enabled #19725

Description

@rohityadav1993

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.

Activity

  1. rohityadav1993 commented on Sep 30, 2026

    @rohityadav1993
    ContributorAuthor
  2. added
    bugSomething is not working as expected
    queryRelated to query processing
    on Oct 5, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    bugSomething is not working as expectedqueryRelated to query processing

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions