Forty million rows, three application instances behind a balancer, one migration that renames a column. The deploy script runs migrations first, then rolls the code instance by instance. For a few minutes old code was writing to a column that no longer existed. The error rate graph looked like a wall. I had approved that migration. It was one line.
Migration tools trained us to think a schema change is a code change. Write a file, run it, done. A schema change on a big table takes time, takes locks, and while it runs the previous version of the application is still serving traffic. Both versions must survive both schemas. That is the whole discipline, and it has a name: expand and contract.
Expand first. Add the new column, nullable, no default that rewrites the table. Cheap. Old code ignores the new column and nothing breaks.
Then deploy code that writes both columns and reads the old one. Then backfill history in batches, a few thousand rows per query, with pauses between them. One big UPDATE is the same outage as one big ALTER in different clothes.
Then switch reads to the new column. Watch it for a day.
Contract last. Dropping the old column is a separate deploy, days later, when you are sure nothing reads it. A rename as a single operation does not exist in this world. A rename is add plus drop, with a week between.
Yes, this turns one line of DDL into four deploys. That is the price. pt-online-schema-change helps with locking, and MySQL 5.7 does online DDL better than 5.5 did, but no tool closes the compatibility window. Only ordering does.
Rehearse on a copy of production data. Staging with a thousand rows tells you nothing about locks. The ALTER that is instant there runs forty minutes on the real table, and you want to learn that number on a Tuesday afternoon.
A migration is not a file. It is a sequence of deploys.