首页
学习
活动
专区
圈层
工具
发布
首页
学习
活动
专区
圈层
工具
MCP广场
社区首页 >专栏 >[MYSQL] row_format=compressed的存储结构浅析

[MYSQL] row_format=compressed的存储结构浅析

原创
作者头像
大大刺猬
发布2025-07-18 18:14:16
发布2025-07-18 18:14:16
17000
代码可运行
举报
文章被收录于专栏:大大刺猬大大刺猬
运行总次数:0
代码可运行

导读

我们之前已经介绍了3种row_format格式:REDUNDANT,COMPACT,DYNAMIC. 现在来讲最后一种:COMPRESSED

有的小伙伴可能会疑惑之前不是讲过压缩吗? 就那个zlib和lz4那俩啊. 那俩是PAGE级别的压缩,除了FSP的'page'都压; 今天讲的是行级别的压缩,只压缩'行'.

行压缩的结构

行格式为压缩的表的创建方式

代码语言:sql
复制
-- 建表时指定为压缩行
create table t20250718_1(id int, c1 varchar(200)) row_format=compressed;

-- 建表后修改为压缩表
alter table t20250718_1 row_format=compressed;

-- 设置页大小为4K
create table db1.t20250718_2(id int) row_format=compressed KEY_BLOCK_SIZE=4;

很简单, 就是执行row_format=compressed.

我们知道Innodb会对表初始分配7个page, 8K的page大小就为56K, 4K的page大小为28K, 所以对于16384的page压缩默认是8K

既然叫行的压缩, 那压缩的肯定就算数据行, 也就是只对FIL_PAGE_INDEX有效. 诶, FIL_PAGE_SDI 也算是FIL_PAGE_INDEX的变种啊, 相当于固定结构的表而已.

既然表元数据信息都可能是压缩的了, 那问题来了, 怎么确定这个表是压缩的呢?

总不能一点点猜吧....

确认表的压缩大小

既然元数据信息被压缩了, 那我们就找更元的元数据信息--FSP, 这个页不会被加密和压缩的. 其中有个叫FSP_SPACE_FLAGS的东西, 如下图::

对于计算方式可参考如下py代码(include/fsp0types.h):

代码语言:python
代码运行次数:0
运行
复制
def GET_FSP_STATUS_FROM_FLAGS(flags):
        logical_size = 16384 if ((flags & 960) >> 6) == 0 else 512 << ((flags & 960) >> 6)
        physical_size = logical_size if ((flags & 30) >> 1) == 0 else 512<<((flags & 30) >> 1)
        compressed = False if ((flags & 30) >> 1) == 0 else True
        return {
                'POST_ANTELOPE':(flags & 1) >> 0,
                'ZIP_SSIZE':(flags & 30) >> 1,
                'ATOMIC_BLOBS':(flags & 32) >> 5,
                'PAGE_SSIZE':(flags & 960) >> 6,
                'DATA_DIR':(flags & 1024) >> 10,
                'SHARED':(flags & 2048) >> 11,
                'TEMPORARY':(flags & 4096) >> 12,
                'ENCRYPTION':(flags & 8192) >> 13,
                'SDI':(flags & 16384) >> 14,
                'logical_size':logical_size, # logical page size (in memory)
                'physical_size':physical_size, # physical page size (in disk)
                'compressed':compressed
        }

这里的ZIP_SSIZE就是实际上在磁盘上存储的大小, 使用4bit表示(512一个block). 比如值为4时, 物理块大小就为: 512<<4(8192), 即磁盘上的块大小为8192.

PAGE_SSIZE就是逻辑大小, 也就是innodb_page_size. 数据解析的时候, 我们先要把压缩后的大小(ZIP_SSIZE)解压到压缩前的大小(PAGE_SSIZE) 然后就可以当作普通页(DYNAMIC)来处理了.

压缩行结构

如果每次数据更新我们都要压缩和解压的话, 成本有点高啊.

所以应该只压缩其中的一部分数据,

新insert进来的, 就直接放压缩数据后面, 待空间不够之后再去压缩.

而要删除的数据也是应该只打标记即可. 于是我们翻阅源码(page_zip_decompress_low)后得到如下结构:

对象

大小

描述

FIL_HEADER+PAGE_HEADER

94

页基础信息

compressed_data

x

压缩的数据

uncompressed_data

y

未压缩部分的数据

.....

未使用的空间

overflow page

20*m

溢出页的记录信息, 还是每条20字节

trx_id+rollptr

13*n

事务和回滚指针相关信息

page diretory

2*n

page dir信息 (基于压缩前的页)

看起来合情合理, 那我们简单的验证下呢.

验证压缩行结构

我们就没必要把页大小还原回去了. 我们直接开解:

先准备测试数据

