SQL 表拆分不是简单地把一张大表分出去,它首先要回答一个核心问题:当前瓶颈来自行数太多,还是字段太多、访问模式不均衡。水平拆分解决的是单表行数膨胀带来的索引树加深、扫描范围变大、锁竞争加剧;垂直拆分解决的是单行过宽、热点字段和大字段混在一起导致缓存效率低、更新相互阻塞。两种拆分的物理形态不同,后续查询、事务、统计方案也完全不同。

一、水平拆分:按行把数据分散出去
水平拆分通常把一张大表拆成结构完全相同的多张表,例如订单表拆成 order_0、order_1、order_2、order_3。应用在写入前根据拆分键计算路由,把数据落到对应分片。拆分键一般选择用户ID、订单ID、租户ID这类高频过滤字段,目的是让大多数查询只命中一个分片。
-- 创建 4 张结构相同的订单分表 CREATE TABLE order_0 ( id BIGINT PRIMARY KEY, user_id BIGINT NOT NULL, amount DECIMAL(10,2) NOT NULL, status TINYINT NOT NULL, created_at DATETIME NOT NULL ); -- order_1、order_2、order_3 的表结构与 order_0 完全一致
路由规则常见的有取模、范围、哈希三种。取模路由执行简单,例如 user_id % 4 可以均匀分散写入压力;范围路由按时间或ID区间分片,便于做连续范围查询,但容易出现新分片写入热点;哈希路由可以结合一致性哈希降低扩容时的数据迁移量。无论选哪种,拆分键必须尽量覆盖业务查询条件。一旦查询不带拆分键,中间件只能把请求广播到所有分片,再合并结果,延迟会明显上升。
-- 带拆分键的查询只会落到单个分片 SELECT id, amount, status FROM order_2 WHERE user_id = 102 AND created_at >= '2024-01-01 00:00:00';
跨分片查询和事务是水平拆分后最需要提前评估的问题。跨分片 JOIN 在多数中间件中性能较差,通常要求把关联字段设计为拆分键,或者先查出一个分片的数据,再到另一个分片查询,由应用层组装。分布式事务不能简单依赖数据库本地事务,需要引入最终一致性、事务消息或 TCC 等方案。除非业务允许短暂不一致,否则不要轻易把强一致事务跨越多个分片。
二、垂直拆分:按列把宽表拆窄
垂直拆分主要解决单行过宽和字段访问频率不均衡的问题。例如用户模块可以把登录、下单等场景频繁使用的基础字段保留在主表,把头像、个人简介、收货地址等大字段或低频字段拆到扩展表。这样 user_base 行更窄,一页能缓存更多数据,减少磁盘 IO 和索引维护成本。
-- 用户基础表:高频访问字段 CREATE TABLE user_base ( id BIGINT PRIMARY KEY, username VARCHAR(64) NOT NULL, password_hash CHAR(60) NOT NULL, mobile VARCHAR(20) NOT NULL, created_at DATETIME NOT NULL ); -- 用户扩展表:低频或大字段 CREATE TABLE user_profile ( user_id BIGINT PRIMARY KEY, nickname VARCHAR(64) NOT NULL, avatar_url VARCHAR(255) NOT NULL, bio TEXT, address VARCHAR(255) );
垂直拆分的收益体现在高频查询上:主表更紧凑,缓存命中率更高,更新昵称或头像这类低频字段不会阻塞登录密码修改。但代价是获取完整用户信息时需要连接查询,例如通过 user_id 关联 user_base 和 user_profile。如果这种连接查询出现得非常频繁,说明拆分粒度可能过细,应在扩展表中冗余部分常用字段,减少运行时 JOIN。
SELECT b.id, b.username, p.nickname, p.avatar_url FROM user_base b JOIN user_profile p ON p.user_id = b.id WHERE b.id = 1001;
垂直拆分后不建议继续保留物理外键。大型系统中物理外键会带来额外的约束检查和相关联的锁,跨表更新时容易产生死锁。通常由应用层保证数据一致性,例如创建用户时同时写入两张表,删除用户时同时清理扩展表。拆分时还要遵循一个原则:访问频率高、长度短的字段优先留在主表,更新频繁但查询较少的字段拆到副表,大文本和二进制字段尽量独立存放。
三、两种拆分的组合与选择
实际项目中,垂直拆分和水平拆分往往先后出现。先通过垂直拆分把核心表变窄、提升单表效率,等行数继续增长到千万甚至亿级后,再对核心表做水平拆分。没有必要一开始就引入分库分表中间件,很多性能问题通过优化索引、清理历史数据、增加只读副本也能解决。过度拆分反而会增加运维复杂度和跨节点事务风险。
| 对比维度 | 水平拆分 | 垂直拆分 |
|---|---|---|
| 拆分对象 | 按行分散 | 按列分散 |
| 解决瓶颈 | 单表行数过大 | 单行过宽、字段访问冲突 |
| 表结构 | 多张结构相同 | 多张结构不同 |
| 关联查询 | 跨分片JOIN困难 | 主扩展表JOIN常见 |
| 事务处理 | 跨分片事务复杂 | 同库事务较简单 |
选择拆分方案时,可以先回答三个问题:字段数量是否很多且访问频率差异明显;单表行数是否已经影响到查询和写入;业务查询条件中是否有一个稳定的字段能覆盖大多数场景。如果字段问题是主要矛盾,先做垂直拆分;如果行数问题是主要矛盾,再做水平拆分。拆分键优先级应为:查询条件覆盖率高、数据分布均匀、值不经常变更。订单场景中买家侧查询通常按用户ID,卖家侧查询按商家ID,两者冲突时就需要冗余数据或建立索引表。
四、拆分过程中最容易忽略的几个点
第一是唯一主键策略。拆分前单表自增ID在分片后会冲突,不能继续使用数据库自增作为全局唯一ID。常见做法是应用层生成雪花ID、号段模式,或者让每个分片使用不同的自增步长。主键一旦确定,后续合并数据、迁移历史表都会简单很多。
-- 分片后不能继续依赖单表自增主键 -- 推荐应用层生成全局唯一 ID,例如雪花算法 CREATE TABLE order_0 ( id BIGINT PRIMARY KEY, -- 由应用写入,保证全局唯一 user_id BIGINT NOT NULL, amount DECIMAL(10,2) NOT NULL, created_at DATETIME NOT NULL );
第二是避免拆分键热点。按时间分片容易让最新一张表承受大部分写入,按自增ID范围分片也会有类似问题。可以引入哈希取模、一致性哈希,或者在时间维度上再做二次分片。第三是不带拆分键的查询要严格限制。运维后台、报表统计这类场景如果直接查分片表,会触发大量广播查询。统计数据应同步到离线数仓或搜索引擎,不要直接压在业务分片上。
表拆分从来都不是一个纯粹的 SQL 问题,它涉及访问模式梳理、主键生成、路由规则和事务边界。先把拆分键和查询场景整理清楚,再决定采用水平拆分、垂直拆分还是两者组合,能够避免上线后反复迁移数据和调整路由。对于已经拆分的表,可以通过慢查询、分片流量分布持续验证拆分是否达到预期,必要时再做二次调整。