Mysql之大字段溢出問題

Mysql版本

  1. 查看版本

    mysql> select version();
    +-----------+
    | version() |
    +-----------+
    | 5.6.44    |
    +-----------+
    1 row in set (0.00 sec)
    

InnoDB數據頁存儲

  1. InnoDB的數據頁默認16KB,數據頁在有新數據寫入時,會預留1/16的空間,預留出來的空間可用於後續的新紀錄寫入,減少頻繁的新增data page的開銷
  2. 每個數據頁至少存儲2行記錄,即理論上行記錄最大長度爲8KB(實際上要小於8KB,因爲頁中還有其他的數據結構佔用空間),最多存放(16KB/2-200)行記錄,即7992行記錄。
  3. 受限於InnoDB的存儲方式,如果數據是順序寫入的,理想情況下,數據頁的填充率是15/16,一般情況很難保證完全順序寫入,所以數據頁的填充率一般是1/2到15/16。從這裏可以看出,每個InnoDB表最好都有一個自增列作爲唯一主鍵,使得行記錄寫入儘可能是順序的。
  4. 當數據頁的填充率不足1/2,InnoDB會進行收縮,釋放空間。

行溢出(off-page)

  1. Mysql的varchar(N)中N指的是字符的長度,最大支持65535個字節,數據最大支持65532,因爲還有別的開銷。65535指的是一行中所有varchar列(字段)的長度總和

    1. latin1字符集創建65535個字節,失敗
    mysql> create table t_var2(
        ->  a varchar(65535)
        -> ) charset=latin1 ENGINE=INNODB;
    ERROR 1118 (42000): Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535. This includes storage overhead, check the manual. You have to change some columns to TEXT or BLOBs
    
    2. latin1字符集創建65532個字節,成功
    mysql> create table t_var2(
        ->  a varchar(65532)
        -> ) charset=latin1 ENGINE=INNODB;
    Query OK, 0 rows affected (0.05 sec)
    
    
    3. UTF-8或者GBK字符集創建65532個字節,沒有設置SQL_MODE爲嚴格模式,可以創建成功,但是有警告
    
    mysql> create table t_var3(
        ->  a varchar(65532)
        -> ) charset=UTF8 ENGINE=INNODB;
    Query OK, 0 rows affected, 1 warning (0.02 sec)
    
    警告含義:Mysql自動將varchar轉換爲text類型
    mysql> show WARNINGS \G;
    *************************** 1. row ***************************
      Level: Note
       Code: 1246
    Message: Converting column 'a' from VARCHAR to TEXT
    1 row in set (0.00 sec)
    
    查看錶結構驗證
    mysql> show create table t_var3 \G;
    *************************** 1. row ***************************
           Table: t_var3
    Create Table: CREATE TABLE `t_var3` (
      `a` mediumtext
    ) ENGINE=InnoDB DEFAULT CHARSET=utf8
    1 row in set (0.01 sec)
    
    
    4. 創建多個varchar列
    mysql> create table t_var4(
        ->  a varchar(20000),
        ->  b varchar(20000),
        ->  c varchar(20000),
        ->  d varchar(20000)
        -> ) charset=latin1 ENGINE=INNODB;
    ERROR 1118 (42000): Row size too large. The maximum row size for the used table type, not counting BLOBs, is 65535. This includes storage overhead, check the manual. You have to change some columns to TEXT or BLOBs
    
    
  2. InnoDB的頁爲16KB,即16384字節,一頁是無法存儲65532個字節的,此時就發生了行溢出,一般情況下,InnoDB的數據都是存放在頁類型爲B+Tree結點中。當發生行溢出時,數據存放在頁類型爲Uncompress BLOB頁中

  3. 什麼情況下會行溢出?只要一行記錄的總和超過8k,就會溢出,varchar(9000) 或者 varchar(3000) + varchar(3000) + varchar(3000),當實際長度大於8k的時候,就會溢出。所以Blob,text,一行數據如果實際長度大於8k會溢出,如果實際長度小於8k則不會溢出,並非所有的blob,text都會溢出

  4. 行溢出的問題

    • 溢出的數據不在存儲在B+Tree中
    • 溢出的數據使用的是uncompress BLOB page,並且存儲獨享

