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性能问题。记住:优化永无止境,但每一次优化都应该有明确的目标和可衡量的结果。

相关推荐
华如锦10 小时前
四:从零搭建一个RAG
java·开发语言·人工智能·python·机器学习·spring cloud·计算机视觉
Tony_yitao10 小时前
22.华为OD机试真题:数组拼接(Java实现,100分通关)
java·算法·华为od·algorithm
JavaGuru_LiuYu10 小时前
Spring Boot 整合 SSE(Server-Sent Events)
java·spring boot·后端·sse
爬山算法10 小时前
Hibernate(26)什么是Hibernate的透明持久化?
java·后端·hibernate
彭于晏Yan10 小时前
Springboot实现数据脱敏
java·spring boot·后端
翼龙云_cloud10 小时前
阿里云渠道商:阿里云自动扩缩容配置教程
运维·服务器·阿里云·云计算
做cv的小昊10 小时前
【TJU】信息检索与分析课程笔记和练习(6)英文数据库检索—web of science
大数据·数据库·笔记·学习·全文检索
luming-0210 小时前
java报错解决:sun.net.utils不存
java·经验分享·bug·.net·intellij-idea
别多香了10 小时前
系统批量运维管理器 paramiko
linux·运维·服务器
北海有初拥10 小时前
Python基础语法万字详解
java·开发语言·python