Mysql之大字段溢出问题

文章目录

  • Mysql版本
  • InnoDB数据页存储
    • 行溢出(off-page)
    • 行格式
      • Compact行记录格式
      • Barracuda
    • 总结
  • 如何优化大字段

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 字节在数据页中,而剩余的数据则会存储在溢出段中,对于行溢出数据,其存放采用下图中所示方法:

    Mysql之大字段溢出问题_第1张图片

  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 个前缀字节。

    Mysql之大字段溢出问题_第2张图片

  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

你可能感兴趣的:(mysql)