图 1:本文封面,点明「垂直拆分、水平拆分」两条主线。
先说明属性:这是一篇经典题解析,题目出自本地历史面试资料(见文末来源说明),与任何公司的当前招聘无关。「单表数据量太大怎么办」是数据库方向的架构题,面试官想听的不是背诵「分库分表」四个字,而是你清不清楚拆开的两种方向、以及拆开之后必然冒出来的新问题。
先给核心结论:拆法只有两种——垂直拆分和水平拆分;真正的分水岭不在「怎么拆」,而在「拆完怎么活」:分布式 ID、跨片查询、分布式事务、扩容迁移,这四个新问题必须在拆之前就有预案。
第一步,先问一句:真的非拆不可吗。
拆分是最后手段,不是第一反应。慢查询先查执行计划、加索引、改写法;单表行数多但查询都走索引、响应正常,就没有拆的紧迫性。拆分的代价是永久性的架构复杂度,答题时先把这句说了,反而显得有工程判断力。
第二步,垂直拆分:按业务与字段竖着切。
两个粒度。一是按字段拆表:把大字段(正文、附件)和冷热字段从主表剥离,主表只留高频访问的列,单行变窄,单页能装更多行。二是按业务拆库:订单、用户、库存各自成库,配合服务化拆分。垂直拆分结构清晰、改造平滑,但它解决不了根本问题——行数还在涨,单表容量天花板迟早再撞上。
第三步,水平拆分:按行横着切。
把同一张表的行按路由规则分散到 N 个分片。路由规则主要有两类:取模分片,按 userId 对分片数取模,数据分布均匀,但扩容时模数一变,绝大多数 key 的分片归属都要变,迁移量大;范围分片,按 ID 或时间区间划分,扩容只需追加新区间,但容易产生热点——最新区间永远最热。路由键的选择直接决定系统的生死:选错了,跨片查询会成为常态。
图 2:两种拆分的取舍,以及拆分后必然面对的四个新问题。
第四步,用代码把「扩容代价」算给面试官看。
取模分片的迁移比例可以当场算出来。下面这段代码用 Node.js 22.15.0 直接运行(node shard.js):
'use strict';
// 取模分片路由:userId 对分片数取模
function modShard(userId, shardCount) {
return userId % shardCount;
}
// 演示扩容代价:分片数从 4 扩到 5,统计需要迁移的 key 比例
function migrationRatio(keys, from, to) {
let moved = 0;
for (const k of keys) if (modShard(k, from) !== modShard(k, to)) moved++;
return moved / keys.length;
}
// 范围分片路由:按 ID 区间划分
const ranges = [
{ min: 0, max: 1_000_000, shard: 0 },
{ min: 1_000_000, max: 2_000_000, shard: 1 },
{ min: 2_000_000, max: 3_000_000, shard: 2 },
];
function rangeShard(userId) {
const hit = ranges.find((r) => userId >= r.min && userId < r.max);
return hit ? hit.shard : -1;
}
const keys = Array.from({ length: 10_000 }, (_, i) => i + 1);
console.log('取模分片 4 -> 5 扩容,需要迁移的 key 比例:',
(migrationRatio(keys, 4, 5) * 100).toFixed(1) + '%');
for (const id of [42, 999_999, 1_000_000, 2_500_001]) {
console.log(`userId=${id} -> 取模(4 片): 分片 ${modShard(id, 4)},范围分片: 分片 ${rangeShard(id)}`);
}
实测输出:
取模分片 4 -> 5 扩容,需要迁移的 key 比例: 80.0%
userId=42 -> 取模(4 片): 分片 2,范围分片: 分片 0
userId=999999 -> 取模(4 片): 分片 3,范围分片: 分片 0
userId=1000000 -> 取模(4 片): 分片 0,范围分片: 分片 1
userId=2500001 -> 取模(4 片): 分片 1,范围分片: 分片 2
4 片扩到 5 片,80% 的 key 要搬家——这就是为什么扩容方案(翻倍迁移、一致性哈希)必须在拆分第一天就想好。
易错点清单。
一,上来就谈拆分,不提「先优化索引与 SQL」这个前提。二,垂直拆分当成终极方案,意识不到行数增长没被解决。三,只说取模分片,讲不出扩容时 key 大规模迁移的代价。四,路由键凭直觉选,不从查询模式出发——高频查询带什么条件,路由键就该是什么。五,对拆分后的四个新问题(分布式 ID、跨片查询、分布式事务、扩容迁移)没有预案,被追问即穿帮。
回到面试现场,建议的答题节奏:先给「能加索引不拆表」的前提,再讲垂直与水平两个方向及各自上限,然后用路由规则对比引出扩容代价,最后主动列出拆分后的四个新问题和应对思路。AI 业务的会话记录、样本库、日志表增长极快,这套拆解思路几乎原样适用。
如果面试官追问「拆分之后要做一个全局排行榜,数据散在八个分片里,怎么查」,你会给出哪几种方案?欢迎写出你的思路。
参考资料
- Node.js v22.x 官方 API 文档(文中代码运行环境为 Node.js 22.15.0,
Array.from、Number.prototype.toFixed等行为以此为准) — https://nodejs.org/docs/latest-v22.x/api/