需要帮助优化一个有趣的 MySQL 查询

Posted

技术标签:

【中文标题】需要帮助优化一个有趣的 MySQL 查询【英文标题】:Need Help on Optimizing an Interesting MySQL Query 【发布时间】:2017-08-09 19:51:22 【问题描述】:

查询优化

我在优化此查询的性能方面需要帮助。此查询基本上是查找与 case when 条件列表匹配的所有期间的累积和。

目前,此查询需要大约 100 秒才能运行,因为它按数据库中的每个帐户分组。我试图通过查看解释输出来优化它,但我找不到让它工作的方法。这是解释输出:

构思时间为 10 秒或更短。期待你的回复。谢谢!

SET @date = '2017-05-17';
SET @offset = 1;

select 
b.act,
CASE 
WHEN b.jdt <= DATE_SUB(@date, INTERVAL 5 DAY) AND b.jdt >= DATE_SUB(@date, INTERVAL 5 + @offset DAY) AND DATEDIFF(a.dt,b.jdt) <=5 THEN 5
WHEN b.jdt <= DATE_SUB(@date, INTERVAL 13 DAY) AND b.jdt >= DATE_SUB(@date, INTERVAL 13 + @offset DAY) AND DATEDIFF(a.dt,b.jdt) <=13 THEN 13
WHEN b.jdt <= DATE_SUB(@date, INTERVAL 25 DAY) AND b.jdt >= DATE_SUB(@date, INTERVAL 25 + @offset DAY) AND DATEDIFF(a.dt,b.jdt) <=25 THEN 25
WHEN b.jdt <= DATE_SUB(@date, INTERVAL 45 DAY) AND b.jdt >= DATE_SUB(@date, INTERVAL 45 + @offset DAY) AND DATEDIFF(a.dt,b.jdt) <=45 THEN 45
WHEN b.jdt <= DATE_SUB(@date, INTERVAL 75 DAY) AND b.jdt >= DATE_SUB(@date, INTERVAL 75 + @offset DAY) AND DATEDIFF(a.dt,b.jdt) <=75 THEN 75
WHEN b.jdt <= DATE_SUB(@date, INTERVAL 105 DAY) AND b.jdt >= DATE_SUB(@date, INTERVAL 105 + @offset DAY) AND DATEDIFF(a.dt,b.jdt) <=105 THEN 105
WHEN b.jdt <= DATE_SUB(@date, INTERVAL 135 DAY) AND b.jdt >= DATE_SUB(@date, INTERVAL 135 + @offset DAY) AND DATEDIFF(a.dt,b.jdt) <=135 THEN 135
WHEN b.jdt <= DATE_SUB(@date, INTERVAL 165 DAY) AND b.jdt >= DATE_SUB(@date, INTERVAL 165 + @offset DAY) AND DATEDIFF(a.dt,b.jdt) <=165 THEN 165
WHEN b.jdt <= DATE_SUB(@date, INTERVAL 195 DAY) AND b.jdt >= DATE_SUB(@date, INTERVAL 195 + @offset DAY) AND DATEDIFF(a.dt,b.jdt) <=195 THEN 195
WHEN b.jdt <= DATE_SUB(@date, INTERVAL 225 DAY) AND b.jdt >= DATE_SUB(@date, INTERVAL 225 + @offset DAY) AND DATEDIFF(a.dt,b.jdt) <=225 THEN 225
ELSE 'other' END AS 'period',

SUM(CASE WHEN a.type = 'JN' AND a.paid = 'Y' AND a.upgraded=0 THEN 1 ELSE 0 END) AS 'Paid_Joins',
SUM(CASE WHEN a.type IN ('SL','RL') AND ttype !='Purchase' THEN (a.amt_usd/100 - a.vat_usd/100) END) AS 'Revenue_Amount'

FROM __customer b
JOIN  __transaction a on b.uid = a.primary_uid 

WHERE
b.affiliate_act regexp '^[a-zA-Z]+[0-9]+'
AND a.dt <= @date 
AND a.dt >= DATE_SUB(@date, INTERVAL 225 + @offset DAY)
AND b.jdt >= DATE_SUB(@date, INTERVAL 225 + @offset DAY)
GROUP BY 1,2
HAVING period != 'other'

更新

表结构:

更新2

我在没有客户表连接的情况下使用相同的查询逻辑查询事务表,看起来它仍在扫描与连接相同的行。由于它会查看数据库中的每个组合,因此我无法考虑添加更有效的 where 子句来限制扫描的行数。

