Последовательности, IDENTITY, генерируемые столбцы

Рейтинг: 45.3% · 9 голосов
Подробный курс по PostgreSQL: SQL по демобазе, типы данных и JSONB, индексы, транзакции и MVCC, VACUUM, EXPLAIN и планировщик, роли и привилегии, репликация, бэкап, настройка под нагрузку и под 1С. Актуально на 2026.
Ответить
Аватара пользователя
Egor_DBA
Сообщения: 47
Зарегистрирован: 11 май 2026, 05:31

Последовательности, IDENTITY, генерируемые столбцы

Сообщение Egor_DBA »

Оглавление курса (47)
  1. Что такое PostgreSQL: история, философия, где применяется
  2. Урок 1. Что нового в PostgreSQL 15 и далее (15 -> 16 -> 17)
  3. Установка и первый запуск: Linux, Windows, Docker
  4. psql детально: метакоманды и работа в консоли
  5. Демобаза Авиаперевозки Postgres Pro: структура и развёртывание
  6. SELECT: проекция, фильтрация, сортировка
  7. Транзакции и ACID
  8. Уровни изоляции транзакций: Read Committed, Repeatable Read, Serializable
  9. Соединения таблиц (JOIN)
  10. Агрегация: GROUP BY, HAVING, GROUPING SETS
  11. Подзапросы и CTE (WITH), рекурсия
  12. Оконные функции
  13. Изменение данных: INSERT/UPDATE/DELETE, UPSERT, MERGE
  14. Числовые и булевы типы, NULL и трёхзначная логика
  15. Строки, текст, дата и время, таймзоны
  16. Массивы, диапазоны, enum, составные типы
  17. JSON и JSONB: операторы и индексация
  18. DDL: CREATE TABLE, схемы, ALTER
  19. Ограничения целостности
  20. Представления и материализованные представления
  21. Последовательности, IDENTITY, генерируемые столбцы (вы здесь)
  22. Индексы: B-tree и когда индекс не используется
  23. Типы индексов: Hash, GiST, SP-GiST, GIN, BRIN
  24. Продвинутые индексы: частичные, по выражению, покрывающие
  25. Обслуживание индексов и таблиц: bloat, REINDEX, CONCURRENTLY
  26. Полнотекстовый поиск
  27. Роли и пользователи
  28. Привилегии: GRANT/REVOKE
  29. Row-Level Security и безопасность данных
  30. Подключение приложения: pg_hba.conf, SSL, пулы соединений
  31. Серверное программирование: функции и процедуры
  32. Триггеры и события
  33. Архитектура PostgreSQL: процессы и память
  34. MVCC: версии строк и видимость
  35. VACUUM, autovacuum, freeze и wraparound
  36. WAL, контрольные точки и долговечность
  37. Блокировки и взаимоблокировки
  38. Планировщик запросов
  39. EXPLAIN и EXPLAIN ANALYZE: чтение планов
  40. Конфигурация сервера: ключевые параметры
  41. Настройка под нагрузку и под 1С
  42. Параллельные запросы и партиционирование
  43. Резервное копирование и восстановление
  44. Репликация: потоковая и hot standby
  45. Логическая репликация и кластерные решения
  46. Внешние данные, расширения и сертификация
  47. Разбор планов запросов на explain.tensor.ru: глубокое чтение EXPLAIN ANALYZE
Урок 20. Последовательности, IDENTITY, генерируемые столбцы

Почти в каждой таблице есть столбец, который должен сам выдавать новые номера: id заказа, номер билета, ключ записи. Делать это руками опасно - два параллельных клиента легко возьмут одно и то же число. В postgresql за уникальные растущие номера отвечает отдельный объект - последовательность (sequence), а поверх неё стоят два слоя удобства: устаревший serial и стандартный GENERATED AS IDENTITY. Отдельная история - генерируемые столбцы, которые вычисляют своё значение из других полей. В этом уроке разберём, как устроен автоинкремент внутри, чем IDENTITY лучше serial и где у всего этого острые углы.

Изображение

Как это работает

