Safely Dropping Columns in Rails
Safely Dropping Columns in Rails
May 21, 2026
Dropping a column from a database table is one of the simplest possible migrations, if you just look at the syntax. But if you run that migration in a large application used by a ton of users where taking the application down for maintenance is not an option, you may run into problems.<br>One thing I didn't realize until researching this post: ActiveRecord caches each model's schema the first time the model is loaded, and in production, with eager loading on, that means the whole schema is effectively cached at boot.<br>This matters because of the deploy order. The migration runs first, then the app server restarts onto the new schema. In between, there's a window where the old process is still serving traffic with its cached view of the schema. If a request comes in during that window and the migration dropped a column, the running app will try to SELECT or INSERT a column that no longer exists and you get errors in production.<br>Rails provides a mechanism to handle this cleanly: ignored_columns. Let's learn what problem it solves and how it works. We'll also look at some techniques to use for safely dropping important columns in heavily trafficked, critical databases. I didn't come up with them, these are just industry-wide best practices and you can learn more about them by reading this book: Refactoring Databases<br>Problem with dropping columns directly<br>Consider a projects table with a category column you no longer need. The naive approach is to write a migration and deploy it.<br>class DropCategoryFromProjects This works fine if your deployment process restarts the application and runs migrations atomically, with no traffic hitting the app in between. But most production deployments don't work that way. In a typical setup, the migration runs first, the column is removed from the database, and then the application code is restarted. During that gap, the old application code is still running with a cached schema that includes category. Every query that touches the projects table, whether it explicitly references category or not, can break.<br>The exact error depends on how ActiveRecord builds its queries. If you're doing Project.create!(name: "foo"), ActiveRecord might generate an INSERT that includes every column it knows about, setting unspecified ones to their defaults. The database will reject the query because the column no longer exists. SELECT queries using * will also return a different number of columns than ActiveRecord expects, causing attribute mapping errors.<br>Even if the deployment is fast and the window is small, it is not zero and under load, that window is more than enough time for requests to fail.<br>The fix is to tell ActiveRecord to stop using the column before you drop it. That's what ignored_columns does.<br>How ignored_columns solves this<br>First, you add the column that you want to drop to an ignored columns list, as follows.<br>class Project Once a column is in ignored_columns, ActiveRecord pretends it doesn't exist. It won't appear in columns_hash, won't have an attribute accessor, and won't be included in any generated SQL. The column is still in the database, but the application no longer knows about it.<br>This turns a risky one-step operation into a safe two-step process:<br>Step 1: Add the column to ignored_columns and deploy the application. At this point, the column still exists in the database, but no code references it. You can verify this by running your test suite. If anything was still using the column, you'll get NoMethodError or query failures in your tests rather than in production.<br>Step 2: After the first deploy is fully rolled out and stable, write and deploy the migration to actually drop the column.<br>class DropCategoryFromProjects Because the application is already ignoring the column, the migration can run without causing any errors. There's no dangerous window. The application doesn't care whether the column exists or not, because it stopped looking for it in the previous deploy.<br>How it works<br>The implementation is worth understanding, because it shows how ActiveRecord's schema caching works and why the timing problem exists in the first place.<br>When you set ignored_columns on a model, the setter does three things:<br># activerecord/lib/active_record/model_schema.rb<br>def ignored_columns=(columns)<br>check_model_columns(@only_columns.present?)<br>reload_schema_from_cache<br>@ignored_columns = columns.map(&:to_s).freeze<br>endFirst, it checks that you're not also using only_columns (a newer addition that takes the opposite approach: you specify the columns you want instead of the ones you don't). You can't use both.<br>Second, it calls reload_schema_from_cache, which clears all the cached column data: @columns_hash, @columns, @column_names, @attributes_builder, and several other cached values. It also sets @schema_loaded = false and recursively invalidates all subclasses.<br>Third, it stores the ignored column names as frozen...