← 返回文章列表

MySQL 垂直分表的优缺点

后端
  • # mysql
  • # database

MySQL 表变大以后,很多人第一反应是分库分表。但“分表”其实有两种很不一样的方向:

  • 水平分表:按行拆,比如 orders_0orders_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)
);

如果大多数接口只需要 idusernameemailstatus 这些字段,而 biosettingsremark 只有个人主页或设置页才会读,就可以拆成:

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 万 行需要扫过的页就越多。

假设一个用户表有如下实际平均长度:

字段组平均每行大小
热字段:idusernameemailstatus、时间字段和少量行开销160 bytes
冷字段:bioavatar_urlsettingsaddressremark2,900 bytes
未拆分时合计3,060 bytes

按每页约 15KB = 15,360 bytes 估算:

表结构平均行大小每页约可放行数读取 10 万行约需页数
未垂直拆分3,060 bytes5 行20,000 页
拆分后的 users 主表160 bytes96 行1,042 页

这是一个估算,不是所有线上场景都会得到 20 倍 的性能提升。但它说明了一个很实际的方向:当列表页、后台检索、状态扫描这类请求只需要热字段时,垂直分表可以显著减少需要读取和缓存的数据页。

如果数据库的 buffer pool 很紧张,这个收益会更明显。未拆分时,缓存 10 万 个用户的热字段可能顺带把大量 biosettingsremark 一起塞进缓存;拆分后,同样的缓存空间可以容纳更多热数据。

优点

  • 减少热路径数据量:把冷字段和大字段拆走后,主表更窄,页密度更高,列表、检索、状态扫描这类查询会更轻。

  • 字段职责更清晰:比如 users 保存基础账号信息,user_profiles 保存资料,user_settings 保存偏好配置,字段边界和模块边界会更容易理解。

  • 降低主表写入压力:简介、头像、设置项这类字段更新时,不必改动主表记录,可以减少主表写入和缓存污染。

  • 便于单独管理:敏感字段、可归档字段、可清理字段拆到独立表后,更容易单独做访问控制、备份和数据清理。

缺点

  • 查询变复杂:原来一次主键查询可以拿到所有信息,拆分后可能需要 JOIN,或者在应用层多查一次再组装。

  • 可能拖慢完整查询:如果一个接口每次都需要完整用户信息,垂直分表反而可能变慢。拆表前应该先看真实查询比例,而不是只看表字段多不多。

  • 一致性成本上升:新增、删除、回填数据时,应用代码和补偿脚本都要同时考虑多张表。

  • 索引设计更麻烦:查询条件一旦跨主表和扩展表,就需要在多表之间权衡索引设计,拆得太细反而不好控制查询计划。

  • 开发心智负担更高:表越多,模型、DAO、迁移脚本、数据订正脚本和排障 SQL 都会变多。

什么时候适合垂直分表

适合垂直分表的场景通常有几个共同特征:

  • 一张表有明显的冷热字段,常用接口只读少量字段。
  • 表中有较大的 TEXTBLOBJSON、长 VARCHAR 字段。
  • 主表列表查询、范围扫描、分页查询压力较大。
  • 扩展字段很多,而且不同业务模块维护不同字段。
  • 敏感字段需要单独控制权限、审计、备份或清理策略。

不太适合垂直分表的场景也很明确:

  • 表很小,访问压力不高。
  • 大多数查询都需要完整字段。
  • 业务还不稳定,字段边界经常变化。
  • 团队还没有能力维护多表事务、迁移和数据一致性。

一个更稳妥的拆分步骤

垂直分表最好不要一上来就按“领域模型”拆很多张表,而是从查询数据出发。

第一步,找出真实热路径。可以看慢查询、接口 QPS、performance_schema、应用日志,确认哪些 SQL 最频繁、最耗时、最依赖主表扫描。

第二步,统计字段使用率。比如用户列表接口是否真的需要 bio?后台检索是否真的需要 settings?如果一个字段只有详情页使用,就有拆出去的价值。

第三步,估算行宽变化。可以用线上抽样数据估算字段平均长度,再结合 InnoDB 页大小粗略计算拆分前后的页密度。这个估算不需要特别精确,但要能说明收益是否足够覆盖复杂度。

第四步,先拆最冷、最大的字段。通常优先拆长文本、配置、描述、备注、富媒体 URL、非核心扩展信息,而不是一开始就拆几十张表。

第五步,保留兼容期。可以先双写,再回填历史数据,最后让读路径切到新表。对线上系统来说,迁移过程比最终表结构更容易出问题。