本节介绍有关 压缩 的一些内部实现细节,适用于 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: 行太大
。
如果您在 innodb_strict_mode
为 OFF 的情况下创建表,并且后续的 INSERT
或 UPDATE
语句尝试创建不适合压缩页大小的索引条目,则操作将失败,并出现错误 ERROR 42000: 行大小太大
。(此错误消息不会命名记录过大的索引,也不会提及索引记录的长度或该特定索引页上的最大记录大小。)要解决此问题,请使用 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 会从磁盘读取压缩页(如果它尚未在缓冲池 中),然后将该页解压缩为其原始形式。本节介绍 InnoDB
如何在压缩表的页方面管理缓冲池。
为了最大程度地减少 I/O 并减少解压缩页的必要性,缓冲池有时包含数据库页的压缩形式和未压缩形式。为了腾出空间以容纳其他需要的数据库页,MySQL 可以从缓冲池中逐出未压缩页,同时将压缩页保留在内存中。或者,如果一段时间内没有访问某个页,则该页的压缩形式可能被写入磁盘,以释放其他数据的空间。因此,在任何给定时间,缓冲池可能包含页的压缩形式和未压缩形式,也可能只包含页的压缩形式,或者两者都不包含。
MySQL 使用最近最少使用(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 节,“通用表空间”。