Миграции PostgreSQL

SQL-миграции PostgreSQL: парные up/down-файлы, современные типы и ограничения, откат каждой операции, неблокирующие индексы, требования к мигратору.

Qu'est-ce que Миграции PostgreSQL ?

Этот навык предоставляет полный набор правил и шаблонов для работы с миграциями PostgreSQL. Он охватывает создание парных файлов up/down с единообразным форматом, использование современных типов данных (например, `bigint GENERATED ALWAYS AS IDENTITY`, `text` с `CHECK`, `timestamptz`, `jsonb`) и соответствующих ограничений, а также правильные стратегии отката каждой операции, включая добавление значений в enum. Особое внимание уделено созданию индексов на живых таблицах с помощью `CREATE INDEX CONCURRENTLY` для избежания блокировки записи, с подробным разбором возможных проблем — от необходимости отдельной миграции без транзакции до диагностики случая, когда «индекс создан, а план — Seq Scan». Навык также описывает критические требования к инструменту миграции: транзакционность, блокировка параллельного запуска, поддержка режима без транзакции для отдельных файлов. Поддерживаемые платформы: любые инструменты для управления миграциями PostgreSQL (например, Flyway, внутренние скрипты).

Compatible avec✓Claude Code~Codex CLI~Cursor
npx skills add https://github.com/scary-production/claude-plugins/tree/HEAD/plugins/go-postgres/skills/postgres-migrations

Installed? Explore more Recherche et analyse de données skills: affaan-m/uspto-database, affaan-m/scholar-evaluation, affaan-m/literature-review · View all 6 →

Demander à votre IA préférée

Ouvre une nouvelle conversation avec cette compétence d'agent déjà préchargée.

Documentation

Que fait Миграции PostgreSQL ?

Главное. Каждая миграция — пара up/down, и down полностью откатывает up. Миграция выполняется в своей транзакции. Исключение одно: индекс на таблице, где уже есть данные и идёт запись, — CREATE INDEX CONCURRENTLY в отдельной миграции без транзакции, одна команда на файл.

Проверка утверждений — CHECKS.md.

Формат файлов

migrations/
├── 20250524120000_create_users.up.sql
├── 20250524120000_create_users.down.sql
├── 20250525090000_add_users_phone.up.sql
└── 20250525090000_add_users_phone.down.sql
  • Префикс — UTC-время YYYYMMDDHHmmss, имя — краткое описание в snake_case.
  • *.up.sql применяет, *.down.sql откатывает. Оба файла обязательны.
  • Применённую миграцию не правь — пиши новую.

Типы и ограничения

ВместоПишиПочему
bigserialbigint GENERATED ALWAYS AS IDENTITYстандарт SQL; BY DEFAULT, если нужно вставлять свои id (импорт)
varchar(n)text NOT NULL CHECK (length(x) <= n)лимит — правило данных, менять его проще
timestamptimestamptzхранит момент времени, а не «часы на стене»
integer-флагboolean
jsonjsonbбинарный формат, индексы и операторы
uuid из расширенияuuid DEFAULT gen_random_uuid()встроена с PostgreSQL 13

Длину текстовой колонки ограничивай CHECK всегда, кроме заведомо длинного текста (описание, комментарий). Неочевидна максимальная длина — спроси пользователя.

ДанныеОграничение
emailCHECK (length(email) <= 320)
имяCHECK (length(name) <= 255)
телефонCHECK (length(phone) <= 50)
slug, кодCHECK (length(code) <= 100)
URLCHECK (length(url) <= 2048)
описаниебез ограничения или CHECK (length(bio) <= 10000)

Шаблон таблицы:

CREATE TABLE users (
    id         bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    name       text        NOT NULL CHECK (length(name) <= 255),
    email      text        NOT NULL CHECK (length(email) <= 320),
    is_active  boolean     NOT NULL DEFAULT true,
    metadata   jsonb,
    created_at timestamptz NOT NULL DEFAULT now(),
    updated_at timestamptz NOT NULL DEFAULT now()
);

