1. 程式人生 > >明明已經刪除了資料,可是表文件大小依然沒變

明明已經刪除了資料,可是表文件大小依然沒變

對於執行很長時間的資料庫來說,往往會出現表佔用儲存空間過大的問題,可是將許多沒用的表刪除之後,表文件的大小並沒有改變,想解決這個問題,就需要了解 InnoDB 如何回收表空間的。 對於一張表來說,佔用空間重要分為兩部分,表結構和表資料。通常來說,表結構定義佔用的空間很小。所以空間的問題主要和表資料有關。 在 MySQL 8.0 前,表結構儲存在以 .frm 為字尾的檔案裡。在 8.0,允許將表結構定義在系統資料表中。 ## 關於表資料的存放 可以將表資料存在共享表空間,或者單獨的檔案中,通過 `innodb_file_per_table` 來控制。 * 如果為 OFF ,表示存在系統共享表空間中,和資料字典一起 * 如果為 ON,每個 InnoDB 表結構儲存在 .idb 為字尾的檔案中 在 5.6.6 以後,預設值為 ON. > 建議將該引數設定為 ON,這樣在不需要時,通過 drop table 命令,系統就會直接刪除該檔案。 > > 但在共享表空間中,即使表刪掉,空間也不會回收。 ``` truncate = drop + create ``` ## 資料刪除流程 但有時使用 `delete `刪除資料時,僅僅刪除的是某些行,但這可能就會出現表空間沒有被回收的情況。 我們知道,MySQL InnoDB 中採用了 B+ 樹作為儲存資料的結構,也就是常說的索引組織表,並且資料時按照頁來儲存的。 在刪除資料時,會有兩種情況: * 刪除資料頁中的某些記錄 * 刪除整個資料頁的內容 比如想要刪除 R4 這條記錄: ![](https://img2020.cnblogs.com/blog/1861307/202008/1861307-20200819200040477-1911824236.png) InnoDB 直接將 R4 這條記錄標記為刪除,稱為可複用的位置。如果之後要插入 ID 在 300 到 700 間的記錄時,就會複用該位置。由此可見,磁碟檔案的大小並不會減少。 而且記錄的複用,只限於符合範圍條件的資料。之後要插入 ID 為 800 的記錄,R4 的位置就不能被複用了。 再比如要是刪除了整個資料頁的內容,假設刪除 R3 R4 R5,為 Page A 資料頁。 這時 InnoDB 就會將整個 Page A 標記為刪除狀態,之後整個資料都可以被複用,沒有範圍的限制。比如要插入 ID=50 的內容就可以直接複用。 並且如果兩個相鄰的資料頁利用率都很小,就會把兩個頁中的資料合到其中一個頁上,另一個頁標記為可複用。 綜上,無論是資料行的刪除還是資料頁的刪除,都是將其標記為刪除的狀態,用於複用,所以檔案並不會減小。對應到具體的操作就是使用 `delete` 命令. 而且,我們還可以發現,對於第一種刪除記錄的情況,由於複用時會有範圍的限制,所以就會出現很多空隙的情況,比如刪除 R4,插入的卻是 ID=800. ## 插入操作也會造成空隙 在插入資料時,如果資料按照索引遞增順序插入,索引的結構會是緊湊的。但如果是隨機插入的,很可能造成索引資料頁分裂。 比如給已滿的 Page A 插入資料。 ![](https://img2020.cnblogs.com/blog/1861307/202008/1861307-20200819200055254-274384333.png) 由於 Page A 滿了,所以要申請 Page B,調整 Page A 的過程到 Page B,這也稱為頁分裂。 結束後 Page A 就有了空隙。 另外對於更新操作也是,先刪除再插入,也會造成空隙。 進而對於大量進行增刪改的表,都有可能存在空洞。如果把空洞去掉,自然空間就被釋放了。 ## 使用重建表 為了把表中的空隙去掉,這時就可以採用重新建一個與表 A 結構相同的表 B,然後按照主鍵 ID 遞增的順序,把資料依次插入到 B 表中。 由於是順序插入,自然 B 表的空隙不存在,資料頁的利用率也更高。之後用表 B 代替表 A,好像起到了收縮表 A 空間的作用。 具體通過: ``` alter table A engine=InnoDB ``` 在 5.5 版本後,該命令和上面提到的流程差不多,而且 MySQL 會自己完成資料,交換表名,刪除舊錶的操作。 ![](https://img2020.cnblogs.com/blog/1861307/202008/1861307-20200819200134176-1786964998.png) 但這就有一個問題,在 DDL 中,表 A 不能有更新,此時有資料寫入表 A 的話,就會造成資料丟失。 在 5.6 版本後引入了 Online DDL。 ## Online DDL Online DDL 在其基礎上做了如下的更新: ![](https://img2020.cnblogs.com/blog/1861307/202008/1861307-20200819200152555-1810396263.png) 重建表的過程如下: 1. 建立一個臨時檔案,掃描表 A 主鍵的所有資料頁。 2. 用生成的資料頁生成 B+ 樹,儲存到臨時檔案中。 3. 生成臨時檔案時,如果有對 A 的操作,將其記錄在日誌檔案中,對應圖中 state 2 的狀態。 4. 臨時檔案生成後,將日誌檔案應用到臨時檔案中,得到與 A 表相同的資料檔案,對應 state 3 狀態。 5. 用臨時檔案替換 A 表的資料檔案。 由於 row log 日誌檔案存在,可以在重建表示,對錶 A 進行 DML 操作。 需要注意的是,在 alter 語句執行前,會先申請 MDL 寫鎖,但在拷貝資料前會退化成 MDL 讀鎖,從而支援 DML 操作。 至於為什麼不大 MDL 去掉,是防止其他執行緒對這個表同時做 DDL 操作。 對於大表來說,該操作很耗 IO 和 CPU 資源,所以在線上操作時,要控制操作時間。如果為了保證安全,推薦使用 [gh-ost](https://github.com/github/gh-ost) 來遷移。 ## Online 和 inplace 首先說一下 inplace 和 copy 的區別: 在 Online DDL 中,表 A 重建後的資料放在 `tmp_file` 中,這個臨時檔案是在 InnoDB 內部創建出來的。整個 DDL 在 InnoDB 內部完成。進而對於 Server 層來說,並沒有資料移動到臨時表中,是一個 "原地" 操作,所以叫 "inplace" . 而在之前普通的 DDL 中,建立後的表 A 是在 tmp_table 是 Server 建立的,所以叫 "copy" 對應到語句其實就是: ``` # alter table t engine=InnoDB 預設為下面 alter table t engine=innodb,ALGORITHM=inplace; # 走的就是 server 拷貝的過程 alter table t engine=innodb,ALGORITHM=copy; ``` 需要注意的是 inplace 和 Online 並不是對應關係: 1. DDL 過程是 Online,則一定是 inplace 2. 如果是 inplace 的 DDL 不應當是 Online,如在 <= 8.0, 新增全文索引和空間索引就屬於這種情況。 ## 拓展 說一下 optimize,analyze,alter table 三種重建表之間的區別: 1. alter table t engine = InnoDB(也就是 recreate)預設的是 Oline DDL 過程。 2. analyze table t 不是重建表,僅僅是對錶的索引資訊做重新統計,沒有修改資料,期間加 MDL 讀鎖。 3. optimize table t 等於上兩步的操作。 > 在事務裡面使用 alter table 預設會自動提交事務,保持事務一致性 如果有時,在重建某張表後,空間不僅沒有變小,甚至還變大了一點點。這時因為,重建的這張表本身沒有空隙,在 DDL 期間,剛好有一些 DML 執行,引入了一些新的空隙。 而且 InnoDB 不會把整張表填滿,每個頁留下 1/16 給後續的更新用,所以可能遠離是緊湊的,但重建後變成的稍有空隙。 ## 總結 現在我們知道,在使用 delete 刪除資料時,其實對應的資料行並不是真正的刪除,InnoDB 僅僅是將其標記成可複用的狀態,所以表空間不會變小。 通常來說,在標記複用空間時分為兩種,一種是僅將某些資料頁中的位置標記為刪除狀態,但這樣的位置只會在一定範圍內使用,會出現空隙的情況。 另一種是將整個資料頁標記成可複用的狀態,這樣的資料頁沒有限制,可直接複用。 為了解決這個問題,我們可以採用重建表的方式,其中在 5.6 版本後,建立表已經支援 Online 的操作,但最後是在業務低峰