概述
一般來說數據庫結構一經設計,不能輕易更改,因為更改DDL(Data Definition Language)操作代價很高,所以在進行數據庫結構設計時需要謹慎。
但是業務發展是未知的,特別是那些變化很大的業務,所以不可避免的需要修改數據庫結構,本文主要對MySQL5.6+ InnoDB存儲引擎字段的修改進行探討。
對於不同的場景,所使用的方式也會大不相同,尤其是修改百萬級,千萬級的表字段時,要特別注意。
DDL操作類型
數據庫結構的DDL操作總體來說有如下幾種:
- 索引操作(Index Operations)
- 鍵操作(Primary Key Operations)
- 列操作(Column Operations)
- 外鍵操作(Foreign Key Operations)
- 表操作(Table Operations)
- 分區操作(Partitioning Operations)
本文主要對列操作(Column Operations)進行探討,其他更詳細的信息參考MySQL官方英文文檔
Online DDL操作
簡述
本文探討的是Online DDL操作,MySQL5.6以上支持,相較於一般DDL,它在實現修改表結構的同時,依然允許DML操作(SELECT,INSERT,UPDATE,DELETE)。
Online DDL主要有兩種方式:IN PLACE和COPY。
IN PLACE:直接在原表上進行修改,相比於COPY方式可以避免重建表帶來的IO和CPU消耗,有更好的性能並支持並發DML操作COPY:創建修改后的臨時表,然后將原表的數據復制到臨時表,執行期間不允許並發DML寫操作,否則會導致臟數據。
在MySQL之前,我們一般使用COPY的方式,借助臨時表,手動修改。
需要注意的是:並不是所有的Online DDL操作都支持IN PLACE方式。
MySQL InnoDB數據存儲方式
在MySQL中,一張表的數據分為兩種,一種是結構數據,記錄者站表包含哪些字段,哪些數據類型,另一種是記錄數據,保存每天記錄的原始數據。它們是用不同的文件進行存儲的。
在mysql指定的data_dir數據存儲目錄可以看到每張表對應一個frm文件,這個文件就是存放着表的結構數據。
INPLACE方式詳細介紹
對於添加索引,添加/刪除列、修改列NULL/NOT NULL屬性等操作,需要修改MySQL內部的數據記錄,對這類操作進行Online DDL操作時,需要重建表(rebuild)。
相反,對於刪除索引,修改列默認值,修改列名等操作不需要修改MySQL內部的數據記錄,只需要修改結構數據frm文件,而不需要重建表(no-rebuild)。
另外,在進行Online DDL操作期間,不同的操作可以選擇不同的鎖機制。主要有以下幾種鎖機制:
LOCK=DEFAULT:默認方式,MySQL自行判斷使用哪種LOCK模式,盡量不鎖表LOCK=NONE:無鎖:允許Online DDL期間進行並發讀寫操作。如果Online DDL操作不支持對表的繼續寫入,則DDL操作失敗,對表修改無效LOCK=SHARED:共享鎖:Online DDL操作期間堵塞寫入,不影響讀取LOCK=EXCLUSIVE:排它鎖:Online DDL操作期間不允許對鎖表進行任何操作
無論任何模式下,Online DDL操作開始都需要一小段時間的排它鎖來准備環境,用於等待該表上的其他操作執行完畢,此時Online DDL操作會提示:waiting meta data lock。
同樣在Online DDL操作結束之前,也會等待Online DDL操作期間的事務完成,此時也會出現排它鎖。
所以需要確保在執行Online DDL之前和執行期間沒有大型DML事務占用該表,否則會出現長時間鎖表甚至死鎖。
Online DDL各種列操作情況
從上面的介紹可以看出,不同的DDL操作,執行的具體細節大不相同,詳見下表:
| Operation | In Place | Rebuilds Table | Permits Concurrent DML | Only Modifies Metadata |
|---|---|---|---|---|
| Adding a column | Yes | Yes | Yes* | No |
| Dropping a column | Yes | Yes | Yes | No |
| Renaming a column | Yes | No | Yes* | Yes |
| Reordering columns | Yes | Yes | Yes | No |
| Setting a column default value | Yes | No | Yes | Yes |
| Changing the column data type | No | Yes | No | No |
| Extending VARCHAR column size | Yes | No | Yes | Yes |
| Dropping the column default value | Yes | No | Yes | Yes |
| Changing the auto-increment value | Yes | No | Yes | No* |
| Making a column NULL | Yes | Yes* | Yes | No |
| Making a column NOT NULL | Yes* | Yes* | Yes | No |
| Modifying the definition of an ENUM or SET column | Yes | No | Yes | Yes |
其中各列指標解釋如下:
In Place:是否支持In Place方式,Yes為優選方案Re Builds Table:是否需要重建表,不重建(No)為優選方案Permits Concurrent DML:是否允許並發DML操作,允許(Yes)為優選方案Only Modifies Metadata:是否值修改表結構數據,即只修改frm文件
列操作方式
下面列舉常用的列操作的執行方法以及注意事項。
添加列(Adding a column)
為表添加一列的方法如下:
ALTER TABLE tbl_name
ADD COLUMN column_name column_definition, ALGORITHM=INPLACE, LOCK=NONE;
添加列時如果附加auto increment選項,則不允許並發DML操作,此操作會重建表,開銷巨大。最優化選項是指定:ALGORITHM=INPLACE, LOCK=SHARED。
刪除列(Dropping a column)
ALTER TABLE tbl_name
DROP COLUMN column_name, ALGORITHM=INPLACE, LOCK=NONE;
重命名列名(Renaming a column)
ALTER TABLE tbl
CHANGE old_col_name new_col_name data_type, ALGORITHM=INPLACE, LOCK=NONE;
如果你的目的只是修改列名,一定要保證修改后的列的數據類型,NULL/NOT NULL等屬性和原來的列一致。
該操作建議指定INPLACE方式,這樣只會更新frm文件,即使修改的列名是外鍵。
重新排列列順序(Reordering columns)
ALTER TABLE tbl_name
MODIFY COLUMN col_name column_definition FIRST, ALGORITHM=INPLACE, LOCK=NONE;
該操作費力不討好,不建議對數據量超過百萬級的大表進行操作,它會對表重建。
修改列數據類型(Changing the column data type)
ALTER TABLE tbl_name
CHANGE c1 c1 BIGINT, ALGORITHM=COPY;
修改數據類型只支持COPY方式。
修改列的默認值(Setting a column default value)
ALTER TABLE tbl
ALTER COLUMN col DROP DEFAULT, ALGORITHM=INPLACE, LOCK=NONE;
修改列的自增熟悉(Changing the auto-increment value)
ALTER TABLE table
AUTO_INCREMENT=next_value, ALGORITHM=INPLACE, LOCK=NONE;
該操作用於修改下一條記錄的自增值,只會修改內存中的值,而不會修改數據文件。
對於分布式系統,經常需要手動制定開始自增的值,可以使用該方法。
修改NULL/NOT NULL屬性(Making a column NULL and Making a column NOT NULL)
-- Making a column NULL
ALTER TABLE tbl_name
MODIFY COLUMN column_name data_type NULL, ALGORITHM=INPLACE, LOCK=NONE;
-- Making a column NOT NULL
ALTER TABLE tbl_name
MODIFY COLUMN column_name data_type NOT NULL, ALGORITHM=INPLACE, LOCK=NONE;
因為設置列為NULL時,該列在原有數據類型空間的基礎上增加一個直接來存儲是否為NULL,所以需要重建表。
當把NULL的列設為NOT NULL時,如果有記錄為NULL,則該操作會失敗。
修改ENUM或SET的定義(Modifying the definition of an ENUM or SET column)
CREATE TABLE t1 (c1 ENUM('a', 'b', 'c'));
ALTER TABLE t1 MODIFY COLUMN c1 ENUM('a', 'b', 'c', 'd'), ALGORITHM=INPLACE, LOCK=NONE;
該方式用於修改一個枚舉或者集合的值,對於在尾部增加枚舉或者集合值的情況,如果增加之后存儲空間沒有變化,就可以使用IN PLACE方式。
反之如果存儲空間發生變化,如從2個字節便到三個字節,或者在中間添加值,那么就需要COPY的方式。
對於那種值的個數不確定或者枚舉名稱變化的場景,建議使用tinyint代替ENUM或者SET來進行存儲。
實際中如何執行DDL修改
綜合上述,可以得出常用的三種方法。
Online DDL
通過執行ALTER等命令直接修改。適用的情況如下:
- 表中數據量較小,低於百萬級別
- 需要MySQL5.6+以上
- 能夠忍受長時間不提供服務的百萬級表,需要一小時以內
手動修改frm文件
該方式適用於不支持Online DDL的場景,只能執行Only Modifies Metadata部分的DDL修改。修改方法如下:
首先找到MySQL數據存儲路徑,可從進程信息中查看:
# 查找mysql進程信息
ps aux|grep mysql
查到當前數據庫的數據存儲目錄,然后cd到所看到的frm表結構文件目錄,備份需要處理的frm文件。
在數據庫創建一個類似的數據表,然后修改該表,再把該表的frm文件和原來的表的frm文件替換。
-- mysql中創建臨時表
create table tbl_temp like tbl;
-- 修改臨時表
ALTER TABLE tbl
ADD COLUMN `count` bigint(20) NOT NULL DEFAULT 0 COMMENT '';
-- 鎖表
flush tables with write lock;
-- 備份源文件
cp tbl.frm tbl.frm.bak
# 替換數據結構文件frm
cp tbl_temp.frm tbl.frm
-- mysql移除讀鎖
unlock tables;
-- 測試修改是否成功
select * from tbl limit 1;
-- 如果出現錯誤,導致連接丟失等,可以回滾
flush tables with write lock;
cp tbl.frm.bak tbl.frm
unlock tables;
手動執行COPY方式
通過復制臨時表,然后修改臨時表,再把原表中的數據復制到臨時表中,並切換臨時表和原表。
當需要對原表中數據進行額外的處理時,只能選擇此方式,該方式會造成大量的磁盤IO,並且執行期間不允許寫入。
對於千萬級別的表,可以分批進行復制,使用一些策略來允許遷移過程中的寫入。
執行修改時需要考慮的因素
首先需要對執行的表數據量進行確認,如果數據量超過百萬級甚至千萬級,需要檢查下面的事項:
- 當前系統內存容量充足
- 當前系統內存使用情況良好
- 當前系統CPU使用空閑
- 執行修改期間是否允許停止服務
- 是否有其他關聯的數據庫,保證數據一致性
如果有一些遺漏的點,歡迎大家補充,這樣也能夠在以后遇到類似問題的時候提供參考。
文章已經同步到個人博客:http://uusama.com/866.html,過兩天把其他的Online DDL操作測試之后再加上。(個把月沒寫文章了,已然懶得像一坨咸魚)