Последовательность - это маленький служебный объект в базе, который хранит одно число (текущее состояние) и умеет атомарно его увеличивать. Когда вы зовёте nextval, сервер под блокировкой берёт текущее значение, прибавляет шаг и отдаёт результат. Ключевая мысль: выдача номера происходит вне транзакционных правил отката. Если вы взяли nextval, а потом откатили транзакцию, номер уже потрачен - он не вернётся в пул. Поэтому в id почти всегда есть дырки, и это нормально, а не баг.

Раздача номеров не блокирует параллельных клиентов надолго именно потому, что не подчиняется откату. Два сеанса, делающих INSERT одновременно, получат два разных значения и не будут ждать друг друга. За скорость отвечает кеш: параметр CACHE говорит, сколько номеров сеанс забирает себе наперёд одним движением, чтобы не дёргать общий объект на каждую вставку.

Теперь слои поверх. Старый способ - тип serial: это не настоящий тип, а сокращение. Postgres создаёт обычный столбец integer, отдельную последовательность и вешает значение по умолчанию nextval. Проблема в том, что связь между столбцом и последовательностью получается рыхлой: последовательность - самостоятельный объект, её можно случайно удалить, в столбец можно вписать любое число вручную мимо счётчика.

Современный и стандартный способ - GENERATED AS IDENTITY (появился в SQL-стандарте, в postgresql с версии 10). Здесь последовательность жёстко принадлежит столбцу, управляется через ALTER TABLE и не живёт своей жизнью. Есть два режима: ALWAYS запрещает вписывать значение руками (только через OVERRIDING), BY DEFAULT разрешает, ведя себя похоже на serial, но аккуратнее. На 2026 год для новых таблиц правильно использовать IDENTITY, а не serial.

Третья вещь - генерируемые столбцы GENERATED ALWAYS AS (выражение) STORED. Это не про автоинкремент, а про вычисляемое поле: его значение всегда выводится из других столбцов той же строки и физически хранится на диске (STORED). Вписать в него значение напрямую нельзя - база сама пересчитает при INSERT и UPDATE. Удобно для нормализованных копий: длина текста, цена с налогом, нижний регистр для поиска.

SQL и примеры

Создадим последовательность и посмотрим на её поведение вручную.

Код: Выделить всё

CREATE SEQUENCE demo_seq START 100 INCREMENT 1 CACHE 1;
SELECT nextval('demo_seq');  -- 100
SELECT nextval('demo_seq');  -- 101
SELECT currval('demo_seq');  -- 101, последнее значение В ЭТОМ сеансе
currval работает только после nextval в том же сеансе - иначе ошибка, потому что серверу неоткуда знать ваше последнее значение.

Сравним два способа автоинкремента. Так выглядит правильная современная таблица:

Код: Выделить всё

CREATE TABLE orders (
    order_id bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    created  timestamptz DEFAULT now()
);
INSERT INTO orders (created) VALUES (now());  -- id присвоится сам
-- попытка задать id руками упадёт:
-- INSERT INTO orders (order_id, created) VALUES (5, now());  -- ОШИБКА
-- продавить можно явно:
INSERT INTO orders (order_id, created)
OVERRIDING SYSTEM VALUE VALUES (5, now());
Генерируемый столбец на демобазе bookings. Добавим к таблице flights вычисляемое поле - запланированную длительность рейса в минутах:

Код: Выделить всё

ALTER TABLE bookings.flights
ADD COLUMN duration_min integer
GENERATED ALWAYS AS (
    EXTRACT(EPOCH FROM (scheduled_arrival - scheduled_departure)) / 60
) STORED;

SELECT flight_no, scheduled_departure, duration_min
FROM bookings.flights
ORDER BY duration_min DESC
LIMIT 5;
Поле duration_min всегда согласовано с временами рейса: при изменении расписания оно пересчитается само, забыть обновить его нельзя.

Частая практическая задача - починить счётчик после ручной загрузки данных. Если вы залили строки в orders напрямую (через COPY или миграцию из 1С) с готовыми id, внутренняя последовательность об этом не знает и начнёт выдавать номера с единицы, ловя дубликаты. Сдвигаем её на максимум:

Код: Выделить всё

SELECT setval(
    pg_get_serial_sequence('orders', 'order_id'),
    (SELECT max(order_id) FROM orders)
);
pg_get_serial_sequence возвращает имя последовательности, привязанной к столбцу, и для serial, и для IDENTITY - не нужно помнить его руками.

