MySQL 性能优化与业务设计校招面试题
MySQL 性能优化与业务设计校招面试题
前面建立了执行、索引、并发和日志的基本认识,现在把它们用于实际判断:慢在哪里,怎样验证改进,如何在重复请求和规模增长下保持业务正确。
本文示例数据
本章以 MySQL 8.4、InnoDB 为主,每条 SQL 都只供隔离练习,不在现有数据库执行。新库名若已存在,应停止初始化并换名,不忽略失败继续操作。以下输出是从样本推演的预期结果,没有实测耗时或执行计划。
CREATE DATABASE mysql_chapter6_demo CHARACTER SET utf8mb4;
USE mysql_chapter6_demo;
CREATE TABLE users (
id BIGINT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
email VARCHAR(100) NOT NULL,
UNIQUE KEY uk_users_email (email)
) ENGINE=InnoDB;
CREATE TABLE products (
id BIGINT PRIMARY KEY,
name VARCHAR(50) NOT NULL,
price DECIMAL(10,2) NOT NULL,
stock INT NOT NULL
) ENGINE=InnoDB;
CREATE TABLE orders (
id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY,
request_no VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
product_id BIGINT NOT NULL,
quantity INT NOT NULL,
status VARCHAR(20) NOT NULL,
amount DECIMAL(12,2) NOT NULL,
created_at DATETIME NOT NULL,
UNIQUE KEY uk_orders_request_no (request_no),
KEY idx_orders_user_time (user_id, created_at, id),
KEY idx_orders_user_status_time (user_id, status, created_at),
KEY idx_orders_time_id (created_at, id),
FOREIGN KEY (user_id) REFERENCES users(id),
FOREIGN KEY (product_id) REFERENCES products(id)
) ENGINE=InnoDB;
INSERT INTO users VALUES
(1, 'Alice', 'alice@example.com'),
(2, 'Bob', 'bob@example.com'),
(3, 'Carol', 'carol@example.com');
INSERT INTO products VALUES (1, 'Book', 50.00, 10), (2, 'Bag', 80.00, 10);
INSERT INTO orders VALUES
(101, 'REQ-101', 1, 1, 2, 'paid', 100.00, '2026-01-01 10:00:00'),
(102, 'REQ-102', 1, 1, 4, 'pending', 200.00, '2026-01-02 10:00:00'),
(103, 'REQ-103', 1, 1, 1, 'paid', 50.00, '2026-01-03 10:00:00'),
(104, 'REQ-104', 2, 2, 1, 'paid', 80.00, '2026-01-03 10:00:00');
此章为便于幂等例采用独立商品价格50.00,不能将它当成其他章节Book的19.90;也不能从已列订单反推初始化库存,它只是实验起点。四行数据只用于核对语义,小表计划不能推广为生产最佳路径。第4题改变订单/库存后须结束所有事务,再只清理该测试请求并将商品1恢复stock=10,随后继续其他实验;不要重复初始化制造唯一冲突。
1. 如何定位和优化 MySQL 慢查询?
相关问法:SQL 变慢先加索引吗?慢查询日志是什么?依赖:orders。
一句话理解与用途
慢查询优化是找出时间花在哪里,再用证据减少不必要工作或等待,而不是看到 SQL 慢就给 WHERE 每列建索引。
应用超时可能来自连接池等待、数据库锁、扫描、排序、存储、网络传输或结果处理。先明确测量边界,才能判断是不是数据库执行本身慢。
从一条业务查询开始
假设同一条“查询某用户最近十条订单”的请求平时较快,峰值明显变慢。先问:参数是否变化?返回量是否变化?是每次都慢,还是只在另一事务运行时慢?这是待诊断场景,不是本文测出的性能数据。
若大量时间在等锁,换一个覆盖索引未必解决;若扫描百万条只返回十条,访问路径才是重点;若返回几十兆内容,网络和反序列化也值得关注。
SELECT id, amount, created_at
FROM orders
WHERE user_id = 1
ORDER BY created_at DESC, id DESC
LIMIT 10;
四行样本的预期返回顺序是103/50、102/200、101/100。先固定这个业务语义,才知道改写后的查询是否遗漏pending订单或改变次序。若只需显示这些列,没必要取所有明细;但把过滤改成paid“加速”会少返回102,不是等价优化。样本里只有三条匹配,也无法用它证明千万行负载下的执行成本。
可重复的诊断步骤
- 采集慢日志、Performance Schema 汇总或应用追踪,定位高频高总耗时的查询,而非只挑最慢的一次。
- 固定代表性 SQL、参数和数据规模,记录耗时、扫描行、返回行、锁等待及资源情况。
- 使用 EXPLAIN 查看访问路径、连接顺序、过滤和排序;需要实测时在隔离环境使用 EXPLAIN ANALYZE。
- 提出具体假设,如日期条件难以索引定位,或缺少匹配排序的联合索引。
- 一次改变一个主要因素,再比较总耗时、尾延迟、扫描与写入成本。
可以将诊断记录组织成“现象→证据→假设→修改→验收”:例如证据显示大量候选因状态被过滤,才研究索引顺序;证据显示持锁者在等外部接口,就先研究事务边界。调整后除了读耗时,还要检查写入、索引空间和其他重要参数是否退化。平均值改善但峰值等待仍很长,也不代表业务超时问题已解决。
配图:慢查询诊断闭环

