MySQL SELECT语句执行缓慢原因排查求助
兄弟,我一眼就看到几个导致你查询慢到超时的关键问题,尤其是逻辑运算符优先级和索引使用的坑,咱们一步步拆解:
核心问题1:WHERE子句的逻辑优先级混乱
这是最致命的问题!你的查询里混用了AND和OR,但OR的优先级比AND低,导致MySQL把你的条件解析成了:
(所有AND条件) OR (f.id = 9779432)
也就是说,只要family ID是9779432,不管它是否满足companyid=1170、交易条件等,都会被纳入结果。这种模糊的逻辑会让优化器无法有效利用索引——因为它要同时处理两种完全不同的过滤规则,大概率会触发全表扫描。
修复方案:用括号明确分组逻辑,比如如果你希望OR的两端都满足交易相关条件,应该写成:
WHERE (f.companyid = 1170 AND f.status = 'A' AND f.id = t.familyid AND f.id = c.familyid AND t.transactiontype = 'P' AND t.taxdeductible = 'Y' AND YEAR(t.date) = 2017) OR (f.id = 9779432 AND f.id = t.familyid AND f.id = c.familyid AND t.transactiontype = 'P' AND t.taxdeductible = 'Y' AND YEAR(t.date) = 2017)
如果特定family ID不需要满足其他条件,就把OR的后半部分单独括起来,但这样可能还是会影响索引效率,后面会给替代方案。
核心问题2:非SARGable条件导致索引失效
你用了YEAR(t.date) = 2017,这个函数包装了索引列date,会让MySQL无法直接使用transactions.date的索引——它必须逐行计算每个日期的年份,相当于全表扫描符合其他条件的交易记录。而你的transactions表有98万行,这绝对是性能杀手。
修复方案:把函数条件改成范围查询,让它变成SARGable(可以利用索引):
t.date BETWEEN '2017-01-01' AND '2017-12-31'
如果你的date字段包含时间,就改成'2017-12-31 23:59:59'来覆盖全年所有时间点。
核心问题3:单字段索引无法满足多条件查询
你创建的都是单字段索引,但你的查询是多条件组合过滤+关联,单字段索引的效率极低,优化器可能直接放弃使用,选择全表扫描。比如:
- transactions表需要同时过滤
familyid、transactiontype、taxdeductible、date,单字段索引无法覆盖这些条件的组合 - families表需要过滤
companyid+status,还要关联id、排序name,单字段索引做不到
修复方案:创建针对性的复合索引:
- transactions表:创建复合索引
(familyid, transactiontype, taxdeductible, date)——把关联字段放最前面,然后是过滤字段,最后是范围字段,这样可以覆盖所有交易相关的过滤条件,避免回表 - families表:创建复合索引
(companyid, status, id, name)——覆盖过滤条件、关联字段和排序字段,消除Using filesort - children表:创建复合索引
(familyid, firstname)——覆盖关联字段和需要聚合的firstname,避免回表
优化后的完整查询语句
改用显式JOIN语法(比隐式逗号JOIN更清晰,优化器更容易生成好的执行计划),加上所有修复点:
SELECT f.id, f.name, GROUP_CONCAT(DISTINCT c.firstname) AS children FROM families f INNER JOIN children c ON f.id = c.familyid INNER JOIN transactions t ON f.id = t.familyid WHERE (f.companyid = 1170 AND f.status = 'A' AND t.transactiontype = 'P' AND t.taxdeductible = 'Y' AND t.date BETWEEN '2017-01-01' AND '2017-12-31') OR (f.id = 9779432 AND t.transactiontype = 'P' AND t.taxdeductible = 'Y' AND t.date BETWEEN '2017-01-01' AND '2017-12-31') GROUP BY f.id, f.name -- 加上f.name符合ONLY_FULL_GROUP_BY模式,避免语法错误 ORDER BY f.name;
额外的性能优化建议
如果OR的两个分支逻辑差异很大,优化器还是可能无法高效利用索引,这时候可以把查询拆成两个独立查询用UNION ALL合并:
-- 第一个分支:符合公司条件的家庭 SELECT f.id, f.name, GROUP_CONCAT(DISTINCT c.firstname) AS children FROM families f INNER JOIN children c ON f.id = c.familyid INNER JOIN transactions t ON f.id = t.familyid WHERE f.companyid = 1170 AND f.status = 'A' AND t.transactiontype = 'P' AND t.taxdeductible = 'Y' AND t.date BETWEEN '2017-01-01' AND '2017-12-31' GROUP BY f.id, f.name UNION ALL -- 第二个分支:特定家庭 SELECT f.id, f.name, GROUP_CONCAT(DISTINCT c.firstname) AS children FROM families f INNER JOIN children c ON f.id = c.familyid INNER JOIN transactions t ON f.id = t.familyid WHERE f.id = 9779432 AND t.transactiontype = 'P' AND t.taxdeductible = 'Y' AND t.date BETWEEN '2017-01-01' AND '2017-12-31' GROUP BY f.id, f.name -- 最后统一排序 ORDER BY name;
这样每个子查询都能完美利用我们创建的复合索引,合并结果的开销远低于单个带OR的查询。
验证优化效果
执行EXPLAIN命令查看执行计划,重点看:
type列:应该是ref或range,而不是ALL(全表扫描)key列:应该显示我们创建的复合索引Extra列:不要出现Using filesort或Using temporary(除非无法避免)
内容的提问来源于stack exchange,提问作者Vincent

