MySQL 垂直分表的优缺点
- # mysql
- # database
MySQL 表变大以后,很多人第一反应是分库分表。但“分表”其实有两种很不一样的方向:
- 水平分表:按行拆,比如
orders_0、orders_1,不同订单落到不同表。 - 垂直分表:按列拆,比如把
users表中的基础信息留在主表,把简介、头像、偏好设置、扩展资料拆到user_profiles。
垂直分表解决的不是“单表行数太多”的问题,而是“单行太宽、冷热字段混在一起、常用查询被不常用大字段拖慢”的问题。
什么是垂直分表
垂直分表是把同一个业务实体的字段按访问频率、字段大小、职责边界拆到多张表中。拆分后,这些表通常仍然用同一个主键做一对一关系。
例如原来有一张用户表:
CREATE TABLE users (
id BIGINT PRIMARY KEY,
username VARCHAR(64) NOT NULL,
email VARCHAR(255) NOT NULL,
status TINYINT NOT NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL,
bio TEXT,
avatar_url VARCHAR(512),
settings TEXT,
address VARCHAR(512),
remark VARCHAR(1024)
);
如果大多数接口只需要 id、username、email、status 这些字段,而 bio、settings、remark 只有个人主页或设置页才会读,就可以拆成:
CREATE TABLE users (
id BIGINT PRIMARY KEY,
username VARCHAR(64) NOT NULL,
email VARCHAR(255) NOT NULL,
status TINYINT NOT NULL,
created_at DATETIME NOT NULL,
updated_at DATETIME NOT NULL
);
CREATE TABLE user_profiles (
user_id BIGINT PRIMARY KEY,
bio TEXT,
avatar_url VARCHAR(512),
settings TEXT,
address VARCHAR(512),
remark VARCHAR(1024)
);
这样拆完以后,用户实体仍然是同一个实体,只是“常用基础信息”和“不常用扩展信息”分别存放。
为什么垂直分表会变快
理解这个问题之前,可以先把 InnoDB 的存储层级想成这样:
tablespace
└── segment
└── extent = 64 pages = 1MB
└── page = 16KB
└── row = 一行记录
page 是 InnoDB 读写和缓存数据的基本单位。垂直分表讨论的“行变窄”最终会落到 page 这一层:一行越宽,一个 page 能放的 row 越少;一行越窄,一个 page 能放的 row 越多。
InnoDB 索引页默认大小是 16KB;顺序插入聚簇索引时,InnoDB 会尽量给页面保留 1/16 的空间,所以页面通常大约填充到 15/16。也就是说,一个顺序写入的叶子页可以粗略按:
16KB * 15 / 16 = 15KB
来估算可容纳的记录数据量。
这就是垂直分表的关键:如果常用查询必须读聚簇索引页,那么行越宽,一页能放的行越少,同样读 10 万 行需要扫过的页就越多。
假设一个用户表有如下实际平均长度:
| 字段组 | 平均每行大小 |
|---|---|
热字段:id、username、email、status、时间字段和少量行开销 | 160 bytes |
冷字段:bio、avatar_url、settings、address、remark | 2,900 bytes |
| 未拆分时合计 | 3,060 bytes |
按每页约 15KB = 15,360 bytes 估算:
| 表结构 | 平均行大小 | 每页约可放行数 | 读取 10 万行约需页数 |
|---|---|---|---|
| 未垂直拆分 | 3,060 bytes | 5 行 | 20,000 页 |
拆分后的 users 主表 | 160 bytes | 96 行 | 1,042 页 |
这是一个估算,不是所有线上场景都会得到 20 倍 的性能提升。但它说明了一个很实际的方向:当列表页、后台检索、状态扫描这类请求只需要热字段时,垂直分表可以显著减少需要读取和缓存的数据页。
如果数据库的 buffer pool 很紧张,这个收益会更明显。未拆分时,缓存 10 万 个用户的热字段可能顺带把大量 bio、settings、remark 一起塞进缓存;拆分后,同样的缓存空间可以容纳更多热数据。
优点
-
减少热路径数据量:把冷字段和大字段拆走后,主表更窄,页密度更高,列表、检索、状态扫描这类查询会更轻。
-
字段职责更清晰:比如
users保存基础账号信息,user_profiles保存资料,user_settings保存偏好配置,字段边界和模块边界会更容易理解。 -
降低主表写入压力:简介、头像、设置项这类字段更新时,不必改动主表记录,可以减少主表写入和缓存污染。
-
便于单独管理:敏感字段、可归档字段、可清理字段拆到独立表后,更容易单独做访问控制、备份和数据清理。
缺点
-
查询变复杂:原来一次主键查询可以拿到所有信息,拆分后可能需要
JOIN,或者在应用层多查一次再组装。 -
可能拖慢完整查询:如果一个接口每次都需要完整用户信息,垂直分表反而可能变慢。拆表前应该先看真实查询比例,而不是只看表字段多不多。
-
一致性成本上升:新增、删除、回填数据时,应用代码和补偿脚本都要同时考虑多张表。
-
索引设计更麻烦:查询条件一旦跨主表和扩展表,就需要在多表之间权衡索引设计,拆得太细反而不好控制查询计划。
-
开发心智负担更高:表越多,模型、DAO、迁移脚本、数据订正脚本和排障 SQL 都会变多。
什么时候适合垂直分表
适合垂直分表的场景通常有几个共同特征:
- 一张表有明显的冷热字段,常用接口只读少量字段。
- 表中有较大的
TEXT、BLOB、JSON、长VARCHAR字段。 - 主表列表查询、范围扫描、分页查询压力较大。
- 扩展字段很多,而且不同业务模块维护不同字段。
- 敏感字段需要单独控制权限、审计、备份或清理策略。
不太适合垂直分表的场景也很明确:
- 表很小,访问压力不高。
- 大多数查询都需要完整字段。
- 业务还不稳定,字段边界经常变化。
- 团队还没有能力维护多表事务、迁移和数据一致性。
一个更稳妥的拆分步骤
垂直分表最好不要一上来就按“领域模型”拆很多张表,而是从查询数据出发。
第一步,找出真实热路径。可以看慢查询、接口 QPS、performance_schema、应用日志,确认哪些 SQL 最频繁、最耗时、最依赖主表扫描。
第二步,统计字段使用率。比如用户列表接口是否真的需要 bio?后台检索是否真的需要 settings?如果一个字段只有详情页使用,就有拆出去的价值。
第三步,估算行宽变化。可以用线上抽样数据估算字段平均长度,再结合 InnoDB 页大小粗略计算拆分前后的页密度。这个估算不需要特别精确,但要能说明收益是否足够覆盖复杂度。
第四步,先拆最冷、最大的字段。通常优先拆长文本、配置、描述、备注、富媒体 URL、非核心扩展信息,而不是一开始就拆几十张表。
第五步,保留兼容期。可以先双写,再回填历史数据,最后让读路径切到新表。对线上系统来说,迁移过程比最终表结构更容易出问题。