用诊断闭环连接证据、假设、受控修改和结果验证。
慢日志不是所有“慢”的完整名单
long_query_time 是重要阈值,但是否记录还受日志开关、最少检查行数等选项影响;部分前置锁等待与应用耗时的计量也不能简单等同。可先查看配置,不在教学示例里直接修改全局生产参数:
SHOW VARIABLES LIKE 'slow_query_log';
SHOW VARIABLES LIKE 'long_query_time';
SHOW VARIABLES LIKE 'min_examined_row_limit';
慢日志还不能直接解释完整接口链:客户端等连接、发送前处理、返回后的反序列化未必都包含在数据库查询计时里。先确认同一次请求的时间边界,再比较日志和应用追踪,避免拿两种不同口径的耗时判断“日志漏记”。收集日志也要注意敏感参数、体积和采样策略。官方:慢查询日志
优化也要守住语义
把 SELECT * 改为必要列可能减少传输;优化索引可减少扫描和排序;拆小批次可降低持锁时间;避免事务内等待外部服务可缓解锁竞争。但每个调整都要验证结果一致性,不能为了速度漏掉业务过滤或事务边界。
缓存、读副本和归档也有价值,但分别引入失效、一致性和历史访问问题,不能掩盖明显低效的核心 SQL。
常见误区
FORCE INDEX 可能让某组参数变快、另一组变慢;加内存对锁等待未必有效;重建索引不能自动修复所有慢查询。优化器计划还受数据分布和统计变化影响,因此上线后仍需观察。
面试回答
我会先确认慢在扫描、锁等待、排序、I/O 还是传输,通过慢日志、运行指标和执行计划形成证据。固定代表性参数后提出原因,一次修改一个主要因素,对比耗时、扫描量和资源,并验证结果正确。索引、SQL 改写、事务缩短和缓存各有适用场景,强制索引、加内存或重建都不是默认第一步。
2. EXPLAIN 怎么看?EXPLAIN ANALYZE 有什么区别?
相关问法:Using index、filesort、rows 代表什么?依赖:users、orders。
一句话理解与用途
执行计划描述数据库打算怎样访问和组合数据。EXPLAIN 主要展示计划与估算;EXPLAIN ANALYZE 实际执行受支持的语句,补充迭代器耗时和行数,帮助发现估算与现实的差距。
先理解“访问路径”,再看字段。只背 type 排名,很容易把一次小表扫描误判成必须修复的大问题。
两条查询比较
EXPLAIN FORMAT=TRADITIONAL
SELECT name FROM users WHERE email = 'alice@example.com';
EXPLAIN FORMAT=TRADITIONAL
SELECT id, email FROM users WHERE email = 'alice@example.com';
在采用邮箱索引的情况下,前者需要取得姓名,后者有覆盖机会。小样本可能产生不同选择,本文不伪造固定 EXPLAIN 输出。
主要字段怎样读
| 字段 | 看什么 | 不要误解为 |
|---|---|---|
| type | const、eq_ref、ref、range、index、ALL 等访问方式 | 脱离行数的绝对性能排名 |
| possible_keys | 可能帮助访问的索引 | 最终一定会用的索引 |
| key | 实际选择的索引 | 非 NULL 就必然高效 |
| key_len | 所用键部分的最大长度信息 | 实际读了多少字节或行 |
| rows | 估算检查行数 | 本次实测准确行数 |
| filtered | 估算过滤后保留比例 | 越低任何情况下都越好 |
| Extra | 覆盖、过滤、排序等附加信息 | 一个字段就能解释全部执行 |
Using index 通常表示覆盖,Using index condition 表示 ICP,Using filesort 表示需额外排序路径,不等于一定落盘;Using temporary 则提示使用内部临时表。id 与查询块有关,NULL 也不能简单解释成“正在创建临时表”。
阅读时可按一条路径问:从哪张表开始、用什么条件定位、估计取多少候选、哪些过滤放在之后、还要做什么排序或取行。假设某节点估计rows=1000、filtered=10%,大致表示估计过滤后约100行,而不是数据库已经实测检查1000行;连接的循环还可能把工作重复很多次。这个数字仅用于解释字段含义,不是本章查询的输出。
type=index 也可能扫描整棵索引,key非NULL并不证明只读几条;反过来四行表的ALL可能已经是便宜方案。判断需要结合规模和输出,不能凭“ALL最差”的口诀在小表强制走一个索引。
怎样看实际执行
在隔离环境,可以对小查询执行:
EXPLAIN ANALYZE FORMAT=TREE
SELECT id FROM orders WHERE user_id = 1 AND status = 'paid';
TREE 输出按迭代器展示实际耗时、行数和循环次数。重点比较预估与实际是否相差很大、哪个节点处理了大量数据、是否被重复执行。父子节点耗时有包含关系,不能机械地全部相加;多次循环的指标也要结合输出定义理解。
若一个内层查找被外层重复调用,单次返回很少不代表总工作少;要连同loops分析。多循环的实际指标还可能按每循环平均展示,不能直接把某一rows值当整个查询总行数。显式FORMAT=TREE便于按迭代器关系阅读,不是人为保证优化器采用某棵索引。本文没有真的运行这条ANALYZE,也没有预填“实际毫秒数”。
MySQL 8.0.18 起提供 EXPLAIN ANALYZE,它会真正执行语句,不能把它当成无成本的只读计划展示。耗时 SELECT 也会占资源,含副作用的可执行路径更需谨慎。官方 EXPLAIN 说明
配图:EXPLAIN 与 EXPLAIN ANALYZE

