不知道大家有沒有遇到這樣的一種情況,線上業務在MySQL表上做增刪改查操作,隨著時間的推移,表裡面的資料越來越多,表資料檔案越來越大,資料庫佔用的空間自然也逐漸增長
為了縮小磁碟上表資料檔案佔用的空間,我們在最大的一張業務表中用delete命令刪除了一半兒的舊資料,刪除之後,磁碟上表資料檔案並沒有縮小,即使刪除整張表的資料,檔案依然沒有變小,這是為什麼呢?
本文將詳細的分析上述問題,並給出正確回收表空間的方法
前置說明
目前大部分MySQL資料庫都是用的 InnoDB 引擎,所以如無特殊說明,文中的例項都是基於InnoDB引擎的
在MySQL配置中有個配置項叫 innodb_file_per_table
將它設定為1之後,
每個表的資料會單獨儲存在一個以 .ibd
為字尾的檔案中
如果 innodb_file_per_table
沒有開啟的話,
表的資料是儲存在系統的共享表空間,這樣即使刪除了表,共享表空間也不會釋放這部分空間
所以,通常情況下,都是將 innodb_file_per_table
選項設定為 1, 同時為了能直觀的看到表資料檔案的大小變化,文中的例項也都是基於開啟了 此選項來說明的
問題重現
新建一張表ta
,表的結構如下
mysql> show create table ta\G
*************************** 1. row ***************************
Table: ta
Create Table: CREATE TABLE `ta` (
`id` int(11) NOT NULL,
`ia` int(11) NOT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
使用下面的儲存過程,向 ta
中批量插入資料
delimiter //
create procedure multinsert(in beg int,in cnt int)
begin
declare icnt int default 0;
declare tmp int default 0;
while icnt < cnt do
set icnt = icnt + 1;
set tmp = beg + icnt;
insert into ta(id,ia) values(tmp,tmp);
end while;
end//
delimiter ;
在MySQL控制檯執行 call multinsert(0,100000)
命令,往 ta
表插入10萬條資料
mysql> call multinsert(0,100000);
mysql> select count(*) from ta;
+----------+
| count(*) |
+----------+
| 100000 |
+----------+
1 row in set (0.02 sec)
檢視磁碟上ta
表的資料檔案 ta.ibd
的大小
[root@ecs-centos-7 test]# cd /var/lib/mysql/test/
[root@ecs-centos-7 test]# ls -l ta.ibd
-rw-r----- 1 mysql mysql 11534336 1月 3 23:14 ta.ibd
從上面的結果可以知道,ta
表插入10萬條資料之後,ta.ibd
大小為 11534336 位元組( 大約 11M )
現在我們使用 delete
命令刪除一半兒表資料( 5萬行記錄 )
mysql> delete from ta where id between 1 and 50000;
Query OK, 10000 rows affected (0.03 sec)
mysql> select count(*) from ta;
+----------+
| count(*) |
+----------+
| 50000 |
+----------+
1 row in set (0.02 sec)
刪除操作完成之後,再次檢視磁碟上 ta.ibd
的大小
[root@ecs-centos-7 test]# cd /var/lib/mysql/test/
[root@ecs-centos-7 test]# ls -l ta.ibd
-rw-r----- 1 mysql mysql 11534336 1月 3 23:14 ta.ibd
從上面的結果可以知道,ta
表刪除了一半兒,也就是5萬行資料之後,ta.ibd
的大小是 11534336 位元組( 約11M )
也就是說 ta
表刪除資料前後,磁碟上表資料檔案並沒有縮小
要弄明白資料檔案為什麼沒有縮小,就需要深入瞭解刪除資料的原理
刪除資料原理
我們都知道,InnoDB裡的資料都是用B+樹組織的,關於B+樹的知識請參考 理解B+樹
圖(1)
上面是InnoDB的索引示意圖,其中用虛線框起來的節點是屬於Page1資料頁,葉子節點儲存的是索引對應的資料,它們按照索引從小到大的順序組成了一個有序陣列
假如我們要刪除Page1頁中索引key值為 13 的資料,也即上圖中紅色部分
InnoDB引擎會把索引key值為13的節點標記為已刪除,它並不會回收節點真實的物理空間,只是將它標記為已刪除的節點,後續是可以複用的,所以,刪除表記錄,磁碟上資料檔案不會縮小
你可能會說,上面只是刪除了Page1頁中一個節點的資料,那如果把Page1頁中節點資料全部刪除了,應該會回收Page1頁的空間吧?
答案是,不會回收
當Page1頁資料全部刪除了,整個資料頁都會被標記為已刪除,並且整個資料頁都可以複用,所以,這種情況下,磁碟上的資料檔案仍然不會縮小
資料的複用
資料的複用涉及到資料節點的插入、刪除、轉移以及資料頁的合併等操作,具體的操作流程相關的細節請參考 理解B+樹,這裡就不再重複說明了
- 資料節點的複用
在上面 圖(1) 中,當刪除了索引key值為 13 的節點後,此節點就被標記為可複用的
如果之後又插入了一條索引key值在 7 到 18 之間的記錄時,就會複用原來索引key值為13的資料節點
但是如果之後插入的記錄的索引key值不在 7 到 18 之間時,可能就無法複用原來索引key值為13的資料節點
也就是說,資料節點的複用,需要索引key值滿足一定的範圍條件
- 資料頁的複用
在 圖(1) 當刪除了Page1資料頁全部資料節點後,Page1整頁都是可複用的,當插入的記錄需要用到新頁的時候,Page1就可以被複用
當相鄰的資料頁利用率比較低的時候,有可能會把它們合併到其中一個數據頁中,這時,另外一個數據頁就空出來了,這個空出來的資料頁就變成可複用的了
哪些操作會造成資料空洞
我們用 delete
命令刪除一條記錄後,InnoDB只是把對應的資料節點標記為已刪除且可複用的,這些可空著的等待使用的資料節點可以看作是一個一個的資料空洞
- 刪除資料
刪除資料的時候,會造成資料空洞,前面已經解釋過,這裡不再贅述了
- 插入資料
如果資料是按照索引大小順序插入,這個時候資料頁是緊湊的,不會出現資料空洞
如果是從索引中間插入的話,有可能會造成頁分裂,分裂之後的頁有可能出現數據空洞,下圖就是插入導致頁分裂的一個例子
如圖所示,分裂前葉子頁面已經滿了,這時資料排列得很緊湊
現在插入了一個索引key值為15的資料,插入之後,Page1 頁分裂成了上圖中 Page1,Page2
兩個頁面
分裂之後,Page1 頁面出現了兩個空洞,這兩個資料節點是可複用的,而 Page2頁面剛好滿了
- 更新資料
更新資料可以看成先刪除再插入,也是有可能造成資料空洞
比如: id
是表 ta
的主鍵, update ta set id = 10 where id = 1
語句把 id = 1
修改為 id = 10
,相當於先刪除 id = 1
的記錄,再插入 id = 10
的記錄,這種情況是會產生資料空洞的
但是如果是類似 update ta set ia = ia + 1 where id = 1
這種沒有更改主鍵值的語句是不會造成空洞的
所以,更新資料可能會造成資料空洞
總結下來就是,表的增刪改操作,可能會造成資料空洞的,而線上的服務會對錶進行大量的增刪改操作,資料空洞存在的可能性比較大
如何收縮表空間
既然一張表,經過大量無規則的增刪改操作之後,會產生大量的資料空洞
那如果我們新建一張和原來有資料空洞的表結構相同的新表,然後把舊錶中的資料按照索引升序依次插入到新表中,待舊錶資料全部插入到新表之後,刪除舊錶,再把新表重新命名為舊錶的名字
由於新表中葉子節點資料是按順序新增的,所以頁面是很緊湊的, 頁面利用率很高,需要的頁面比舊錶少了很多,這樣舊錶中索引上的空洞在新表就不存在了,新表資料檔案佔用的磁碟空間自然就會縮小,這樣就實現了表空間的收縮的目的
下面介紹的幾種收縮表空間的方法,雖然方法不同,但是基本的原理都是通過重建表的形式來達到目的的
- truntace table 表名
此操作等於 drop + create
,先刪除表,然後再建立一個同名的新表,當然,再執行 truncate table
命令之前需要先儲存一份舊錶的資料, 命令執行完成之後,再把這份資料匯入新表
- alter table 表名 engine=InnoDB
這個操作是遍歷舊錶主鍵索引的資料頁,把資料頁中的記錄生成B+樹結構,儲存到磁碟上的臨時檔案中,資料頁遍歷完了之後,用臨時檔案替換掉舊錶的資料檔案
從MySQL5.6版本之後,這個操作是 Online DDL 的,需要說明的是,這種方法需要掃描表資料檔案,對於大表來說是非常耗時的,如果是針對線上服務的話,需要避開業務高峰期,小心操作。
注意:
在重建表的時候,InnoDB 不會把整張表佔滿,每個頁留了大概10%左右的資料節點 給後續的更新用, 也就是說,其實重建表之後並不是最緊湊的
假如有這麼一個過程: 將表 t 重建一次,
插入一部分資料,但是插入的這些資料,用掉了一部分的預留空間,
這種情況下,再重建一次表 t,就可能會出現重建表後比重建之前佔用的空間還要大
小結
本文從一個實際的問題出發,重現問題、分析問題到解決問題,每一步都進行了詳細的分析,限於篇幅,有些細節沒有深入,需要讀者自行了解