ActiveRecord has two behaviors by default that, when combined, can make it challenging to safely add a column to an existing table in high traffic applications.
First, it leverages a prepared statement cache. This allows the database to reuse queries of the same shape where only variables change, so that the query planner doesn’t have to re-evaluate a SQL statement each time, if only the value 3 changes in a query like:
SELECT users.* FROM users WHERE id = 3
When you see something in your logs that looks like
SELECT users.* FROM users WHERE id = $1 [["id", 3]]
this is Rails using prepared statements.
Second, by default, all columns from a table are selected (SELECT users.*), which makes SQL logs smaller (possibly a lot smaller for tables with lots of columns) and is sort of the de facto presumption that your ActiveRecord model always needs all columns on the underlying table.
With these defaults, if you run a migration that adds a column to an existing table, you may see a sudden blip of PreparedStatementCacheExpired exceptions. This is because your running application has existing database connections leveraging the prepared statement cache, and because we added a column, the arity or shape of SELECT table.* has changed, which is unexpected. Rails handles these exceptions to some extent by updating the statement cache, so you may only see one exception per thread or worker (per connection), however, this can still result in some user-facing 500s or retried background jobs, which is not ideal.
Working Around This Issue
The easiest way to do this historically has been to add an entry to the ignored_columns array on your model. For example:
class User < ActiveRecord::Base
self.ignored_columns += ['a_new_setting']
# ...
end
If you deploy this version of your application, Rails will no longer select all columns from your table, but rather select each individual column (with the exception of the ignored ones). So, your SQL statement may instead look like SELECT users.id, users.email, users.first_name, users.last_name anywhere you are using the User model. Once you have confirmed this version of your application is running in production, you can safely run your migration to add your column, because the prepared statement cache will not be broken by the addition of the new column. After the migration completes, you can deploy another version of your application with the ignored_columns removed.
Another Option
While the above works fine, it can be cumbersome to have a pair of deployments every time you want to add a column with zero downtime (one to add the ignored column entry, and a second to run the migration and remove the ignored column entry). For tables that have frequent additions (perhaps a settings or configuration table), we can instead leverage the relatively recent enumerate_columns_in_select_statements option. While this can be configured globally, I think it is sufficient to use it sparingly. For example:
class Settings < ActiveRecord::Base
self.enumerate_columns_in_select_statements = true
# ...
end
See the documentation for more details, or this pull request for the original discussion.