Thread
Commits
Same data as JSON:
GET /api/v1/messages/:b64id/commits
the thread's linked commits as JSON, with link sources.
API reference →
-
Ignore nullingrels when looking up statistics
- e28033fe1af8 18.0 cited
-
BUG #19481: multivariate MCV expression stats not applied for equivalent predicates on nullable side of LEFT JOI
PG Bug reporting form <noreply@postgresql.org> — 2026-05-16T10:07:20Z
The following bug has been logged on the website: Bug reference: 19481 Logged by: muyehu Email address: yankairong@ruc.edu.cn PostgreSQL version: 18.4 Operating system: ubuntu22.04 Description: I found what appears to be a planner selectivity issue involving multivariate MCV statistics on expressions. I can reproduce a case where expression MCV stats are applied for a plain scan, but apparently not for an equivalent predicate when the same relation appears on the nullable side of a LEFT JOIN. Tested version: PostgreSQL 18.4 on x86_64-pc-linux-gnu Minimal self-contained repro: DROP TABLE IF EXISTS mcv_bug; CREATE TABLE mcv_bug (a int, b int, c int); INSERT INTO mcv_bug SELECT i, i, i FROM generate_series(1,1000) AS g(i); CREATE STATISTICS mcv_bug_stats (mcv) ON (coalesce(mod(a,20),1)), (coalesce(mod(b,10),1)), (coalesce(mod(c,5),1)) FROM mcv_bug; ANALYZE mcv_bug; EXPLAIN (ANALYZE, VERBOSE) SELECT * FROM mcv_bug WHERE coalesce(mod(a,20),1) = 1 AND coalesce(mod(b,10),1) = 1 AND coalesce(mod(c,5),1) = 1; EXPLAIN (ANALYZE, VERBOSE) SELECT * FROM (VALUES (1)) AS d(x) LEFT JOIN mcv_bug m ON true WHERE coalesce(mod(m.a,20),1) = 1 AND coalesce(mod(m.b,10),1) = 1 AND coalesce(mod(m.c,5),1) = 1; Observed output on my system: Seq Scan on public.mcv_bug (cost=0.00..31.00 rows=50 width=12) (actual time=0.013..0.074 rows=50 loops=1) Output: a, b, c Filter: ((COALESCE(mod(mcv_bug.a, 20), 1) = 1) AND (COALESCE(mod(mcv_bug.b, 10), 1) = 1) AND (COALESCE(mod(mcv_bug.c, 5), 1) = 1)) Rows Removed by Filter: 950 Nested Loop Left Join (cost=0.00..41.01 rows=1 width=16) (actual time=0.013..0.140 rows=50 loops=1) Output: 1, m.a, m.b, m.c Filter: ((COALESCE(mod(m.a, 20), 1) = 1) AND (COALESCE(mod(m.b, 10), 1) = 1) AND (COALESCE(mod(m.c, 5), 1) = 1)) Rows Removed by Filter: 950 -> Result (cost=0.00..0.01 rows=1 width=0) (actual time=0.001..0.001 rows=1 loops=1) -> Seq Scan on public.mcv_bug m (cost=0.00..16.00 rows=1000 width=12) (actual time=0.010..0.065 rows=1000 loops=1) Output: m.a, m.b, m.c What seems wrong is that the plain query gets the expected estimate (rows=50, matching the actual result), but the LEFT JOIN variant with the same filter conditions falls back to a severe underestimate (rows=1, while the actual result is 50). Expected behavior: I would expect the second query to receive a selectivity estimate comparable to the first one, or at least to continue benefiting from the same expression MCV statistics, since the predicate expressions are otherwise equivalent. Possible implementation hint: This looks related to expression matching for extended statistics after outer-join nullability decoration. In selfuncs.c, examine_variable() already has logic and comments about stripping nullingrels before trying to match expressions to extended statistics or expression indexes. However, the extended-statistics path used for clause or expression matching appears to still rely on direct expression equality in a way that no longer matches once Vars on the nullable side of an outer join carry nullingrel markings. This may be related to commit e28033fe1af8037e0fec8bb3a32fabbe18ac06b1 ("Ignore nullingrels when looking up statistics"), which appears to fix a closely related class of nullingrels-related statistics lookup issues. However, the attached testcase still reproduces on PostgreSQL 18.4, suggesting there may be a remaining gap specifically in multivariate expression MCV clause matching on the nullable side of an outer join. So this may be a residual nullingrels issue specific to multivariate expression MCV matching. If useful, I can test a patch or provide additional reduced cases. ## Reduced SQL Testcase Compared to the original repro, this version removes the first `ANALYZE` because the later `ANALYZE mcv_bug;` is sufficient to collect both regular and extended statistics after `CREATE STATISTICS`. ```sql DROP TABLE IF EXISTS mcv_bug; CREATE TABLE mcv_bug (a int, b int, c int); INSERT INTO mcv_bug SELECT i, i, i FROM generate_series(1,1000) AS g(i); CREATE STATISTICS mcv_bug_stats (mcv) ON (coalesce(mod(a,20),1)), (coalesce(mod(b,10),1)), (coalesce(mod(c,5),1)) FROM mcv_bug; ANALYZE mcv_bug; EXPLAIN (ANALYZE, VERBOSE) SELECT * FROM mcv_bug WHERE coalesce(mod(a,20),1) = 1 AND coalesce(mod(b,10),1) = 1 AND coalesce(mod(c,5),1) = 1; EXPLAIN (ANALYZE, VERBOSE) SELECT * FROM (VALUES (1)) AS d(x) LEFT JOIN mcv_bug m ON true WHERE coalesce(mod(m.a,20),1) = 1 AND coalesce(mod(m.b,10),1) = 1 AND coalesce(mod(m.c,5),1) = 1; ``` ## Short Summary The issue still reproduces on PostgreSQL 18.4: - Plain scan estimate: `rows=50`, actual `rows=50` - `LEFT JOIN` variant estimate: `rows=1`, actual `rows=50` That strongly suggests the extended expression MCV stats are still not being matched for equivalent predicates on the nullable side of the outer join. -
Re: BUG #19481: multivariate MCV expression stats not applied for equivalent predicates on nullable side of LEFT JOI
Andrei Lepikhov <lepihov@gmail.com> — 2026-05-18T06:40:39Z
On 16/05/2026 12:07, PG Bug reporting form wrote: > That strongly suggests the extended expression MCV stats are still not being > matched for equivalent predicates on the nullable side of the outer join. The query explain tells the whole story: Nested Loop Left Join Filter: ((COALESCE(mod(m.a, 20), 1) = 1) AND (COALESCE(mod(m.b, 10), 1) = 1) AND (COALESCE(mod(m.c, 5), 1) = 1)) -> Result -> Seq Scan on mcv_bug m As you can see, this clause is a JOIN clause. Your coalesce filter applies to the result of the join. But m.a,m.b, and m.c might be changed (nullified) in this join. So, your extended statistic isn't applicable here. To be relevant, it should calculate the number of not-matched LHS tuples and account for them. In your case, the ON clause is 'true', so all tuples will be matched. It is a degenerate case and might potentially be improved, but it doesn't look like a bug. -- regards, Andrei Lepikhov, pgEdge