awesome-everything EN
↑ Обратно к восхождению

Базы данных

Безопасные DDL-паттерны: NOT VALID, CONCURRENTLY и исправления небезопасных операций

Суть Для каждой небезопасной DDL-операции существует безопасный многошаговый эквивалент; дисциплина — знать это соответствие и всегда использовать безопасную форму в продакшне.
Высота — путь к senior
НольJuniorMiddleSenior
Ты на middle-высоте — в небе
◷ 14 min

Команда запускает CREATE INDEX ON orders(user_id) в 10:00. На таблице с 200 млн строк это удерживает ACCESS EXCLUSIVE 20 минут — чтения и записи заблокированы. Тот же индекс, построенный через CREATE INDEX CONCURRENTLY, занимает 45 минут, но ничего не блокирует. Один и тот же индекс, одни данные, кардинально разный эффект на продакшн.

CREATE INDEX CONCURRENTLY

Обычный CREATE INDEX удерживает ACCESS EXCLUSIVE всё время построения. На больших таблицах это блокирует чтения и записи на минуты.

CREATE INDEX CONCURRENTLY строит индекс в два этапа, оба под SHARE UPDATE EXCLUSIVE (не блокирует DML):

  1. Этап 1: сканирует таблицу и строит черновой индекс, пока продолжается обычный трафик.
  2. Этап 2: нагоняет записи, произошедшие во время этапа 1; помечает индекс валидным.

Компромиссы:

  • Занимает в 2–3 раза дольше, чем не-конкурентный.
  • Не может выполняться внутри блока транзакции — если инструмент миграции оборачивает миграции в BEGIN..COMMIT, нужно пометить эту миграцию как нетранзакционную.
  • Может дать сбой в середине построения и оставить INVALID-индекс (виден в pg_indexes где indisvalid = false).

Продакшн-инструкция для конкурентного индекса:

  1. Всегда используйте CREATE INDEX CONCURRENTLY в продакшне.
  2. После миграции проверьте: SELECT indexname, indisvalid FROM pg_indexes JOIN pg_class ... WHERE indisvalid = false — должно вернуть ничего.
  3. Если найден INVALID-индекс: DROP INDEX CONCURRENTLY (тоже неблокирующий), затем повторите.
ОперацияБлокировкаБлокирует DMLВ транзакции?
CREATE INDEXACCESS EXCLUSIVEДаДа
CREATE INDEX CONCURRENTLYSHARE UPDATE EXCLUSIVEНетНет — должен выполняться вне BEGIN
DROP INDEX CONCURRENTLYSHARE UPDATE EXCLUSIVEНетНет

ADD CONSTRAINT NOT VALID + VALIDATE CONSTRAINT

Обычное добавление CHECK или FOREIGN KEY constraint получает ACCESS EXCLUSIVE и сканирует всю таблицу.

Двухшаговый паттерн:

-- Шаг 1: добавить constraint, применять только для НОВЫХ строк (мгновенно, краткий ACCESS EXCLUSIVE)
ALTER TABLE orders
  ADD CONSTRAINT orders_status_valid
  CHECK (status IN ('pending', 'active', 'closed')) NOT VALID;

-- Шаг 2: проверить существующие строки (SHARE UPDATE EXCLUSIVE — не блокирует DML)
ALTER TABLE orders VALIDATE CONSTRAINT orders_status_valid;

NOT VALID означает: применять для новых строк с этого момента; существующие строки не проверяются. VALIDATE CONSTRAINT затем сканирует все строки под SHARE UPDATE EXCLUSIVE, что не блокирует INSERT, UPDATE или DELETE. Сканирование может выполняться при живом трафике.

Этот паттерн применяется к:

  • CHECK-ограничениям
  • FOREIGN KEY-ограничениям: ADD CONSTRAINT ... FOREIGN KEY ... NOT VALID + VALIDATE CONSTRAINT
  • NOT NULL через CHECK (col IS NOT NULL) NOT VALID (см. урок 02)

Небезопасные операции и их безопасные эквиваленты

Небезопасный DDL → безопасная замена
ADD COLUMN с волатильным DEFAULT → ADD без default + бэкфил + SET DEFAULT
Исправление
ALTER COLUMN TYPE (не бинарно-совместимый) → новая колонка + dual-write + бэкфил + замена + дроп
Исправление
ADD NOT NULL на существующей nullable → NOT VALID + VALIDATE + SET NOT NULL
Исправление
ADD FOREIGN KEY → ADD CONSTRAINT NOT VALID + VALIDATE CONSTRAINT
Исправление
CREATE INDEX → CREATE INDEX CONCURRENTLY
Исправление
DROP COLUMN (код ещё читает) → сначала деплой кода, дренирование, затем DROP
Исправление
RENAME COLUMN → полный expand-contract за несколько дней (урок 05)
Исправление

