本节介绍有关 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 树节点(聚簇索引和辅助索引的节点)的压缩与用于存储长 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 会从磁盘读取压缩页面(如果它尚未在 缓冲池 中),然后将页面解压缩到其原始形式。本节介绍 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 节,“通用表空间”。