Source linked

Postgres 19 Finally Ships Native Temporal Tables - Here's How It Works

Postgres 19 adds native SQL:2011 temporal table support, replacing the old btree_gist exclusion constraint hack with a clean range type and WITHOUT OVERLAPS syntax.

postgresqlpostgres 19temporal tablessql 2011pg bitemporalbtree gist

Twenty-three years after SQL:2011 defined APPLICATION TIME periods, Postgres 19 finally ships a native temporal table implementation — and it kills the btree_gist + exclusion constraint nonsense that's been the community workaround for ages.

The Old Approach: GiST Exclusion Constraints and Manual Pain

Before 19, tracking a product's price history meant writing a hack like this:

CREATE EXTENSION IF NOT EXISTS btree_gist;

CREATE TABLE products (
 product_id INT NOT NULL,
 product_name TEXT NOT NULL,
 price NUMERIC(10,2) NOT NULL,
 valid_from DATE NOT NULL,
 valid_to DATE NOT NULL,
 CONSTRAINT no_time_travel CHECK (valid_from < valid_to)
);

ALTER TABLE products ADD CONSTRAINT no_overlapping_prices
 EXCLUDE USING gist (
 product_id WITH =,
 daterange(valid_from, valid_to) WITH &&
 );

It works — but only if your team understands GiST indexes, remembers to load the btree_gist extension, and can parse that exclusion constraint syntax. More importantly, Postgres itself never knew this was temporal data; it just saw columns and a weird constraint. Every update forced the application to manually split rows and stitch ranges. No query planner optimization, no native DML syntax.

The Long Road: pg_bitemporal and the Fight for Core Integration

Henrietta Dombrovskaya and Chad Slaughter spent years building pg_bitemporal, an extension that layered bitemporal tracking on top of the same GiST exclusion trick — but doubled: one for valid time, one for transaction time. Their tables used TSTZRANGE columns and single exclusion constraints that enforced both dimensions simultaneously.

CREATE TABLE bi_temporal.customers (
 cust_nbr INTEGER,
 cust_nm TEXT,
 cust_type TEXT,
 effective_range TSTZRANGE,
 asserted_range TSTZRANGE,
 row_created_at TIMESTAMPTZ,
 EXCLUDE USING gist (
 cust_nbr WITH =,
 effective_range WITH &&,
 asserted_range WITH &&
 )
);

It worked, but an extension can't teach the query planner about temporal predicates or integrate with the constraint system at the engine level. The community needed core support.

Postgres 19: Range Types and WITHOUT OVERLAPS

Now, instead of separate valid_from and valid_to columns, you use a single DATERANGE column and a WITHOUT OVERLAPS constraint that the planner understands natively:

CREATE TABLE products (
 product_id INT NOT NULL,
 product_name TEXT NOT NULL,
 price NUMERIC(10,2) NOT NULL,
 valid_at DATERANGE NOT NULL,
 PRIMARY KEY (product_id, valid_at WITHOUT OVERLAPS)
);

That's it. No extensions, no GiST magic, no manual row splitting. The syntax matches SQL:2011, the exclusion is enforced at the engine level, and the query planner can now reason about temporal ranges directly. It's not the full bitemporal picture — transaction time is still absent — but it's the biggest step Postgres has taken toward native temporal support since the feature was first proposed.

Postgres 19 ships next year. Time to retire those btree_gist hacks and start writing temporal queries the standard way.


Source: Looking Forward to Postgres 19: It's About Time
Domain: pgedge.com

Read original source ->

External source stays available while the OJO article and comment thread stay local.

Comments load interactively on the live page.