CREATE UNIQUE INDEX ix_users_email ON users (email);

Откат

updown
CREATE TABLE tDROP TABLE IF EXISTS t
ALTER TABLE t ADD COLUMN cALTER TABLE t DROP COLUMN IF EXISTS c
CREATE INDEX ixDROP INDEX IF EXISTS ix
CREATE INDEX CONCURRENTLY ixDROP INDEX CONCURRENTLY IF EXISTS ix — тоже без транзакции
CREATE TYPE t AS ENUM (...)DROP TYPE IF EXISTS t
ALTER TYPE t ADD VALUE 'v'DROP VALUE в PostgreSQL нет — пересоздание типа или пустой down
ALTER TABLE t ALTER COLUMN c SET NOT NULLALTER TABLE t ALTER COLUMN c DROP NOT NULL
ALTER TABLE t ADD CONSTRAINT cALTER TABLE t DROP CONSTRAINT IF EXISTS c

IF EXISTS в down — чтобы откат был идемпотентным.

Типовые изменения

Колонка с дефолтом. С константным DEFAULT таблица не переписывается (PostgreSQL 11+), команда мгновенная. С volatile-дефолтом (gen_random_uuid(), clock_timestamp()) таблица переписывается целиком под эксклюзивной блокировкой — на большой живой таблице добавь колонку без дефолта и заполняй пачками.

ALTER TABLE users ADD COLUMN role text NOT NULL DEFAULT 'user' CHECK (length(role) <= 50);

Внешний ключ — индекс по ссылающейся колонке заводи сразу: PostgreSQL его сам не создаёт, а без него удаление родителя и join идут перебором.

CREATE TABLE orders (
    id      bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
    user_id bigint NOT NULL REFERENCES users (id),
    total   numeric NOT NULL
);
CREATE INDEX ix_orders_user_id ON orders (user_id);

Значение в enum. ALTER TYPE ... ADD VALUE работает в транзакции, но новое значение нельзя использовать до коммита (55P04 unsafe use of new value). Если мигратор выполняет миграцию в транзакции, добавляй значение и используй его (UPDATE, DEFAULT, CHECK) в разных миграциях.

ALTER TYPE payment_method ADD VALUE 'bank_transfer';

down: DROP VALUE нет. Значение безвредно — оставь down пустым с комментарием; нет — пересоздай тип (новый тип, ALTER COLUMN ... TYPE ... USING, удаление старого).

Индексы на живых таблицах

Таблица уже с данными и в неё пишут — индекс через CREATE INDEX CONCURRENTLY (CIC). Обычный CREATE INDEX блокирует запись на всё время сборки. Индекс вместе с новой таблицей в той же миграции — обычный CREATE INDEX. Размер базы не спасает: висящая транзакция ломает новый индекс при любом размере (ниже).

up:

CREATE INDEX CONCURRENTLY ix_orders_customer_id ON orders (customer_id);

down:

DROP INDEX CONCURRENTLY IF EXISTS ix_orders_customer_id;
  • Отдельная миграция без транзакции, одна команда в файле. В блоке транзакции CIC запрещён («cannot run inside a transaction block»). Несколько команд, отправленных одним запросом, PostgreSQL выполняет неявной транзакцией, и CIC падает с той же ошибкой.
  • Мигратор не умеет миграцию без транзакции — не клади CIC в обычную миграцию. Скажи пользователю, что нужен этот режим (раздел «Что требовать от мигратора»), или выполни CIC отдельно и запиши миграцию вручную.
  • CIC ждёт все транзакции со старым снимком, даже те, что читают другие таблицы. Миграция может идти долго.
  • После применения проверь индекс. CIC не транзакционный: при сбое остаётся INVALID-индекс, а сама миграция этого не покажет.
