Когда таблица переваливает за 100 миллионов строк, обычная миграция через ALTER TABLE превращается в план на выходные с даунтаймом. Если повезёт.
Почему transactional DDL критичен
PostgreSQL умеет выполнять DDL внутри транзакций — это transactional DDL. Alembic это поддерживает, но при партиционировании важно не забыть про
autocommit_block(). Без него некоторые операции (создание партиций) могут упасть вне транзакции, и сервер уйдёт в простой.Expand-contract стратегия
Вот как выглядит zero-downtime подход:
Фаза 1 (Expand) — создаём новую партиционированную таблицу параллельно старой. Не трогаем существующую схему.
Фаза 2 (Migrate) — организуем двойную запись. Апдейты пишутся и в старую, и в новую структуру. Через триггеры или приложение.
Фаза 3 (Contract) — переключаем чтение на новую схему. Удаляем старую таблицу, когда убедились, что всё работает.
В коде Alembic это выглядит примерно так:
def upgrade():
with op.get_context().autocommit_block():
op.execute("""
CREATE TABLE orders_new (LIKE orders INCLUDING ALL)
PARTITION BY RANGE (created_at)
""")
# Дальше — батчевая вставка данных с паузами
Главные грабли
* Deferred constraints. Если есть внешние ключи, их лучше отключать на время миграции, иначе потом не запихнете данные.
* Batch processing. Не пытайтесь перелить 100M строк одним INSERT. Делайте чанками по 1000 записей с
time.sleep(0.1). Иначе транзакция отожмёт всё.* Versioned migrations. Сохраняйте старые партиции хотя бы неделю после переключения. На проде всегда вылезает "ой, а мы забыли перенести поле".
Production-oriented пример
Создаём теневую таблицу, льём данные батчами, потом атомарно переименовываем:
def upgrade():
op.create_table('orders_shadow',
sa.Column('id', sa.Integer, primary_key=False),
postgresql_partition_by='RANGE (created_at)'
)
connection = op.get_bind()
total = connection.execute("SELECT count(*) FROM orders").scalar()
for offset in range(0, total, 1000):
connection.execute(f"""
INSERT INTO orders_shadow
SELECT * FROM orders
ORDER BY id LIMIT 1000 OFFSET {offset}
""")
time.sleep(0.1)
connection.execute("""
ALTER TABLE orders RENAME TO orders_old;
ALTER TABLE orders_shadow RENAME TO orders;
""")
Предостережения, которые обычно игнорируют
* Тестируйте на клоне прода. Не на тестовой базе с 10 строками.
* Используйте
--sql флаг Alembic, чтобы посмотреть, что он сгенерирует, и руками проверить план.* Мониторьте
log_statement и deadlocks. Партиционирование не спасает от блокировок при массовом INSERT.* Имейте rollback-скрипт для каждой фазы. Если что-то пошло на этапе переименования, откат должен быть за секунды.
Вывод: Партиционирование через transactional DDL — это не магия, а расчёт, который окупается, когда таблицы растут на 10% в месяц, иначе проще оставить как есть.