PostgreSQL CookbookСхемаALTER TABLE: мышление миграций
0 / 63 (0%)

ALTER TABLE: мышление миграций

Brew выкатил миграцию в разгар рабочего дня. Строчка выглядела безобидно: ALTER TABLE orders ALTER COLUMN amount TYPE numeric(12,2). На таблице в десятки миллионов строк эта команда взяла ACCESS EXCLUSIVE-блокировку и принялась переписывать всю таблицу заново — минут на восемь. Всё это время ни один заказ нельзя было ни прочитать, ни записать: приложение упёрлось в таймауты, касса вставала. А соседняя миграция в том же релизе — ADD COLUMN ... DEFAULT 'active' — отработала мгновенно и никого не заметила. Разница не в синтаксисе: одни ALTER трогают только метаданные, другие переписывают таблицу целиком под блокировкой.

Цель юнита — не «список безопасных ALTER наизусть», а рефлекс: перед миграцией спросить «это мгновенно или это переписывание/долгая блокировка?». Наблюдать цену будем через relfilenode — идентификатор физического файла таблицы. Он меняется только при полном переписывании; если ALTER лишь поправил метаданные, файл прежний. Плюс разберём двухфазное добавление констрейнта (NOT VALIDVALIDATE), которое отделяет быструю смену метаданных от долгого сканирования.

Мгновенно против переписывания

Часть ALTER — это правка системного каталога, без касания данных на диске. Классика — ADD COLUMN с константным DEFAULT: с PG11 новое значение хранится как метаданные таблицы, существующие строки не трогаются (relfilenode тот же). Мгновенно, независимо от размера таблицы.

Другая часть требует переписать каждую строку, потому что меняется физическое представление данных. ALTER COLUMN ... TYPE, меняющий тип (intbigint, textnumeric), — почти всегда переписывание: Postgres создаёт новый файл, перегоняет туда все строки в новом формате и подменяет relfilenode. На большой таблице это долгая ACCESS EXCLUSIVE-блокировка — то самое, что положило кассу Brew.

⚠️ Важная оговорка: даже «мгновенный» ADD COLUMN на миг берёт ACCESS EXCLUSIVE. Если в этот момент по таблице идёт долгий запрос, миграция встанет в очередь за ним — а за миграцией встанут все остальные. Поэтому в проде миграции пускают с lock_timeout (см. «Заборчик»).

Двухфазный констрейнт: NOT VALID → VALIDATE

Добавить CHECK (или FOREIGN KEY) обычным способом — это просканировать всю таблицу, проверяя старые строки, под сильной блокировкой. PG умеет разбить это на две фазы:

  1. ADD CONSTRAINT ... NOT VALID — мгновенно: констрейнт заводится и применяется к новым строкам, но старые не сканируются (convalidated = f). Короткая блокировка.
  2. VALIDATE CONSTRAINT — отдельной командой досканировать старые строки. Берёт лишь SHARE UPDATE EXCLUSIVEне блокирует чтение и запись, идёт в фоне.

Так «добавить правило» перестаёт быть стоп-краном: горячая фаза — мгновенная, а долгая проверка не держит приложение.

Очередь блокировок и цена ALTER

Главная ловушка миграции не в «переписывании», а в очереди блокировок: даже мгновенный ALTER на миг берёт ACCESS EXCLUSIVE, и если перед ним висит долгая транзакция — встаёт он, а за ним весь трафик:

plaintext
время →
T1  долгий SELECT по orders   ╞════════ держит ACCESS SHARE ════════╡
T2  миграция: ALTER ADD COLUMN      ╞···· ждёт ACCESS EXCLUSIVE ····╡══╡
T3  обычный INSERT в orders               ╞······ стоит за миграцией ······╡

                      даже «мгновенный» ALTER встал в очередь за T1 —
                      и весь поток записи (T3) встал уже за ALTER

А вот как соотносятся сами операции по цене:

ALTERЧто происходитЦена
ADD COLUMN ... DEFAULT <константа>правка метаданных (с PG11)мгновенно
ADD CONSTRAINT ... NOT VALIDметаданные; старые строки не сканируютсямгновенно (короткая блокировка)
VALIDATE CONSTRAINTдосканирует старые строкифоном, SHARE UPDATE EXCLUSIVE (не блокирует запись)
ALTER COLUMN ... TYPE (смена представления)переписывает каждую строкудолгая ACCESS EXCLUSIVE
ADD COLUMN ... DEFAULT now() (волатильный)переписывает таблицудолгая ACCESS EXCLUSIVE
ADD COLUMN ... NOT NULL (без валидного CHECK)сканирует таблицу целикомблокировка на время скана

Что показывает наш код

Урок — в demo.sql на лабораторном столе с 1000 строк (базовые таблицы не трогаем). Перед каждым ALTER снимаем relfilenode, после — сравниваем:

