13 | 爲何表數據刪掉一半,表文件大小不變?

常常會有同窗來問我,個人數據庫佔用空間太大,我把一個最大的表刪掉了一半的數據,怎麼表文件的大小仍是沒變?數據庫

那麼今天,我就和你聊聊數據庫表的空間回收,看看如何解決這個問題。安全

這裏,咱們仍是針對MySQL中應用最普遍的InnoDB引擎展開討論。一個InnoDB表包含兩部分,即:表結構定義和數據。在MySQL 8.0版本之前,表結構是存在以.frm爲後綴的文件裏。而MySQL 8.0版本,則已經容許把表結構定義放在系統數據表中了。由於表結構定義佔用的空間很小,因此咱們今天主要討論的是表數據。性能

接下來,我會先和你說明爲何簡單地刪除表數據達不到表空間回收的效果,而後再和你介紹正確回收空間的方法。優化

參數innodb_file_per_table

表數據既能夠存在共享表空間裏,也能夠是單獨的文件。這個行爲是由參數innodb_file_per_table控制的:spa

  1. 這個參數設置爲OFF表示的是,表的數據放在系統共享表空間,也就是跟數據字典放在一塊兒;線程

  2. 這個參數設置爲ON表示的是,每一個InnoDB表數據存儲在一個以 .ibd爲後綴的文件中。日誌

從MySQL 5.6.6版本開始,它的默認值就是ON了。code

我建議你不論使用MySQL的哪一個版本,都將這個值設置爲ON。由於,一個表單獨存儲爲一個文件更容易管理,並且在你不須要這個表的時候,經過drop table命令,系統就會直接刪除這個文件。而若是是放在共享表空間中,即便表刪掉了,空間也是不會回收的。server

因此,將innodb_file_per_table設置爲ON,是推薦作法,咱們接下來的討論都是基於這個設置展開的。blog

咱們在刪除整個表的時候,可使用drop table命令回收表空間。可是,咱們遇到的更多的刪除數據的場景是刪除某些行,這時就遇到了咱們文章開頭的問題:表中的數據被刪除了,可是表空間卻沒有被回收。

咱們要完全搞明白這個問題的話,就要從數據刪除流程提及了。

數據刪除流程

咱們先再來看一下InnoDB中一個索引的示意圖。在前面第4第5篇文章中,我和你介紹索引時曾經提到過,InnoDB裏的數據都是用B+樹的結構組織的。

圖1 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已經滿了,這時我要再插入一行數據,會怎樣呢?

圖2 插入數據致使頁分裂

能夠看到,因爲page A滿了,再插入一個ID是550的數據時,就不得再也不申請一個新的頁面page B來保存數據了。頁分裂完成後,page A的末尾就留下了空洞(注意:實際上,可能不止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名字的來源。

我記得有同窗在第6篇講表鎖的文章《全局鎖和表鎖 :給表加個字段怎麼索這麼多阻礙?》的評論區留言說,DDL以前是要拿MDL寫鎖的,這樣還能叫Online DDL嗎?

確實,圖4的流程中,alter語句在啓動的時候須要獲取MDL寫鎖,可是這個寫鎖在真正拷貝數據以前就退化成讀鎖了。

爲何要退化呢?爲了實現Online,MDL讀鎖不會阻塞增刪改操做。

那爲何不乾脆直接解鎖呢?爲了保護本身,禁止其餘線程對這個表同時作DDL。

而對於一個大表來講,Online DDL最耗時的過程就是拷貝數據到臨時表的過程,這個步驟的執行期間能夠接受增刪改操做。因此,相對於整個DDL過程來講,鎖的時間很是短。對業務來講,就能夠認爲是Online的。

須要補充說明的是,上述的這些重建方法都會掃描原表數據和構建臨時文件。對於很大的表來講,這個操做是很消耗IO和CPU資源的。所以,若是是線上服務,你要很當心地控制操做時間。若是想要比較安全的操做的話,我推薦你使用GitHub開源的gh-ost來作。

Online 和 inplace

說到Online,我還要再和你澄清一下它和另外一個跟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的。

若是說這兩個邏輯之間的關係是什麼的話,能夠歸納爲:

  1. DDL過程若是是Online的,就必定是inplace的;

  2. 反過來未必,也就是說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。

小結

今天這篇文章,我和你討論了數據庫中收縮表空間的方法。

如今你已經知道了,若是要收縮一個表,只是delete掉表裏面不用的數據的話,表文件的大小是不會變的,你還要經過alter table命令重建表,才能達到表文件變小的目的。我跟你介紹了重建表的兩種實現方式,Online DDL的方式是能夠考慮在業務低峯期使用的,而MySQL 5.5及以前的版本,這個命令是會阻塞DML的,這個你須要特別當心。

最後,又到了咱們的課後問題時間。

假設如今有人碰到了一個「想要收縮表空間,結果拔苗助長」的狀況,看上去是這樣的:

  1. 一個表t文件大小爲1TB;

  2. 對這個表執行 alter table t engine=InnoDB;

  3. 發現執行完成後,空間不只沒變小,還稍微大了一點兒,好比變成了1.01TB。

你以爲多是什麼緣由呢 ?

你能夠把你以爲可能的緣由寫在留言區裏,我會在下一篇文章的末尾把你們描述的合理的緣由都列出來,之後其餘同窗就不用掉到這樣的坑裏了。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一塊兒閱讀。

上期問題時間

在上期文章最後,我留給你的問題是,若是一個高配的機器,redo log設置過小,會發生什麼狀況。

每次事務提交都要寫redo log,若是設置過小,很快就會被寫滿,也就是下面這個圖的狀態,這個「環」將很快被寫滿,write pos一直追着CP。

這時候系統不得不中止全部更新,去推動checkpoint。

這時,你看到的現象就是磁盤壓力很小,可是數據庫出現間歇性的性能下跌。

相關文章
相關標籤/搜索