明确估算与实际执行的区别,避免将 ANALYZE 当无副作用查看。
面试回答
EXPLAIN 展示访问计划,我会结合 type、key、rows、filtered 和 Extra 判断定位、过滤、回表与排序,而不是只看是否有索引。rows 是估算,Using index 通常指覆盖,filesort 不一定落盘。EXPLAIN ANALYZE 会实际执行并提供迭代器的实测数据,适合在隔离环境核对估算偏差和工作量,不能不评估风险直接跑生产重查询。
3. 为什么深分页慢?有哪些优化方式?
相关问法:延迟回表能消除偏移扫描吗?依赖:orders。
一句话理解与用途
偏移分页要求先找到并跳过前面的很多条结果,再返回少量数据。页越深,通常需要丢弃的候选越多,因此不能把 LIMIT 1000000,10 理解成索引直接跳到“第一百万个合格结果”。
索引擅长按键定位,不天然维护任意筛选条件下每一条结果的排名。
原始查询的工作量
SELECT id, amount, created_at
FROM orders
ORDER BY created_at DESC, id DESC
LIMIT 1000000, 10;
四行样本返回空集;设想有足够大数据,常见执行需要扫描并略过大量排在前面的索引项,还可能为它们回表。没有匹配排序的索引时,成本会再叠加排序等工作。
先用可核对的小页展示普通偏移查询:
SELECT id, amount, created_at
FROM orders
ORDER BY created_at DESC, id DESC
LIMIT 2 OFFSET 2;
排序先得到104、103、102、101,跳过前两条后返回102、101。这不是索引中直接存着“第二页”的页号;页数只是应用把排序结果按条数划分。深页问题就是这种从前面定位并略过的工作随着偏移增长,不能因为LIMIT很小就忽略OFFSET。
方法一:先找少量主键,再获取其他列
SELECT o.id, o.amount, o.created_at
FROM orders AS o
JOIN (
SELECT id, created_at
FROM orders
ORDER BY created_at DESC, id DESC
LIMIT 2, 2
) AS page ON page.id = o.id
ORDER BY page.created_at DESC, page.id DESC;
样本先按 104、103、102、101 排列,跳过前两条后返回 102、101。内部若使用覆盖的排序索引,可把大量偏移扫描限制在较窄的索引上,外层只获取最终页的其他列。
这叫延迟回表,主要减少读完整行的工作,没有消除 OFFSET 本身。外层仍要明确排序,不能依赖 JOIN 自然保留子查询顺序。
方法二:用上页末尾作为游标
第一页为104、103,因此上一页末尾为时间 2026-01-03 10:00:00、ID 103,下一页:
SELECT id, amount, created_at
FROM orders
WHERE created_at < '2026-01-03 10:00:00'
OR (created_at = '2026-01-03 10:00:00' AND id < 103)
ORDER BY created_at DESC, id DESC
LIMIT 2;
预期得到102、101,与普通OFFSET和延迟回表例比较的是同一第二页。有相应访问路径时,可围绕末尾键继续查,而不是从第一页开始计数。只传时间会遗漏或重复同一时间的订单,所以完整游标必须包含决定顺序的全部必要键。
| 方式 | 从哪里继续 | 偏移工作 | 本例输出 |
|---|---|---|---|
| 普通OFFSET | 有序结果开头 | 略过104、103 | 102、101 |
| 延迟回表 | 窄索引上的有序开头 | 仍略过104、103,最后取目标行 | 102、101 |
| 完整游标 | 时间/id末尾键之后 | 避免按页数从头计数的典型路径 | 102、101 |
这张表描述典型工作分工,不是实测扫描次数。排序中相同时间的104、103由id决定;若改成只按时间排序,再靠单一时间游标翻页,就没有同样的确定边界。官方:LIMIT与排序
配图:三种分页比较同一第二页