代码语言:sql
复制
create table db1.t20250718_compressed(c1 int, c2 varchar(20), c3 text) row_format=compressed;
insert into  db1.t20250718_compressed values(1,'xx','yy');
insert into  db1.t20250718_compressed values(1,'zz',repeat('a',10000));

然后使用python来打开ibd文件并解析. 先看下fsp中的FSP_SPACE_FLAGS的相关信息

代码语言:python
代码运行次数:0
运行
复制
# 声明 GET_FSP_STATUS_FROM_FLAGS 略(见上文)
import struct,zlib
f = open('/data/mysql_3314/mysqldata/db1/t20250718_compressed.ibd','rb')
data = f.read(38+112)
FSP_SPACE_FLAGS = struct.unpack('>L',data[38+16:][:4])[0]
print(GET_FSP_STATUS_FROM_FLAGS(FSP_SPACE_FLAGS))

我们可以看到:物理页大小是8K, 逻辑页大小是16K, 存在sdi信息....

然后我们来解析数据, 由于我们不知道压缩的数据大小, 所以我们得基于流来解析,故使用zlib.decompressobj

代码语言:python
代码运行次数:0
运行
复制
f.seek(8192*4,0)
data = f.read(8192)
d = zlib.decompressobj()
c = d.decompress(data[94:])
# 初始化基础页信息
unpage = data[:94]
# infimum & supremum
unpage += struct.pack('>BBB',0x01,0x00,0x02)
unpage += data[-2:]
unpage += struct.pack('>8B',0x69, 0x6e, 0x66, 0x69, 0x6d, 0x75, 0x6d, 0x00)
unpage += b'\x03'
unpage += struct.pack('>12B',0x00,0x0b,0x00,0x00,0x73,0x75,0x70,0x72,0x65,0x6d,0x75,0x6d)
# 加上压缩页信息
unpage += c
# 加上未压缩部分的信息
unpage += d.unused_data

# 解析page dir信息:
n_dense = struct.unpack('>H',data[42:44])[0] & 32767
page_dir = struct.unpack(f'>{n_dense-2}H',data[-(2*(n_dense-2)):])
print(page_dir )

看到了熟悉的offset, 这里的page dir是记录的所有行(不含infimum & supremum)的偏移量. 然后我们来解析trxid和rollptr

代码语言:python
代码运行次数:0
运行
复制
# trxid&rollptr
trxid_rollptr_data = data[-(2*rows+13*rows):-(2*rows)]
for i in range(rows):
	print(int.from_bytes(trxid_rollptr_data[i*13:][:6],'big'),int.from_bytes(trxid_rollptr_data[i*13:][6:13],'big'))

这部分信息其实没多大用...

然后我们再来看数据

代码语言:python
代码运行次数:0
运行
复制
f.seek(8192*4,0)
data = f.read(8192)
d = zlib.decompressobj()
c = d.decompress(data[94:])
# 初始化基础页信息
unpage = data[:94]
# infimum & supremum
unpage += struct.pack('>BBB',0x01,0x00,0x02)
unpage += data[-2:]
unpage += struct.pack('>8B',0x69, 0x6e, 0x66, 0x69, 0x6d, 0x75, 0x6d, 0x00)
unpage += b'\x03'
unpage += struct.pack('>12B',0x00,0x0b,0x00,0x00,0x73,0x75,0x70,0x72,0x65,0x6d,0x75,0x6d)
# 加上压缩页信息
unpage += c
# 加上未压缩部分的信息
unpage += d.unused_data

我们的压缩数据部分为啥只有个b'\r\x1b\x08\x00~\x01'呢? 这也不是我们的数据啊!

前面有讲: Insert部分数据会先先非压缩部分, 不然每次都解压又压缩的, 成本老高了.

那这6字节是啥呢? 你猜(提示:可以更改表结构,数据类型来观察其变化)

如果此时我们重新设置表row_format=compressed,则会将之前未压缩部分的数据进行压缩.

代码语言:sql
复制
alter table db1.t20250718_compressed row_format=compressed;

细心的小伙伴可能会发现压缩前每行数据前面好像有个 ID/NO 之类的东西, 但是压缩进来后就没了

溢出页我们就不看了, 格式是和DYNAMIC一样的, 只是存储的位置换到了rollptr位置处.

总结

mysql有2种压缩方式, 1种是基于行的(row_format=compressed),另一种是基于page的(compression=zlib/lz4); 后者需要OS的文件系统支持才行. 当然我们的ibd2sql下个版本也会支持这种格式的.

参考:

https://dev.mysql.com/doc/refman/8.0/en/innodb-row-format.html

https://github.com/mysql/mysql-server/tree/trunk/storage/innobase

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 导读
  • 行压缩的结构
    • 确认表的压缩大小
    • 压缩行结构
    • 验证压缩行结构
  • 总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档