← На главную

Postgres 19 получил нативную поддержку темпоральных таблиц

12.06.2026 16:44 · hackernews

Postgres 19 наконец-то получил встроенную поддержку темпоральных таблиц. Стандарт SQL:2011 с APPLICATION TIME и FOR PORTION OF появился больше десяти лет назад, но PostgreSQL только догоняет. Раньше, чтобы отслеживать историю цен или изменений данных, приходилось извращаться: заводить отдельные колонки valid_from и valid_to, подключать расширение btree_gist и писать руками exclusion constraint через GiST-индекс. Это работало, но было громоздко, неинтуитивно, и Postgres не понимал, что данные временные — всю логику корректности тащило приложение.

Долгое время эту нишу закрывали энтузиасты. Генриетта Домбровская (Хетти) вместе с Чедом Слотером разработали расширение pg_bitemporal на PL/pgSQL. Оно добавляло битемпоральные таблицы, отслеживающие и valid time (когда факт был верен в реальности), и transaction time (когда база данных его узнала). Это позволяло отвечать на вопросы вроде «что мы думали о цене во вторник, исходя из тех данных, что у нас были?». Расширение опиралось на те же EXCLUDE USING gist, но сдвоенные, и добавляло кучу функций. Работало, но расширение не могло встроиться в ядро: планировщик запросов не видел темпоральные предикаты, не было нативного DML-синтаксиса.

В Postgres 19 сделали application-time половину битемпоральной системы. Теперь не нужно два столбца для диапазона — используем один колонку-диапазон (valid_at DATERANGE NOT NULL), а в первичном ключе пишем PRIMARY KEY (product_id, valid_at WITHOUT OVERLAPS). Никакого btree_gist руками, никаких exclusion constraints — Postgres сам под капотом ставит GiST-индекс и проверяет, что диапазоны не пересекаются. Синтаксис стал чистым и понятным.

Но главная фишка — FOR PORTION OF. Раньше, чтобы изменить цену на промежуток внутри существующего периода, приходилось вручную резать строки: удалять, вставлять новые куски, не забыть про «огрызки» с обеих сторон. Ошибка — и в таймлайне дыры или наложения. Теперь один UPDATE products FOR PORTION OF valid_at FROM '2025-03-01' TO '2025-09-01' SET price = 10.99 WHERE product_id = 1. Postgres сам автоматически разрезает существующие строки: старый диапазон ужимается слева, справа, а между ними вставляется новая запись с новой ценой. Если надо вырезать кусок истории — работает и DELETE FOR PORTION OF. Delete может даже увеличить количество строк, разбивая одну на две, но преемственность сохраняется.

Появились и темпоральные внешние ключи с ключевым словом PERIOD. Например, FOREIGN KEY (product_id, PERIOD valid_at) REFERENCES products (product_id, PERIOD valid_at) — Postgres проверяет, что родительская запись существует на протяжении всего диапазона дочерней. Если у продукта цена определена только до середины 2026 года, вариант с валидностью до 2027 будет отклонён.

Пока нет системного времени (transaction time) — только valid time. Команда pg_bitemporal заполняла этот пробел с 2015 года, но нативная поддержка transaction time, вероятно, появится в Postgres 20. Ограничение и у темпоральных внешних ключей: только NO ACTION при удалении, без CASCADE. Тем не менее, для прикладной истории данных (цены, отделы сотрудников) возможностей Postgres 19 уже хватает.

Читать оригинал →