三种访问方式都比较第二页 102、101,完整游标末项统一为 103。
取舍与边界
游标适合“下一页”或滚动加载,不天然支持任意跳到第十万页。额外查询一个深位置主键,也可能已经扫描了大量行,并不是免费优化。
并发插入、删除或修改排序字段会影响分页体验。唯一排序解决同值歧义,不自动提供跨请求不变的数据快照;业务若要求固定结果,还需设计一致性边界或冻结筛选范围。
面试回答
深分页慢主要因为 OFFSET 通常要扫描并丢弃前面大量结果。延迟回表先在覆盖索引上找目标主键,再取最终页数据,减少回表但不消除偏移扫描。游标分页利用上页最后的完整排序键继续定位,更适合顺序翻页,但不方便任意跳页。还要保证排序确定,并考虑并发数据变化。
4. 如何利用 MySQL 实现业务幂等?
相关问法:唯一索引能防止重复扣款吗?依赖:orders、products,请求号由调用方对同一业务重试保持不变。
一句话理解与用途
幂等是同一业务请求重复到达,只产生一次应有的业务效果。网络超时后客户端不知道订单是否已提交,合理重试不应该生成两笔订单或扣两次库存。
自增 ID 只能区别数据库行,不能知道两次不同插入其实来自同一个请求。必须有稳定的业务请求标识。
以唯一请求号建立竞争点
本章订单表的 request_no 有非空唯一约束,并保存user_id、product_id、quantity、amount用于核对请求内容。商品1单价50.00,本次数量1、金额50.00,初始库存10。以下是每步成功时的路径;任一步失败或库存更新不是1行,应用都必须回滚而不能继续提交:
START TRANSACTION;
INSERT INTO orders(request_no,user_id,product_id,quantity,status,amount,created_at)
VALUES ('REQ-NEW-001',1,1,1,'pending',50.00,'2026-01-04 10:00:00');
UPDATE products SET stock = stock - 1 WHERE id = 1 AND stock > 0;
SELECT ROW_COUNT() AS stock_updated;
COMMIT;
这只是展示关键结构。真实应用必须确认库存更新影响一行,否则 ROLLBACK,不能无货也提交订单;还应在同一事务写入必要订单明细和结果信息。
ROW_COUNT()应在库存UPDATE后立即取得,表示这里需要应用读取并分支;这份成功路径不包含服务器自动IF判断,不应作为忽略错误继续COMMIT的批处理。初始库存10时,预期一笔新订单和库存9一起提交;假设库存已经0,则影响0行,应用回滚刚插入的本次订单,库存仍0。pending表示订单创建状态,不表示支付已成功。
两个请求同时到达会怎样
- A 尝试插入请求号 REQ-NEW-001,随后处理库存。
- B 同时插入同号,由唯一约束协调,不能独立再成功插一行。
- 若 A 提交,B 得到重复键结果后,不再执行扣库存,而是结束失败事务并读取已有订单结果。
- 若 A 回滚,B 的后续行为可能转为成功插入,应按实际数据库结果继续,不能把锁等待直接等同于“业务已经成功”。
重复分支读取已有结果时,要处理 RR 旧快照问题,例如结束当前事务后发起新的读取。重复键还要确认是请求号冲突,而不是忽略任意其他唯一键错误。
结束失败事务后,可以新读取:
SELECT id, user_id, product_id, quantity, amount, status
FROM orders WHERE request_no = 'REQ-NEW-001';
若返回用户1/商品1/数量1/金额50.00,与重试输入一致,就复用已保存订单状态;若同号请求改成金额60.00,应拒绝内容不一致,而不是照样返回“成功”。不存在时还要结合实际插入失败、锁超时或A回滚判断有限重试,不可将所有异常都当重复请求。请求号的稳定性来自调用方协议:每次重试换新号,就绕过了幂等判断。
配图:唯一请求号保障幂等

