1、概念
有些時候數據庫占用的空間比較大,所以把表數據刪除很多,但是數據庫表占用大小沒有改變。
本章說一下,數據表空間回收。
一個InnoDB表包含兩部分,即:表結構定義和數據。在MySQL 8.0版本以前,表結構是存在以.frm為后綴的文件里。而MySQL 8.0版本,則已經允許把表結構定義放在系統數據表中了。因為表結構定義占用的空間很小,所以主要討論的是表數據。
2、參數innodb_file_per_table
表數據既可以存在共享表空間里,也可以是單獨的文件。這個行為是由參數innodb_file_per_table控制的:
1)這個參數設置為OFF表示的是,表的數據放在系統共享表空間,也就是跟數據字典放在一起;
2)這個參數設置為ON表示的是,每個InnoDB表數據存儲在一個以 .ibd為后綴的文件中。
從MySQL 5.6.6版本開始,它的默認值就是ON了。
建議不論使用MySQL的哪個版本,都將這個值設置為ON。因為,一個表單獨存儲為一個文件更容易管理,而且在你不需要這個表的時候,通過drop table命令,系統就會直接刪除這個文件。而如果是放在共享表空間中,即使表刪掉了,空間也是不會回收的。
所以,將innodb_file_per_table設置為ON,是推薦做法。
我們在刪除整個表的時候,可以使用drop table命令回收表空間。但是,我們遇到的更多的刪除數據的場景是刪除某些行,這時就遇到問題是:表中的數據被刪除了,但是表空間卻沒有被回收。
3、數據刪除流程
InnoDB里的數據都是用B+樹的結構組織的。
假設,我們要刪掉R4這個記錄,InnoDB引擎只會把R4這個記錄標記為刪除。如果之后要再插入一個ID在300和600之間的記錄時,可能會復用這個位置。但是,磁盤文件的大小並不會縮小。
現在,你已經知道了InnoDB的數據是按頁存儲的,那么如果我們刪掉了一個數據頁上的所有記錄,會怎么樣?
答案是,整個數據頁就可以被復用了。
數據頁的復用和記錄的復用是不同的。
記錄的復用,只限於符合范圍條件的數據。比如上面的這個例子,R4這條記錄被刪除后,如果插入一個ID是400的行,可以直接復用這個空間。但如果插入的是一個ID是800的行,就不能復用這個位置了。
而當整個頁從B+樹里面摘掉以后,可以復用到任何位置。以圖1為例,如果將數據頁page A上的所有記錄刪除以后,page A會被標記為可復用。這時候如果要插入一條ID=50的記錄需要使用新頁的時候,page A是可以被復用的。
如果相鄰的兩個數據頁利用率都很小,系統就會把這兩個頁上的數據合到其中一個頁上,另外一個數據頁就被標記為可復用。
進一步地,如果我們用delete命令把整個表的數據刪除呢?結果就是,所有的數據頁都會被標記為可復用。但是磁盤上,文件不會變小。
你現在知道了,delete命令其實只是把記錄的位置,或者數據頁標記為了“可復用”,但磁盤文件的大小是不會變的。也就是說,通過delete命令是不能回收表空間的。這些可以復用,而沒有被使用的空間,看起來就像是“空洞”。
實際上,不止是刪除數據會造成空洞,插入數據也會。
如果數據是按照索引遞增順序插入的,那么索引是緊湊的。但如果數據是隨機插入的,就可能造成索引的數據頁分裂。
假設圖1中page A已經滿了,這時我要再插入一行數據,會怎樣呢?
可以看到,由於page A滿了,再插入一個ID是550的數據時,就不得不再申請一個新的頁面page B來保存數據了。頁分裂完成后,page A的末尾就留下了空洞(注意:實際上,可能不止1個記錄的位置是空洞)。
另外,更新索引上的值,可以理解為刪除一個舊的值,再插入一個新值。不難理解,這也是會造成空洞的。
也就是說,經過大量增刪改的表,都是可能是存在空洞的。所以,如果能夠把這些空洞去掉,就能達到收縮表空間的目的。
而重建表,就可以達到這樣的目的。
1、重建表
如果你現在有一個表A,需要做空間收縮,為了把表中存在的空洞去掉,你可以怎么做呢?
你可以新建一個與表A結構相同的表B,然后按照主鍵ID遞增的順序,把數據一行一行地從表A里讀出來再插入到表B中。
由於表B是新建的表,所以表A主鍵索引上的空洞,在表B中就都不存在了。顯然地,表B的主鍵索引更緊湊,數據頁的利用率也更高。如果我們把表B作為臨時表,數據從表A導入表B的操作完成后,用表B替換A,從效果上看,就起到了收縮表A空間的作用。
這里,你可以使用alter table A engine=InnoDB命令來重建表。在MySQL 5.5版本之前,這個命令的執行流程跟我們前面描述的差不多,區別只是這個臨時表B不需要你自己創建,MySQL會自動完成轉存數據、交換表名、刪除舊表的操作。
圖3 改鎖表DDL
顯然,花時間最多的步驟是往臨時表插入數據的過程,如果在這個過程中,有新的數據要寫入到表A的話,就會造成數據丟失。因此,在整個DDL過程中,表A中不能有更新。也就是說,這個DDL不是Online的。
而在MySQL 5.6版本開始引入的Online DDL,對這個操作流程做了優化。
簡單描述一下引入了Online DDL之后,重建表的流程:
1)建立一個臨時文件,掃描表A主鍵的所有數據頁;
2)用數據頁中表A的記錄生成B+樹,存儲到臨時文件中;
3)生成臨時文件的過程中,將所有對A的操作記錄在一個日志文件(row log)中,對應的是圖中state2的狀態;
4)臨時文件生成后,將日志文件中的操作應用到臨時文件,得到一個邏輯數據上與表A相同的數據文件,對應的就是圖中state3的狀態;
5)用臨時文件替換表A的數據文件。
圖4 Online DDL
可以看到,與圖3過程的不同之處在於,由於日志文件記錄和重放操作這個功能的存在,這個方案在重建表的過程中,允許對表A做增刪改操作。這也就是Online DDL名字的來源。
圖4的流程中,alter語句在啟動的時候需要獲取MDL寫鎖,但是這個寫鎖在真正拷貝數據之前就退化成讀鎖了。
為什么要退化呢?為了實現Online,MDL讀鎖不會阻塞增刪改操作。
那為什么不干脆直接解鎖呢?為了保護自己,禁止其他線程對這個表同時做DDL。
而對於一個大表來說,Online DDL最耗時的過程就是拷貝數據到臨時表的過程,這個步驟的執行期間可以接受增刪改操作。所以,相對於整個DDL過程來說,鎖的時間非常短。對業務來說,就可以認為是Online的。
需要補充說明的是,上述的這些重建方法都會掃描原表數據和構建臨時文件。對於很大的表來說,這個操作是很消耗IO和CPU資源的。因此,如果是線上服務,你要很小心地控制操作時間。如果想要比較安全的操作的話,我推薦你使用GitHub開源的gh-ost來做。
5、online和inplace
一個跟DDL有關的、容易混淆的概念inplace的區別。
在圖3中,我們把表A中的數據導出來的存放位置叫作tmp_table。這是一個臨時表,是在server層創建的。
在圖4中,根據表A重建出來的數據是放在“tmp_file”里的,這個臨時文件是InnoDB在內部創建出來的。整個DDL過程都在InnoDB內部完成。對於server層來說,沒有把數據挪動到臨時表,是一個“原地”操作,這就是“inplace”名稱的來源。
如果你有一個1TB的表,現在磁盤間是1.2TB,能不能做一個inplace的DDL呢?
答案是不能。因為,tmp_file也是要占用臨時空間的。
我們重建表的這個語句alter table t engine=InnoDB,其實隱含的意思是:
alter table t engine=innodb,ALGORITHM=inplace;
跟inplace對應的就是拷貝表的方式了,用法是:
alter table t engine=innodb,ALGORITHM=copy;
當你使用ALGORITHM=copy的時候,表示的是強制拷貝表,對應的流程就是圖3的操作過程。
但我這樣說你可能會覺得,inplace跟Online是不是就是一個意思?
其實不是的,只是在重建表這個邏輯中剛好是這樣而已。
比如,如果我要給InnoDB表的一個字段加全文索引,寫法是:
alter table t add FULLTEXT(field_name);
這個過程是inplace的,但會阻塞增刪改操作,是非Online的。
如果說這兩個邏輯之間的關系是什么的話,可以概括為:
DDL過程如果是Online的,就一定是inplace的;
反過來未必,也就是說inplace的DDL,有可能不是Online的。截止到MySQL 8.0,添加全文索引(FULLTEXT index)和空間索引(SPATIAL index)就屬於這種情況。
最后,我們再延伸一下。
在第10篇文章《MySQL為什么有時候會選錯索引》的評論區中,有同學問到使用optimize table、analyze table和alter table這三種方式重建表的區別。這里,我順便再簡單和你解釋一下。
從MySQL 5.6版本開始,alter table t engine = InnoDB(也就是recreate)默認的就是上面圖4的流程了;
analyze table t 其實不是重建表,只是對表的索引信息做重新統計,沒有修改數據,這個過程中加了MDL讀鎖;
optimize table t 等於recreate+analyze。
6、小結
這章討論了數據庫中收縮表空間的方法。
如果要收縮一個表,只是delete掉表里面不用的數據的話,表文件的大小是不會變的,你還要通過alter table命令重建表,才能達到表文件變小的目的。介紹了重建表的兩種實現方式,Online DDL的方式是可以考慮在業務低峰期使用的,而MySQL 5.5及之前的版本,這個命令是會阻塞DML的,這個你需要特別小心。