行格式

  1. MySQL 5.6版本的InnoDB引擎當前支持COMPACTREDUNDANTDYNAMICCOMPRESSED四種行格式,默認是COMPACT格式

  2. 可以通過如下命令查看錶使用的行格式,表t是Compact的行格式

    mysql> SHOW TABLE STATUS LIKE 't' \G;
    *************************** 1. row ***************************
               Name: t
             Engine: InnoDB
            Version: 10
         Row_format: Compact
               Rows: 6
     Avg_row_length: 2730
        Data_length: 16384
    Max_data_length: 0
       Index_length: 16384
          Data_free: 0
     Auto_increment: NULL
        Create_time: 2019-09-05 14:58:38
        Update_time: NULL
         Check_time: NULL
          Collation: utf8_general_ci
           Checksum: NULL
     Create_options:
            Comment:
    1 row in set (0.01 sec)
    
    
  3. MySQL 5.6 默認使用 innodb_file_format 爲 Antelope( compact 和 redundant 合稱爲Antelope)

    mysql> show variables like 'innodb_file_format';
    +--------------------+----------+
    | Variable_name      | Value    |
    +--------------------+----------+
    | innodb_file_format | Antelope |
    +--------------------+----------+
    1 row in set (0.00 sec)
    

Compact行記錄格式

  1. Compact 行記錄目標是高效地存儲數據,一個頁中存放的行數據越多,其性能就越高

  2. Compact 行記錄格式的首部是一個非 NULL 變長字段長度列表,並且其是按照列的順序逆序放置的,其長度爲

    • 若列的長度小於 255 字節,用 1 字節表示
    • 若列的長度大於 255 個字節,用 2 字節表示
  3. 變長字段的長度最大不可以超過 2 字節,這是因在 MySQL 數據庫中 VARCHAR 類型的最大長度限制爲 65535。對於 blob,text,varchar(8099) 這樣的大字段,innodb 只會存放前 768 字節在數據頁中,而剩餘的數據則會存儲在溢出段中,對於行溢出數據,其存放採用下圖中所示方法:

  4. 因爲InnoDB表是索引組織表,每個頁至少有兩行記錄,因此如果頁中不能存放下一條記錄,那麼InnoDB會自動將行數據放到溢出頁中,因爲默認頁是16KB,那麼一行數據如果超出8KB,則會出現錯誤

    CREATE TABLE `testblob` (
      `blob1` blob NOT NULL,
      `blob2` blob NOT NULL,
      `blob3` blob NOT NULL,
      `blob4` blob NOT NULL,
      `blob5` blob NOT NULL,
      `blob6` blob NOT NULL,
      `blob7` blob NOT NULL,
      `blob8` blob NOT NULL,
      `blob9` blob NOT NULL,
      `blob10` blob NOT NULL,
      `blob11` blob NOT NULL
    ) ENGINE=InnoDB DEFAULT CHARSET=latin1;
    
    insert into testblob select repeat('a',1000),repeat('b',1000),repeat('c',1000),repeat('d',1000),repeat('e',1000),repeat('f',1000),repeat('g',1000),repeat('h',1000),repeat('i',1000),repeat('j',1000),repeat('k',1000);
    
    出現錯誤:
    ERROR 1118 (42000): Row size too large (> 8126). Changing some columns to TEXT or BLOB or using ROW_FORMAT=DYNAMIC or ROW_FORMAT=COMPRESSED may help. In current row format, BLOB prefix of 768 bytes is stored inline.
    
  5. 總結

    • 外部存儲頁不共享,即使多出一個字節也是獨享16KB的頁面(若存儲字段值只是比行的要求多了一個字節,也會分配16KB頁面來存儲剩下的字節,浪費了頁面的大部分空間)。InnoDB一次只爲一個列分配一個頁的擴展存儲空間,直到使用了超過32個頁以後,就會一次性分配64個頁面,如果有一個值只是稍微超過了32個頁的大小,實際上就需要使用96個頁面。
    • 數據頁中存儲了768字節的數據,剩餘部分存儲到外部存儲頁中
    • 不管是char還是varchar,在compact格式下NULL值不佔用任何存儲空間
    • 當實際長度大於255的時候,變長字段長度列表需要用兩個字節存儲,也就意味着每一行數據都會增加1個字節
    • char的最大限制是N<=255字節,varchar 的最大限制是N<=65535字節,一行所有varchar列總和小於65535
    • innodb單個索引列的長度不能大於767 bytes;聯合索引長度和不能大於3072 bytes