跟随同号竞争、失败和合法重试,检查库存只能产生一次业务扣减。
为什么先查再插不够
A、B 都先 SELECT,都没查到,然后都认为可以插入。检查与写入之间存在竞态,所以仍需唯一约束兜底。加锁可以序列化,却不能自动识别“这次请求已经做过”,必须结合持久化业务状态。
同一请求号还应绑定请求内容。相同号却携带不同金额,不能直接当成合法重试;可存关键参数或摘要进行校验。
| 观察分支 | 订单记录 | 商品1库存 | 应用动作 |
|---|---|---|---|
| A成功,B合法重试 | 同一请求号一笔 | 9 | 返回原订单,不再次扣库存 |
| A未提交并回滚 | A的记录撤销 | 恢复10 | B按实际竞争结果决定是否处理 |
| 独立库存0实验 | 本次订单回滚 | 0 | 明确库存不足,不能提交空成功 |
唯一约束把竞争集中到持久化请求身份;事务再把这个身份与业务变更绑定。不在同一事务中,可能留下“已登记请求但没扣库存”或“已扣库存却没登记请求”的中间失败,第二次就难以正确判断。不要用INSERT IGNORE把其他约束错误也静默吞掉。官方:唯一约束错误
状态更新与外部副作用
UPDATE orders SET status='paid' WHERE id=102 AND status='pending' 可以限制一次合法状态迁移,应用根据影响行数处理后续逻辑,但它不自动包办所有扣款或消息发送。
外部支付与发送消息不在 MySQL 本地事务中。可以结合事务内事件记录、可靠投递与消费幂等协调,不能声称数据库回滚会撤销已经发出去的短信或支付。
面试回答
MySQL 幂等通常用稳定业务请求号加唯一约束,把请求记录、业务变更和结果在同一事务提交。重复请求读取既有结果,而不是再次执行扣款。先查再插有竞态,自增主键和锁也不能单独识别重复业务。还要校验同号请求内容、检查条件更新结果,并对外部支付或消息另行设计一致性。
5. 什么是分库分表?什么时候才需要?
相关问法:多少数据就该分表?分区表与分片一样吗?依赖:用户、订单的概念模型。
一句话理解与用途
分库分表是按业务或数据规则,把原来集中管理的数据拆到多个库或表。目标可能是业务隔离、减小单表规模,或者把存储和写入负载分散到不同实例,而不只是“表太大了就切”。
它会把原来单库容易完成的查询和事务变成跨边界协作,所以应先确认收益确实超过复杂度。
四种常见拆法
| 方式 | 切分什么 | 示例 |
|---|---|---|
| 垂直分库 | 不同业务表集合 | 用户库与订单库独立 |
| 垂直分表 | 同一实体的不同字段 | 商品常用字段与大段详情分表 |
| 水平分表 | 相同结构的不同记录 | 订单按用户路由到 orders_0、orders_1 |
| 水平分库 | 相同业务的记录分散到多个库或实例 | 不同用户订单进入不同数据库实例 |
垂直拆分侧重按业务或访问方式分离,水平拆分侧重分摊同类数据。实际系统可以组合使用,但每加一个边界,就要处理新的关联、迁移和运维问题。
用用户字段理解垂直分表:id/name等频繁读取字段保留在users_base,bio等大内容放users_profile,仍通过同一用户id关联;不是把Alice放一表、Bob放另一表。后者若两表结构相同才是水平分行。再看水平分库与同库分表:前者可在独立实例之间分担硬件负载,后者即使单表变小,仍共享这个实例的资源,目标收益不同。
用一个增长过程理解决策
最初订单查询慢,检查发现缺少用户与时间联合索引,先补合理索引可能已经解决。后来多年历史数据很少访问,可以归档并缩小热点集。若瓶颈主要是读取,缓存与读副本也可能更合适。
只有当合理优化、容量规划和可接受的单机扩容仍难满足存储、写吞吐或隔离需求时,才更有理由引入分片。不同硬件、行宽和查询模式的上限不同,不能统一规定“超过两千万行必须分表”。
配图:分库分表有四种拆法

