SQL性能优化:子查询优化

引言

最近在排查一个财务报表系统的性能问题时,发现一个关键查询执行时间长达60秒,严重影响了用户体验。经过一系列优化后,查询时间降至6秒,性能提升了10倍。本文记录这次优化过程的技术细节和思考,希望对遇到类似问题的开发者有所帮助。

问题背景

系统有一个基金收益统计报表,需要展示每个基金的各项财务指标。原始SQL查询如下(简化版):

SELECT df.fund_id AS fundId, df.fund_name AS fundName,(SELECT accounting_method FROM dma_fund_accounting_method_checkbox WHERE fund_name = df.fund_name AND data_type = '清算维度') AS dimension, GREATEST(ROUND((1-MAX(sr.tcl_share_rate))*COALESCE(SUM(excess_income_amt), 0)/10000.0, 2),0)+ ROUND(COALESCE(SUM(payment_fund_rent_amt), 0)/10000.0, 2) AS totalFundIncomeAmt,-- 20+个类似的复杂计算字段... FROM dma_fund_cesy_fund_dim_qd df LEFT JOIN dma_s_tcl_share_rate sr ON df.fund_id = sr.fund_id AND sr.fund_id NOT IN ('12','14')-- 其他JOIN和WHERE条件GROUP BY df.fund_id, df.fund_name

性能问题分析

1. 子查询的N+1问题

这是最严重的性能问题。在SELECT子句中的相关子查询:

(SELECT accounting_method FROM dma_fund_accounting_method_checkbox

WHERE fund_name = df.fund_name AND data_type = '清算维度') AS dimension

问题分析:

  • 对于主查询返回的每一行,都会执行一次这个子查询

  • 如果主查询返回1000行,子查询执行1000次

  • 每次执行都需要解析SQL、优化、执行,开销巨大

优化方案:

改为LEFT JOIN,一次获取所有需要的数据:

LEFT JOIN dma_fund_accounting_method_checkbox amc

ON amc.fund_name = df.fund_name

AND amc.data_type = '清算维度'

2. 聚合函数的重复计算

原始查询中多次重复计算相同的聚合:

-- 计算了5次 COALESCE(SUM(excess_income_amt), 0)-- 计算了2次COALESCE(SUM(payment_fund_rent_amt), 0)

每次计算都需要扫描数据并执行聚合操作,大量浪费CPU和内存资源。

3. JOIN条件中的NOT IN

sql

LEFT JOIN dma_s_tcl_share_rate sr ON df.fund_id = sr.fund_id

AND sr.fund_id NOT IN ('12','14')

在JOIN条件中使用NOT IN可能导致:

  1. 无法有效利用索引

  2. 执行全表扫描

  3. 增加查询优化器的复杂度

优化方案

方案一:使用CTE预计算(推荐)

sql

WITH fund_aggregates AS (-- 预计算所有聚合结果 SELECT fund_id, fund_name,SUM(excess_income_amt) AS total_excess_income_amt,SUM(payment_fund_rent_amt) AS total_payment_fund_rent_amt,-- 其他聚合字段... MAX(accounting_type) AS accounting_typeFROM dma_fund_cesy_fund_dim_qdWHERE -- 查询条件 GROUP BY fund_id, fund_name ), filtered_share_rate AS (-- 预先过滤共享率数据 SELECT DISTINCT fund_id, tcl_share_rateFROM dma_s_tcl_share_rate WHERE fund_id NOT IN ('12','14'))SELECT fa.fund_id AS fundId, fa.fund_name AS fundName, amc.accounting_method AS dimension,-- 使用预计算结果进行计算 GREATEST(ROUND((1-COALESCE(sr.tcl_share_rate,0)) * COALESCE(fa.total_excess_income_amt,0)/10000.0, 2), 0)+ ROUND(COALESCE(fa.total_payment_fund_rent_amt,0)/10000.0, 2) AS totalFundIncomeAmt,*-- 其他字段...*FROM fund_aggregates fa LEFT JOIN dma_fund_accounting_method_checkbox amc ON amc.fund_name = fa.fund_name AND amc.data_type = '清算维度'LEFT JOIN filtered_share_rate sr ON fa.fund_id = sr.fund_id;

方案二:使用临时表(适合复杂场景)

sql

-- 创建临时表存储聚合结果 CREATE TEMPORARY TABLE tmp_fund_aggregates ASSELECT fund_id, fund_name,-- 聚合计算... FROM dma_fund_cesy_fund_dim_qd GROUP BY fund_id, fund_name;-- 然后基于临时表进行查询 SELECT fa.fund_id AS fundId, fa.fund_name AS fundName,-- ... FROM tmp_fund_aggregates fa -- ... 其他JOIN-- 清理临时表DROP TEMPORARY TABLE tmp_fund_aggregates;

索引优化

为关键查询字段创建合适的索引:

-- 为子查询表创建 覆盖索引 CREATE INDEX idx_checkbox_fund_data ON dma_fund_accounting_method_checkbox(fund_name, data_type, accounting_method);-- 为JOIN条件创建索引 CREATE INDEX idx_share_rate_fund ON dma_s_tcl_share_rate(fund_id, tcl_share_rate);-- 为主表创建 复合索引CREATE INDEX idx_fund_dim_main ON dma_fund_cesy_fund_dim_qd(fund_id, fund_name, accounting_type);

LEFT JOIN vs INNER JOIN的陷阱

在优化过程中,发现一个容易混淆的点:

FROM fund_aggregates fa

LEFT JOIN dma_fund_accounting_method_checkbox amc

ON amc.fund_name = fa.fund_name

AND amc.data_type = '清算维度'

重要发现:即使加上AND amc.data_type = '清算维度'条件,这仍然是LEFT JOIN,不是INNER JOIN。

两者的区别:

|-------|-----------|------------|
| 特性 | LEFT JOIN | INNER JOIN |
| 匹配原则 | 返回左表所有行 | 只返回匹配行 |
| 右表无匹配 | 右表字段为NULL | 左表行被过滤 |
| 结果集大小 | 可能大于左表 | 小于等于左表 |

一对多JOIN的陷阱:

如果右表存在重复记录,LEFT JOIN会产生多行结果:

-- 左表:1行(基金A)-- 右表:2行(基金A有2个会计方法)-- 结果:2行!这可能导致聚合计算错误翻倍

解决方案:

-- 使用DISTINCT或 子查询 确保唯一性LEFT JOIN (SELECT DISTINCT fund_name, accounting_methodFROM dma_fund_accounting_method_checkboxWHERE data_type = '清算维度') amc ON amc.fund_name = fa.fund_name

代码层面的优化

在Java应用层,我们可以进一步优化:

1. 使用MyBatis的缓存机制

xml

<!-- 启用 二级缓存 --> <cache eviction="LRU" flushInterval="60000" size="512" readOnly="true"/><!-- 对查询结果缓存 -->

<select id="getFundReport" resultMap="fundReportMap" useCache="true"><!-- SQL 查询 --></select>

2. 分页查询优化

java

// 使用 游标 分页代替传统分页public List<FundReport> getFundReportByCursor(Long lastId, int pageSize) {return sqlSession.selectList("FundMapper.getFundReportByCursor", Map.of("lastId", lastId, "pageSize", pageSize));}

xml

<select id="getFundReportByCursor" resultMap="fundReportMap">

SELECT * FROM (

-- 优化后的查询

) t

WHERE t.fundId > #{lastId}

ORDER BY t.fundId

LIMIT #{pageSize}

</select>

经验总结

  1. 避免SELECT子句中的相关子查询:这是性能杀手

  2. 预计算聚合结果:使用CTE或临时表减少重复计算

  3. 合理使用索引:为JOIN条件和WHERE条件创建复合索引

  4. 理解JOIN类型:明确区分LEFT JOIN和INNER JOIN的应用场景

  5. 关注数据唯一性:一对多JOIN可能导致数据重复和计算错误

  6. 应用层配合优化:结合缓存、分页、异步处理等方案

结语

SQL优化是一个系统性的工程,需要从数据库设计、查询编写、索引优化到应用层缓存全方位考虑。通过这次优化,我们不仅解决了眼前的性能问题,更建立了一套完整的SQL性能监控和优化体系。

希望本文的经验能帮助大家在实际工作中更好地处理SQL性能问题。记住:优化永无止境,但每一次优化都应该有明确的目标和可衡量的结果。

相关推荐
深蓝轨迹17 小时前
@Autowired与@Resource:Spring依赖注入注解核心差异剖析
java·python·spring·注解
不想看见40417 小时前
C++八股文【详细总结】
java·开发语言·c++
Bdygsl17 小时前
MySQL(1)—— 基本概念和操作
数据库·mysql
huaweichenai17 小时前
java的数据类型介绍
java·开发语言
zongzizz17 小时前
Oracle 11g 两节点rac在机房断电重启后PL/SQL和客户端连接数据库报错ORA-12541
数据库·oracle
qq_4176950517 小时前
实战:用OpenCV和Python进行人脸识别
jvm·数据库·python
身如柳絮随风扬17 小时前
什么是左匹配规则?
数据库·sql·mysql
weisian15117 小时前
Java并发编程--17-阻塞队列BlockingQueue:生产者-消费者模式的最佳实践
java·阻塞队列·blockqueue
奔跑的呱呱牛17 小时前
GeoJSON 在大数据场景下为什么不够用?替代方案分析
java·大数据·servlet·gis·geojson
xinhuanjieyi17 小时前
ruoyimate导入sql\antflow\bpm_init_db.sql报错
android·数据库·sql