ALTER COLUMN TYPE: исключение для бинарно-совместимых типов

Большинство изменений типа перезаписывают таблицу. Исключения — бинарно-совместимые типы с одинаковым физическим представлением:

  • varchar(50)varchar(100) (шире, те же байты)
  • varchartext (эквивалентное хранение)
  • textvarchar (то же)

Postgres обнаруживает их и пропускает перезапись — краткий ACCESS EXCLUSIVE, строки не пишутся. Не-совместимые изменения (intbigint, textint, jsonjsonb) требуют либо полной перезаписи, либо паттерна expand-contract через теневую колонку (урок 05).

Почему это работает

Почему VALIDATE CONSTRAINT использует SHARE UPDATE EXCLUSIVE вместо ACCESS EXCLUSIVE? VALIDATE доступен только для чтения — он сканирует строки, чтобы убедиться, что они проходят constraint, но не изменяет ни одну строку. Только DDL-операции, изменяющие структуру таблицы или перезаписывающие строки, нуждаются в ACCESS EXCLUSIVE. Проверка только для чтения нуждается лишь в достаточной изоляции для предотвращения одновременного DDL, меняющего определение constraint в середине сканирования.

Викторина

CREATE INDEX CONCURRENTLY даёт сбой на полпути на таблице с 300 млн строк. В каком состоянии находится индекс и каково правильное исправление?

Викторина

Какой способ правильно добавить CHECK constraint на таблицу с 100 млн строк без блокировки записей?

Расставь шаги по порядку

Упорядочите исправления небезопасного DDL от наиболее частых в продакшне до наименее:

  1. 1 ADD COLUMN с волатильным DEFAULT → ADD COLUMN без default + бэкфил пакетами + SET DEFAULT
  2. 2 ALTER COLUMN TYPE (не-совместимый) → новая колонка + dual-write + бэкфил + замена чтений + дроп старой
  3. 3 ADD NOT NULL → ADD CHECK NOT VALID + VALIDATE + SET NOT NULL + DROP CHECK
  4. 4 ADD FOREIGN KEY → ADD CONSTRAINT NOT VALID + VALIDATE CONSTRAINT
  5. 5 CREATE INDEX → CREATE INDEX CONCURRENTLY (+ проверить indisvalid после)
  6. 6 DROP COLUMN (код ещё читает) → сначала деплой кода, чтобы прекратить чтение, дренировать старые поды, затем DROP
  7. 7 RENAME COLUMN → полный expand-contract за несколько дней (урок 05)
Вспомните перед уходом
  1. 01
    Каковы два режима сбоя CREATE INDEX CONCURRENTLY и как обрабатывать каждый?
  2. 02
    Почему CREATE INDEX CONCURRENTLY должен выполняться вне блока транзакции и как инструменты миграции это обрабатывают?
  3. 03
    Объясните разницу между ADD CONSTRAINT и ADD CONSTRAINT NOT VALID и почему VALIDATE CONSTRAINT не блокирует DML.
Итог

Для каждого небезопасного DDL существует продакшн-безопасный эквивалент. CREATE INDEX CONCURRENTLY берёт SHARE UPDATE EXCLUSIVE вместо ACCESS EXCLUSIVE, оставляя DML незаблокированным во время построения — но не может выполняться в транзакции и оставляет INVALID-индексы при сбое (исправление: DROP INDEX CONCURRENTLY + повтор). ADD CONSTRAINT ... NOT VALID мгновенно применяет constraint для новых строк без сканирования существующих; VALIDATE CONSTRAINT затем сканирует под SHARE UPDATE EXCLUSIVE, блокируя только параллельный DDL, но не DML. Бинарно-совместимые изменения типа (varchar(50)varchar(100)) пропускают перезапись; не-совместимые требуют паттерна expand-contract через теневую колонку. Линтер Squawk обнаруживает небезопасные формы в CI, чтобы команды замечали их до слияния.

Связанные уроки
встречается в258
Продолжить восхождение ↑Expand-contract: нулевой простой для ломающих изменений схемы
хоткеи развернуть
поиск
K
пред. пьеса
k
след. пьеса
j
тиры
t
это меню
?
sources3
expand
  1. 01
  2. 02
  3. 03

Trademarks belong to their respective owners. Editorial reference only.