本節說明關於 InnoDB 資料表壓縮的一些內部實作細節。此處提供的資訊可能對效能調校有幫助,但對於壓縮的基本使用而言並非必要知悉。
壓縮演算法
某些作業系統會在檔案系統層級實作壓縮。檔案通常會分割成固定大小的區塊,這些區塊會壓縮成可變大小的區塊,這很容易導致碎片。每次修改區塊內部的任何內容時,都會在寫入磁碟之前重新壓縮整個區塊。這些特性使得這種壓縮技術不適合在更新密集型資料庫系統中使用。
MySQL 藉由知名的 zlib 函式庫實作壓縮,該函式庫實作了 LZ77 壓縮演算法。此壓縮演算法成熟、穩健,並且在 CPU 使用率和資料大小縮減方面都很有效率。此演算法是「無損」的,因此永遠可以從壓縮形式重建原始的未壓縮資料。LZ77 壓縮的工作原理是尋找要壓縮的資料中重複的資料序列。資料中的值模式會決定壓縮效果,但一般使用者資料通常會壓縮 50% 或更多。
與應用程式執行的壓縮或某些其他資料庫管理系統的壓縮功能不同,InnoDB 壓縮同時適用於使用者資料和索引。在許多情況下,索引可能佔資料庫總大小的 40-50% 或更多,因此這種差異非常顯著。當壓縮對資料集運作良好時,InnoDB 資料檔案(單表檔案表空間或通用表空間 .ibd
檔案)的大小會是未壓縮大小的 25% 到 50% 或可能更小。根據工作負載,這個較小的資料庫反過來可以減少 I/O 並提高吞吐量,而 CPU 使用率的增加成本適中。您可以透過修改 innodb_compression_level
設定選項來調整壓縮級別和 CPU 負擔之間的平衡。
InnoDB 資料儲存和壓縮
InnoDB 資料表中的所有使用者資料都儲存在包含 B 樹索引(叢集索引)的頁面中。在其他一些資料庫系統中,這種索引類型稱為「索引組織表」。索引節點中的每一列都包含(使用者指定或系統產生的)主索引鍵的值以及資料表的所有其他欄位。
InnoDB 資料表中的次要索引也是 B 樹,包含數值對:索引鍵和指向叢集索引中一列的指標。該指標實際上是資料表的主索引鍵的值,如果需要索引鍵和主索引鍵之外的欄位,則會使用該值來存取叢集索引。次要索引記錄必須始終適合單個 B 樹頁面。
B 樹節點(叢集索引和次要索引)的壓縮處理方式與用於儲存長 VARCHAR
、BLOB
或 TEXT
欄位的溢位頁面壓縮不同,如下節所述。
B 樹頁面的壓縮
由於 B 樹頁面經常更新,因此需要特殊處理。最小化 B 樹節點分割的次數,以及最小化解壓縮和重新壓縮其內容的需求非常重要。
MySQL 使用的一種技術是在 B 樹節點中以未壓縮的形式維護一些系統資訊,從而方便某些就地更新。例如,這允許對列進行刪除標記和刪除,而無需任何壓縮操作。
此外,MySQL 嘗試避免在索引頁面變更時進行不必要的解壓縮和重新壓縮。在每個 B 樹頁面內,系統會保留一個未壓縮的「修改日誌」,以記錄對頁面所做的變更。小型記錄的更新和插入可以寫入此修改日誌,而無需完全重新建構整個頁面。
當修改日誌的空間用完時,InnoDB 會解壓縮頁面,套用變更並重新壓縮頁面。如果重新壓縮失敗(稱為壓縮失敗的情況),則 B 樹節點會分割,並且重複此過程直到更新或插入成功。
為了避免在寫入密集型工作負載(例如,OLTP 應用程式)中頻繁發生壓縮失敗,MySQL 有時會在頁面中保留一些空白空間(填補),以便修改日誌更快填滿,並且在仍有足夠空間避免分割的情況下重新壓縮頁面。每個頁面中保留的填補空間大小會隨著系統追蹤頁面分割的頻率而變化。在頻繁寫入壓縮資料表的繁忙伺服器上,您可以調整 innodb_compression_failure_threshold_pct
和 innodb_compression_pad_pct_max
設定選項來微調此機制。
一般而言,MySQL 要求 InnoDB 資料表中的每個 B 樹頁面至少可以容納兩筆記錄。對於壓縮資料表,此要求已放寬。B 樹節點的葉子頁面(無論是主索引鍵還是次要索引)只需要容納一筆記錄,但該記錄必須以未壓縮的形式符合每頁修改日誌的要求。如果 innodb_strict_mode
為 ON
,MySQL 會在 CREATE TABLE
或 CREATE INDEX
期間檢查最大列大小。如果列不符合,則會發出以下錯誤訊息:ERROR HY000: Too big row
。
如果在 innodb_strict_mode
為 OFF 時建立資料表,並且後續的 INSERT
或 UPDATE
陳述式嘗試建立不符合壓縮頁面大小的索引項目,則該操作會失敗並出現 ERROR 42000: Row size too large
。(此錯誤訊息不會命名記錄過大的索引,也不會提及索引記錄的長度或該特定索引頁面上的最大記錄大小。) 若要解決此問題,請使用 ALTER TABLE
重新建立資料表,並選擇較大的壓縮頁面大小 (KEY_BLOCK_SIZE
),縮短任何欄位前綴索引,或使用 ROW_FORMAT=DYNAMIC
或 ROW_FORMAT=COMPACT
完全停用壓縮。
innodb_strict_mode
不適用於通用表空間,後者也支援壓縮資料表。通用表空間的表空間管理規則會獨立於 innodb_strict_mode
嚴格執行。如需更多資訊,請參閱第 15.1.21 節「CREATE TABLESPACE 陳述式」。
壓縮 BLOB、VARCHAR 和 TEXT 欄位
在 InnoDB 資料表中,不在主索引鍵中的 BLOB
、VARCHAR
和 TEXT
欄位可能會儲存在單獨配置的溢位頁面上。我們將這些欄位稱為頁面外欄位。它們的值會儲存在溢位頁面的單一連結清單中。
對於在 ROW_FORMAT=DYNAMIC
或 ROW_FORMAT=COMPRESSED
中建立的資料表,BLOB
、TEXT
或 VARCHAR
欄位的值可能會完全儲存在頁面外,具體取決於其長度和整個列的長度。對於儲存在頁面外的欄位,叢集索引記錄僅包含指向溢位頁面的 20 位元組指標,每個欄位一個指標。是否將任何欄位儲存在頁面外取決於頁面大小和列的總大小。當列太長而無法完全放入叢集索引的頁面時,MySQL 會選擇最長的欄位進行頁面外儲存,直到列符合叢集索引頁面的大小為止。如上所述,如果列本身不符合壓縮頁面大小,則會發生錯誤。
使用 ROW_FORMAT=REDUNDANT
和 ROW_FORMAT=COMPACT
的資料表會將 BLOB
、VARCHAR
和 TEXT
欄位的前 768 個位元組與主索引鍵一起儲存在叢集索引記錄中。768 位元組的前綴後接一個 20 位元組指標,指向包含剩餘欄位值的溢位頁面。
當資料表處於 COMPRESSED
格式時,所有寫入溢位頁面的資料都會以「原樣」壓縮;也就是說,MySQL 會將 zlib 壓縮演算法套用至整個資料項目。除了資料之外,壓縮的溢位頁面還包含未壓縮的頁首和頁尾,其中包括頁面總和檢查碼和指向下一個溢位頁面的連結等。因此,如果資料具有高度可壓縮性(文字資料通常如此),則可以為較長的 BLOB
、TEXT
或 VARCHAR
欄位取得非常顯著的儲存節省效果。影像資料(例如 JPEG
)通常已經過壓縮,因此從儲存在壓縮資料表中獲益不大;雙重壓縮可能會浪費 CPU 週期,但幾乎不會或根本不會節省空間。
溢位頁面與其他頁面的大小相同。包含十個儲存在頁面外的欄位的列會佔用十個溢位頁面,即使這些欄位的總長度只有 8K 位元組也是如此。在未壓縮的資料表中,十個未壓縮的溢位頁面會佔用 160K 位元組。在具有 8K 頁面大小的壓縮資料表中,它們僅佔用 80K 位元組。因此,對於具有長欄位值的資料表,使用壓縮資料表格式通常更有效率。
對於單表單檔表空間,使用 16K 壓縮頁面大小可以減少 BLOB
、VARCHAR
或 TEXT
資料行的儲存和 I/O 成本,因為這些資料通常可以很好地壓縮,因此即使 B 樹節點本身佔用的頁面數量與未壓縮形式相同,也可能需要較少的溢位頁面。一般表空間不支援 16K 壓縮頁面大小 (KEY_BLOCK_SIZE
)。有關更多資訊,請參閱第 17.6.3.3 節「一般表空間」。
壓縮與 InnoDB 緩衝池
在壓縮的 InnoDB
表中,每個壓縮頁面(無論是 1K、2K、4K 還是 8K)都對應一個 16K 位元組的未壓縮頁面(如果設定 innodb_page_size
,則大小會更小)。若要存取頁面中的資料,如果 MySQL 的 緩衝池中還沒有該頁面,則 MySQL 會從磁碟讀取壓縮頁面,然後將頁面解壓縮為原始形式。本節將說明 InnoDB
如何管理具有壓縮表頁面的緩衝池。
為了最大限度地減少 I/O 並減少解壓縮頁面的需求,緩衝池有時會同時包含資料庫頁面的壓縮和未壓縮形式。為了騰出空間給其他所需的資料庫頁面,MySQL 可以從緩衝池逐出未壓縮的頁面,同時將壓縮的頁面留在記憶體中。或者,如果頁面有一段時間未被存取,則頁面的壓縮形式可能會被寫入磁碟,以釋放空間給其他資料。因此,在任何給定時間,緩衝池可能同時包含頁面的壓縮和未壓縮形式,或者僅包含頁面的壓縮形式,或者兩者都不包含。
MySQL 使用最近最少使用 (Least-Recently-Used, LRU) 清單來追蹤要保留在記憶體中的頁面以及要逐出的頁面,以便熱門(經常存取)資料傾向於保留在記憶體中。存取壓縮表時,MySQL 會使用自適應 LRU 演算法,以在記憶體中達成壓縮頁面和未壓縮頁面的適當平衡。此自適應演算法會感知系統是以 I/O 限制還是 CPU 限制的方式執行。目標是避免在 CPU 繁忙時花費太多處理時間解壓縮頁面,並避免在 CPU 有閒置週期可用於解壓縮可能已在記憶體中的壓縮頁面時執行過多的 I/O。當系統受 I/O 限制時,演算法會優先逐出頁面的未壓縮副本,而不是兩個副本都逐出,以便為其他磁碟頁面駐留在記憶體中騰出更多空間。當系統受 CPU 限制時,MySQL 會優先逐出壓縮和未壓縮頁面,以便將更多記憶體用於「「熱門」」頁面,並減少僅以壓縮形式在記憶體中解壓縮資料的需求。
壓縮與 InnoDB 重做記錄檔
在將壓縮頁面寫入資料檔之前,MySQL 會將頁面的副本寫入重做記錄(如果自上次寫入資料庫以來,頁面已重新壓縮)。這樣做是為了確保重做記錄可用於當機復原,即使在不太可能發生的情況下,zlib
函式庫已升級,而該變更會為壓縮資料帶來相容性問題。因此,使用壓縮時,記錄檔的大小可能會增加,或者需要更頻繁的檢查點。記錄檔大小或檢查點頻率的增加量取決於壓縮頁面被修改的次數,這些修改會導致需要重組和重新壓縮。
若要在單表單檔表空間中建立壓縮表,必須啟用 innodb_file_per_table
。在一般表空間中建立壓縮表時,並不需要依賴 innodb_file_per_table
設定。有關更多資訊,請參閱第 17.6.3.3 節「一般表空間」。