Skip to content

Commit 65832ff

Browse files
bitnerclaude
andcommitted
Docs: add CHECK constraint exclusion comparison to performance section + benchmark
The built-in way to prune on a non-key column is a data-range CHECK per partition + constraint exclusion. Add an apples-to-apples comparison: both prune to one partition with the same execution win; CHECK exclusion plans ~2.6x faster (~5us vs ~31us per partition), but table_range avoids the manual, enforced (insert-blocking), rescan-on-change constraint management. Add the measurement as section 3 of bench/benchmark.sql. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
1 parent 85b6926 commit 65832ff

2 files changed

Lines changed: 67 additions & 1 deletion

File tree

README.md

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,6 +121,33 @@ partition count. table_range cannot match that (see
121121
case (scanning every partition), it still wins on total time whenever the partitions are
122122
sizeable.
123123

124+
**Comparison to `CHECK` constraint exclusion.** The built-in way to prune on a non-key
125+
column is to put a data-range `CHECK (col BETWEEN lo AND hi)` on each partition and let the
126+
planner's constraint exclusion refute it. That is the most direct apples-to-apples
127+
baseline. Same table, 2,000 partitions, same `nk = <value>` predicate:
128+
129+
| Same `=` predicate, 2,000 partitions | Planning | Execution | Scans |
130+
|---|---|---|---|
131+
| `CHECK` constraint exclusion (`constraint_exclusion=on`) | ~32 ms | ~0.08 ms | 1 partition |
132+
| table_range pruning | ~84 ms | ~0.08 ms | 1 partition |
133+
| no pruning | ~22 ms | ~24 ms | all 2,000 |
134+
135+
Both are O(partitions) and give the **identical execution win**. Constraint exclusion plans
136+
~2.6× faster — it is C code testing an already-loaded `CHECK` expression (~5 µs/partition),
137+
while table_range reads each partition's index page (~31 µs/partition). What table_range
138+
buys for that extra planning cost is everything `CHECK` constraints make you give up:
139+
140+
- **No manual management**`CREATE INDEX` builds and owns the ranges; you don't compute
141+
and attach a constraint per partition and keep it correct.
142+
- **No enforcement / no blocked inserts** — a real `CHECK` *rejects* out-of-range rows;
143+
table_range's summary simply widens to cover new data, so inserts never fail.
144+
- **Incremental maintenance** — changing a `CHECK` means `DROP`/`ADD CONSTRAINT` with a
145+
full-partition revalidation scan; table_range widens in place in `aminsert`, no rescan.
146+
147+
So table_range offers constraint-exclusion-class pruning without manual, enforced,
148+
rescan-on-change constraints. Closing the ~2.6× planning gap (the per-partition index read)
149+
is an active optimization target.
150+
124151
Each partition's summary is read from its own index page and cached for the duration of
125152
one plan; the per-column compare function and the query constant are resolved once per
126153
plan and reused across partitions (so the per-partition cost is a typed min/max compare,

bench/benchmark.sql

Lines changed: 40 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,46 @@ EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF) SELECT count(*) FROM bench_b WHERE nk =
8888
DROP TABLE bench_b CASCADE;
8989

9090
-- ====================================================================================
91-
-- 3. (Optional) The lock-table wall. At ~10,000 partitions a non-key predicate exhausts
91+
-- 3. table_range vs. CHECK constraint exclusion: the built-in way to prune on a non-key
92+
-- column is a data-range CHECK on each partition. Same table, same predicate; we toggle
93+
-- constraint_exclusion vs. table_range.enable_pruning to compare the two mechanisms.
94+
-- ====================================================================================
95+
DROP TABLE IF EXISTS bench_c CASCADE;
96+
CREATE TABLE bench_c (region int, nk bigint) PARTITION BY LIST (region);
97+
-- Each partition gets a data-driven CHECK on the non-key column nk.
98+
SELECT format(
99+
'CREATE TABLE bench_c_%s PARTITION OF bench_c FOR VALUES IN (%s); '
100+
'ALTER TABLE bench_c_%s ADD CONSTRAINT bench_c_%s_nk CHECK (nk >= %s AND nk <= %s);',
101+
g, g, g, g, g * 1000, g * 1000 + 999)
102+
FROM generate_series(1, 2000) g \gexec
103+
INSERT INTO bench_c
104+
SELECT g, g * 1000 + s FROM generate_series(1, 2000) g, generate_series(0, 49) s;
105+
VACUUM ANALYZE bench_c;
106+
CREATE INDEX bench_c_tr ON bench_c USING table_range (nk);
107+
108+
SET constraint_exclusion = on;
109+
SET table_range.enable_pruning = off;
110+
SELECT count(*) FROM bench_c WHERE nk = 1000025; -- warm
111+
\echo '==== C: CHECK constraint exclusion (table_range off) ===='
112+
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF) SELECT count(*) FROM bench_c WHERE nk = 1000025;
113+
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF) SELECT count(*) FROM bench_c WHERE nk = 1000025;
114+
115+
SET constraint_exclusion = off;
116+
SET table_range.enable_pruning = on;
117+
SELECT count(*) FROM bench_c WHERE nk = 1000025; -- warm
118+
\echo '==== C: table_range pruning (CHECK exclusion off) ===='
119+
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF) SELECT count(*) FROM bench_c WHERE nk = 1000025;
120+
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF) SELECT count(*) FROM bench_c WHERE nk = 1000025;
121+
122+
SET constraint_exclusion = off;
123+
SET table_range.enable_pruning = off;
124+
SELECT count(*) FROM bench_c WHERE nk = 1000025; -- warm
125+
\echo '==== C: no pruning (both off, scans all 2,000 partitions) ===='
126+
EXPLAIN (ANALYZE, COSTS OFF, TIMING OFF) SELECT count(*) FROM bench_c WHERE nk = 1000025;
127+
DROP TABLE bench_c CASCADE;
128+
129+
-- ====================================================================================
130+
-- 4. (Optional) The lock-table wall. At ~10,000 partitions a non-key predicate exhausts
92131
-- the lock table on default settings:
93132
-- ERROR: out of shared memory
94133
-- HINT: You might need to increase "max_locks_per_transaction".

0 commit comments

Comments
 (0)