在数据库优化中,没有普适的"银弹"。确定方案时要考虑业务场景、数据规模和系统架构的三角平衡。在类似Solr索引构建的批处理场景中,"分而治之"的多次聚合查询往往是更务实、可持续的选择。
搜索引擎(如Solr、Elasticsearch)的数据构建过程与常规业务查询存在显著差异。在搜索引擎场景下,我们通常需要定期全量重建索引或批量增量更新,这一特性决定了其对数据源查询的特殊要求。
当从关系型数据库(如MySQL)向搜索引擎同步数据时,传统的复杂SQL查询面临严峻挑战:
- 数据量大:全量构建可能需要读取上亿条记录
- 关联复杂:需要将多个表的数据整合为搜索引擎的扁平化文档
- 性能要求高:索引构建应在合理时间内完成,不影响源数据库正常业务
今天给大家分享一个发生在合规类业务系统中的真实案例。
一、案例:业务中的性能瓶颈
本次优化背景:某合规类业务系统的Solr 索引构建需求—— 为保障 Solr 检索时能精准关联核心对象的多类机构信息(含登记机构、备案机构、检查机构、收款机构等编码数据),需从业务库中批量查询并提取该类关联数据。
原先我找AI帮我写的,完美的复杂查询语句,虽能一次性覆盖所有机构字段的关联逻辑,但在测试环境执行耗时最长达数小时,触发数据库超时告警。
具体 SQL 示例如下:
SELECT
main.main_id, -- 核心主体唯一标识
-- 类型1:登记机构(子表1,筛选type=1/2)
GROUP_CONCAT(DISTINCT sub1.org_code) AS reg_org_code,
-- 类型2:检查机构(子表2)
GROUP_CONCAT(DISTINCT sub2.org_code) AS che_org_code,
-- 类型3:备案机构(子表1,筛选type=3)
GROUP_CONCAT(DISTINCT sub3.org_code) AS record_org_code,
-- 类型4:收款机构(主表自带字段)
main.pay_org_code AS pay_org_code
FROM main_table main
-- 左联子表1:获取登记机构、备案机构(同表不同筛选条件)
LEFT JOIN sub_table1 sub1
ON main.id = sub1.main_id
AND sub1.type IN (1,2)
LEFT JOIN sub_table2 sub2
ON main.id = sub2.main_id
LEFT JOIN sub_table1 sub3
ON main.id = sub3.main_id
AND sub3.type = 3
WHERE
main.status = 0 -- 主表有效数据筛选
AND main.user_code IN ('USER_2025001') -- 业务查询条件
GROUP BY
main.id,
main.main_id,
main.pay_org_code;
原 SQL 的核心瓶颈
- 多表左联导致中间结果集膨胀
子表 1 被左联 2 次(分别查登记 / 备案机构),子表 2 左联 1 次,若子表 1 单条
main_id对应 100 条记录,仅子表 1 的两次左联就会产生 100×100=10000 条中间记录,后续聚合需处理大量冗余数据; - 聚合时频繁 “回表”
GROUP_CONCAT需读取org_code字段,但子表无覆盖索引,数据库需先通过关联条件找到主键,再回表读取org_code,磁盘 IO 开销极大; GROUP_CONCAT计算压力集中多字段同时聚合且需去重(
DISTINCT),数据库需对海量中间结果做排序、去重、拼接,CPU 占用率高。
二、探索:从技术最优到业务权衡
1. 初步方案:覆盖索引 —— 技术层面的最优解
(1)方案原理
慢 SQL 的核心痛点在于多表关联后产生海量中间结果集,且聚合时需频繁 “回表” 读取数据。
“覆盖索引”方案,是指索引中包含查询所需的全部字段(关联条件、过滤条件、聚合字段),数据库无需再去数据表中读取额外数据(即 “避免回表”),直接通过索引即可完成过滤、关联和聚合,大幅减少磁盘 IO 开销。
(2)具体设计思路
针对查询中的多个子表,设计包含 “关联字段 + 过滤字段 + 聚合字段” 的联合索引:
-
对需聚合的子表,索引需覆盖 “与主表的关联 ID”(如
main_id)、“筛选条件字段”(如type、status)、“需拼接的机构编码字段”(如org_code); -
单个联合索引可尝试覆盖同表的多个聚合场景(如子表 1 的
type=1/2和type=3),减少索引数量。
(3)未采纳的核心原因
经评估后放弃该方案,主要顾虑两点:
- 索引维护成本
系统现有索引数量已较多,新增索引会增加数据写入(插入 / 更新 / 删除)时的同步开销,尤其子表数据量较大时,可能影响写入性能;
- 索引膨胀风险
过多索引会占用额外存储空间,且长期可能出现 “未被高频使用的冗余索引”,增加后续索引治理难度。
三、业内方案:计算压力的 “转移与拆分”
当无法通过索引优化数据库性能时,业内常用思路是将数据库的 “复杂计算压力” 转移到应用层,核心逻辑与搜索引擎的 “存储简化 + 计算迁移” 理念相通,通过 “检索原始数据 + 上层计算合并” 实现需求,具体分为两类方案:
1. 方案一:取原始数据,应用层聚合
原理与操作
数据库仅返回所有符合条件的原始数据(不做GROUP_CONCAT等聚合操作),由应用层(如 Python)通过set去重、join拼接等逻辑处理结果。例如:
-
数据库执行 “主表左联子表”,返回
main_id、org_code、type
等原始字段; -
应用层按
main_id分组,用set(org_code)自动去重,再按type分类拼接成 “登记机构”“备案机构” 等字段。
优劣势
-
优势:单次查询获取所有数据,减少网络往返;数据库逻辑极简,仅需过滤和返回数据;
-
劣势:若原始数据量庞大(如单对象关联数百条记录),会增加网络传输压力和应用层内存消耗。
2. 方案二:执行多次聚合查询
原理与操作
将原查询的多个聚合需求拆分为独立的小查询,每次仅处理一个聚合目标,最后在应用层拼接结果。
核心是 “减少单次查询的中间结果集大小”,避免多表关联带来的性能损耗。例如:
-
先查主表获取核心关联 ID 和基础字段;
-
再针对每个聚合需求单独查子表(如 “登记机构”“备案机构” 分别查询);
-
应用层将多轮查询结果按关联 ID 合并。
优劣势
-
优势:单次查询逻辑简单(无多表关联),结果集小(仅聚合后的数据),网络和内存压力可控;
-
劣势:查询次数增加,可能存在网络往返延迟(可通过并行查询优化)。
四、最终采纳:结合业务的平衡方案
1. 方案选择逻辑
结合业务实际(子表数据量大、索引维护敏感、查询需低延迟),最终选择 “分多次查询 + 同来源聚合合并” 的方案,核心是在 “减少查询次数” 和 “降低单次复杂度” 之间找平衡:
-
避免 “应用层聚合” 的大流量传输问题;
-
通过 “合并同表需求” 减少查询次数,规避过多网络往返。
2. 落地步骤
步骤 1:查主表 —— 获取核心关联 ID 与基础字段
先查询主表,获取两个关键信息:
-
关联子表的 “唯一 ID”(如
main_id),作为后续子表查询的过滤条件; -
主表自带的基础字段,无需关联子表即可获取。
示例 SQL:
SELECT
main_id,
user_code,
pay_org_code -- 主表自带
FROM main_table
WHERE
status = 0
AND user_code IN ('USER_2025001');
步骤 2:合并同表聚合 —— 减少子表扫描次数
将来自同一子表的多个聚合需求(如 “登记机构”“备案机构”)合并为一次查询,通过CASE WHEN区分筛选条件,一次获取两个聚合结果,避免重复扫描子表。示例 SQL(子表 1 聚合):
SELECT
main_id,
-- 登记机构:筛选type=1/2,去重拼接
GROUP_CONCAT(DISTINCT CASE WHEN type IN (1,2) THEN org_code END) AS reg_org_code,
-- 备案机构:筛选type=3,去重拼接
GROUP_CONCAT(DISTINCT CASE WHEN type=3 AND status=0 THEN org_code END) AS record_org_code
FROM sub_table1
WHERE main_id = ? -- 步骤1获取的main_id
GROUP BY main_id;
步骤 3:查另一子表 —— 独立获取剩余聚合需求
针对来自其他子表的聚合需求(如 “医保机构”),单独发起一次查询,筛选条件为步骤 1 获取的 “关联 ID”,仅返回聚合后的结果。
SELECT
main_id,
GROUP_CONCAT(DISTINCT org_code) AS che_org_code
FROM sub_table2
WHERE
main_id = ? -- 步骤1获取的main_id
GROUP BY main_id;
步骤 4:应用层拼接 —— 组装最终结果
在应用层(如 Python)按 “关联 ID” 将三次查询结果拼接,得到与原查询一致的字段集,完成数据组装。
3. 优化效果
-
性能提升:查询耗时从数小时降至秒级,资源消耗平滑,避免数据库峰值压力。
-
维护成本降低:无需新增联合索引,减少DBA运维负担;代码逻辑清晰。
-
可扩展性:后续新增聚合需求时,仅需新增单次子表查询,无需修改复杂关联逻辑。
五、总结:方案选择的决策逻辑与场景适配性
本次慢SQL优化的核心在于放弃"覆盖索引"和"应用层聚合"方案,选择"多次聚合查询"方案。
以下是三个方案的对比总结及最终选择的关键依据:
1. 三个方案对比与淘汰原因
| 方案 | 核心逻辑 | 优势 | 在本案例中的缺陷 |
|---|---|---|---|
| 覆盖索引 | 通过联合索引包含查询所需字段,避免回表 | 查询性能高,减少I/O | 索引维护成本高,写入压力大;无法解决多表关联的中间结果集膨胀问题 |
| 应用层聚合 | 数据库返回原始数据,代码层完成关联聚合 | 数据库压力小,架构灵活 | 网络传输开销大,应用层内存消耗高,不适合大数据量场景 |
| 多次聚合查询 | 拆分复杂查询为多个独立聚合查询,分步获取结果 | 平衡数据库压力与应用层复杂度 | 需合理控制查询次数,避免N+1问题 |
方案淘汰原因:
- 覆盖索引:虽能提升单次查询性能,但需为多子表创建联合索引(如
(main_id, type, org_code)),导致索引体积膨胀(预计增加15GB+),且增删改操作触发索引更新,加剧写入延迟,不符合高频写入的业务场景。 - 应用层聚合:需将全量原始数据传输至应用层,网络传输开销大,且内存聚合计算可能引发OOM风险(如子表数据量达千万级时)。
2. "多次聚合查询"的关键决策依据
分阶段聚合查询(主表查询→子表1合并聚合→子表2独立查询)实现优化的合理性:
- 业务场景适配:
Solr索引构建需全量批量数据处理,而非实时交互查询。多次查询可将压力分散至不同时段,避免单次复杂查询长时间占用数据库资源,契合Solr异步批量消费数据的特性。 - 数据特性匹配:
子表数据量大,且关联字段(main_id)分布稀疏。通过分次聚合,每次仅处理一个维度的数据(如先登记机构再备案机构),减少中间结果集规模,避免内存溢出。 - 系统架构协同:
- 数据库层:简单聚合查询(如
GROUP BY main_id)可充分利用索引,且单次查询资源消耗可控。 - 应用层:合并同源查询(如子表1的登记/备案机构),一次获取多聚合结果,减少查询次数至O(1)而非O(N)。
- 数据库层:简单聚合查询(如
结论:技术决策的本质是场景适配
在数据库优化中,没有普适的"银弹"。确定方案时要考虑业务场景、数据规模和系统架构的三角平衡。在类似Solr索引构建的批处理场景中,"分而治之"的多次聚合查询往往是更务实、可持续的选择。
473

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



