1.表中的頁是完全空閑的
2.表中的頁沒有被完全填充,還有部分空閑空間
有三種情況下,insert操作會導致表碎片:
1.帶有rollback的insert
2.失敗了的insert操作
3.頁分裂導致的碎片
測試環境:Server version: 5.7.23-log MySQL Community Server (GPL)
表:frag、ins_frag、frag_page_spl
> create table ins_frag like frag; Query OK, 0 rows affected (0.01 sec) > begin; Query OK, 0 rows affected (0.00 sec) > insert into ins_frag select * from frag; Query OK, 5964924 rows affected (2 min 23.04 sec) Records: 5964924 Duplicates: 0 Warnings: 0 > # ll -ltrh |grep frag -rw-r----- 1 mysql mysql 29K Nov 8 09:38 frag.frm -rw-r----- 1 mysql mysql 29K Nov 8 09:41 ins_frag.frm -rw-r----- 1 mysql mysql 2.3G Nov 8 09:46 frag.ibd -rw-r----- 1 mysql mysql 2.3G Nov 8 09:48 ins_frag.ibd
到此,已經執行了insert操作,但是還沒有提交或回滾insert操作。表占用了2.3G的磁盤空間。
現在,開始回滾上面的insert操作。
> select count(*) from ins_frag; +----------+ | count(*) | +----------+ | 5964924 | +----------+ 1 row in set (1.08 sec) > rollback; Query OK, 0 rows affected (2 min 27.15 sec) > select count(*) from ins_frag; +----------+ | count(*) | +----------+ | 0 | +----------+ 1 row in set (0.00 sec) > # ll -ltrh |grep frag -rw-r----- 1 mysql mysql 29K Nov 8 09:38 frag.frm -rw-r----- 1 mysql mysql 29K Nov 8 09:41 ins_frag.frm -rw-r----- 1 mysql mysql 2.3G Nov 8 09:46 frag.ibd -rw-r----- 1 mysql mysql 2.3G Nov 8 10:01 ins_frag.ibd
回滾結束后,表ins_frag仍然占用了2.3GB的磁盤空間。
> SELECT -> table_schema AS 'DATABASE', -> TABLE_NAME AS 'TABLE', -> CONCAT ( ROUND( ( data_length + index_length ) / ( 1024 * 1024 * 1024 ), 2 ), 'G' ) 'TOTAL', -> CONCAT ( ROUND( data_free / ( 1024 * 1024 * 1024 ), 2 ), 'G' ) 'DATAFREE' -> FROM -> information_schema.TABLES -> WHERE -> TABLE_NAME = 'ins_frag'; +----------+----------+-------+----------+ | DATABASE | TABLE | TOTAL | DATAFREE | +----------+----------+-------+----------+ | ysoap | ins_frag | 2.18G | 2.23G | +----------+----------+-------+----------+ 1 row in set (0.00 sec) >
說明insert回滾后,產生了碎片。
現在我們重構這個表,釋放空間。
> alter table ins_frag engine=innodb; Query OK, 0 rows affected (0.03 sec) Records: 0 Duplicates: 0 Warnings: 0 > # ll -ltrh |grep frag -rw-r----- 1 mysql mysql 29K Nov 8 09:38 frag.frm -rw-r----- 1 mysql mysql 2.3G Nov 8 09:46 frag.ibd -rw-r----- 1 mysql mysql 29K Nov 8 10:16 ins_frag.frm -rw-r----- 1 mysql mysql 128K Nov 8 10:16 ins_frag.ibd
案例2:失敗的insert操作
在會話1中,開啟一個事務執行insert操作。但是會在會話2中kill掉會話1。
會話1:
# ll -ltrh |grep frag -rw-r----- 1 mysql mysql 29K Nov 8 09:38 frag.frm -rw-r----- 1 mysql mysql 2.3G Nov 8 09:46 frag.ibd -rw-r----- 1 mysql mysql 29K Nov 8 10:16 ins_frag.frm -rw-r----- 1 mysql mysql 128K Nov 8 10:16 ins_frag.ibd > begin; Query OK, 0 rows affected (0.00 sec) > insert into ins_frag select * from frag; #運行過程中
會話2將會話1殺掉:
> pager grep -i insert ; show processlist; PAGER set to 'grep -i insert' | 1603454 | root | localhost | ysoap | Query | 5 | Sending data | insert into ins_frag select * from frag | 16 rows in set (0.00 sec) > kill 1603454; Query OK, 0 rows affected (0.00 sec) >
回到會話1
> insert into ins_frag select * from frag; ERROR 2013 (HY000): Lost connection to MySQL server during query > select count(*) from ins_frag; +----------+ | count(*) | +----------+ | 0 | +----------+ 1 row in set (0.00 sec) > # ll -ltrh |grep frag -rw-r----- 1 mysql mysql 29K Nov 8 09:38 frag.frm -rw-r----- 1 mysql mysql 2.3G Nov 8 09:46 frag.ibd -rw-r----- 1 mysql mysql 29K Nov 8 10:16 ins_frag.frm -rw-r----- 1 mysql mysql 1.2G Nov 8 10:28 ins_frag.ibd
insert操作被中途kill了。表中沒有數據。但是,磁盤上還是占用了1.2GB的物理空間。
> SELECT -> table_schema AS 'DATABASE', -> TABLE_NAME AS 'TABLE', -> CONCAT ( ROUND( ( data_length + index_length ) / ( 1024 * 1024 * 1024 ), 2 ), 'G' ) 'TOTAL', -> CONCAT ( ROUND( data_free / ( 1024 * 1024 * 1024 ), 2 ), 'G' ) 'DATAFREE' -> FROM -> information_schema.TABLES -> WHERE -> TABLE_NAME = 'ins_frag'; +----------+----------+-------+----------+ | DATABASE | TABLE | TOTAL | DATAFREE | +----------+----------+-------+----------+ | ysoap | ins_frag | 1.04G | 1.15G | +----------+----------+-------+----------+ 1 row in set (0.01 sec) >
現在我們再次重構這個表,釋放空間。
> alter table ins_frag engine='innodb'; Query OK, 0 rows affected (0.02 sec) Records: 0 Duplicates: 0 Warnings: 0 > # ll -ltrh |grep frag -rw-r----- 1 mysql mysql 29K Nov 8 09:38 frag.frm -rw-r----- 1 mysql mysql 2.3G Nov 8 09:46 frag.ibd -rw-r----- 1 mysql mysql 29K Nov 8 10:34 ins_frag.frm -rw-r----- 1 mysql mysql 128K Nov 8 10:34 ins_frag.ibd
案例3:頁分裂導致的碎片
在內部,InnoDB記錄存儲在InnoDB頁中。默認情況下,每個頁大小是16K,但是可以選擇更改頁面大小。
如果InnoDB頁沒有足夠的空間容納新的記錄或索引條目,它將被分成兩個頁面,每個頁大約有50%的空間是滿的。這意味着,即使對於只有插入的工作負載,沒有回滾或刪除,最終也可能只有75%的平均頁面利用率——因此這種內部頁面碎片的損失為25%。
索引是通過排序構建的,如果表有很多插入到索引中的隨機位置,就會導致頁分割。
為了做一個實驗,我創建了一個具有排序索引(降序)的表:
> show create table frag_page_spl\G *************************** 1. row *************************** Table: frag_page_spl Create Table: CREATE TABLE `frag_page_spl` ( `ID` varchar(64) NOT NULL, `TYPE` varchar(255) DEFAULT NULL, `TIME` datetime(3) NOT NULL, `USER_ID` varchar(255) DEFAULT NULL, `TASK_ID` varchar(64) DEFAULT NULL, `PINST_ID` varchar(64) DEFAULT NULL, `ACTION` varchar(255) DEFAULT NULL, `MESSAGE` varchar(4000) DEFAULT NULL, `FULL_MSG` longblob, PRIMARY KEY (`ID`), KEY `idx_pinstid` (`PINST_ID`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 1 row in set (0.00 sec)
可以通過INFORMATION_SCHEMA.INNODB_METRICS監控表的頁分裂活動。不過需要開啟innodb monitor。
> set global innodb_monitor_enable=all;
創建6個並發線程,隨機插入數據,完成后查看:
> select name,count,type,status,comment from information_schema.innodb_metrics where name like '%index_page_spl%'\G *************************** 1. row *************************** name: index_page_splits count: 52186 type: counter status: enabled comment: Number of index page splits 1 row in set (0.05 sec) mysql> SELECT -> table_schema as 'DATABASE', -> table_name as 'TABLE', -> CONCAT(ROUND(( data_length + index_length ) / ( 1024 * 1024 ), 2), 'M') 'TOTAL', -> CONCAT(ROUND(data_free / ( 1024 * 1024 ), 2), 'M') 'DATAFREE' -> FROM information_schema.TABLES -> where table_name='frag_page_spl'; +----------+---------------+----------+----------+ | DATABASE | TABLE. | TOTAL | DATAFREE | +----------+---------------+----------+----------+ | percona | frag_page_spl | 2667.55M | 127.92M | +----------+---------------+----------+----------+ 1 row in set (0.00 sec)
從innodb_metrics來看,我們可以看到頁面分裂計數器增加了。輸出顯示發生了52186個頁分割操作,創建了127.92 MB的碎片。
一旦創建了頁分裂,惟一的方法就是將創建的頁面降至合並閾值以下。當這種情況發生時,InnoDB通過合並操作將數據從分裂頁面中移出。MERGE_THRESHOLD對於表和特定的索引是可配置的。
另一種重新組織數據的方法是optimize表。這可能是一個代價昂貴和漫長的過程,但通常也處理太多頁面位於稀疏區域的情況中唯一方法。