@@ -12,41 +12,41 @@ without it.
1212
1313## Quick Start
1414
15- Two equivalent interfaces build and maintain the per-partition summaries. Use whichever
16- fits your workflow.
17-
18- ### Function interface
15+ Summaries are built and maintained through a custom index access method, so pruning
16+ follows the normal index lifecycle (` pg_dump ` /restore, ` REINDEX ` , ` DROP INDEX ` ).
1917
2018``` sql
2119CREATE EXTENSION pg_table_range;
2220
23- -- Register one or more columns of a partitioned (or plain) table and build summaries.
24- -- Pass the relation as an OID; cast a name with ::regclass::oid.
25- SELECT table_range_create(' events' ::regclass::oid , ARRAY[' val' , ' created_at' ]);
21+ -- Summarize one or more columns of a partitioned (or plain) table.
22+ CREATE INDEX events_tr ON events USING table_range (val, created_at);
2623
27- -- Queries now prune partitions whose summarized range cannot match the predicate.
24+ -- Queries now prune partitions whose summary cannot match the predicate.
2825-- Verify with EXPLAIN: non-matching partitions disappear from the plan.
2926EXPLAIN (COSTS OFF) SELECT * FROM events WHERE val >= 250 ;
3027
31- -- Recompute after bulk loads (also clears staleness); or drop registration entirely.
32- SELECT table_range_refresh(' events' ::regclass::oid );
33- SELECT table_range_drop(' events' ::regclass::oid );
34- ```
35-
36- ### Index interface
37-
38- ``` sql
39- -- Builds the same summaries via a custom index access method.
40- CREATE INDEX events_tr ON events USING table_range (val, created_at);
41-
42- -- Pruning works immediately; REINDEX rebuilds summaries after heavy churn.
28+ -- Recompute after heavy churn; or drop the summaries entirely.
4329REINDEX INDEX events_tr;
4430DROP INDEX events_tr; -- removes the summaries it built
4531```
4632
4733The index is never used for scans — it exists only to build and own the summaries — so
4834it adds no scan-time overhead and is never chosen by the planner for data access.
4935
36+ ### Supported column types (no setup, including PostGIS)
37+
38+ ` CREATE INDEX … USING table_range ` works on any ** btree-comparable** type and any
39+ ** range** type out of the box. The required operator classes are provisioned
40+ automatically by mirroring the types that already have a btree/range operator class — and
41+ that mirror re-runs whenever an extension is installed, so ** PostGIS geometry works the
42+ moment you ` CREATE EXTENSION postgis ` , with no extra step** :
43+
44+ ``` sql
45+ CREATE EXTENSION postgis; -- geometry opclass auto-registers
46+ CREATE INDEX places_tr ON places USING table_range (geom);
47+ EXPLAIN (COSTS OFF) SELECT * FROM places WHERE geom && ST_MakeEnvelope(0 ,0 ,10 ,10 );
48+ ```
49+
5050## How it works
5151
5252- ** Summaries.** For each leaf partition and indexed column, one row in
@@ -69,23 +69,30 @@ it adds no scan-time overhead and is never chosen by the planner for data access
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.** Data changes mark the affected partition's summaries
73- * stale* , and stale summaries are never used for pruning — so a change can never cause a
74- missing row. The function interface installs row-level triggers
75- ( ` INSERT ` / ` UPDATE ` / ` DELETE ` / ` TRUNCATE ` ); the index interface marks stale from
76- ` aminsert ` . ` table_range_refresh ` (or ` REINDEX ` ) recomputes and re-enables pruning.
77- A ` sql_drop ` event trigger removes summaries for any dropped relation or index .
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.
7878
7979## Performance
8080
81- The win is at ** planning time** on wide trees, where the planner would otherwise build
82- paths for every partition. On a 1000-partition table queried by a non-key column
83- (` bench/planning_benchmark.sql ` , PostgreSQL 18):
81+ The benefit is at ** execution** : a selective predicate on a non-key column scans only the
82+ matching partition instead of every partition. On 100 partitions × 30k rows = 3M rows
83+ (` bench/planning_benchmark.sql ` , PostgreSQL 18, warm):
84+
85+ | | Total query time (plan + exec) |
86+ | ---| ---|
87+ | pruning off (scans all 100 partitions) | ~ 125 ms |
88+ | pruning on (scans 1 partition) | ~ 18 ms |
8489
85- | | Planning time | Result |
86- | ---| ---| ---|
87- | pruning off | ~ 125 ms | 50 rows |
88- | pruning on | ~ 17 ms | 50 rows |
90+ Pruning is ** not** a free planning-time win: it adds a small per-plan overhead (loading
91+ summaries once, then evaluating each partition — single-digit to low-tens of ms on
92+ hundreds of partitions). It pays off when the partitions it eliminates are large enough
93+ that avoiding their scan outweighs that overhead — so it helps most on ** large
94+ partitions with a selective non-key predicate** , and can be a slight net cost on tiny
95+ partitions. Use ` table_range.enable_pruning ` to measure both ways on your workload.
8996
9097Summaries are loaded ** once per plan** (not per partition); the
9198` e2e_per_plan_cache_loads_once_regardless_of_partitions ` test asserts exactly one
@@ -114,20 +121,18 @@ Everything not listed is conservatively **kept** (never mispruned):
114121
115122## Catalog
116123
117- - ` table_range_summary ` — one summary row per (owner , leaf partition, column):
124+ - ` table_range_summary ` — one summary row per (index , leaf partition, column):
118125 ` index_oid ` , ` relid ` , ` attnum ` , ` kind ` (` minmax ` or ` overlap ` ), ` type_name ` ,
119126 ` min_summary ` , ` max_summary ` , ` has_nulls ` , ` all_nulls ` , ` stale ` , ` tuple_version ` .
120- - ` table_range_registered ` — parents registered through the function interface and their
121- columns.
122127
123128## Project layout
124129
125130| File | Responsibility |
126131| ------| ----------------|
127132| ` src/lib.rs ` | GUCs, ` _PG_init ` , catalog/bootstrap SQL, test wiring |
128- | ` src/summary_build.rs ` | SPI summary build/refresh/drop, registration, staleness triggers |
133+ | ` src/summary_build.rs ` | SPI summary build (scalar min/max + range/geometry extent) |
129134| ` src/prune_hook.rs ` | planner + pathlist hooks, per-plan cache, typed in-memory evaluation |
130- | ` src/index_am.rs ` | ` table_range ` index access method and operator classes |
135+ | ` src/index_am.rs ` | ` table_range ` index access method + automatic operator-class provisioning |
131136| ` src/e2e_tests.rs ` , ` src/index_am_tests.rs ` | end-to-end tests |
132137
133138## Building and testing
@@ -149,7 +154,5 @@ range-type tests, which exercise the same code path.
149154
150155- ` NOT IN ` / ` <> ALL ` , ` NOT (...) ` , expression predicates, and parameterized
151156 prepared-statement plans are kept rather than pruned.
152- - Summaries are exact at build/refresh time; between changes and a refresh the affected
153- partitions are simply not pruned (always correct, just less selective).
154- - The index interface marks a partition stale on insert; recompute with ` REINDEX ` (or
155- ` table_range_refresh ` for the function interface) to re-enable pruning after churn.
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 ` .
0 commit comments