TGViewer
About Python [ru] About Python [ru] @python_tesst · 6.45K subscribers
Post #2821 330
⁣Zero-downtime миграции схем в SQLAlchemy 2.0: партиционирование и transactional DDL в Alembic

Когда таблица переваливает за 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% в месяц, иначе проще оставить как есть.
More from @python_tesst
  1. Sep 28, 2026Post #3191
  2. Sep 27, 2026OpenAI метит в подписку за 500 баксов Что там в описании тарифа? Пока что от ChatGPT Pro о…
  3. Sep 27, 2026Post #3189
  4. Sep 27, 2026Свежая обложка The Economist подъехала Журналисты: да мы вообще не сгущаем краски Те же жу…
  5. Sep 27, 2026Ночная годнота: Docker выкатила первые официальные скиллы для ИИ-агентов, которые ковыряют…
  6. Sep 27, 2026Post #3186
Threads Profile ViewerView any public Threads profile without an account.Open ThreadLook →Writing with AI? Make it sound human.Metric37 rewrites AI drafts so they read naturally. Free AI detector, 1,500 words free.Try Metric37 →