CommitFest 7161 Review: Apply Extended Statistics to Join Clause During Parameterized Path Costing
code-review · generated from canonical Markdown
code-review
postgresql-master
commitfest-7161-review2026-10-08T09:26:29.961921+00:00
Front matter
schema: review-report-v2 target: "CommitFest 7161: Apply extended statistics to join clause during parameterized path costing" source: postgresql-master slug: commitfest-7161-review head: 061065e28f086e3e2d7191a7785b9f218b7976d8 base: 061065e28f086e3e2d7191a7785b9f218b7976d8 verdict: "ready-for-committer (test follow-up required)" blocker_counts: verified: 0 unverified: 0 coverage_requirement: required coverage_status: measured
CommitFest 7161 Review: Apply Extended Statistics to Join Clause During Parameterized Path Costing
Author: Ilia Evdokimov (Tantor Labs) CommitFest: PG20-3 (2026-11-01 – 2026-11-30), patch 7161 Patch version: v1, +42 −3, 2 files Review date: 2026-10-08 Model: glm-5.2:cloud
Verdict
ready-for-committer (test follow-up required)
No BLOCKER findings. No correctness/security MAJOR findings. One test-only MAJOR (F-1: no regression tests for the new behavior). The patch is functionally correct and the author's repro confirms the intended behavior. The test gap should be addressed before commit.
Blocker split: verified=0, unverified=0
Patch Summary
The patch makes functional-dependency extended statistics applicable when costing parameterized base-relation paths. Previously, clauselist_selectivity_ext only applied extended stats when all clauses referenced a single relation (find_single_rel_for_clauses returned non-NULL). For a parameterized scan, clauses include join quals like t1.a = t2.b, so find_single_rel_for_clauses returns NULL and no extended stats were used — even though varRelid identifies the scanned relation.
Two coordinated changes:
src/backend/optimizer/path/clausesel.c:158-166— when
use_extended_stats && rel == NULL && varRelid != 0, look up paramrel = find_base_rel(root, varRelid) and, if it is an RTE_RELATION with stats, call dependencies_clauselist_selectivity on it, multiplying the result into s1.
src/backend/statistics/dependencies.c:743-788—
dependency_is_compatible_clause now allows up to two varnos (was BMS_SINGLETON), and in the OpExpr branch, when neither arg is pseudoconstant, accepts Var(relid) = Var(other-single-rel) and treats the other relation's Var as a pseudoconstant.
Runtime Verification
Built PostgreSQL with meson+ninja in a disposable scratch build with coverage instrumentation (b_coverage=true). Ran the author's repro and additional scenarios against a scratch PostgreSQL instance:
- Without stats:
rows=2per probe (independence assumption: 1/1000 × 1/100) - With dependency stats:
rows=200per probe (matching actual 200 rows/probe) - LEFT JOIN: also benefits (
rows=200with stats) - 3-way joins with outer joins: no crash, correct estimates
- Plan change: Index Scan → Bitmap Heap Scan due to improved cost estimate
No crash observed in any tested scenario (inner join, LEFT JOIN, RIGHT JOIN, FULL JOIN, 3-way joins with outer joins, OR clauses).
Changed-Code Coverage
Counter epoch: fresh disposable gcov epoch at HEAD Execution lane: host-disposable (passed)
Coverage Totals
| Metric | clausesel.c | dependencies.c | Total |
|---|---|---|---|
| Changed executable lines | 4 | 16 | 20 |
| Covered changed lines | 4 | 9 | 13 |
| Compiler-uncovered lines | 0 | 7 | 7 |
| Structural uncovered lines | 0 | 0 | 0 |
| Changed branches | 10 | 20 | 30 |
| Covered changed branches | 7 | 6 | 13 |
Uncovered Lines (7)
| File | Lines | Function | Coverability | Reason |
|---|---|---|---|---|
dependencies.c | 745 | dependency_is_compatible_clause | coverable-now | 0-varno clause edge case (bms_num_members == 0) |
dependencies.c | 781-785 | dependency_is_compatible_clause | infeasible-with-reason | Right-arg-is-relid path: unreachable through normal SQL because EC clause generation always places inner rel Var on left |
dependencies.c | 787 | dependency_is_compatible_clause | infeasible-with-reason | Neither-arg-matches-relid: unreachable in parameterized path context due to EC Var ordering |
Uncovered Branches (17)
| File | Line | Branch | Coverability | Reason |
|---|---|---|---|---|
clausesel.c | 158 | 1, 3 | coverable-now | False-branch paths of if condition — exercised by non-parameterized queries |
clausesel.c | 162 | 1 | coverable-now | False-branch: paramrel has no stats — exercised before CREATE STATISTICS |
dependencies.c | 743 | 2, 3 | coverable-now | False-branch of > 2 check — exercised by 1-2 varno clauses |
dependencies.c | 776 | 2 | coverable-now | False-branch: largrelids not singleton |
dependencies.c | 777 | 2, 4 | coverable-now | False-branch: largrelids singleton != relid |
dependencies.c | 779 | 2 | coverable-now | False-branch: rargrelids == relid (same-relation clause) |
dependencies.c | 781-784 | all | infeasible-with-reason | Right-arg-is-relid else-if: unreachable due to EC Var ordering normalization |
Excluded Changed Coordinates
None — all changed lines have gcov coverage coordinates.
Non-Instrumented Changes
None — all changed files are C source files with coverage instrumentation.
Findings
— MAJOR (tests): No test coverage for the new behavior
Location: src/test/regress/sql/stats_ext.sql (absence) Class: tests Verification: verified (code inspection + runtime testing)
The patch changes planner estimates for parameterized paths but adds no regression tests. Existing stats_ext.sql covers functional dependencies only for single-relation WHERE restrictions. Without tests, a future refactor could silently revert the behavior.
Repair: Append a test section to stats_ext.sql that creates correlated columns with functional dependency statistics, forces a parameterized nested loop join, and verifies the per-probe row estimate changes. Test patch validated by applying to scratch source, rebuilding, and confirming rows=1 without stats → rows=10 with stats on the inner Index Scan.
Test example (regress):
CREATE TABLE dep_param_t1 (a int, b int, c int) WITH (autovacuum_enabled = off); CREATE TABLE dep_param_t2 (a int, b int) WITH (autovacuum_enabled = off); INSERT INTO dep_param_t1 SELECT i%100, i%100, i%100 FROM generate_series(1,1000) i; INSERT INTO dep_param_t2 SELECT i, i FROM generate_series(0,99) i; CREATE INDEX dep_param_t1_a_b_idx ON dep_param_t1 (a, b); ANALYZE dep_param_t1, dep_param_t2; SET enable_hashjoin = off; SET enable_mergejoin = off; SET enable_bitmapscan = off; EXPLAIN (COSTS ON) SELECT * FROM dep_param_t2, dep_param_t1 WHERE dep_param_t1.a = dep_param_t2.a AND dep_param_t1.b = dep_param_t2.b; CREATE STATISTICS dep_param_st (dependencies) ON a, b FROM dep_param_t1; ANALYZE dep_param_t1; EXPLAIN (COSTS ON) SELECT * FROM dep_param_t2, dep_param_t1 WHERE dep_param_t1.a = dep_param_t2.a AND dep_param_t1.b = dep_param_t2.b;
Oracle: rows=1 without stats → rows=10 with stats on inner Index Scan.
— MINOR (correctness): find_base_rel can ERROR on unexpected varRelid
Location: src/backend/optimizer/path/clausesel.c:160 Class: correctness Verification: verified (code inspection + runtime testing)
find_base_rel(root, varRelid) calls elog(ERROR) if the slot is NULL or out of range. All current callers pass valid base-rel varRelids, so no crash is reachable today. Using find_base_rel_noerr + NULL check is a low-cost defensive improvement.
Repair:
RelOptInfo *paramrel = find_base_rel_noerr(root, varRelid); if (paramrel && paramrel->rtekind == RTE_RELATION && paramrel->statlist != NIL)
Validated: applied to scratch source, rebuilt, ran join tests — no regression.
— MINOR (maintainability): Bitmapset leak — largrelids/rargrelids not freed
Location: src/backend/statistics/dependencies.c:773-774 Class: maintainability Verification: verified (code inspection)
The new pull_varnos results are never bms_free'd. Planner context resets per-query, but this is inconsistent with PostgreSQL's bitmapset hygiene.
Repair: Add bms_free(largrelids); bms_free(rargrelids); before all exit paths. Validated: applied, rebuilt, no regression.
— MINOR (correctness): Outer-join/nullingrels scope limitation
Location: src/backend/statistics/dependencies.c:743, 773-784 Class: correctness Verification: cannot-verify (scope limitation, not a code defect)
pull_varnos includes varnullingrels, which could cause BMS_SINGLETON tests to fail for outer-join-nullled Vars. However, runtime testing showed the patch works for LEFT JOIN parameterized scans because EC-generated clauses use plain Vars without nulling relids. This is a scope limitation, not a bug. No repair needed for current scope; should be addressed if the caller surface expands to pass nullled Var clauses.
— MINOR (docs): No documentation update
Location: doc/src/sgml/planstats.sgml:458 Class: docs Verification: cannot-verify (documentation change)
The Functional Dependencies docs don't mention the new parameterized-path application. A short paragraph should be added noting that dependency statistics are also applied when costing parameterized scan paths.
— NIT (maintainability): s1 *= vs s1 = stylistic inconsistency
Location: src/backend/optimizer/path/clausesel.c:163 Class: maintainability Verification: cannot-verify (stylistic observation)
The *= form is correct and arguably preferable. No repair needed.
— MINOR (maintainability): pull_varnos(NULL,...) PHV handling — REFUTED
Location: src/backend/statistics/dependencies.c:773 Class: maintainability Verification: refuted
pull_varnos(NULL, ...) is intentional and correct: PHVs are not relevant for functional dependency compatibility. If a PHV appeared, the clause would fail the IsA(clause_expr, Var) check at line 902 and be rejected.
Verification Summary
| Finding | Severity | Class | Verification | Repair | Validation |
|---|---|---|---|---|---|
| F-1 | MAJOR | tests | verified | test patch | passed |
| F-2 | MINOR | correctness | verified | find_base_rel_noerr | passed |
| F-3 | MINOR | maintainability | verified | bms_free added | passed |
| F-4 | MINOR | correctness | cannot-verify | waived (scope limitation) | waived |
| F-5 | MINOR | docs | cannot-verify | waived (doc change) | waived |
| F-6 | NIT | maintainability | cannot-verify | waived (stylistic) | waived |
| F-7 | MINOR | maintainability | refuted | N/A | N/A |
Repair Validation Summary
| Finding | Patch SHA-256 | Methods | Status |
|---|---|---|---|
| F-1 | 7a3e49085402da1974d30cd4a30b9ecdbcd14642e580e14392dc7be7d27aedf6 | apply-check, diff-check, compile, targeted-test | passed |
| F-2 | 562a165131fd2ee41163f0a75bdd1657c2d6a510e93fcba8df5c24517b31e420 | apply-check, diff-check, compile, targeted-test | passed |
| F-3 | c9f10d48bfae3350caff3a2634d56076a5f85212e6624f8800675f8cf6dc52a9 | apply-check, diff-check, compile, targeted-test | passed |
| F-4 | waived | N/A | waived |
| F-5 | waived | N/A | waived |
| F-6 | waived | N/A | waived |
| F-7 | N/A (refuted) | N/A | N/A |
pgsql-hackers Thread Ledger
Thread: 7b1bd383-fa74-4c0e-a75a-52ff9c753750@tantorlabs.com Status: reconciled (3 messages)
| Message | Date | Sender | Kind | Summary |
|---|---|---|---|---|
7b1bd383...@tantorlabs.com | 2026-08-17 | Ilia Evdokimov | patch | Original submission with repro showing rows=2 → rows=200 |
e651725f...@vondra.me | 2026-08-21 | Tomas Vondra | review | Asked about MCV/ndistinct; noted scan-vs-join estimate inconsistency |
24bf4ee2...@tantorlabs.com | 2026-09-15 | Ilia Evdokimov | author-reply | Explained MCV/ndistinct are separate works; chose single-rel side first |
Finding links:
- F-4: author-deferred (Vondra raised join-level estimation; author deferred to follow-up)
- F-7: refuted (refuted by verification)
Aggregated Test Plan
- F-1 test (regress):
src/test/regress/sql/stats_ext.sql— parameterized
join with functional dependencies, assert rows=1 → rows=10 transition. Covers clausesel.c:158-165 and dependencies.c:743-780.
- F-2/F-3 repairs validated with the same test plus inner/LEFT/RIGHT join
scenarios — no regression.
Provenance / Model Routes
| Seat | Model | Task |
|---|---|---|
| pg-patch-reviewer | glm-5.2:cloud | Patch review (six-heading) |
| pg-security-reviewer | glm-5.2:cloud | Security + C memory safety |
| pg-finding-verifier | glm-5.2:cloud | Finding verification + repair validation |
| pg-change-coverage-reviewer | glm-5.2:cloud | Changed-code coverage analysis |
| Coordinator | glm-5.2:cloud | Orchestration, verdict, report |
Residual Risk
- The patch treats the join-partner Var as a pseudoconstant, averaging over
its value distribution. This is a sound first-order approximation but may be less accurate for highly skewed join keys. The author acknowledges this and plans MCV/ndistinct as follow-up work.
- The scan-vs-join estimate inconsistency noted by Vondra (50 × 200 ≠ 98) is
not resolved by this patch and remains an open item for join-level estimation follow-up.
- The right-arg-is-relid path (lines 781-785) is unreachable through normal SQL
due to EC clause Var ordering normalization. This is defensive code that is correct but untestable through regression tests.
Maintainer CC
Tomas Vondra (reviewer of extended statistics code), as the thread reviewer.