要压住这套分库分表的报表查询,机器的 IO 和内存余量得先备足。HostEONS 提供纯 SSD RAID 10 的 KVM VPS,单库数据量上来以后随机读能少很多磁盘寻道;Lightlayer 的独立物理服务器带宽规格更高,报表任务跨多个分片扫数据时,网络不会被中间环节卡住。下面这些排查手段和建议,是在真实业务上一步步验证出来的。
我们这套订单系统是两年前分库分表的,16 个库,订单表按 user_id 取模分片。上线头一年多, 业务量从日均 20 万单涨到 200 万单,一切看着都挺正常。直到上个月财务同学找过来,说月度报表 跑了 4 个半小时还没出结果,去年同样的报表 40 分钟就出来了。我们一开始以为是数据量涨了, 查了一圈发现不对单月订单才 6000 万行,比去年同期多了一倍而已。
排查的第一个方向是慢查询日志。把报表那条 SQL 捞出来一看,走了 4 个分片,
每片都扫了 180 万行,全表扫描,没有命中任何索引。一看 SQL 就明白了:
WHERE created_at BETWEEN ... AND ... AND status = 3,
时间范围加状态过滤,而分片键是 user_id。created_at 和 status 都不是分片键,
所以每个分片都得全表扫一遍。这属于典型的「不带分片键的查询必然广播到所有分片」。
先别急着改架构,得先确认慢查询到底打到了几个库。中间件层加一行日志就够了, 把一次报表请求实际路由到哪几个数据源、每个数据源耗时多少,全打出来。
结果很干净:每次报表请求都稳定命中全部 16 个分片,单分片平均耗时 17 秒, 16 片串行叠加就是 4 分半。这个数字和财务同学说的 4 个半小时对得上(他们那边还要叠加几个维度的汇总)。 换句话说,问题不是「某一个库慢」,而是「16 个库同时被扫了一遍」。

但这里有个更隐蔽的问题。分片键是 user_id,取模分片理论上应该是均匀的, 可我去看了下各分片的行数分布,差距大得离谱:db_03 有 470 万行,db_11 只有 190 万行, 差了 2.4 倍。
查下来原因是历史遗留。早期版本的分片算法是 user_id % 16,但 user_id 是自增主键, 而早期是按注册顺序分配的,早期注册的用户 id 连续、后来做了 id 段位调整。 再叠加上一大批从老系统迁移过来的用户 id 集中在某个区间,结果就是分布彻底歪了。
所以真实的问题有两层:一是查询没有带分片键导致全分片广播, 二是部分分片本身数据量就是别人的两三倍。这两层叠加,最慢的那个分片决定了整条链路的耗时, 而 16 个分片串行等待,快的那些全在陪跑。

第一个动作是给报表加上真正的分片键过滤。报表的业务口径是按「租户 + 时间」统计,
而每个用户只属于一个租户,租户和 user_id 是一对多的关系。这里用了个取巧的办法:
先从租户维度的映射表里查出这个租户下的所有 user_id 范围,
再把 user_id 列表塞进 IN 条件,让分片中间件能算出实际命中的分片集合。
改完之后,租户 A 的报表只命中 2 个分片,耗时从 4 分半降到 50 秒左右。 但这不是最优解,因为 IN 列表如果太长,解析开销和 SQL 长度都会上去。
第二个动作更彻底:把报表拆成「按分片并行执行 + 结果归并」的模式。 中间件把请求分发到 16 个分片并发跑,每个分片只返回聚合后的中间结果(只回 60 多行按天汇总), 而不是把 6000 万行拉回来再在应用层聚合。这一步把 4 分半压到了 6 分钟以内, 而且扛住了后来又一轮翻倍的数据量。

全表扫描的问题还是得靠索引解决。给每张分表补了 idx_created_status(created_at, status)
联合索引,单独跑一次分片的扫描耗时从 17 秒降到 2.3 秒。
加上索引之后,「分片并行 + 索引」这个组合才真正把广播查询的代价压到了可接受范围。

索引和查询改完,数据倾斜还摆在那儿。db_03 那 470 万行每次都是最慢的, 拖住整个并行的尾巴。所以还是得做一次数据迁移,把热点分片的数据拆出去。
这里踩了个坑:一开始想用「双写 + 校验 + 切读」的方案, 结果双写期间新老逻辑的分片算法不一样,导致同一个订单在两个地方算出来的分片号对不上, 校验直接报出几十万条不一致。最后改成迁移期间新旧双写用同一个分片函数、 只在校验通过后才切换读流量,这个顺序反了就是灾难。
迁移完成后重新统计,16 个库的行数落在 310 万到 390 万之间,最大最小差从 2.4 倍收窄到 1.26 倍, 并行执行的「木桶效应」基本消除了。整体月度报表耗时稳定在 3 分 40 秒左右, 比改造前的 4 小时半快了 7 倍多。
一是分片数不是越多越好。我们从 8 个扩到 16 个的时候,报表耗时反而变长了, 因为广播查询的开销随分片数线性增长。16 个库是我们权衡广播成本和单库容量之后的折中, 再多就该考虑独立的分析库或者数仓了。
二是分片键选错的代价,比选错算法大得多。算法错了数据分布还能靠迁移修, 分片键选错了意味着每次查询都要跨库,架构上就是错的。
三是慢查询监控要按分片维度看。只看总耗时发现不了倾斜, 我们现在的监控里会带上每个分片的行数和单分片耗时, 超过平均值 1.5 倍就告警,这次就是靠这个提前发现 db_03 不对劲。
整个过程前后花了三周,改动包括查询改写、索引补充、分片函数对齐、数据迁移四块。 回头看,最开始应该先做数据倾斜的统计,而不是直接怀疑机器性能不够 一开始就打开各分片的行数分布,能少走很多弯路。