Paweł Świątkowski
Posted on February 15, 2022
Sometimes you don't get it right in the first attempt.
When some time ago I announced that we need to rename one of the database tables in our recently launched service, some of my colleagues looked at me like I said something really stupid. Their argument was that is won't work without causing a downtime and generally it's not worth it. Their stance was that it's better to live with a too generic table name than to attempt a risky operation. In my opinion it was worth it and I pushed the change through, without any downtime.
Their instincts were right, however. If you do it naively by simply calling ALTER TABLE RENAME
, it will harm your production. Fortunately, with a little more ceremony you can do it right and here is how.
What's the problem, really?
Most of deployment processes these days consist of few sequential steps to get a new version up and running. They vary from project to project, but usually contain these three in this order:
- Make the new code ready (build a Docker image, compile, copy to server)
- Run migrations
- Restart application instances to use new code
The problem is that there is a non-zero gap between 2. and 3. are finished. In fact, it's not that uncommon to take a minute or longer, as you have to let the servers finish serving existing requests etc. Now, when migrations are run and your renaming-the-table migration among them, the old table does not exist, but the old application instances still reference it. As a result they still try to query a non-existing table, resulting in an exception (most likely).
This is something we want to avert.
View to the rescue
So, if only we could use some kind of an alias to route requests to old table name to a new name...
Fortunately, we can. Since a couple of versions PostgreSQL supports usage of a regular dynamic views along its old materialized views. These views are, in essence, an alias for a query. Every time you select from a view, it is translated to an underlying query. What's even better? Simple views are updatable, which means you can insert to them or update them - and it will be perfectly reflected in an underlying table. Specifically, we can use them to just create aliases.
Armed with that knowledge, we may write our migration as something like this:
def up
rename_table :old_name, :new_name
execute "CREATE VIEW old_name AS SELECT * FROM new_name"
# also rename sequences, indices etc. for consistency
execute "ALTER SEQUENCE old_name_id_seq RENAME TO new_name_id_seq"
execute "ALTER INDEX old_name_something_id_index RENAME TO new_name_something_id_index"
end
This will be fast. Renaming a table is fast. Creating a view is fast. Actually, our migrator reported 0.0s
for this migration (which means less that 50 ms IIRC) for a table with few million records. Most likely it won't cause any negative side-effects for your application. You just want to remember to delete the view next time the deployment is done.
Note that because of using execute
the migration is irreversible. You definitely should write a corresponding down
method in case things go wrong.
And that's it. We renamed the table. Now we can move on with our lives, without having weird legacy names and have BI/Analytics team ask questions about it every now and then.
Update
As /u/fatkodima noted on Reddit, this approach might still cause some trouble, because by default ActiveRecord cannot read columns from views. He also is a creator of a gem online_migrations which helps with that issue in Rails.
Posted on February 15, 2022
Join Our Newsletter. No Spam, Only the good stuff.
Sign up to receive the latest update from our blog.