Found while re-reviewing #1107, which fixes the wider form of this. Not a blocker for
that PR — it narrows this defect rather than introducing it, and the plan choice is the
same before and after. Filing so the remainder is not lost.
The rule the planner uses, and the rule the executor uses
Both eligibility and pricing ask the same question: does this clause mention
sortKey[0]?
/* eligibility */
skips = (p->sortKeyLen > 0 &&
bms_is_member(p->sortKey[0] - FirstLowInvalidHeapAttributeNumber, restrictCols));
/* pricing, after #1107 */
pull_varattnos((Node *) ri->clause, rel->relid, &cols);
if (bms_is_member(sortAttno - FirstLowInvalidHeapAttributeNumber, cols))
clauses = lappend(clauses, ri);
Mentioning the sort key is not the same as being prunable by it. A single
RestrictInfo that ORs a sort-key range with a predicate on another column mentions the
sort key, is therefore counted whole, and contributes a selectivity the sort order cannot
deliver.
The executor already makes the distinction the planner does not. It reports
Columnar Usable Skip Predicates, and that counter is exactly the difference between the
two cases below.
Measured
pgcolumnar-audit, /usr/local/pg18a (assert), PR #1107 at b15fb8a. 20,000 rows,
stripe_row_limit => 1000, chunk_group_row_limit => 500, physical order scrambled so
the base scan cannot prune. Projection onsk over (sk, kind) sorted on sk.
kind = 'odd' where sk % 1000 = 0, so its 20 rows are spread across the whole sk
domain.
control WHERE sk BETWEEN 1 AND 2000 -- 2000 rows, contiguous
residual WHERE sk BETWEEN 1 AND 2000 OR kind = 'odd' -- 2018 rows, 18 of them scattered
Both are priced the same:
|
control |
residual |
| proj run cost |
36.83 |
50.85 |
| base run cost |
368.67 |
504.50 |
| ratio |
0.0999 |
0.1008 |
The runtime is not the same. From EXPLAIN (ANALYZE, BUFFERS) on the residual query:
projection ON projection OFF
Columnar Pushed-Down Filters: 0 0
Columnar Usable Skip Predicates: 0 0
Columnar Vectors Skipped: 0 0
Columnar Chunk Groups Read: 20 20 (of 20)
Columnar Vector Decodes: 120 80
Execution Time: 2.653 ms 1.996 ms
The projection prunes nothing — no pushed-down filter, no usable skip predicate, no
zone-map probe — reads every chunk group, decodes 50% more vectors, and finishes 33%
slower. It is quoted at 10% of the base.
The control, for contrast, earns its discount: Usable Skip Predicates: 2,
Vectors Skipped: 20, and Rows Removed by Filter falls from 18000 to 8000.
Why it is not a regression
Before #1107 the constant 0.5 priced this query at half the base, which also beats the
base and is also chosen. So the planner picks the projection for this shape either way;
#1107 changes how confidently. I am recording that explicitly so the entry is not read as
something that PR broke.
The shape of a fix
Key the test on usability as a skip predicate on the sort key, not on the sort key being
mentioned — the property the executor already computes and prints. A clause that is an OR
with a non-sort-key disjunct is not usable, and should contribute selectivity 1.0 to the
projection's price even though it mentions the key.
A guard arm belongs with it, because both wrong versions of this rule are silent: an
over-inclusive one quotes a discount nothing delivers, and an under-inclusive one declines
a projection that would have won. The existing suite's prsk fixture already has the two
columns needed; this needs one more query shape.
Reproduction
test/projection_scan_cost.sh's fixture with one query added. I ran it as a standalone
probe rather than a suite edit, since the suite belongs to #1107 while that is open.
CREATE TABLE pror (sk int, kind text) USING pgcolumnar;
SELECT pgcolumnar.set_options('pror', stripe_row_limit => 1000, chunk_group_row_limit => 500);
INSERT INTO pror SELECT sk, CASE WHEN sk % 1000 = 0 THEN 'odd' ELSE 'usual' END
FROM generate_series(1, 20000) sk ORDER BY md5(sk::text);
SELECT pgcolumnar.add_projection('pror', 'onsk', ARRAY['sk','kind'], ARRAY['sk']);
ANALYZE pror;
SET max_parallel_workers_per_gather = 0; SET jit = off;
SET pgcolumnar.enable_ungrouped_vector_agg = off;
SET pgcolumnar.enable_group_vectorization = off;
EXPLAIN (ANALYZE, BUFFERS)
SELECT sk FROM pror WHERE sk BETWEEN 1 AND 2000 OR kind = 'odd';
One note on the fixture, because my first version of it was vacuous: with the range at
sk BETWEEN 1 AND 100 both arms land on the one-stripe floor (20 stripes, so 0.05) and
the two cases are indistinguishable. The sort-key range has to be well above the floor for
the question to be askable at all.
🤖 Generated with Claude Code
https://claude.ai/code/session_012RSw4qMHS7ByE7PY8Ns4cs
Found while re-reviewing #1107, which fixes the wider form of this. Not a blocker for
that PR — it narrows this defect rather than introducing it, and the plan choice is the
same before and after. Filing so the remainder is not lost.
The rule the planner uses, and the rule the executor uses
Both eligibility and pricing ask the same question: does this clause mention
sortKey[0]?Mentioning the sort key is not the same as being prunable by it. A single
RestrictInfothat ORs a sort-key range with a predicate on another column mentions thesort key, is therefore counted whole, and contributes a selectivity the sort order cannot
deliver.
The executor already makes the distinction the planner does not. It reports
Columnar Usable Skip Predicates, and that counter is exactly the difference between thetwo cases below.
Measured
pgcolumnar-audit,
/usr/local/pg18a(assert), PR #1107 atb15fb8a. 20,000 rows,stripe_row_limit => 1000,chunk_group_row_limit => 500, physical order scrambled sothe base scan cannot prune. Projection
onskover(sk, kind)sorted onsk.kind = 'odd'wheresk % 1000 = 0, so its 20 rows are spread across the wholeskdomain.
Both are priced the same:
The runtime is not the same. From
EXPLAIN (ANALYZE, BUFFERS)on the residual query:The projection prunes nothing — no pushed-down filter, no usable skip predicate, no
zone-map probe — reads every chunk group, decodes 50% more vectors, and finishes 33%
slower. It is quoted at 10% of the base.
The control, for contrast, earns its discount:
Usable Skip Predicates: 2,Vectors Skipped: 20, andRows Removed by Filterfalls from 18000 to 8000.Why it is not a regression
Before #1107 the constant
0.5priced this query at half the base, which also beats thebase and is also chosen. So the planner picks the projection for this shape either way;
#1107 changes how confidently. I am recording that explicitly so the entry is not read as
something that PR broke.
The shape of a fix
Key the test on usability as a skip predicate on the sort key, not on the sort key being
mentioned — the property the executor already computes and prints. A clause that is an OR
with a non-sort-key disjunct is not usable, and should contribute selectivity 1.0 to the
projection's price even though it mentions the key.
A guard arm belongs with it, because both wrong versions of this rule are silent: an
over-inclusive one quotes a discount nothing delivers, and an under-inclusive one declines
a projection that would have won. The existing suite's
prskfixture already has the twocolumns needed; this needs one more query shape.
Reproduction
test/projection_scan_cost.sh's fixture with one query added. I ran it as a standaloneprobe rather than a suite edit, since the suite belongs to #1107 while that is open.
One note on the fixture, because my first version of it was vacuous: with the range at
sk BETWEEN 1 AND 100both arms land on the one-stripe floor (20 stripes, so 0.05) andthe two cases are indistinguishable. The sort-key range has to be well above the floor for
the question to be askable at all.
🤖 Generated with Claude Code
https://claude.ai/code/session_012RSw4qMHS7ByE7PY8Ns4cs