用四象限区分业务、列、跨库行与同库行的拆分。
分表是否一定变快
同一实例里把一张表拆成十张,不会自动增加 CPU、内存和磁盘能力。查询能够精准路由时可能减少单表工作量;每次都扫十张再汇总,则可能更复杂甚至更慢。
数据库原生分区表仍是一张逻辑表,由引擎按分区规则管理,可以分区裁剪,但不等于应用分片到多个独立实例。两者的唯一约束、访问和运维边界也不同。
分区也不是没有约束成本:MySQL分区表的唯一键通常要求包含分区表达式使用的列等条件,不能先把单库唯一性方案照搬后再认为分区只是透明加速。跨实例分片则常没有一个普通唯一索引替所有实例统一检查业务键,需要路由一致、全局编号或其他业务设计。官方:分区与唯一键
主要代价
跨片 JOIN、全局排序聚合、跨片事务、唯一编号、扩容搬迁和备份校验都更困难。原来一条 SQL 的问题可能变成多实例请求与应用合并,因此分片键必须匹配主要访问模式,详见第 6 题。
面试回答
分库分表通过垂直或水平拆分,解决业务隔离、单表规模及单机容量吞吐问题。垂直按业务或字段拆,水平按记录规则拆。是否需要应基于实际瓶颈,先评估索引、归档、扩容和读写分离,没有统一行数阈值。同机分表不自动增加硬件能力,原生分区也不等于跨实例分片,还必须评估跨片查询和事务成本。
6. 如何设计并实施分库分表?有哪些代价?
相关问法:分片键如何选?扩容为什么麻烦?依赖:订单按用户分片的示意规则。
一句话理解与用途
分片设计要让应用明确知道“写到哪、从哪读、扩容时怎样搬”,并在迁移过程中保证不丢、不重、不读错。中间件可以承载路由,但不能替代业务边界设计。
先选分片键,再选算法
如果主要查询是某用户的订单,按 user_id 分片便于单片定位,同一用户相关事务也更容易留在一片。若后台主要按全站时间统计,则还需要汇总、分析系统或额外访问方案,不能指望一个分片键满足所有需求。
示意规则为 shard = user_id % 3:用户 1 到分片 1,用户 2 到分片 2,用户 3 到分片 0。插入和查询都必须使用同一个规则;不能插入按 ID 取模,查询却按用户号取模。
表名或库名不是普通值参数,应由受控路由结果选择,不能拼接未经验证的用户输入。跨库主键还需要全局唯一策略,仅靠每库自增会重复。
如果查询仅拿到order_id,却没有user_id或可解码的路由信息,就不能凭空知道该去哪个片。可以让API携带分片键、建立可靠查询映射,或在标识方案中设计可解释的路由部分;否则可能需要多片查找。这项查询成本应在分片前决定,而不是上线后把“广播所有片”当默认无代价行为。
范围与哈希的取舍
按时间范围切分便于时间查询、历史归档,但最新分片可能很热;按哈希分布通常更均匀,却不方便全局范围定位,也无法消除某个超级用户自己的热点。
直接从模 3 改为模 4,大量记录的目标会变化。逻辑分片、路由表等可以减少路由与物理位置的紧耦合,但仍需真实迁移和校验,并非算法一换就完成扩容。
例如只作逻辑推演的user_id=4,旧规则4%3=1,新规则4%4=0。旧数据仍在片1,若读入口先切新规则去片0,就可能查不到,尽管数据没有删除。因此迁移需要规则版本和明确切换边界;读写分别使用不同版本会引入暂时错路由。哈希均匀分布平均用户也不代表平均请求量,一个超级用户仍可能把自己的片打热。
配图:同一分片规则负责写与读

