How can I add a non-null column with a default using Doctrine Migrations without causing downtime?
0 reputation · 27 Mar 2024, 03:12 UTC
0 reputation · 27 Mar 2024, 03:12 UTC
I need to modify the schema of a small application that must stay online, and I want to use Doctrine Migrations to perform the change.
The table contains a moderate amount of data, and I cannot afford a long lock that would block reads or writes. I am unsure which sequence of migration steps—adding the column as nullable, deploying updated code, backfilling existing rows, and then making the column non-null—will keep the application available, especially when specifying a default value.
What is the safest order of Doctrine migration operations to add a non-null column with a default? How should I handle the default value during the migration to avoid table locks? Are there any Doctrine-specific options or flags that minimize locking during the schema change?
To add a non-null column with a default value without causing significant table locks or downtime, you must avoid a single ALTER TABLE statement that forces the database to rewrite every row. The safest approach is a multi-stage rollout that separates the schema change from the data population.
Follow these three distinct phases to ensure the application remains available during the transition:
Create a migration that adds the column without the NOT NULL constraint. This operation is typically fast and avoids long-term locks in most modern database engines.
$this->addSql('ALTER TABLE my_table ADD new_column VARCHAR(255) DEFAULT \'default_value\'');
Note: Even with a default, keeping the column nullable prevents the database from needing to validate existing rows against a strict constraint immediately upon creation.
Deploy the code that handles the new column, then run a data migration to update existing NULL values. To avoid locking the table for the duration of the update, perform this in batches rather than a single query.
// Example of a batched update in a migration or command
while ($rowsAffected > 0) {
$this->connection->executeStatement(
'UPDATE my_table SET new_column = \'default_value\' WHERE new_column IS NULL LIMIT 1000'
);
$rowsAffected = $this->connection->affectedRows();
}
Once all existing rows are populated and the application code is successfully writing the new value for all new records, apply the final constraint.
$this->addSql('ALTER TABLE my_table MODIFY new_column VARCHAR(255) NOT NULL');
pt-online-schema-change.ADD COLUMN is often fast, the UPDATE phase is the highest risk for locking. Batching is essential for tables with millions of rows.$this->addSql()` is recommended over $this->changeColumn()` for these specific steps to ensure the exact SQL syntax for locking and defaults is preserved.Before applying these to production, verify the following in a staging environment with a production-sized dataset:
SHOW CREATE TABLE my_table; (MySQL) or \d my_table (PostgreSQL) after Phase 1 to confirm the column is nullable.ALTER TABLE in Phase 3 completes quickly because the database only needs to verify the constraint, not populate data.Diagnostic Detail Needed: To provide a more optimized SQL strategy, please specify your database engine (e.g., MySQL 8.0, PostgreSQL 15) and the approximate row count of the table.
Use comments to ask for clarification. Post a solution as an answer.
26,525 reputation · 27 Mar 2024, 05:09 UTC
While the multi-phase approach is the safest general strategy, the actual performance impact depends heavily on your database engine version. For those using MySQL 8.0.12+, adding a column with a default value can often be performed using the INSTANT algorithm, which avoids a full table rewrite and minimizes locking.
If you are on a modern version, you can verify if a migration will be instant by explicitly specifying the algorithm in your addSql call:
$this->addSql('ALTER TABLE my_table ADD new_column VARCHAR(255) DEFAULT \'default_value\' ALGORITHM=INSTANT');
If the engine does not support an instant change for that specific operation, the database will return an error rather than proceeding with a slow, locking operation. For older versions or other engines (like PostgreSQL), the batched backfill remains the necessary path to avoid blocking production traffic.