Частые грабли
  • Ожидание, что id будут без пропусков. Откаты и кеш гарантированно делают дырки. Если нужна сплошная нумерация документов - это отдельная бизнес-логика со своей таблицей-счётчиком, а не sequence.
  • Ручная вставка в serial-столбец мимо последовательности. Потом первый же автоинкремент даёт дубликат и нарушение первичного ключа. С IDENTITY ALWAYS этой ловушки нет - база не пустит.
  • Перенос данных из 1С или дампа без setval. Строки легли, а счётчик остался на старте - и новые INSERT падают. Всегда сдвигайте последовательность после массовой заливки.
  • currval без nextval в сеансе - ошибка, а не ноль. И currval/lastval не видят значений из других сеансов, не используйте их для поиска чужих id.
  • bigint против integer. integer кончается на 2.1 млрд. Для растущих таблиц сразу берите bigint - переделка типа первичного ключа под нагрузкой это боль.
  • Попытка записать в GENERATED ALWAYS ... STORED столбец или построить его на изменчивой функции вроде now(). Разрешены только IMMUTABLE-выражения от столбцов той же строки.
  • DEFAULT nextval не копируется при CREATE TABLE ... LIKE без INCLUDING DEFAULTS, а владение последовательностью так вообще не переносится - легко получить таблицу без автоинкремента.
Мини-лаба
  • Создайте таблицу products с ключом prod_id bigint GENERATED ALWAYS AS IDENTITY и столбцами name text, price numeric.
  • Вставьте 3 строки без указания prod_id, убедитесь, что номера выдались сами.
  • Добавьте генерируемый столбец price_vat numeric GENERATED ALWAYS AS (price * 1.20) STORED и проверьте значения.
  • Попробуйте сделать INSERT с явным prod_id - получите ошибку, затем повторите с OVERRIDING SYSTEM VALUE.
  • Залейте строку с заведомо большим prod_id через OVERRIDING, после чего вставьте обычную строку и поймайте конфликт ключа.
  • Почините счётчик через setval и pg_get_serial_sequence, повторите вставку - теперь без ошибки.
  • Через psql выполните \d products и найдите в описании привязанную последовательность и определение генерируемого столбца.
Контрольные вопросы
  • Почему в столбце с автоинкрементом появляются пропуски и почему это не считается ошибкой?
  • Чем GENERATED AS IDENTITY надёжнее serial и в чём разница между режимами ALWAYS и BY DEFAULT?
  • Что вернёт currval, если в текущем сеансе ещё ни разу не вызывали nextval по этой последовательности?
  • Зачем после загрузки данных из 1С или дампа вызывать setval и как узнать имя нужной последовательности?
  • Можно ли в генерируемом столбце STORED использовать now() или обращаться к другой таблице, и почему?
  • В каких случаях для первичного ключа стоит сразу брать bigint, а не integer?
👍3 ❤️3 🔥2 😄 🤔
Аватара пользователя
charon
Сообщения: 1
Зарегистрирован: 22 май 2026, 13:38

Re: Последовательности, IDENTITY, генерируемые столбцы

Сообщение charon »

Перетащил базу из 1С через дамп, и новые INSERT падали с дубликатом ключа - оказалось не сделал setval. Спасибо за pg_get_serial_sequence, теперь понятно как имя seq доставать.
👍1 ❤️2 🔥 😄 🤔
Аватара пользователя
gall
Сообщения: 2
Зарегистрирован: 12 май 2026, 22:34

Re: Последовательности, IDENTITY, генерируемые столбцы

Сообщение gall »

А правда что в PG 17 STORED-столбцы стало можно в логической репликации публиковать? Раньше вроде только обычные колонки уезжали.
👍1 ❤️1 🔥1 😄 🤔
Ответить
← Предыдущая глава
Представления и материализованные представления
Следующая глава →
Индексы: B-tree и когда индекс не используется

Все главы курса «PostgreSQL: от первого запроса до продакшена»

Поделиться темой: ✈ Telegram VK

Вернуться в «PostgreSQL: от первого запроса до продакшена»

Кто сейчас на конференции

Сейчас этот форум просматривают: нет зарегистрированных пользователей и 1 гость