检查读写使用同一分片键和规则,样本路由结果一致。
一次迁移的完整思路
- 明确新规则、数据归属、唯一约束和事务边界,先验证关键查询能否路由。
- 建立新结构,确定全量复制与增量日志衔接的起点。
- 复制历史数据,同时持续同步后续变更,保证更新、删除也能正确应用。
- 核对行数、关键字段、金额合计及抽样明细,不能只比较总行数。
- 追平增量,在受控切换窗口或其他一致性协议下切换写入和读取,避免漏同步或重复生效。
- 保留观察期与回退路径;若新库已接收独有写入,回退前还要处理反向同步,不能直接把流量拨回旧库。
双写两个库若没有可靠失败处理,可能一边成功一边失败。迁移工具或中间件必须配合校验、幂等和明确切换协议,不是把“开启双写”当成一致性证明。
全量复制期间业务继续变化,因此复制起点必须与增量边界衔接;只复制已有行会遗漏后续更新和删除。若同一变更被投递两次,应用还应利用稳定身份/顺序控制做到安全重放。校验也要按分片键和业务范围检查:两个库总行数相同,完全可能是漏一行又多一行,或某笔金额错误。
切换时先满足已约定的追平与写入协调条件,再转移权威写入口;不能让旧、新两边在没有冲突处理的情况下同时独立接受写。切换后新库产生的新订单若尚未同步回旧库,直接回切就会让它们对读者消失。“留着旧库”只是回退材料的一部分,反向追平、验证和入口控制才让回退可行。
配图:分片迁移怎样安全切换

沿全量、增量、校验和受控切换检查迁移,不漏新写入的回退条件。
必须提前承担的代价
跨片 JOIN 可能拆成多次请求,聚合和分页需要合并;唯一约束通常只能在单片直接保证;跨片事务需要更复杂协议或业务调整。分片越多,备份、监控、扩容与故障处理也越复杂。
面试回答
我会先按主要查询和事务边界选分片键,再设计一致的读写路由、全局 ID 和热点策略。迁移要覆盖全量复制、增量衔接、数据校验、受控切换和可行回退,而不是只改取模数。分片带来跨片 JOIN、聚合、事务和唯一性成本,中间件能实现路由,但不能替业务决定这些边界。
阅读导航