SET @date = '2017-05-17';
SET @offset = 2;
SET @start = DATE_SUB(@date, INTERVAL 225 + @offset DAY);

explain
select 
    a.account,        
    SUM(CASE WHEN a.type = 'JN' AND a.paid = 'Y' AND a.upgraded=0 
             THEN 1 
             ELSE 0 
        END) AS 'Paid_Joins'       
FROM __transaction a        
WHERE a.account regexp '^[a-zA-Z]+[0-9]+'
  AND a.dt <= @date 
  AND a.dt >= @start
-- AND b.affiliate_act = 'el4557'
GROUP BY 1

此处扫描的行数与存在连接时相同。

【问题讨论】:

性能问题应该包括EXPLAIN ANALYZE和一些关于表大小、索引、当前时间性能、期望时间等的信息。Slow是一个相对术语,我们需要一个真实的值来比较。 mysqlHow do I obtain a Query Execution Plan? 我认为 MySQL 中没有 EXPLAIN ANALYZE,但我尝试附上类似的东西可能会有所帮助。 我们需要您的创建表和索引,以便我们了解您已经拥有的内容。我提供的链接告诉你如何获得查询执行计划。但是对于您发布的显示FULL TABLE SCAN 的图片,您似乎没有日期索引。你可能需要一个复合索引(uid, date) 我告诉过你使用复合索引。此外,您正在尝试解决一个大问题,而不是先尝试解决一个较小的问题。删除所有CASEGROUP BY 使用新索引测试WHERE,如果情况好转,请开始添加更多部分。 显示创建索引。那张桌子上有多少天?是否有可能所有日期都在该范围内? 【参考方案1】:

看起来您正在尝试将帐户活动分解为客户历史交易的存储桶范围。但是,查看您的日期测试,看起来每个存储桶基本上是 2 天,例如

5 days results in 5/11-5/12
13 days results in 5/3-5/4
25 days results in 4/21-4/22

取而代之的是满满一桶的前任:

5/11 - 5/17
5/2  - 5/10
4/21 - 5/2
??? - 4/20

如果我使用您的日期/间隔设置运行简单

SELECT DATE_SUB(@date, INTERVAL 5 DAY) - @offset   (result 20170511 looking like a number, not a date)
and
SELECT DATE_SUB(@date, INTERVAL 5 DAY) (result 2017-05-12 expected date)

因此,您的范围将代表 2017-05-11

如果您正在寻找一天的活动,您实际上可能意味着执行以下操作

SELECT DATE_SUB(@date, INTERVAL 5+@offset DAY) (result 2017-05-11  expected date)
and
SELECT DATE_SUB(@date, INTERVAL 5 DAY) (result 2017-05-12 expected date)

为了进行基于日期/时间的列以进行日期范围比较,我通常会 >= 开始日期,并且比有问题的 NEXT 天少,因此会捕获一天中的每一小时/分钟/秒

'2017-05-11' >= jdt AND jdt < '2017-05-12'

这只会为您提供 5 月 11 日至晚上 11:59:59 的所有内容,但不包括 5 月 12 日的日期。如果您打算像我描述的那样实际进行桶范围,我将为您提供更清洁的解决方案,请告诉我。

此外,为了您的表现,您正在查看给定日期之前的所有交易,因此您几乎浏览了整个交易表。表中还有哪些其他“类型”的事务可以通过索引帮助丢弃?看来您只关心“JN”、“SL”和“RL”。也就是说,我会在 (type, dt) 上的事务表上有一个索引。对于您的客户表,我将在 (uid, act) 上有一个覆盖索引,因此它不需要转到客户的所有页面数据来检索他们的帐户。它根据 UID 限定加入,并且帐户 # 随之而来。

【讨论】:

我还在交易中添加了复合索引 (type,dt),在客户中添加了 (uid,act),但解释输出仍然显示相同。 我使用单天来测试性能,但每个存储桶的日期范围可能会动态变化,它们取决于两个参数“日期”和“偏移量”【参考方案2】:

确保 a.primary_uid、a.dt 和 b.uid 已编入索引。尝试复合 a.dt、a.primary_uid。

处理了 5,187,819 行后,DATE_SUB 可能被调用 103,756,380 次,具体取决于优化器如何解释代码。这就是为什么我建议将它从将被调用十次的查询中拉出来。

