Zero-downtime міграція схеми БД: патерн Expand-Contract

Зміна схеми бази даних часто призводить до простоїв і блокувань, що критично для бізнесу. Ми застосовуємо перевірений патерн Expand-Contract, щоб провести міграцію без зупинки роботи застосунку. Наша команда реалізує проєкт під ключ — від аудиту до підтримки, забезпечуючи надійність і безпеку змін.
Часті запитання

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 міграції ми надаємо:

  1. Аудит поточної схеми — виявлення вузьких місць, залежностей, розмірів таблиць.
  2. План міграції — декомпозиція на фази Expand-Contract з часовими оцінками.
  3. Реалізація міграцій — написання SQL-скриптів, бекфіл-процедур, скриптів відкату.
  4. Тестування — прогін на staging з об'ємом, порівнянним з production.
  5. Документація — опис процесу, runbook для операторів, postmortem-шаблон.
  6. Підтримка після деплою — моніторинг помилок, спостереження за метриками бази даних.

Процес реалізації

Етап Зміст Строк
Аудит схеми Аналіз поточних таблиць, розмірів, типів змін 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).

Наші компетенції:

Останні роботи

  • Розробка сайту компанії B2B ADVANCE
    Розробка сайту компанії B2B ADVANCE
    1542
  • Розробка веб-додатків для компанії FEEDME
    Розробка веб-додатків для компанії FEEDME
    1394
  • Розробка веб-сайту для компанії БЕЛФІНГРУП
    Розробка веб-сайту для компанії БЕЛФІНГРУП
    1081
  • Розробка інтернет магазину для компанії FURNORO
    Розробка інтернет магазину для компанії FURNORO
    1369
  • Розробка логотипу компанії B2B Advance
    Розробка логотипу компанії B2B Advance
    816
  • Розробка веб-додатків для компанії Enviok
    Розробка веб-додатків для компанії Enviok
    1142