Привет, Хабр!
Temporal tables позволяют следить за историями изменений уровне движка. SQL Server сам хранит полную историю изменений каждой строки — без триггеров, без дополнительного кода и без самописного аудита. Фича появилась в SQL Server 2016 и к сегодняшнему дню обросла возможностями. Разберём, как все устроено и как использовать.
Идея: каждая строка знает, когда она была актуальна
Temporal table — обычная таблица с двумя дополнительными столбцами: начало и конец периода актуальности строки. Плюс связанная таблица истории, куда SQL Server автоматически складывает предыдущие версии строк при UPDATE и DELETE.
CREATE TABLE dbo.Products ( ProductId INT PRIMARY KEY, Name NVARCHAR(200) NOT NULL, Price DECIMAL(18,2) NOT NULL, Discount INT NOT NULL DEFAULT 0, -- Период актуальности (обязательно для temporal) ValidFrom DATETIME2 GENERATED ALWAYS AS ROW START NOT NULL, ValidTo DATETIME2 GENERATED ALWAYS AS ROW END NOT NULL, PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo) ) WITH ( SYSTEM_VERSIONING = ON ( HISTORY_TABLE = dbo.ProductsHistory ) );
ValidFrom и ValidTo — системные столбцы, которые SQL Server заполняет автоматически. ValidFrom — момент, когда строка стала актуальной. ValidTo — момент, когда она перестала быть актуальной. Для текущих строк ValidTo = 9999-12-31 23:59:59.
HISTORY_TABLE — куда складывать историю. Если не указать — SQL Server создаст таблицу с автоматическим именем.
Как это работает
INSERT — UPDATE — текущая строка копируется в таблицу истории (с DELETE — строка перемещается в таблицу истории с |
Обычные DML‑запросы работают как раньше:
-- Создаём товар INSERT INTO dbo.Products (ProductId, Name, Price, Discount) VALUES (1, N'Ноутбук', 75000.00, 0); -- Меняем цену UPDATE dbo.Products SET Price = 69900.00 WHERE ProductId = 1; -- Добавляем скидку UPDATE dbo.Products SET Discount = 15 WHERE ProductId = 1; -- Снижаем скидку UPDATE dbo.Products SET Discount = 10 WHERE ProductId = 1;
После этих операций в dbo.Products — одна строка с текущими данными. В dbo.ProductsHistory — три строки с предыдущими значениями, каждая с точным временем от и до.
Запросы по времени: FOR SYSTEM_TIME
SQL Server поддерживает специальный синтаксис для запросов к данным на определённый момент или за период:
AS OF — состояние на конкретный момент:
-- Какая цена и скидка были вчера в 15:00? SELECT Name, Price, Discount FROM dbo.Products FOR SYSTEM_TIME AS OF '2026-03-22T15:00:00' WHERE ProductId = 1;
SQL Server сам найдёт строку, у которой ValidFrom <= '2026-03-22T15:00:00' < ValidTo.
FROM... TO — все версии за период:
-- Все изменения за последнюю неделю SELECT Name, Price, Discount, ValidFrom, ValidTo FROM dbo.Products FOR SYSTEM_TIME FROM '2026-03-16' TO '2026-03-23' WHERE ProductId = 1 ORDER BY ValidFrom;
Вернёт все строки, которые были актуальны хотя бы частично в указанном периоде. Включая текущую версию.
BETWEEN — аналогично FROM...TO, но включает обе границы.
CONTAINED IN — строки, полностью попадающие в интервал (и начало, и конец внутри):
-- Версии, которые были актуальны полностью внутри марта SELECT * FROM dbo.Products FOR SYSTEM_TIME CONTAINED IN ('2026-03-01', '2026-03-31') WHERE ProductId = 1;
ALL — вообще все версии, включая текущую и всю историю:
SELECT Name, Price, Discount, ValidFrom, ValidTo FROM dbo.Products FOR SYSTEM_TIME ALL WHERE ProductId = 1 ORDER BY ValidFrom;
Превращаем существующую таблицу в temporal
Не обязательно создавать таблицу заново. Можно добавить версионирование к существующей:
-- Шаг 1: добавляем столбцы периода ALTER TABLE dbo.Clients ADD ValidFrom DATETIME2 GENERATED ALWAYS AS ROW START HIDDEN NOT NULL DEFAULT SYSUTCDATETIME(), ValidTo DATETIME2 GENERATED ALWAYS AS ROW END HIDDEN NOT NULL DEFAULT CONVERT(DATETIME2, '9999-12-31 23:59:59'), PERIOD FOR SYSTEM_TIME (ValidFrom, ValidTo); -- Шаг 2: включаем версионирование ALTER TABLE dbo.Clients SET (SYSTEM_VERSIONING = ON (HISTORY_TABLE = dbo.ClientsHistory));
HIDDEN — столбцы не будут возвращаться в SELECT *. Они есть, но не мешают существующему коду. Существующие запросы, ORM, приложения — всё продолжит работать как раньше.
Таблица истории: что внутри
Таблица истории — обычная таблица SQL Server. Те же столбцы, что и основная, но без ограничений (PRIMARY KEY, UNIQUE и другие constraints не наследуются). Можно добавить индексы для ускорения запросов по времени:
-- Кластерный индекс на период — ускоряет FOR SYSTEM_TIME CREATE CLUSTERED INDEX IX_ProductsHistory_Period ON dbo.ProductsHistory (ValidTo, ValidFrom) WITH (DATA_COMPRESSION = PAGE);
DATA_COMPRESSION = PAGE — история обычно большая, компрессия экономит место.
Таблицу истории нельзя напрямую модифицировать (INSERT/UPDATE/DELETE), пока включено SYSTEM_VERSIONING.
Retention policy: автоматическая очистка
История растёт. Для таблицы с миллионами строк и частыми обновлениями таблица истории может стать огромной. Есть поддержка retention policy:
ALTER TABLE dbo.Products SET (SYSTEM_VERSIONING = ON ( HISTORY_TABLE = dbo.ProductsHistory, HISTORY_RETENTION_PERIOD = 1 YEAR ));
SQL Server автоматически удалит строки истории старше года. Для этого нужно включить TEMPORAL_HISTORY_RETENTION на уровне базы:
ALTER DATABASE MyDB SET TEMPORAL_HISTORY_RETENTION ON;
Практический пример: аудит изменений
«Кто поменял скидку клиенту и когда?». Можно узнать так:
SELECT c.ClientName, h.Discount AS OldDiscount, c.Discount AS CurrentDiscount, h.ValidFrom AS ChangedFrom, h.ValidTo AS ChangedTo FROM dbo.Clients c JOIN dbo.ClientsHistory h ON c.ClientId = h.ClientId WHERE c.ClientId = 42 AND h.Discount <> c.Discount ORDER BY h.ValidFrom DESC;
Или сравнить состояние на две даты:
-- Что изменилось за последние 24 часа SELECT curr.ClientName, prev.Discount AS DiscountYesterday, curr.Discount AS DiscountToday FROM dbo.Clients FOR SYSTEM_TIME AS OF GETUTCDATE() curr JOIN dbo.Clients FOR SYSTEM_TIME AS OF DATEADD(DAY, -1, GETUTCDATE()) prev ON curr.ClientId = prev.ClientId WHERE curr.Discount <> prev.Discount;
Два FOR SYSTEM_TIME AS OF с разными моментами — и у вас diff между вчера и сегодня.
Тонкости
UTC. Нельзя TRUNCATE. Temporal table не поддерживает Изменение схемы. Добавить столбец можно. Удалить немножко сложнее: нужно сначала выключить SYSTEM_VERSIONING, изменить обе таблицы (основную и историю), включить обратно. |
ALTER TABLE dbo.Products SET (SYSTEM_VERSIONING = OFF); ALTER TABLE dbo.Products DROP COLUMN SomeOldColumn; ALTER TABLE dbo.ProductsHistory DROP COLUMN SomeOldColumn; ALTER TABLE dbo.Products SET (SYSTEM_VERSIONING = ON ( HISTORY_TABLE = dbo.ProductsHistory ));
Производительность. UPDATE на temporal table чуть дороже — SQL Server делает INSERT в таблицу истории. Для OLTP с тысячами UPDATE/s это ощутимо. Для большинства бизнес‑приложений незаметно. Индекс на (ValidTo, ValidFrom) в таблице истории обязателен для быстрых запросов FOR SYSTEM_TIME.

Temporal Tables закрывают только часть задачи: позволяют восстановить историю изменений. Но когда встаёт вопрос, как не потерять данные, как восстановиться после ошибки и как уверенно работать с SQL Server в боевых условиях, одной этой фичи уже недостаточно. Курс «Разработчик MS SQL Server» здесь уместен как раз потому, что даёт более широкий рабочий контекст: не только отдельные механизмы, но и понимание надёжности, восстановления и эксплуатации базы.
Проверьте, насколько вы готовы к таким сценариям:
15-минутное тестирование покажет пробелы и точки роста
➦[Узнать свой уровень]
Приходите на открытый урок 9 апреля в 20:00:
Разбор моделей восстановления и реальных кейсов спасения баз
➦ [Прийти на урок 9 апреля]