尝试预先计算日期间隔,这样就不会针对 case 语句的每次迭代计算它们。您还可以预先计算日期间隔减去偏移量。如果这有帮助,我会把它留给你。

SET @date = '2017-05-17';
SET @offset = 1;
SET @dtint5 = DATE_SUB(@date, INTERVAL 5 DAY);
SET @dtint13 = DATE_SUB(@date, INTERVAL 13 DAY);
SET @dtint25 = DATE_SUB(@date, INTERVAL 25 DAY);
SET @dtint45 = DATE_SUB(@date, INTERVAL 45 DAY);
SET @dtint75 = DATE_SUB(@date, INTERVAL 75 DAY);
SET @dtint105 = DATE_SUB(@date, INTERVAL 105 DAY);
SET @dtint135 = DATE_SUB(@date, INTERVAL 135 DAY);
SET @dtint165 = DATE_SUB(@date, INTERVAL 165 DAY);
SET @dtint195 = DATE_SUB(@date, INTERVAL 195 DAY);
SET @dtint225 = DATE_SUB(@date, INTERVAL 225 DAY);

select 
b.act,
CASE 
WHEN b.jdt <= @dtin5 AND b.jdt >= @dtint5 - @offset AND DATEDIFF(a.dt,b.jdt) <=5 THEN 5
WHEN b.jdt <= @dtint13 AND b.jdt >= @dtint13 - @offset AND DATEDIFF(a.dt,b.jdt) <=13 THEN 13
WHEN b.jdt <= @dtint25 AND b.jdt >= @dtint25 - @offset AND DATEDIFF(a.dt,b.jdt) <=25 THEN 25
WHEN b.jdt <= @dtint45 AND b.jdt >= @dtint45 - @offset AND DATEDIFF(a.dt,b.jdt) <=45 THEN 45
WHEN b.jdt <= @dtint75 AND b.jdt >= @dtint75 - @offset AND DATEDIFF(a.dt,b.jdt) <=75 THEN 75
WHEN b.jdt <= @dtint105 AND b.jdt >= @dtint105 - @offset AND DATEDIFF(a.dt,b.jdt) <=105 THEN 105
WHEN b.jdt <= @dtint135 AND b.jdt >= @dtint135 - @offset AND DATEDIFF(a.dt,b.jdt) <=135 THEN 135
WHEN b.jdt <= @dtint165 AND b.jdt >= @dtint165 - @offset AND DATEDIFF(a.dt,b.jdt) <=165 THEN 165
WHEN b.jdt <= @dtint195 AND b.jdt >= @dtint195 - @offset AND DATEDIFF(a.dt,b.jdt) <=195 THEN 195
WHEN b.jdt <= @dtint225 AND b.jdt >= @dtint225 - @offset AND DATEDIFF(a.dt,b.jdt) <=225 THEN 225
ELSE 'other' 
END AS 'period',

SUM(CASE WHEN a.type = 'JN' AND a.paid = 'Y' AND a.upgraded=0 THEN 1 ELSE 0     END) AS 'Paid_Joins',
SUM(CASE WHEN a.type IN ('SL','RL') AND ttype !='Purchase' THEN (a.amt_usd/100 - a.vat_usd/100) END) AS 'Revenue_Amount'

FROM __customer b
JOIN  __transaction a on b.uid = a.primary_uid 

WHERE a.dt <= @date

GROUP BY 1,2

【讨论】:

谢谢。但是,我尝试对所有日期进行硬编码,但看起来查询时间并没有得到改善。 在聚合之前处理了多少行(a.dt 这不是日期的硬编码,而是我试图从选择迭代中删除的函数调用 date_sub。 向 where 子句添加更多条件以减少正在处理的行数。 继续为每个查询变体使用 EXPLAIN 输出。尝试在表 __transaction 上定义 复合索引 primary_uid, dt 它是您要从解释输出中删除的“使用临时”和“使用文件排序”。建议您也尝试颠倒查询中表的顺序: FROM __transaction t JOIN __customer c ON c.uid = t.primary_uid (ps:我不喜欢暗示 a,b,c 等顺序的别名)

以上是关于需要帮助优化一个有趣的 MySQL 查询的主要内容,如果未能解决你的问题,请参考以下文章

帮助优化 MySQL 查询

MySQL 查询/表需要优化

MySQL查询优化:如何优化投票计算?

在 MySQL 5.7 中优化查询

MySQL性能优化:为什么查询速度这么慢

MySQL查询优化之explain的深入解析