Barracuda

  1. InnoDB 1. 0.x 版本開始引入了新的文件格式(file format,用戶可以理解爲新的頁格式),以前支持的 Compact 和 Redundant 格式稱爲 Antelope 文件格式,新的文件格式稱爲 Barracuda 文件格式。Barracuda 文件格式下擁有兩種新的行記錄格式:Compressed 和 Dynamic。

  2. 新的兩種記錄格式對於存放在 BLOB 中的數據採用了完全的行溢出的方式,如圖所示,在數據頁中只存放 20 個字節的指針,實際的數據都存放在 Off Page 中,而之前的 Compact 和 Redundant 兩種格式會存放 768 個前綴字節。

    1460000005160419

  3. Compressed 行記錄格式的另一個功能就是,存儲在其中的行數據會以 zlib 的算法進行壓縮,因此對於 BLOB、TEXT、VARCHAR 這類大長度類型的數據能夠進行非常有效的存儲,但要求更高的CPU,buffer pool裏面可能會同時存儲數據的壓縮版和非壓縮版,所以也多佔用部分內存。

  4. Dynamic格式存儲大數據的特點

    • 當數據頁放不下時,MySQL會將大數據全部放在外部存儲頁,數據頁只留指向外部存儲頁的指針(20字節)。
    • 外部存儲頁不共享,即使多餘一個字節也是獨享16KB的頁面
    • 列存儲是否放到off-page頁,主要取決於行大小,它會把行中最長的那一列放到off-page,直到數據頁能存放下兩行。TEXT/BLOB列 <=40 bytes 時總是存放於數據頁

總結

  1. 當一行中的數據不能在數據頁中放下,需要申請外部存儲頁時,MySQL需要決定將哪一列的數據放到外部存儲頁,遵循的規則如下:
    • 長度固定的字段不會被放到外部存儲頁(int、char(N)等)
    • 長度小於20字節的字段不會被放到外部存儲頁,如果放到外部存儲頁,不僅會單獨佔據16KB,還要額外的20字節指針,所以沒有必要
    • 對於Compact和REDUNDANT格式的行數據,長度小於768字節的字段不會被放到外部存儲頁,而是放到數據頁(B+Tree Node),長度大於768字節的,前768字節依然會放到數據頁,剩餘的會放到外部存儲頁(off-page)
    • 當有多個大數據字段滿足上面條件,需要被放到外部存儲頁時(比如一個7000字節,一個6000字節,需要選擇一個字段放到外部存儲頁時),MySQL會優先選擇大的字段放到外部存儲頁,因爲這樣可以最大限度的省下數據頁的空間,使得更多的字段能夠被放到數據頁。

如何優化大字段

  1. 這裏的大數據字段包括:varchar、varbinary、text、blob。
  2. 如果有多個大字段,儘可能將所有數據序列化、壓縮之後,存儲在同一個列裏,避免發生多次off-page行溢出(off-page)。
  3. 如果無法將所有列整合到一個列,可以退而求其次,根據每個列最大長度進行排列組合後拆分成多個子表,儘量使得每個子表的總行長度小於8KB,減少發生off-page的頻率
  4. 將text等大字段從主表中拆分出來
    • 存儲到key-value中
    • 存儲在單獨的一張子表中,並且壓縮必須保證一行記錄小於8k
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章