@@ -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,12 +69,12 @@ 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
@@ -84,10 +84,15 @@ paths for every partition. On a 1000-partition table queried by a non-key column
8484
8585| | Planning time | Result |
8686| ---| ---| ---|
87- | pruning off | ~ 125 ms | 50 rows |
88- | pruning on | ~ 17 ms | 50 rows |
87+ | pruning off | ~ 210 ms | 50 rows |
88+ | pruning on | ~ 100 ms | 50 rows |
89+
90+ Pruning removes ~ 110 ms of child-path planning here. Note the absolute numbers are higher
91+ than they could be: because summaries are owned by a real index, PostgreSQL loads index
92+ metadata for every partition during planning (a flat overhead, ~ 85 ms on this bare
93+ 1000-partition table — proportionally smaller when partitions already carry indexes).
8994
90- Summaries are loaded ** once per plan** (not per partition); the
95+ Summaries themselves are loaded ** once per plan** (not per partition); the
9196` e2e_per_plan_cache_loads_once_regardless_of_partitions ` test asserts exactly one
9297catalog load for a 64-partition query.
9398
@@ -114,20 +119,18 @@ Everything not listed is conservatively **kept** (never mispruned):
114119
115120## Catalog
116121
117- - ` table_range_summary ` — one summary row per (owner , leaf partition, column):
122+ - ` table_range_summary ` — one summary row per (index , leaf partition, column):
118123 ` index_oid ` , ` relid ` , ` attnum ` , ` kind ` (` minmax ` or ` overlap ` ), ` type_name ` ,
119124 ` 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.
122125
123126## Project layout
124127
125128| File | Responsibility |
126129| ------| ----------------|
127130| ` src/lib.rs ` | GUCs, ` _PG_init ` , catalog/bootstrap SQL, test wiring |
128- | ` src/summary_build.rs ` | SPI summary build/refresh/drop, registration, staleness triggers |
131+ | ` src/summary_build.rs ` | SPI summary build (scalar min/max + range/geometry extent) |
129132| ` 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 |
133+ | ` src/index_am.rs ` | ` table_range ` index access method + automatic operator-class provisioning |
131134| ` src/e2e_tests.rs ` , ` src/index_am_tests.rs ` | end-to-end tests |
132135
133136## Building and testing
@@ -149,7 +152,5 @@ range-type tests, which exercise the same code path.
149152
150153- ` NOT IN ` / ` <> ALL ` , ` NOT (...) ` , expression predicates, and parameterized
151154 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.
155+ - Summaries are exact at build time; an insert that extends a partition marks it stale
156+ (not pruned, but still correct) until the next ` REINDEX ` .
0 commit comments