@@ -25,9 +25,10 @@ CREATE INDEX events_tr ON events USING table_range (val, created_at);
2525-- Verify with EXPLAIN: non-matching partitions disappear from the plan.
2626EXPLAIN (COSTS OFF) SELECT * FROM events WHERE val >= 250 ;
2727
28- -- Recompute after heavy churn; or drop the summaries entirely.
28+ -- Inserts maintain the summary automatically; REINDEX only re-tightens after many
29+ -- deletes. DROP INDEX removes the summary with the index.
2930REINDEX INDEX events_tr;
30- DROP INDEX events_tr; -- removes the summaries it built
31+ DROP INDEX events_tr;
3132```
3233
3334The index is never used for scans — it exists only to build and own the summaries — so
@@ -49,17 +50,16 @@ EXPLAIN (COSTS OFF) SELECT * FROM places WHERE geom && ST_MakeEnvelope(0,0,10,10
4950
5051## How it works
5152
52- - ** Summaries.** For each leaf partition and indexed column, one row in
53- ` table_range_summary ` records the ` has_nulls ` / ` all_nulls ` flags plus either the
54- column's btree ` min ` /` max ` (scalar columns) or a single covering ** extent** — a covering
55- range for range types (` range_merge(range_agg(col)) ` ) or the bounding box for PostGIS
56- geometry (` ST_Extent(col) ` ).
57- - ** Planning.** A ` planner_hook ` loads all non-stale summaries once per top-level plan
58- (a single query, cached for the duration of planning). A ` set_rel_pathlist_hook ` then
59- evaluates each partition's restriction clauses against its cached summary and calls
60- ` mark_dummy_rel ` on any partition that provably cannot match — eliminating it before
61- child paths are generated. Wide partition trees therefore do not pay a per-partition
62- lookup.
53+ - ** Summaries live in the index.** Like BRIN, each leaf partition's summary is stored in
54+ that partition's index — one record per indexed column on the index's ** metapage** , not
55+ in any side table. It holds the ` has_nulls ` / ` all_nulls ` flags plus either the column's
56+ btree ` min ` /` max ` (scalar columns) or a single covering ** extent** — a covering range
57+ for range types (` range_merge(range_agg(col)) ` ) or the bounding box for PostGIS geometry
58+ (` ST_Extent(col) ` ).
59+ - ** Planning.** For each partition the planner builds, a ` set_rel_pathlist_hook ` reads the
60+ summary from that partition's index (cached for the plan) and evaluates the partition's
61+ restriction clauses against it, calling ` mark_dummy_rel ` on any partition that provably
62+ cannot match — eliminating it before child paths are generated.
6363- ** Typed comparisons.** Min/max vs. constant comparisons use each column type's own
6464 btree compare function, so ** any btree-comparable type works** : ` bigint ` / ` int ` /
6565 ` smallint ` , ` numeric ` , ` real ` / ` double precision ` , ` text ` / ` varchar ` , ` date ` ,
@@ -69,12 +69,14 @@ EXPLAIN (COSTS OFF) SELECT * FROM places WHERE geom && ST_MakeEnvelope(0,0,10,10
6969 is pruned by testing the constant against the partition's stored extent with
7070 PostgreSQL's own ` && ` operator — so a partition is eliminated when its extent cannot
7171 overlap the query.
72- - ** Automatic correctness.** An insert that extends a partition marks its summary
73- * stale* (via the index's ` aminsert ` ), and stale summaries are never used for pruning —
74- so a change can never cause a missing row. Deletes only shrink a partition's true
75- range, so the summary stays conservatively wide and remains safe. ` REINDEX ` recomputes
76- and re-enables pruning after churn, and a ` sql_drop ` event trigger removes a dropped
77- index's (or table's) summaries.
72+ - ** Incremental maintenance (no REINDEX).** ` aminsert ` widens the summary in place as
73+ rows are inserted — the same way BRIN maintains its ranges. Because the summary only
74+ ever needs to be over-inclusive, these updates need no MVCC: an insert within the
75+ existing range writes nothing; one that extends it grows the min/max/extent. Pruning
76+ therefore stays correct ** and** active across inserts without any rebuild. Deletes only
77+ shrink a partition's true range, leaving the summary conservatively wide (still safe);
78+ ` VACUUM ` /` REINDEX ` can re-tighten it for selectivity. ` DROP INDEX ` removes the summary
79+ with the index's storage — there is no side table to clean up.
7880
7981## Performance
8082
@@ -119,20 +121,20 @@ Everything not listed is conservatively **kept** (never mispruned):
119121- ` table_range.enable_pruning ` (default ` on ` ) — master switch.
120122- ` table_range.log_pruning_debug ` (default ` off ` ) — log each prune decision.
121123
122- ## Catalog
124+ ## Storage
123125
124- - ` table_range_summary ` — one summary row per (index, leaf partition, column):
125- ` index_oid ` , ` relid ` , ` attnum ` , ` kind ` (` minmax ` or ` overlap ` ), ` type_name ` ,
126- ` min_summary ` , ` max_summary ` , ` has_nulls ` , ` all_nulls ` , ` stale ` , ` tuple_version ` .
126+ There is no catalog table — each partition's summary lives on its ` table_range ` index's
127+ metapage (block 0), written by ` ambuild ` and updated in place by ` aminsert ` , like BRIN.
127128
128129## Project layout
129130
130131| File | Responsibility |
131132| ------| ----------------|
132- | ` src/lib.rs ` | GUCs, ` _PG_init ` , catalog/bootstrap SQL, test wiring |
133- | ` src/summary_build.rs ` | SPI summary build (scalar min/max + range/geometry extent) |
133+ | ` src/lib.rs ` | GUCs, ` _PG_init ` , test wiring |
134+ | ` src/index_storage.rs ` | per-index summary on the metapage: page I/O (Generic WAL) + (de)serialization |
135+ | ` src/summary_build.rs ` | build a leaf's summary by scanning its data (used by ` ambuild ` ) |
134136| ` src/prune_hook.rs ` | planner + pathlist hooks, per-plan cache, typed in-memory evaluation |
135- | ` src/index_am.rs ` | ` table_range ` index access method + automatic operator-class provisioning |
137+ | ` src/index_am.rs ` | ` table_range ` index AM: build, incremental ` aminsert ` widening, opclass provisioning |
136138| ` src/e2e_tests.rs ` , ` src/index_am_tests.rs ` | end-to-end tests |
137139
138140## Building and testing
@@ -154,5 +156,6 @@ range-type tests, which exercise the same code path.
154156
155157- ` NOT IN ` / ` <> ALL ` , ` NOT (...) ` , expression predicates, and parameterized
156158 prepared-statement plans are kept rather than pruned.
157- - Summaries are exact at build time; an insert that extends a partition marks it stale
158- (not pruned, but still correct) until the next ` REINDEX ` .
159+ - Inserts keep summaries current incrementally, but deletes only relax them (the summary
160+ can stay wider than the live data until a ` VACUUM ` /` REINDEX ` re-tightens it) — always
161+ correct, just potentially less selective.
0 commit comments