sql: preserve filter semantics in distinct aggregations - #174770
Open
Alignyx wants to merge 1 commit into
Open
Conversation
The pre-aggregation DISTINCT stage deduplicates on aggregate argument columns without considering FILTER columns. Rows with equal arguments can have different filter outcomes, so this stage can discard values that a filtered aggregate must see. For example, the query in cockroachdb#131087 returns (4, 4, 0) instead of (4, 4, 1). Require all aggregates to be DISTINCT and unfiltered before inserting this stage. Each final aggregate retains its existing filter and distinct handling. Add SQL logic regressions for complementary and identical filters, reversed input, NULLs, different arguments, a larger table scan, unfiltered controls, and empty input. Fixes cockroachdb#131087 Release note (bug fix): Fixed incorrect results from queries combining DISTINCT aggregates and FILTER clauses when rows with the same aggregate arguments had different filter outcomes. Each aggregate now retains the rows required by its own filter before deduplicating its arguments.
|
Thank you for contributing to CockroachDB. Please ensure you have followed the guidelines for creating a PR. My owl senses detect your PR is good for review. Please keep an eye out for any test failures in CI. 🦉 Hoot! I am a Blathers, a bot for CockroachDB. My owner is dev-inf. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #131087.
Queries combining DISTINCT aggregates and FILTER clauses can return incorrect results when the same aggregate argument occurs in rows with different filter outcomes. This change prevents a pre-aggregation DISTINCT stage from discarding rows that a filtered aggregate still needs.
Problem and root cause
For the input below, the three counts should be
(4, 4, 1). Before this fix, they are(4, 4, 0):DistSQLPlanner.planAggregatorsinserts local DISTINCT processors when every aggregation specification hasDistinct=true. The DISTINCT key is the union of the aggregates' argument columns (ColIdx); their filter columns (FilterColIdx) are omitted.For this query, that key is just
id. The rows(1, true)and(1, false)are deduplicated before either filtered aggregate evaluates its predicate. If the first row survives, the FALSE-filtered count loses its only contributing value. Reversing the input can instead lose a value from the TRUE-filtered count. Even aggregates sharing the same FILTER can be affected when duplicate arguments have different filter outcomes.The aggregate processors already implement the required order: apply each aggregate's FILTER, then deduplicate the arguments that passed that filter. The incorrect result originates in the earlier physical-plan optimization.
How the problem was localized
The investigation used a metamorphic repair workflow with the issue's expected full result
(4, 4, 1)as an explicit oracle:count(DISTINCT id) FILTER (WHERE p)withcount(DISTINCT CASE WHEN p THEN id END). Other references compute the aggregates in separate scalar subqueries. All four generated references returned the expected full result.EXPLAIN (VEC)before and after the fix. The failing plan containscolexec.UnorderedDistinctbeforerowexec.orderedAggregator; the repaired plan omits the former. An unfiltered DISTINCT control retains its pre-aggregation DISTINCT operator and its correct result.The initial diagnosis and one-line eligibility change were proposed by DeepSeek. The proposed fix was then independently checked by source inspection and deterministic result comparisons, including additional inputs outside the original repair run.
Fix
The pre-aggregation DISTINCT optimization now requires every aggregate to be DISTINCT and to have no FILTER:
The final aggregate specifications retain their DISTINCT and FILTER settings, so each aggregate sees all of the rows it needs before performing its own deduplication. Unfiltered all-DISTINCT aggregations remain eligible for the optimization. A comment records why filtered aggregates must be excluded.
This is a conservative eligibility restriction. It can increase the rows processed or transmitted for duplicate-heavy filtered aggregations. Extending the pre-aggregation key to include all filter columns could be investigated separately; this PR does not claim a performance improvement.
Regression coverage and validation
The existing
aggregateSQL logic test file gains a standalonedistinct_filtersubtest with nine queries covering:Validated against upstream commit
8812064a015d2faf99d3fc7e15880f94042954b0:aggregatefile after fixlocal(4,4,0)vs(4,4,1)local-vec-off(4,4,0)vs(4,4,1)fakedist(3 nodes)(4,4,0)vs(4,4,1)fakedist-vec-off(3 nodes)(4,4,0)vs(4,4,1)The baseline runner stops at the first mismatch; the before-fix result above is the original issue query. After the fix, each focused run executes all nine SQL queries plus setup/cleanup. The complete-file runs have no skipped subtests. The 685 checks include statements and queries, rather than 685 separate Go test functions.
The native targets can be run with:
Repeating with
--test_filter='^TestLogic_aggregate$'runs the surrounding aggregate file. Local execution used--norun_validations;crlfmt -fast -tab 2on the changed Go file,gofmt, andgit diff --checkpassed separately. No generated test registration or BUILD file changes are needed because the existing aggregate test entry reads this fixture.Prior independent experiments on the initial checkout ran 52 SQL observations across vectorize on/off: the baseline matched 20 expected results, and the candidate matched all 52. The configured colexec suite also passed all 82 tests. These are supplementary results; the native tests above validate the actual PR base and patch.
For clarity, in this checkout FILTER aggregates use the row processor even with
vectorize=on. Running both settings checks their respective plans and surrounding operators; it does not establish independent native-vectorized FILTER aggregation coverage. The full CockroachDB repository suite and performance benchmarks have not been run.Release note (bug fix): Fixed incorrect results from queries combining DISTINCT aggregates and FILTER clauses when rows with the same aggregate arguments had different filter outcomes. Each aggregate now retains the rows required by its own filter before deduplicating its arguments.