分库分表后SQL全崩?这些坑我替你踩完了

还记得我们第一次把2亿条订单数据拆成16个分片段上线的那天,本来信心满满觉得性能肯定能起飞,结果上线刚十分钟告警就炸了:订单列表接口超时率冲到35%,订单count统计接口最长要15秒返回,运营翻列表到第30页直接把ShardingProxy节点干OOM,整个订单链路卡了20多分钟,最后紧急切回单库回滚代码,全组加班排查了一整夜。很多人觉得分库分表是解决大数据量性能问题的银弹,把表一拆就万事大吉,根本没想过拆完之后原来写的SQL90%都会出问题,从单库毫秒级查询到跨分片雪崩,可能就是一次上线的距离。我前前后后主导过三次核心库的分库分表落地,踩过的坑能写半本书,今天把分库分表场景下的SQL改写、避坑、调优经验全部分享给你,看完你再做分库分表,绝对不会上线就崩。
分库分表场景下SQL改写与性能调优实战

一、为什么分库分表之后,原来的SQL全变慢了
很多人对分库分表的认知停留在“把大表拆成小表,查询就变快”,根本没意识到分库分表之后SQL的执行逻辑已经完全变了。单库场景下,MySQL自己会做执行计划优化,所有数据都在本地,哪怕写的稍微差点,最多也就是慢一点,不会出大问题;但分库分表之后,所有SQL都要经过中间件(ShardingSphere、MyCat这类)做解析、路由、改写、结果归并四个步骤,数据分散在不同物理节点上,任何一步设计不好,性能都会指数级下降。
我给你算过一笔账,同样一条SQL,单库和分16个分片的执行逻辑差异有多大,整理成了对比表:
表格
SQL类型 单库执行逻辑 分库分表后执行逻辑 性能下降倍数
等值查询(带分片键) 本地索引查找,一次返回结果 直接路由到对应分片,和单库逻辑一致 1倍(无性能损失)
等值查询(不带分片键) 本地B+树查找 全路由到所有分片,每个分片执行完拉回结果归并 16倍(等于分片数)
深分页LIMIT 10000,20 扫描10020条记录返回20条 每个分片扫描10020条,拉16万条到内存排序归并 50~100倍
跨分片ORDER BY排序 利用索引直接返回有序结果 每个分片返回有序结果,中间件做内存多路归并排序 2~10倍
跨分片COUNT/聚合 本地聚合后返回单个结果 每个分片做预聚合,拉回中间件做二次聚合计算 5~20倍
跨库JOIN关联 本地嵌套循环关联 拉取所有关联表数据到内存做嵌套循环匹配 10~100倍
你看,只要你的SQL带上分片键,路由到单个分片,性能和单库是一模一样的;但只要不带分片键,或者做跨分片的复杂操作,性能会立刻暴跌,这也是为什么很多人分库分表之后发现性能反而更差的核心原因——根本不是分库分表没用,是写的SQL不符合分布式场景的规则。

