Generated и identity колонки
GENERATED ALWAYS AS IDENTITY — стандартная, более чистая замена serial; STORED generated колонки вычисляются из других колонок; а последовательности нетранзакционны — пропуски нормальны, никогда не полагайся на безпропускные id.
Система выставления счетов полагалась на то, что её bigserial id заказов без пропусков — номера счетов должны быть 1, 2, 3 без дыр для аудиторов. Потом загруженная неделя откатанных транзакций оставила id вида 1001, 1002, 1005, 1009. Данных никто не потерял; последовательность просто выдала 1003 и 1004 транзакциям, которые abort’нулись, а последовательности не откатываются. Финансисты решили, что строки исчезли. Нет — это схема дала обещание, которое Postgres никогда не даёт. Пропуски в последовательности нормальны, ожидаемы и by design.
IDENTITY: стандартный способ автонумерации
Для суррогатного первичного ключа современный выбор по SQL-стандарту — GENERATED ALWAYS AS IDENTITY (или BY DEFAULT). Он заменяет старые псевдотипы serial/bigserial:
CREATE TABLE orders (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id bigint NOT NULL,
created_at timestamptz NOT NULL DEFAULT now()
);
INSERT INTO orders (user_id) VALUES (42); -- id назначается автоматическиGENERATED ALWAYS AS IDENTITY— колонка принадлежит движку; обычныйINSERT, пытающийся податьid, отвергается (надо использоватьOVERRIDING SYSTEM VALUE, чтобы заставить). Это более безопасный дефолт — он не даёт приложению случайно вставить id, который столкнётся с последовательностью.GENERATED BY DEFAULT AS IDENTITY— автоназначает, когда опускаешь значение, но допускает явное. Используй это только когда иногда надо подавать id (например, импорт данных).
Почему identity бьёт serial
serial никогда не был настоящим типом — это сахар, который тихо создавал последовательность, ставил её дефолтом колонки и делал колонку владельцем последовательности. Эта косвенность вызывала реальную операционную боль:
- Странности владения/прав. Последовательность — отдельный объект со своими привилегиями; выдача
INSERTна таблицу не давала usage на последовательность, так что зажатые роли ловили «permission denied for sequence». СIDENTITYпоследовательность внутренняя, а модель прав — на колонке. - Хрупкие дампы и переименования. Дефолт
serial— буквальныйnextval('orders_id_seq'), впечённый в колонку; переименование таблицы или восстановление в другую схему могло оставить дефолт указывающим на неверное имя последовательности. - Никакой реальной защиты.
serialне мешает приложению вставить явный id и рассинхронизировать последовательность;GENERATED ALWAYSмешает.
IDENTITY — это SQL-стандарт, чище интроспектируется и рекомендуемый выбор для новых таблиц. serial выживает в основном в legacy-схемах.
STORED generated колонки: вычисление из других колонок
Колонка GENERATED ALWAYS AS (expr) STORED вычисляется из других колонок той же строки и физически хранится — ты её никогда не пишешь, и она остаётся согласованной автоматически. Идеально для денормализованного значения, которое хочешь индексировать или фильтровать без триггера:
CREATE TABLE products (
id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
price_cents bigint NOT NULL,
tax_cents bigint NOT NULL,
total_cents bigint GENERATED ALWAYS AS (price_cents + tax_cents) STORED
);
-- total_cents обновляет себя при любом изменении price или tax; можно индексировать.Выражение должно быть детерминированным и ссылаться только на текущую строку (никаких подзапросов, никакого now(), никаких других таблиц). STORED пишет значение на диск (стоит хранения и времени записи), но чтения бесплатны и оно индексируемо. Postgres сейчас поддерживает только STORED; колонки VIRTUAL (вычисление при чтении) пока нет.
Последовательности нетранзакционны — пропуски нормальны
На этом строится Hook. Последовательность — это общий счётчик, живущий вне транзакционных правил, чтобы тысячи конкурентных вставок не блокировали друг друга в ожидании следующего номера. Следствие: nextval не откатывается. Если транзакция захватила id 1003 и потом abort’нулась, 1003 потерян навсегда — следующая вставка получит 1004. Последовательности также предвыделяют блоки (CACHE n) на backend, так что краш или многосоединительная нагрузка разбрасывает id ещё сильнее.
Итак: id identity/serial — это уникальный, монотонно возрастающий суррогат, никогда не счётчик и никогда не безпропускный. Если тебе действительно нужны безпропускные, видимые человеку последовательные номера (номера счетов, юридические документы), ты строишь это отдельно строкой-сериализатором, которую UPDATE ... RETURNING под блокировкой, принимая конкуренцию. Не проси суррогатный ключ быть тем, чем он структурно не является.
Identity против uuid
Оба дают суррогатный ключ без выбора приложением; компромисс:
bigintidentity — 8 байт, монотонный, идеальная локальность индекса (дозапись к правому краю B-tree) и читаемый человеком. Но он утекает информацию (id 5000 говорит конкуренту число твоих строк и темп роста) и требует центральной базы для выдачи, что неудобно между шардами или для id, генерируемых на клиенте до вставки.uuid— 16 байт, глобально уникален без координации (генерируй где угодно, даже офлайн) и неугадываем (v4). Но случайный v4 фрагментирует индекс (из урока 01), и он вдвое шире — большие ключи значат большие индексы и более толстые внешние ключи всюду, где на них ссылаются.
Вместе эти два пункта значат: когда проектируешь колонку-ключ, ты одновременно принимаешь решение о физической структуре индекса, требованиях координации и утечке информации — а не просто выбираешь «уникальный id». Прагматичный дефолт: bigint identity для внутренних ключей; uuid (предпочтительно v7 для локальности, v4 когда важна неугадываемость), когда нужна распределённая генерация или нельзя утекать счётчики.
▸Почему это работает
Почему последовательности намеренно нетранзакционны, а не «починены» до безпропускности? Потому что безпропускность требовала бы сериализовать каждую вставку через единую блокировку счётчика — каждая транзакция ждёт коммита или abort’а предыдущей, прежде чем узнать свой id. На таблице с тысячами вставок в секунду это катастрофа пропускной способности. Пропуск — цена конкурентности, и это выгодная сделка: уникальный возрастающий ключ без конкуренции. Ошибка — вчитывать смысл («сколько заказов существует», «строки не удалялись») в значение, которое обещает лишь уникальность и грубый порядок.
Почему у первичного ключа bigint identity (или serial) появляются пропуски в значениях?
В чём главное преимущество GENERATED ALWAYS AS IDENTITY над старым serial?
Заполни пропуск: STORED generated колонка вычисляет своё значение из других колонок той же строки и пишет его на диск, так что колонка вроде total_cents остаётся согласованной _______ триггера.
- 01Почему предпочесть GENERATED ALWAYS AS IDENTITY serial?
- 02Почему у id identity/serial есть пропуски и что никогда нельзя предполагать?
- 03Identity против uuid — когда что побеждает и для чего STORED generated колонка?
Для суррогатных ключей GENERATED ALWAYS AS IDENTITY — это замена serial по SQL-стандарту: последовательность внутренняя, модель прав и владения чище, дампы и переименования надёжны, а ALWAYS блокирует приложение от вставки явного id, который рассинхронизировал бы счётчик (используй BY DEFAULT только когда надо подавать id). GENERATED ALWAYS AS (expr) STORED вычисляет детерминированное значение из той же строки, персистит его и позволяет индексировать — способ без триггера держать производную колонку согласованной. Структурная истина для усвоения: последовательности нетранзакционны, так что nextval никогда не откатывается, и id identity/serial несут постоянные пропуски by design — они гарантируют уникальность и грубый возрастающий порядок, никогда не счётчик и никогда не безпропускность (действительно безпропускные номера счетов строй отдельно сериализующим заблокированным счётчиком). И выбирай тип ключа по нужде: bigint identity для компактных, локальных, внутренних ключей; uuid (v7 для локальности индекса, v4 для неугадываемости), когда нужна генерация без координации или нельзя утекать число строк. Теперь, когда стейкхолдер попросит «сделать id безпропускными для аудиторов», ты знаешь, почему Postgres не может сделать это дёшево — и как выглядит настоящая, принимающая конкуренцию альтернатива.
Практика
Начни сверху. Задачи идут от простого к сложному: вспомнить факт, применить к случаю, затем senior-уровень. Открой, попробуй, потом открой ответ.
Что-то непонятно?
Задай вопрос по этому уроку. Вопросы анонимны и попадают напрямую автору — урок станет лучше.