Flite Careers

3 Gotchas When Changing a MySQL Column Attribute

In my experienceALTER TABLEis one of the more maligned and misunderstood features of MySQL. It has a lot of potential to cause unexpected problems if you don’t understand it. Here are a few common gotchas when using ALTER TABLE to modify a column attribute:

1) Alter Table is slow

For those of us who spend a lot of time working with MySQL this is expected behavior. Nonetheless, many people are still surprised when they’re doing something simple like disallowing NULL values in a column, and it takes hours to run on a table with several GB of data. The reason for this is that most column changes affect the row format, and that requires rebuilding all of the rows for the entire table.

Here are some recommendations to mitigate this issue:

  • Use master-master active/passive replication so you can always execute DDL on a passive DB
  • Use pt-online-schema-change if your schema permits it
  • Design your schema to minimize changes to large tables. Do the design work up front to avoid things like allowing NULL values when you don’t really want to

2) It’s easy to accidentally change other attributes

This can silently corrupt your schema if you don’t understand how it works. Again, for those of use who have worked with relational databases for years this one is not as surprising, but I’ve seen plenty of programmers fall into this trap.

Say I want to change the title column in sakila.film from varchar(255) to varchar(50) because movie titles are usually pretty short. So I try the simplest ALTER TABLE statement I can think of:

1
2
alter table sakila.film
  modify column title varchar(50);

Here’s the schema before the change:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mysql> desc sakila.film;
+----------------------+---------------------------------------------------------------------+------+-----+-------------------+-----------------------------+
| Field                | Type                                                                | Null | Key | Default           | Extra                       |
+----------------------+---------------------------------------------------------------------+------+-----+-------------------+-----------------------------+
| film_id              | smallint(5) unsigned                                                | NO   | PRI | NULL              | auto_increment              |
| title                | varchar(255)                                                        | NO   | MUL | NULL              |                             |
| description          | text                                                                | YES  |     | NULL              |                             |
| release_year         | year(4)                                                             | YES  |     | NULL              |                             |
| language_id          | tinyint(3) unsigned                                                 | NO   | MUL | NULL              |                             |
| original_language_id | tinyint(3) unsigned                                                 | YES  | MUL | NULL              |                             |
| rental_duration      | tinyint(3) unsigned                                                 | NO   |     | 3                 |                             |
| rental_rate          | decimal(4,2)                                                        | NO   |     | 4.99              |                             |
| length               | smallint(5) unsigned                                                | YES  |     | NULL              |                             |
| replacement_cost     | decimal(5,2)                                                        | NO   |     | 19.99             |                             |
| rating               | enum('G','PG','PG-13','R','NC-17')                                  | YES  |     | G                 |                             |
| special_features     | set('Trailers','Commentaries','Deleted Scenes','Behind the Scenes') | YES  |     | NULL              |                             |
| last_update          | timestamp                                                           | NO   |     | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
+----------------------+---------------------------------------------------------------------+------+-----+-------------------+-----------------------------+
13 rows in set (0.05 sec)

Now I run the ALTER TABLE statement:

1
2
3
4
mysql> alter table sakila.film
    ->   modify column title varchar(50);
Query OK, 1000 rows affected (0.43 sec)
Records: 1000  Duplicates: 0  Warnings: 0

Here’s the schema after the change:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
mysql> desc sakila.film;
+----------------------+---------------------------------------------------------------------+------+-----+-------------------+-----------------------------+
| Field                | Type                                                                | Null | Key | Default           | Extra                       |
+----------------------+---------------------------------------------------------------------+------+-----+-------------------+-----------------------------+
| film_id              | smallint(5) unsigned                                                | NO   | PRI | NULL              | auto_increment              |
| title                | varchar(50)                                                         | YES  | MUL | NULL              |                             |
| description          | text                                                                | YES  |     | NULL              |                             |
| release_year         | year(4)                                                             | YES  |     | NULL              |                             |
| language_id          | tinyint(3) unsigned                                                 | NO   | MUL | NULL              |                             |
| original_language_id | tinyint(3) unsigned                                                 | YES  | MUL | NULL              |                             |
| rental_duration      | tinyint(3) unsigned                                                 | NO   |     | 3                 |                             |
| rental_rate          | decimal(4,2)                                                        | NO   |     | 4.99              |                             |
| length               | smallint(5) unsigned                                                | YES  |     | NULL              |                             |
| replacement_cost     | decimal(5,2)                                                        | NO   |     | 19.99             |                             |
| rating               | enum('G','PG','PG-13','R','NC-17')                                  | YES  |     | G                 |                             |
| special_features     | set('Trailers','Commentaries','Deleted Scenes','Behind the Scenes') | YES  |     | NULL              |                             |
| last_update          | timestamp                                                           | NO   |     | CURRENT_TIMESTAMP | on update CURRENT_TIMESTAMP |
+----------------------+---------------------------------------------------------------------+------+-----+-------------------+-----------------------------+
13 rows in set (0.06 sec)

Take a look at the title column. It’s now nullable when it wasn’t before. What happened? When you change a column you need to restate the entire column definition, including all of the following where relevant:

  • data type
  • nullability
  • character set
  • collation
  • default value
  • comment

If you do not explicitly set the full column definition in your ALTER TABLE statement then MySQL will happily substitute the default value for any attributes you skip. In my case I did not explicitly state that the column was NOT NULL, so MySQL implicitly changed it to NULL. Oops.

My technique for avoiding this gotcha is to run SHOW CREATE TABLE and copy the relevant column definition from the create table statement into my ALTER TABLE statement.

If I had done that, then I would have come up with the following DDL, which changes the column length only and does not affect its nullability:

1
2
alter table sakila.film
  modify column title varchar(50) not null;

3) Schema changes are not always backwards compatible with application code

Assuming there is an application using with your database, you need to keep the application code in mind when making schema changes.

For example, if you are changing a column to disallow NULL values, but the code has sometimes set NULL values in the past, then you should add a default value for the column. That way if you have to revert your application to a previous version that send NULL values the database will be backwards compatible.

Likewise, when dropping or renaming a column you should consider the impact it will have on your application. At Flite we usually follow this pattern to drop a database column from the schema:

  • Make the column nullable and/or give it a default value
  • Remove all references to the column from our code
  • Wait a week
  • Drop the column

By waiting a week between removing the column references from code and actually dropping the column we again allow ourselves the ability to revert the code one release and still have it be compatible with the database.

Comments

Comments