二、分库分表最容易踩的SQL坑,每个都能搞崩线上
我们当时上线后梳理了所有慢SQL,发现99%的问题都是几个固定的坑,每个坑之前单库的时候根本不是问题,到了分布式场景下直接成了P0故障的导火索。
1、不带分片键的全路由查询,性能直接差N倍
分片键是你拆表用的那个维度,比如订单表用user_id做分片键,同一个用户的所有订单都会落在同一个分片上。如果SQL带上了user_id,中间件直接能定位到具体分片,一次查询就能返回结果;但如果SQL不带分片键,比如按order_no查订单、按create_time范围查订单,中间件根本不知道数据在哪个分片,只能把SQL发给所有16个分片,每个分片都执行一遍,再把结果拉回来归并,性能直接差16倍,QPS高了能把所有分片库和中间件的CPU全打满。
我们上线第一天的慢查询TOP10全是这类SQL,比如有个查订单详情的接口,开发只传了order_no没传user_id,每次查都要扫16个分片,原来单库10毫秒的查询变成了180毫秒,高峰期直接把中间件的连接池占满了。后来我们做了两个优化:一是给order_no做了全局索引映射,建了一张小表存order_no对应的user_id和分片位置,查order_no的时候先查映射表拿到分片键,再路由到对应分片,性能直接回到8毫秒;二是在SQL审核阶段加了硬卡口,核心业务表的查询SQL必须带分片键,不带的直接卡CI不让上线。
sql
-- 反例:不带分片键user_id,全路由扫16个分片,执行时间180毫秒
SELECT * FROM order_info WHERE order_no = 'DD20250601123456';
-- 优化后:拿到分片键直接路由到单分片,执行时间8毫秒
SELECT * FROM order_info WHERE user_id = 12345 AND order_no = 'DD20250601123456';
2、深分页查询,直接干爆中间件内存
深分页在单库就慢,到了分库分表场景直接是灾难级别的。比如你写LIMIT 10000,20,单库场景只要扫10020条记录,扔掉前10000条返回20条;但分16个分片的话,中间件会给每个分片发LIMIT 0,10020的SQL,把每个分片的前10020条数据全拉到中间件内存,再做归并排序取第10001到10020条,总共要拉16*10020=16万条数据到内存,如果翻到第100页,就要拉160万条数据,中间件直接OOM。
当时上线第一天运营翻订单列表到第50页,直接打挂了两个ShardingProxy节点,就是因为这个问题:两个节点堆内存占满,Full GC都回收不了,最后只能重启。后来我们把所有C端的分页全改成了游标分页(书签式分页),永远带上一页最后一条记录的id和create_time作为查询条件,每次只查下一页的20条,每个分片也只需要返回20条结果,不管翻多少页性能都是毫秒级,再也不会出现拉大量数据到内存的问题。
sql
-- 反例:深分页,全分片拉取数据内存归并,执行时间12秒,容易OOM
SELECT * FROM order_info WHERE create_time > '2025-01-01' ORDER BY id DESC LIMIT 10000, 20;
-- 优化后:游标分页+带分片键,单分片范围查询,执行时间15毫秒
SELECT * FROM order_info
WHERE user_id = ? AND create_time > '2025-01-01' AND id < '上一页最后一条ID'
ORDER BY id DESC LIMIT 20;
对于后台管理系统必须跳转到任意页的场景,我们用了二次查询法:第一步只查所有分片的主键ID,内存排序找到当前页的ID范围,第二步拿着ID去对应分片查详情,不需要拉所有字段,数据传输量减少90%以上,哪怕翻到几百页也不会慢。
3、跨分片排序聚合,性能差到离谱
很多人写SQL习惯用COUNT(*)、GROUP BY、ORDER BY,这些操作在单库很正常,到了跨分片场景就会特别慢。比如你要查某个时间段所有订单的总金额,中间件要让每个分片先统计自己分片内的总金额,再把16个分片的结果拉回来加起来,要是带复杂WHERE条件,每个分片都要扫几万条数据,几秒都出不了结果。如果是GROUP BY,中间件还要把每个分片的分组结果拉回来,再按分组维度做二次聚合,数据量稍大内存就扛不住。
我们当时订单列表的总条数COUNT接口,每次要3秒多才能返回,后来干脆做了一张独立的统计宽表,通过Binlog异步同步订单数据,按天、按状态、按商家维度提前把COUNT算好,查的时候直接查宽表,只要10毫秒就能返回。对于大跨度的GROUP BY、多维度统计需求,我们全移到了离线数仓做,不让线上库跑实时大聚合。
这里还要提醒一个坑:跨分片排序的时候,一定要保证每个分片的排序字段上有索引,让每个分片返回的结果本身就是有序的,这样中间件做归并排序的时候效率特别高;如果分片内排序没走索引,每个分片都要做文件排序,拉回来的结果是乱序的,归并的开销会大到无法想象。
4、跨分片JOIN,基本等于“自杀式查询”
分库分表之后,JOIN是当之无愧的性能杀手。如果两个关联表的数据在同一个分片,那还能做本地JOIN,性能和单库一样;但如果数据不在同一个分片,中间件只能把两个表的数据全拉到内存里,自己做嵌套循环关联,要是两个表各有几十万行,内存里要匹配几亿次,不仅慢,还会直接把中间件搞挂。
我之前见过一个开发在分库分表之后还写5表关联的SQL,执行一次要22秒,把整个分片集群的CPU都打到了100%,影响了所有核心业务。后来我们定了死规矩:分库分表后禁止跨分片JOIN,所有关联需求用三个方案解决:
第一个是字段冗余,用空间换时间,比如订单表要关联查用户昵称、手机号,直接把这两个字段冗余存在订单表里,更新用户信息的时候异步更新订单的冗余字段即可,查订单的时候不需要关联任何表,性能最好。
第二个是ER分片绑定,如果两个表是强关联的一对多关系(比如订单表和订单商品表),就用同一个分片键做分片,绑定成ER表,保证同一个父表的所有子表数据都落在同一个分片上,关联的时候直接在单分片内做JOIN,和单库性能完全一致。
第三个是全局表,比如字典表、配置表这种数据量小、变动少的表,在每个分片里都存一份,写操作的时候广播到所有分片更新,关联的时候直接读本地分片的表,不需要跨节点。
实在满足不了的关联需求,就拆成多次单表查询,在应用层按ID做内存组装,比跨库JOIN快几十倍,而且好维护。
5、跨分片分布式事务,性能直接打对折
单库的时候你开一个事务更新好几张表,是本地事务,性能高,不会有一致性问题;分库分表之后,如果一个事务要更新不同分片的数据,就变成了分布式事务,不管用XA强一致事务还是TCC柔性事务,性能都比本地事务差50%以上,XA事务还要长时间持有跨节点的锁,并发高了很容易出现死锁、锁等待。
我们当时的优化原则非常简单:尽量不让跨分片事务出现。所有写操作必须带上分片键,保证同一个事务的所有写操作都落在同一个分片上,用本地事务提交,性能和单库完全一致;实在需要跨分片更新的场景(比如用户下单扣库存、加积分),我们全用RocketMQ事务消息做最终一致性,不用强一致分布式事务,性能高,也不容易出锁问题。

