Zero-downtime міграція схеми БД: патерн Blue-Green
Найпоширеніша причина downtime під час деплою — зміна схеми БД. ALTER TABLE на таблиці в 200 млн рядків у PostgreSQL тримає AccessExclusiveLock кілька хвилин. Весь цей час запити на запис висять у черзі, connection pool забивається, застосунок віддає 500. Для високонавантаженого e-commerce такий простій може коштувати мільйони гривень. Ми спеціалізуємося на zero-downtime міграціях і використовуємо Expand-Contract — єдиний надійний патерн, перевірений на таблицях об'ємом понад 1 ТБ. Він дозволяє змінювати схему без зупинки застосунку. В e-commerce кожна хвилина простою коштує до $5 000, тому критично важливо уникнути блокувань.
Blue-green database migration — це не дві окремі бази даних із синхронізацією. У реальності це розширення схеми так, щоб одночасно працювали і стара, і нова версія застосунку, читаючи з однієї БД. Ми пропонуємо повний цикл: аудит, проєктування, реалізацію та тестування. Отримайте консультацію з zero-downtime міграції — зв'яжіться з нами для оцінки вашого проєкту.
Чому Expand-Contract — єдиний надійний патерн?
Фундаментальний принцип zero-downtime міграції схеми називається Expand-Contract (або Parallel Change). Він складається з трьох фаз, кожна з яких деплоїться окремо.
Фаза Expand — розширюємо схему, не ламаючи старий код:
-- Додаємо новий стовпець як nullable (!)
ALTER TABLE orders ADD COLUMN payment_method VARCHAR(64);
-- Або перейменування через додавання нового стовпця
ALTER TABLE users ADD COLUMN first_name VARCHAR(255);
ALTER TABLE users ADD COLUMN last_name VARCHAR(255);
-- Індекс створюємо CONCURRENTLY — без блокування таблиці
CREATE INDEX CONCURRENTLY idx_orders_payment_method ON orders(payment_method);Після цього деплоїться версія застосунку v2, яка пише і в старі, і в нові стовпці одночасно. Стара версія v1 продовжує працювати — вона не знає про нові стовпці, але це не проблема, оскільки вони nullable.
Фаза Migration — бекфіл даних у фоні:
-- Заповнюємо нові стовпці батчами, не блокуючи таблицю
DO $$
DECLARE
batch_size INT := 10000;
last_id BIGINT := 0;
max_id BIGINT;
BEGIN
SELECT MAX(id) INTO max_id FROM users;
WHILE last_id < max_id LOOP
UPDATE users
SET first_name = split_part(name, ' ', 1),
last_name = trim(substr(name, strpos(name, ' ')))
WHERE id > last_id AND id <= last_id + batch_size AND first_name IS NULL;
last_id := last_id + batch_size;
PERFORM pg_sleep(0.1); -- невелика пауза між батчами
END LOOP;
END $$;Фаза Contract — прибираємо старе після повного переходу:
-- Тільки після того, як v1 повністю виведена з експлуатації
ALTER TABLE users DROP COLUMN name;
ALTER TABLE orders ALTER COLUMN payment_method SET NOT NULL; Де це ламається: типові помилки та їх рішення
Найчастіша помилка — додати NOT NULL без DEFAULT у фазі Expand. PostgreSQL до версії 11 при цьому переписував всю таблицю. З PostgreSQL 11+ ADD COLUMN NOT NULL DEFAULT 'value' працює миттєво (значення зберігається в системному каталозі), але тільки якщо DEFAULT статичний, а не виклик функції.
Друга пастка — унікальні індекси. CREATE UNIQUE INDEX CONCURRENTLY може завершитися з помилкою duplicate key, якщо дані вже містять дублі. Тоді індекс залишається в стані INVALID — його треба дропнути і розібратися з даними перед тим, як створювати знову.
Третя проблема — зовнішні ключі. ALTER TABLE ADD CONSTRAINT FOREIGN KEY у PostgreSQL бере ShareRowExclusiveLock на обидві таблиці й повністю сканує дочірню таблицю. На великих таблицях це займе хвилини. Рішення — створювати FK з NOT VALID, потім валідувати окремо:
-- Крок 1: додати FK без перевірки існуючих рядків (швидко)
ALTER TABLE orders ADD CONSTRAINT fk_orders_users FOREIGN KEY (user_id) REFERENCES users(id) NOT VALID;
-- Крок 2: валідація у фоні (не блокує DML, тільки бере ShareUpdateExclusiveLock)
ALTER TABLE orders VALIDATE CONSTRAINT fk_orders_users; Як уникнути блокувань при додаванні зовнішніх ключів?
Використовуйте двохкроковий підхід: спочатку NOT VALID, потім окрема валідація. Це знижує час блокування з хвилин до мілісекунд. Валідація сканує таблицю, але не заважає звичайним операціям — тільки коротке блокування ShareUpdateExclusiveLock для оновлення словника.
Автоматизація перевірок у CI/CD
У pipeline перед деплоєм варто запускати статичний аналіз міграцій. Інструмент squawk (для PostgreSQL) перевіряє SQL-файли на небезпечні патерни:
---
# .github/workflows/db-migration-check.yml
- name: Check migrations safety
run: |
squawk db/migrations/*.sql \
--exclude=prefer-text-field \
--pg-version=14.0
Squawk впіймає ADD COLUMN NOT NULL без DEFAULT, DROP INDEX без CONCURRENTLY, ALTER TABLE RENAME та інші операції, що вимагають AccessExclusiveLock. Порівняно з ручним рев'ю, Squawk знаходить 90% проблем у 10 разів швидше.
Для автоматичного тестування самої міграції — підняти копію production БД (через pg_dump/restore або логічну реплікацію) і прогнати міграцію там перед застосуванням на prod.
Чек-лист для безпечної міграції
- Перевірити, що немає NOT NULL без DEFAULT у Expand
- Переконатися, що унікальні індекси створюються CONCURRENTLY і немає дублів
- Для FK використовувати NOT VALID + окрема валідація
- Запустити squawk перед деплоєм
- Протестувати на копії production
- Підготувати скрипти відкату
| Перевірка | Чим небезпечна | Як детектить squawk |
|---|---|---|
| ADD COLUMN NOT NULL без DEFAULT | Блокує таблицю на весь час додавання | ✅ |
| DROP INDEX без CONCURRENTLY | Блокує таблицю | ✅ |
| ALTER TABLE RENAME | Вимагає AccessExclusiveLock | ✅ |
| ADD FOREIGN KEY без NOT VALID | Сканує таблицю під ShareRowExclusiveLock | ⚠️ (вимагає додаткового аналізу) |
Що входить у нашу роботу
При замовленні blue-green міграції ми надаємо:
- Аудит поточної схеми — виявлення вузьких місць, залежностей, розмірів таблиць.
- План міграції — декомпозиція на фази Expand-Contract з часовими оцінками.
- Реалізація міграцій — написання SQL-скриптів, бекфіл-процедур, скриптів відкату.
- Тестування — прогін на staging з об'ємом, порівнянним з production.
- Документація — опис процесу, runbook для операторів, postmortem-шаблон.
- Підтримка після деплою — моніторинг помилок, спостереження за метриками бази даних.
Процес реалізації
| Етап | Зміст | Строк |
|---|---|---|
| Аудит схеми | Аналіз поточних таблиць, розмірів, типів змін | 1 день |
| Проєктування | Декомпозиція змін на фази Expand-Contract | 1-2 дні |
| Реалізація | Написання міграцій, бекфіл-скриптів | 2-5 днів |
| Тестування | Прогін на staging з production-об'ємами даних | 1-2 дні |
| Деплой | Поетапний rollout за фазами | За розкладом |
Для простих змін (додавання nullable стовпця, новий індекс) — 2-3 дні з тестуванням. Для складних переструктурувань таблиць із десятками мільйонів рядків — до 2-3 тижнів, включаючи бекфіл і моніторинг.
Оцінимо ваш проєкт за один робочий день. Напишіть нам — підберемо оптимальний план міграції з гарантією zero-downtime. Наш досвід — десятки успішних міграцій на PostgreSQL у highload-проєктах. Зв'яжіться з нами, щоб обговорити ваше завдання.
Згідно з документацією PostgreSQL, ALTER TABLE з NOT VALID мінімізує блокування (PostgreSQL Documentation).







