Safe Database Migrations in Production
Cover photo by Tyler on Unsplash
What is wrong with this migration?
ALTER TABLE users ADD COLUMN age INT;Looks fine, right? On a local machine or a dev staging environment, this runs in milliseconds. It finishes, the test suite passes, and you move on. But run this on a table with fifty million rows while your application is under heavy load, and you might just lock your entire database. It is a classic trap. I have seen developers take down production services for hours because a simple column addition caused a full table rewrite or an exclusive lock.
But I just use my ORM to handle everything
“Why worry? My ORM handles migrations automatically. It is safer that way, and I dont need to touch raw SQL.”
Youre right that ORMs make common tasks easier, and for many CRUD applications, they work well. However, letting an abstraction hide the underlying database behavior is risky. When you run an ORM-managed migration against a production table, the ORM often generates an ALTER TABLE statement that triggers a table lock. If that table is being queried constantly, your application will back up, requests will time out, and your support team will start getting angry emails. I generally prefer writing my own migration scripts for anything on a production-scale table (I even check the index size before running anything) to ensure I know exactly what is happening.
How to avoid downtime
First, break it up. Adding a column is often fine if it is nullable or has a default value that does not require a rewrite. If you need to rename a column or change a data type, never try to do it in one step. Add the new column, write to both, backfill the data, update your code to read from the new column, and finally drop the old one. It takes more work. It is definitely more tedious.
Is this really necessary for every migration?
“This sounds like overkill. I just deploy at 3 AM when traffic is low.”
Scheduling deployments for off-peak hours is a decent strategy, but it is a band-aid, not a solution. If your system grows, you will eventually have customers in every timezone. You cannot hide from traffic forever. Plus, if a migration goes wrong at 3 AM, you are the one waking up to fix it. I have been there, and I don’t recommend it. Being able to run migrations during business hours is a sign of a healthy deployment pipeline.
A practical workflow
- Use a tool that allows you to see the lock duration before executing.
- Always verify that your migration script is idempotent. You should be able to run it twice without error.
- If you are using PostgreSQL, look into
CREATE INDEX CONCURRENTLYinstead of a standardCREATE INDEX.
I should mention that my advice here leans heavily toward relational databases like PostgreSQL. If you are using a schemaless NoSQL store, this advice changes significantly, and you might be more worried about application-level data parsing than database-level locks. I am not an expert in every NoSQL flavor out there, so tread carefully if you are working with systems that do not enforce strict schema constraints.
At the end of the day, you should view your database schema as a live, sensitive interface. It is not just storage. It is the core of your application state. Treat it with the same caution you apply to your public API endpoints. Start thinking about the cost of a migration in terms of time and concurrency, not just lines of code.