三、分库分表后SQL编写的10条军规,照着写就不会出故障
踩了无数坑之后,我们总结了10条写SQL的硬规则,所有开发必须严格遵守,落地之后再也没出过分库分表相关的线上故障:
1、所有在线业务的查询SQL必须带上分片键,保证路由到单分片执行,禁止无分片键的全路由SQL上线,特殊场景必须做全局索引映射。
2、禁止写大offset的深分页,C端场景全用游标分页,后台跳页场景必须用二次查询法,禁止拉取大量数据到中间件内存归并。
3、禁止跨分片JOIN,优先用字段冗余、ER绑定、全局表解决关联需求,必须关联的拆成单表查询在应用层组装。
4、禁止大跨度的跨分片COUNT、GROUP BY、ORDER BY,统计类需求走宽表或者离线数仓,禁止在线上主库跑实时大规模聚合。
5、尽量避免跨分片分布式事务,写操作必须落到单分片,跨分片一致性用事务消息做最终一致性。
6、每个分片的索引设计和单库规则一致,分片键必须建索引,过滤、排序字段建对应联合索引,禁止分片内全表扫描。
7、IN查询的元素数量不要超过50个,避免SQL过长、路由分片过多导致性能下降。
8、单事务操作的记录数不要超过100条,批量操作按分片键分组,每批100条以内分批次提交,不要一次提交上千条数据。
9、禁止写SELECT *,只查需要的字段,减少跨分片传输的数据量,降低中间件的内存压力。
10、所有复杂查询、报表查询必须走从库,禁止在主库跑大查询,避免影响线上写业务。

四、真实优化案例:从12秒到18毫秒,订单列表优化全流程
我们当时上线后最慢的一个接口是商家后台的订单列表,优化前的SQL是这样的:
sql
-- 优化前的SQL,执行时间12.1秒,全路由+深分页+跨库JOIN
SELECT o.*, u.nickname, u.mobile
FROM order_info o
LEFT JOIN user_info u ON o.user_id = u.id
WHERE o.merchant_id = ? AND o.create_time BETWEEN '2025-01-01' AND '2025-06-01' AND o.order_status = 1
ORDER BY o.create_time DESC
LIMIT 5000, 20;
我们拆解了一下问题:首先merchant_id不是分片键,SQL全路由到16个分片;其次是5000条offset的深分页,每个分片要扫5020条数据;然后跨分片JOIN用户表,要拉用户数据做内存关联,最后还要排序归并,慢是必然的。
我们按规则一步步优化:
第一步解决JOIN问题:把nickname、mobile字段冗余到订单表,去掉LEFT JOIN,不需要跨表关联查用户信息。
第二步解决全路由问题:商家后台的多条件筛选、分页查询全走Elasticsearch,订单数据通过Binlog实时同步到ES,筛选、分页、排序全在ES里做,拿到当前页的订单ID和对应的user_id,再直接路由到对应分片查订单详情,不需要全路由扫所有分片。
第三步解决分页问题:ES的分页用search_after做游标分页,不做深分页,拿到ID之后都是按主键单条查询,每个查询都路由到单分片,性能极高。
第四步优化分片内索引:给每个分片的order_info表建联合索引(merchant_id, order_status, create_time, id),分片内查询直接走覆盖索引,不需要回表。
优化之后,整个接口的平均响应时间从12秒降到了20毫秒以内,性能提升了600多倍,哪怕翻到几百页也不会卡顿,更不会出现OOM的问题。
很多人觉得分库分表是架构升级的“标配”,不管自己公司业务量多大,上来就要做分库分表,觉得这样显得技术厉害。实际上我做了这么多年架构,最大的感受是:分库分表从来不是银弹,它是解决单库容量瓶颈和性能瓶颈的最后手段,会带来SQL编写、事务、运维、监控上的成倍复杂度。如果你的单表数据没到千万级,单库能扛住性能,完全没必要拆库拆表,通过索引优化、读写分离、缓存就能解决绝大多数问题。如果真的到了必须拆的地步,一定要提前把所有业务SQL过一遍,提前把坑填上,不要等上线崩了才去救火,毕竟业务可用性永远比“看起来厉害的架构”重要。

💡注意:本文所介绍的软件及功能均基于公开信息整理,仅供用户参考。在使用任何软件时,请务必遵守相关法律法规及软件使用协议。同时,本文不涉及任何商业推广或引流行为,仅为用户提供一个了解和使用该工具的渠道。
你在生活中时遇到了哪些问题?你是如何解决的?欢迎在评论区分享你的经验和心得!
希望这篇文章能够满足您的需求,如果您有任何修改意见或需要进一步的帮助,请随时告诉我!
感谢各位支持,可以关注我的个人主页,找到你所需要的宝贝。
博文入口:山峰哥-CSDN博客 复制到【浏览器】打开即可,宝贝入口:常用软件 宝贝:精品文件
作者郑重声明,本文内容为本人原创文章,纯净无利益纠葛,如有不妥之处,请及时联系修改或删除。诚邀各位读者秉持理性态度交流,共筑和谐讨论氛围~

1615

被折叠的 条评论
为什么被折叠?