SELECT c.relname, i.indisvalid, i.indcheckxmin
FROM pg_index i JOIN pg_class c ON c.oid = i.indexrelid
WHERE c.relname = 'ix_orders_customer_id';
  • indisvalid = false — DROP INDEX CONCURRENTLY IF EXISTS и создай заново, либо REINDEX INDEX CONCURRENTLY.
  • Повтор упавшей CIC-миграции. Без IF NOT EXISTS она упадёт с «relation already exists» на INVALID-индексе. С IF NOT EXISTS — хуже: команда молча пропустит INVALID-индекс, и миграция «пройдёт». Поэтому IF NOT EXISTS в CIC не пиши: сначала убери невалидный индекс, потом повторяй.

Индекс есть, а план — Seq Scan

Индекс валиден, а планировщик его не берёт — проверь indcheckxmin (запрос выше). true: запросы не используют индекс, пока живут транзакции, начатые до его создания. Причина — UPDATE: в таблице остались цепочки HOT-версий, на которые индекс не ссылается. Одни INSERT такого не дают. Не пересоздавай индекс и не ищи причину в статистике. Найди старые транзакции:

SELECT pid, xact_start, backend_xmin, state, left(query, 60) AS query
FROM pg_stat_activity
WHERE backend_xmin IS NOT NULL
ORDER BY xact_start;

Дождись их завершения или прекрати (pg_terminate_backend(pid)) — только после согласования с пользователем. Воспроизвести перед релизом: в одной сессии открытая транзакция repeatable read с чтением таблицы, в другой — UPDATE строк и CREATE INDEX; indcheckxmin станет true.

Что требовать от мигратора

  • Каждая миграция — своя транзакция, вместе с записью в журнал применённых: упавшая откатывается целиком, уже применённые остаются.
  • Блокировка от параллельного запуска (например, advisory lock): реплики, стартовавшие вместе, не накатывают одно и то же дважды.
  • Режим без транзакции для отдельной миграции — пометка в файле (например, первая строка -- migrate:no-transaction). Такой файл отправляется одной командой, мимо транзакции. Цена: применение и запись в журнал уже не атомарны. Упал процесс между ними — повтор упадёт на «already exists»; проверь indisvalid, при валидном индексе запиши миграцию в журнал вручную.
  • Создание пары файлов без подключения к базе.

Individual skills in this repo

This repo contains 4 individual skills — each has its own dedicated page.

scary-production/claude-plugins

Работа с PostgreSQL в Go через pgxpool (pgx v5) и avito-tech/go-transaction-manager: пул и устойчивый старт, структуры с тегами db и CollectRows, репозиторий через DefaultTrOrDB, транзакции через Do, propagation и savepoint, повтор транзакции при 40001/40P01/55P03, защита от SQL-инъекций, классификация ошибок и ON CONFLICT, batch и CopyFrom. Конкурентная запись и блокировки — скилл postgres-concurrency. PostgreSQL in Go via pgxpool and go-transaction-manager: pool, repositories, transactions, retries, errors, bulk loads.

scary-production/claude-plugins

Конкурентная запись в PostgreSQL без потерянных обновлений и write skew: условие в UPDATE, SELECT FOR NO KEY UPDATE против FOR UPDATE, exclusion-ограничения против пересечения интервалов, serializable и повтор при 40001/40P01/55P03, очередь задач на SKIP LOCKED, lock_timeout и NOWAIT, advisory-блокировки, разбор зависаний. Примеры — Go (pgx и go-transaction-manager), правила — про сам PostgreSQL. PostgreSQL concurrency: lost updates, write skew, row locks, exclusion constraints, isolation and retries, SKIP LOCKED queues, advisory locks.

scary-production/claude-plugins

Как найти, что тормозит в PostgreSQL, и доказать, что правка помогла. Профиль по pg_stat_statements, сравнение вариантов по числу страниц (BUFFERS), а не по секундам, разбор плана от места, где оценка строк впервые разошлась с фактом. N+1 из Go-цикла сворачиваем в один запрос. Правим условия, FK, статистику и память, а не боремся с планировщиком. Postgres query tuning: profile first, compare by buffers, fix row estimates, remove N+1.

scary-production/claude-plugins

Показывает стили строки состояния на примере или включает один из них (full, compact, bars, minimal) в настройках пользователя или проекта.

Skills associés