Skip to content

perf(calcite): count COUNT(DISTINCT col) in the scan; top-N ties in scan order - #562

Merged
dfa1 merged 2 commits into
mainfrom
feat/count-distinct
Oct 18, 2026
Merged

dfa1 merged 2 commits into
mainfrom
feat/count-distinct

Conversation

@dfa1

@dfa1 dfa1 commented Oct 18, 2026

Copy link
Copy Markdown
Owner

Stacked on #561 (rebased onto main once it merges). Two commits:

  1. fix: ORDER BY ... LIMIT (perf(calcite): answer ORDER BY ... LIMIT from a bounded heap over the scan #557) returned tied rows in heap order; Calcite's sort is stable, so ties now come out in scan order. The tests missed it because their oracle (order by x + 0) is simplified by Calcite back to x and was pushed down too; oracles are now CASE WHEN id > -1 THEN x END and the tests assert they really scan.
  2. perf: an ungrouped COUNT(DISTINCT col) is answered by VortexTable.distinctCount: integers, temporals, doubles (same equality as Double.equals, which Calcite's DISTINCT uses) and booleans by their bits in a primitive LongIntMap, dictionary strings by the codes a chunk uses, other strings through a set. Grouped/FILTERed distincts, decimals, binary and nested stay with Calcite.

Raincloud appian-order (4.28M rows): count(distinct order_customerId) 194 → 46 ms.

Tests

  • CountDistinctTest: integer with nulls, doubles with -0.0/0.0/NaN, dictionary strings, two at once; each against a scanning oracle. SortPushDownTest and the jni sort cross-check use scanning oracles.
  • vortex-jni cross-check: count(distinct g) and a filtered count(distinct n) over a Rust-written file.

🤖 Generated with Claude Code

https://claude.ai/code/session_01WtuVGQ5h5Lk6Zn4JnkxF1K

dfa1 and others added 2 commits October 18, 2026 13:17
…te does

The top-N heap kept the right rows of a tie (the first ones scanned) but
returned them in heap order, while Calcite's sort is stable and returns
them in scan order. SQL leaves the order of ties open, but a query
should not change its answer depending on whether the push-down fired.
Rows now rank by scan position after their keys.

The tests missed it because their oracle wrapped the sort key in "+ 0",
which Calcite simplifies back to the bare column, so the oracle was
pushed down too and compared the heap with itself. The oracle is now
CASE WHEN id > -1 THEN key END, and the tests assert it really scans.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WtuVGQ5h5Lk6Zn4JnkxF1K
An ungrouped COUNT(DISTINCT col) was left to Calcite, which hashes a
boxed value per row. The aggregate rule now answers it, alongside the
other calls it can answer, from VortexTable.distinctCount: a pass over
the rows the WHERE keeps that counts integers, temporals, doubles and
booleans by their bits in a primitive LongIntMap (for doubles the same
equality as Double.equals, which Calcite's DISTINCT uses), dictionary
strings by marking the codes a chunk uses and decoding each once, and
other strings through a set. Decimals, binary and nested columns, and a
grouped or FILTERed COUNT(DISTINCT), stay with Calcite.

Raincloud appian-order (4.28M rows), best of 3:
  count(distinct order_customerId)   194 -> 46 ms

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01WtuVGQ5h5Lk6Zn4JnkxF1K
@dfa1
dfa1 force-pushed the feat/count-distinct branch from 527d459 to e3d27cb Compare October 18, 2026 11:17
@dfa1
dfa1 merged commit bb65111 into main Oct 18, 2026
9 checks passed
@dfa1
dfa1 deleted the feat/count-distinct branch October 18, 2026 11:18
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.

1 participant