当单表数据量达到千万甚至亿级别,或者表的字段数过多、频繁出现慢查询与锁冲突时,优化索引和 SQL 往往已经到顶,这时就该考虑拆分表结构了。大表拆分主要有两种策略:垂直拆分和水平拆分,二者解决的是不同维度的问题。
14.3.1 垂直拆分:按列拆分,把“宽表”变“窄表”
什么是垂直拆分
垂直拆分是把一张“字段很多”的表,按照列的相关性或访问频率拆成多张表。每张表包含原表的一部分字段,同时保留相同的主键,保证能关联起来。
常见的拆分方式是:
- 主表:存放最常用、最核心的字段。比如用户表可以放
id, username, password_hash, phone, status, created_at。 - 扩展表:存放不常用的、占用空间大的字段。比如
user_profile表放id, nickname, avatar, bio, birthday,user_settings表放id, language, notification_enabled。
为什么要做垂直拆分
- 减少单行数据大小:InnoDB 是按页(16KB)读取的,一行数据过大,一个页能存放的行数就少,导致查询时 I/O 效率降低。把大字段拆分出去,主表行更紧凑,同样的缓存能放下更多热点数据。
- 减少锁竞争与 IO 成本:更新某个不常用的大字段时,主表的查询并不会被阻塞,因为它们是不同的表。查询核心业务字段时也不需要加载那些无用的长文本。
- 更好利用覆盖索引:主表字段少,更容易设计出覆盖常用查询的索引,避免回表。
垂直拆分的代价
- 关联查询变多:原本一条
SELECT *就能拿到的所有字段,现在可能需要 JOIN 它对应的扩展表。好在如果按主键关联,且扩展表数据量也合适,性能影响不大。 - 事务需要跨表:插入或更新一个完整的业务对象时,可能需要同时操作主表和扩展表,这要求使用事务保证一致性,但在同一个数据库内,这并不是大问题。
- 扩展表的数量要克制:不建议把表拆得太零碎,一般拆分出一到两张扩展表即可。拆得过细会导致开发和维护成本急剧上升。
实用建议
- 拆分前先确认问题是不是真的出在“行宽”。用
SHOW TABLE STATUS LIKE '表名'查看平均行长度,如果明显偏大(比如超过 2KB),再结合慢查询确认哪些字段是包袱。 - 优先拆出
TEXT、BLOB、JSON等大字段,以及更新很少但读取也少的字段。 - 拆分后注意调整常用查询,尽量只查主表,仅在确实需要扩展信息时再 JOIN 扩展表。
14.3.2 水平拆分:按行拆分,把“大表”变“多表”
什么是水平拆分
水平拆分是将一张数据量巨大的表,按照某种规则将行数据分布到多张结构完全相同的表中。这些表可以放在同一个数据库内,也可以分布到不同的数据库实例上(即分库分表)。
比如订单表有 5000 万行,按 user_id % 32 的规则分成 32 张表:orders_0 到 orders_31。每个用户的所有订单落在固定的那张表中。
什么时候需要水平拆分
- 单表数据量过大:通常 MySQL 单表超过 2000 万行后,索引树的深度可能增加,DDL 变更、备份恢复、全表扫描的成本都显著变高。这时即使查询走索引,性能也可能开始下降。
- 写入压力无法承受:单表的写入受限于单机的 IO 能力,拆分成多库多表后,写入可以分散到多个实例,整体吞吐量线性提升。
- 数据量持续高速增长:如果业务未来一年内单表会膨胀到数亿级别,提前做好拆分设计比后期抢救容易得多。
水平拆分的核心:分片键(Shard Key)与分片算法
选择一个合适的分片键是水平拆分中最重要的一步,直接影响查询效率和未来的扩容。
- 分片键选择原则:
- 绝大多数查询都带有这个字段,避免“全分片扫描”。
- 数据分布均匀,避免热点分片(比如按时间分片,最近的分片会被疯狂访问)。
- 与业务主体相关联,比如用户系统按
user_id,订单系统按buyer_id或order_id(视主要查询模式而定)。
- 常见分片算法:
- 哈希取模:
id % 分片数,分布均匀,但扩容时分片数量变化会导致数据大量迁移。 - 一致性哈希:减少扩容时的数据迁移量,但实现稍复杂,一般借助中间件。
- 范围分片:按时间范围或 ID 区间分片,利于按范围查询,但容易写入热点落在最新分片上。
- 地理位置或业务线:比如按城市、业务线分库,更接近垂直拆分的扩大版,适用于天然隔离的数据。
水平拆分带来的挑战
一旦实施了水平拆分,原来单表上的很多便利就消失了,必须提前规划应对方案:
- 跨分片查询:统计总数、多条件检索如果未带分片键,只能到所有分片上查询再聚合,复杂度和延迟大幅增加。对此要尽量让所有关键查询携带分片键,或者使用专门的搜索引擎(如 Elasticsearch)来处理复杂检索。
- 全局唯一 ID:水平拆分后不能再依赖
AUTO_INCREMENT在单表内生成唯一 ID,需要引入分布式 ID 方案,例如雪花算法(Snowflake)、基于 Redis 的自增、号段模式等。 - 分布式事务:如果一个业务操作需要修改分布在两个不同分库上的数据,保证 ACID 就非常困难。通常只能依赖柔性事务(如本地消息表、Seata 的 AT 模式)来保证最终一致性,而不能指望本地数据库事务。
- 运维复杂度:全量备份、表结构变更、数据迁移等都需要面对的是几十甚至上百张表、多个库,需要使用脚本或中间件自动化管理。
如何落地
- 能用单机扛住就不分:预算允许的情况下,先升级硬件、优化索引和 SQL、做读写分离,直到确实遇到单表瓶颈。
- 优先选用成熟中间件:如 ShardingSphere、MyCat 等,它们可以屏蔽掉大部分分片逻辑,让应用层依然像使用单一数据库一样进行查询。你只需要配置好分片规则,中间件会自动路由。
- 渐进式拆分:先对增长最快的那一两张超级大表进行拆分,积累经验后再推广,不要一开始就全部分库分表。
- 关注数据迁移和扩容:设计分片策略时就要想好未来从 32 分片扩到 64 分片时的数据迁移方案。双写、增量同步、灰度切换是常用的平滑扩容手段。
垂直拆分与水平拆分的组合
在实际的大型系统中,两种拆分策略往往同时使用:先按业务模块垂直分库(如用户库、订单库、商品库),再针对某个库中数据量巨大的表进行水平分表。这样既利用了垂直拆分的业务隔离优势,又解决了水平拆分的数据量瓶颈。
总之,大表拆分是数据库架构演进的必经之路,但并不是所有项目都需要。当你决定拆分时,务必以实际性能瓶颈和业务查询模式为依据,而不是为了拆分而拆分。一个清晰的分片键设计和合适的时间点选择,比技术本身的复杂度更重要。