sql
SELECT pg_relation_filenode('alter_lab') AS fn \gset before1_
ALTER TABLE alter_lab ADD COLUMN status TEXT NOT NULL DEFAULT 'active';   -- метаданные
SELECT pg_relation_filenode('alter_lab') AS fn \gset after1_
SELECT (:before1_fn = :after1_fn) AS filenode_unchanged;                  -- t
 
ALTER TABLE alter_lab ALTER COLUMN n TYPE bigint;                         -- переписывание
-- ... сравнение → f
 
ALTER TABLE alter_lab ADD CONSTRAINT n_positive CHECK (n > 0) NOT VALID;  -- convalidated = f
ALTER TABLE alter_lab VALIDATE CONSTRAINT n_positive;                     -- convalidated = t

filenode_unchanged = t означает «тот же файл, таблица не переписана»; f — «новый файл, было переписывание». convalidated показывает фазу констрейнта.

Запуск

sh
docker compose up -d
make lecture L=02-schema-and-constraints/02-06-alter-table-migration-mindset T=db-reset
make lecture L=02-schema-and-constraints/02-06-alter-table-migration-mindset

Вывод:

plaintext
== 1) ADD COLUMN с константным DEFAULT — мгновенно (только метаданные) ==
 filenode_unchanged 
--------------------
 t
 
 
== 2) ALTER COLUMN ... TYPE int -> bigint — таблица ПЕРЕПИСана (новый relfilenode) ==
 filenode_unchanged 
--------------------
 f
 
 
== 3) ADD CONSTRAINT CHECK ... NOT VALID — мгновенно (старые строки не сканируются) ==
 validated_after_not_valid 
---------------------------
 f
 
 
== 4) VALIDATE CONSTRAINT — отдельный шаг (не блокирует запись) ==
 validated_after_validate 
--------------------------
 t

ADD COLUMN ... DEFAULT 'active' оставил relfilenode прежним (t) — мгновенная смена метаданных. ALTER COLUMN n TYPE bigint файл сменил (f) — таблицу переписали целиком (на проде это и есть та самая блокировка). CHECK ... NOT VALID завёлся непроверенным (convalidated = f), а VALIDATE отдельным шагом досканировал и пометил его проверенным (t) — горячая часть мгновенна, долгая не держит запись.

Заборчик

Что мы упростили: relfilenode — хороший индикатор «переписали / нет», но не вся картина, и дальше начинается территория, которую держит твой DBA:

  • Блокировка важнее переписывания. Даже мгновенный ADD COLUMN берёт ACCESS EXCLUSIVE, и если перед ним висит долгая транзакция, очередь блокировок положит запись на ровном месте — поэтому миграции пускают с коротким lock_timeout и ретраями, а не «насухо».
  • Большое переписывание делают не в лоб. ALTER TYPE на горячей таблице в проде разбивают на шаги: добавляют новую колонку, бэкфилят данные пачками в фоне, потом атомарно подменяют — либо используют онлайн-инструменты (pg_repack, оркестраторы вроде Reshape).
  • Нюансы версий. ADD COLUMN с волатильным дефолтом (now(), функция) уже переписывает; добавление NOT NULL исторически сканирует таблицу (PG12 умеет пропустить скан, если есть валидный CHECK (col IS NOT NULL)).

Граница курса: оркестрация zero-downtime-миграций — это уже ближе к DBA/DevOps; твоя задача — узнавать опасный ALTER в ревью миграции и не катить переписывание горячей таблицы в рабочее время.

Что забрать с собой

  • relfilenode меняется только при полном переписывании таблицы — это наблюдаемый индикатор «мгновенно vs переписывание».
  • ADD COLUMN с константным DEFAULT — мгновенно (метаданные); ALTER COLUMN ... TYPE со сменой представления — переписывание под ACCESS EXCLUSIVE.
  • ADD CONSTRAINT ... NOT VALID (мгновенно) + VALIDATE CONSTRAINT (фоном, SHARE UPDATE EXCLUSIVE) — двухфазное добавление правила без стоп-крана.
  • Даже мгновенный ALTER берёт ACCESS EXCLUSIVE на миг → в проде нужен lock_timeout; большое переписывание делают через new-column + batched backfill + swap.
  • Рефлекс перед миграцией: «это правит метаданные или переписывает таблицу/держит долгую блокировку?».

На этом модуль 02 «Схема, DDL и ограничения» закрыт: IDENTITY/DEFAULT, NOT NULL/PK и выбор ключа, внешние ключи, UNIQUE/CHECK, генерируемые столбцы/домены и мышление миграций. Дальше — модуль 03 «CRUD-беглость»: уверенные INSERT ... RETURNING, SELECT с пагинацией, безопасные UPDATE/DELETE, upsert и трезвая семантика NULL.

·Модуль 03

Этот урок ещё впереди

Курс изучается по порядку — чтобы открыть этот шаг, сначала завершите предыдущие. Так контекст накапливается без пропусков.

/ вы пытались открыть
Схема / ALTER TABLE: мышление миграций