Fix COUNT bug when pulling up scalar subqueries (fallback planner)#397
Open
Alena0704 wants to merge 1 commit into
Open
Fix COUNT bug when pulling up scalar subqueries (fallback planner)#397Alena0704 wants to merge 1 commit into
Alena0704 wants to merge 1 commit into
Conversation
Alena0704
force-pushed
the
count-aggr-first-fallback
branch
from
July 2, 2026 20:05
e640835 to
68c09fb
Compare
Alena0704
marked this pull request as draft
July 2, 2026 20:12
Alena0704
force-pushed
the
count-aggr-first-fallback
branch
3 times, most recently
from
July 2, 2026 21:06
9ffda49 to
0f6cfed
Compare
Alena0704
force-pushed
the
count-aggr-first-fallback
branch
4 times, most recently
from
July 14, 2026 13:23
148ead5 to
c9b09dc
Compare
Alena0704
marked this pull request as ready for review
July 14, 2026 15:59
Alena0704
force-pushed
the
count-aggr-first-fallback
branch
7 times, most recently
from
July 16, 2026 15:55
caca6e8 to
49938bd
Compare
…elated
scalar subquery with an aggregate is pulled up into an INNER join with a
grouped subquery (convert_EXPR_to_join), which drops outer rows that have
no match. The original subquery keeps them: it computes the aggregate over
empty input, so e.g. COUNT yields 0 there:
select ... from t1
where t1.a > (select count(*) from t2 where t2.a = t1.d);
A row with no match in t2 must be compared as "t1.a > 0" and can pass, but
the INNER join dropped it.
To fix this, pull the subquery up into a LEFT join, so no-match rows survive
as null-extended rows, and rewrite the comparison to return the same value
the subquery would:
outer OP CASE WHEN match_flag THEN expr ELSE empty_input_default END
match_flag is a constant TRUE column added to the subquery. For a matched
row the CASE returns the real expression; for a null-extended row the flag
is NULL and the CASE returns the empty-input default (0 for COUNT, NULL for
other aggregates).
The comparison runs above the LEFT join as a filter, not as the join
condition: as a join qual it would null-extend matched rows that fail it,
and the default would let them back in.
The LEFT join is not always needed. If a no-match row cannot pass the
comparison anyway -- e.g. "1 = (select count(*) ...)" turns into "1 = 0"
for it -- dropping it is fine and the INNER join is kept as before. This is
detected by substituting the empty-input default into the comparison and
constant-folding it. Ordinary sum/avg/min/max comparisons fall into this
group: their empty-input value is NULL, and a comparison with NULL does not
pass, so those plans do not change.
If the comparison cannot be placed above the join (the sublink is in an
outer join's ON clause) or the subquery's targetlist is correlated, the
pull-up bails out and the sublink runs as a SubPlan, as before.
Co-Authored-By: excaliiibur <excaliiibur@foxmail.com>
Alena0704
force-pushed
the
count-aggr-first-fallback
branch
from
July 17, 2026 09:21
49938bd to
acff7ac
Compare
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.
With the Postgres planner (
optimizer=offor an ORCA fallback), a correlatedscalar subquery with an aggregate is pulled up into an INNER join with a
grouped subquery (
convert_EXPR_to_join), which drops outer rows that haveno match. The original subquery keeps them: it computes the aggregate over
empty input, so e.g. COUNT yields 0 there:
A row with no match in t2 must be compared as "t1.a > 0" and can pass, but
the INNER join dropped it.
To fix this, pull the subquery up into a LEFT join, so no-match rows survive
as null-extended rows, and rewrite the comparison to return the same value
the subquery would:
match_flag is a constant TRUE column added to the subquery. For a matched
row the CASE returns the real expression; for a null-extended row the flag
is NULL and the CASE returns the empty-input default (0 for COUNT, NULL for
other aggregates).
The comparison runs above the LEFT join as a filter, not as the join
condition: as a join qual it would null-extend matched rows that fail it,
and the default would let them back in.
The LEFT join is not always needed. If a no-match row cannot pass the
comparison anyway -- e.g. "1 = (select count(*) ...)" turns into "1 = 0"
for it -- dropping it is fine and the INNER join is kept as before. This is
detected by substituting the empty-input default into the comparison and
constant-folding it. Ordinary sum/avg/min/max comparisons fall into this
group: their empty-input value is NULL, and a comparison with NULL does not
pass, so those plans do not change.
If the comparison cannot be placed above the join (the sublink is in an
outer join's ON clause) or the subquery's targetlist is correlated, the
pull-up bails out and the sublink runs as a SubPlan, as before.
Co-Authored-By: excaliiibur excaliiibur@foxmail.com