Fixing Partial Index Matching Regressions in PostgreSQL 17/18: A Deep Dive into NullTest Const-Simplification
Introduction
This pgsql-bugs thread started from a real-world regression report: a partial index that was used in PostgreSQL 16 stopped being selected in PostgreSQL 17. The case looked odd at first (WHERE flag IS NOT NULL on a column already declared NOT NULL), but it exposed a planner consistency issue around expression simplification.
The discussion evolved from bug triage into a planner internals fix focused on how eval_const_expressions() handles NullTest clauses when expressions are loaded from catalogs and then remapped.
Technical Analysis
Root cause
The key issue was not partial indexes per se, but when and how expression trees get const-simplified:
- Expressions from relcache/catalog paths (index expressions, index predicates, stats expressions, constraints) may be simplified before
Varnodes are rewritten to the query's targetvarno. - In some code paths,
eval_const_expressions()was called withroot = NULL, which disables certainNullTestreductions. - As a result, expressions that should become equivalent did not canonicalize to the same form, so implication/matching logic could miss usable partial indexes.
The thread explicitly calls out that planner behavior must not diverge by context for the same logical expression.
Patch evolution (exploratory patch -> v1 -> v2 -> v3 -> v4)
- Exploratory patch (
further_eval_const_expressions_processing_on_partial_indexes.patch): a minimal idea to re-run simplification on index predicates. - v1: broad single patch touching constraints, stats, index expressions, and index predicates together.
- v2: split into two patches:
0001: constraints and statistics expressions.0002: index expressions and predicates, includingON CONFLICTarbiter matching paths.
- v3: rebased/consolidated the index-focused part after
0001had been pushed independently. - v4: added stronger rationale on planning-time overhead and expanded regression coverage, including index-expression and index-predicate cases.
The final merged result was pushed to master, while backpatch options for v17/v18 remained unresolved in the thread.
SQL examples
The following SQL reproduces the class of planner behavior discussed and mirrors the regression tests added in the patch series. These examples require a PostgreSQL build containing the fix (merged to master), not stock v17/v18 releases.
CREATE TABLE pred_tab (a int, b int NOT NULL, c int NOT NULL);
INSERT INTO pred_tab SELECT i, i, i FROM generate_series(1, 1000) i;
-- Predicate index path
CREATE INDEX pred_tab_pred_idx ON pred_tab (a)
WHERE b IS NOT NULL AND c IS NOT NULL;
ANALYZE pred_tab;
EXPLAIN (COSTS OFF)
SELECT * FROM pred_tab
WHERE a < 3 AND b IS NOT NULL AND c IS NOT NULL;
-- Expression index path
CREATE INDEX pred_tab_exprs_idx
ON pred_tab ((a < 5 AND b IS NOT NULL AND c IS NOT NULL));
EXPLAIN (COSTS OFF)
SELECT * FROM pred_tab
WHERE (a < 5 AND b IS NOT NULL AND c IS NOT NULL) IS TRUE;
Expected shape after the fix: planner can reduce NullTest-related forms consistently and choose the corresponding index scan paths in these patterns.
Community Insights
The review thread shows a few recurring PostgreSQL design themes:
- Consistency over ad hoc behavior: simplification should behave the same across planner contexts.
- Correctness first, then cost: reviewers questioned the extra
eval_const_expressions()pass; the patch justified why overhead is small and localized. - Incremental landing: splitting v2 into two patches made review and commit flow smoother; the non-index half landed first.
- Release-branch caution: participants noted an awkward version window (not in older versions, fixed in master, unclear for v17/v18 backpatching).
Technical Details
The core implementation strategy was:
- Ensure
Varremapping (ChangeVarNodes) is done before the decisive simplification pass where needed. - Re-run
eval_const_expressions(root, ...)for index expressions and predicates after varno adjustments. - Apply the same principle in
infer_arbiter_indexes()soON CONFLICTindex inference sees equivalently simplified trees. - Expand regression tests to validate both predicate indexes and expression indexes, plus surrounding planner behavior expectations.
This makes catalog-loaded expressions converge toward the same canonical form as query quals, restoring reliable predicate implication and index matching.
Current Status
The thread progressed through multiple patch revisions (v1 to v4) and was ultimately committed to PostgreSQL master. The open question at thread close was policy/feasibility for backpatching to v17 and v18.
Conclusion
This bug report became a useful case study in planner expression hygiene: small differences in simplification context can have major plan-shape effects. By standardizing when const-simplification runs relative to varno fixing (and by providing a valid planner root), PostgreSQL restores partial-index usability for affected patterns and reduces surprising version-to-version behavior changes.