The post has been translated automatically. Original language: Russian
Adding a column to a table on a local database is a matter of seconds. On a table with hundreds of millions of rows in the product, the same operation can lock the table for writing for a long time or completely shut down the service — and this discrepancy in expectations most often causes nighttime incidents after a "harmless" release.
The problem is that many DDL operations in relational databases require locking the table: adding a column with a default value, changing the type of an existing column, adding a NOT NULL constraint - depending on the specific DBMS and version, these operations may require a complete rewrite of the table under lock, during which normal read and write requests simply They get in line.
The general principle of safe migration is to break down one risky change into several small and reversible steps, each of which is individually safe. A classic example is adding a mandatory column: instead of a single step "add NOT NULL column", the operation is divided into adding a nullable column, filling in the values gradually, in separate batches, without long locks, and only after that — adding the NOT NULL constraint when all the rows are already filled.
A similar logic applies when renaming or changing the column type: instead of a direct change, a new column is added, the application temporarily writes to both at the same time, the data is transferred to the background, and only when all readers are switched to the new column, the old one is deleted in a separate, final step. This requires coordination between the release of the application and the state of the database schema: the code must be able to work simultaneously with the old and new data structures during the transition period, and not switch simultaneously.
A separate risk category is migrations that start automatically when the application is deployed. If such a migration holds the lock for a long time, and other connections try to get up to it in parallel, the cascade lock effect occurs: a growing backlog of requests that are fast in themselves, but waiting in line for one long operation.
A practical tip is to test potentially heavy migrations on copies of a comparable—sized sales database before release, and not just on a small test database where the problem simply does not have time to manifest itself. What works instantly on a test table with thousands of rows can take hours on a table with hundreds of millions of rows and block the service for the entire time.
Добавить столбец в таблицу на локальной базе — секундное дело. На таблице с сотнями миллионов строк в проде та же самая на вид операция может заблокировать таблицу на запись на длительное время или вовсе положить сервис — и вот это несоответствие ожиданий чаще всего и становится причиной ночных инцидентов после «безобидного» релиза.
Проблема в том, что многие DDL-операции в реляционных БД требуют блокировки таблицы: добавление столбца со значением по умолчанию, изменение типа существующего столбца, добавление ограничения NOT NULL — в зависимости от конкретной СУБД и версии эти операции могут потребовать полной перезаписи таблицы под блокировкой, во время которой обычные запросы на чтение и запись просто встают в очередь.
Общий принцип безопасной миграции — разбивать одно рискованное изменение на несколько маленьких и обратимых шагов, каждый из которых по отдельности безопасен. Классический пример — добавление обязательного столбца: вместо одного шага «добавить NOT NULL столбец» операция делится на добавление nullable столбца, заполнение значений постепенно, отдельными пакетами, без долгих блокировок, и только после этого — добавление ограничения NOT NULL, когда все строки уже заполнены.
Похожая логика применяется и при переименовании или изменении типа столбца: вместо прямого изменения — добавляется новый столбец, приложение временно пишет в оба одновременно, данные переносятся в фоне, и только когда все читатели переключены на новый столбец, старый удаляется отдельным, последним шагом. Это требует координации между релизом приложения и состоянием схемы БД: код должен уметь работать одновременно со старой и новой структурой данных на протяжении переходного периода, а не переключаться одномоментно.
Отдельная категория риска — миграции, которые запускаются автоматически при деплое приложения. Если такая миграция долго держит блокировку, а параллельно на неё пытаются встать другие соединения, возникает эффект каскадной блокировки: growing backlog запросов, которые сами по себе быстрые, но ждут в очереди за одной долгой операцией.
Практический совет — тестировать потенциально тяжёлые миграции на копии продовой базы сопоставимого размера до релиза, а не только на маленькой тестовой базе, где проблема просто не успевает проявиться. То, что на тестовой таблице из тысячи строк отрабатывает мгновенно, на таблице с сотнями миллионов строк может занять часы и заблокировать сервис на всё это время.