MySQL 8 - 计算年度指标
Posted
技术标签:
【中文标题】MySQL 8 - 计算年度指标【英文标题】:MySQL 8 - Calculating Year-over-Year Metrics 【发布时间】:2020-08-19 09:57:37 【问题描述】:我一直在尝试计算每月回报的同比增长,并且一直在重写相同的查询数小时,但没有运气。我见过解决方案,但它们都是其他数据库解决方案。
我正在尝试基本上实现以下目标:
这是我构建的查询,尽管由于子查询每行运行,我从未真正完成过运行(运行了 15 分钟以上)。
这个表有 2m+ 行,索引很好,速度很快,但子查询会杀死它。
可能是完全错误的方法,但这就是我所拥有的。
SELECT
YEAR(thisyear.trandte) AS `Year`,
MONTH(thisyear.trandte) AS `YearMonth`,
SUM(lastyear.totamount) AS LastYearSales,
SUM(thisyear.totamount) AS ThisYearSales
FROM
sync_invoice_lines thisyear
LEFT JOIN
sync_invoice_lines lastyear ON
MONTH(thisyear.trandte) = (MONTH(lastyear.trandte)) AND
YEAR(thisyear.trandte) = (YEAR(lastyear.trandte) - 1)
WHERE
thisyear.type = 'IN' AND
lastyear.type = 'IN' AND
thisyear.sync_active = 1 AND
lastyear.sync_active = 1 AND
GROUP BY `Year`, `YearMonth`
【问题讨论】:
您当前的查询效率非常低,并且会返回完全错误的数字。您以相同的年份/月份连接所有行,即如果您在 2020 年 7 月获得 1000 行,则在 SUM 之前的连接之后您将获得 1000*1000 行。 【参考方案1】:如果表中所有月份的数据没有任何间隔,那么你只需要窗口函数LAG()
来获取去年同月totamount
的总和:
SELECT YEAR(trandte) AS Year,
MONTH(trandte) AS Month,
SUM(totamount) AS ThisYearSales,
LAG(SUM(totamount), 12) OVER (ORDER BY YEAR(trandte), MONTH(trandte)) AS LastYearSales
FROM sync_invoice_lines
WHERE type = 'IN' AND sync_active = 1
GROUP BY Year, Month
如果月份之间有间隔,则从上述查询中创建一个CTE
并对其执行LEFT
self join:
WITH cte AS (
SELECT YEAR(trandte) AS Year,
MONTH(trandte) AS Month,
SUM(totamount) AS Sales
FROM sync_invoice_lines
WHERE type = 'IN' AND sync_active = 1
GROUP BY Year, Month
)
SELECT c1.Year,
c1.Month,
c1.Sales AS ThisYearSales,
c2.Sales AS LastYearSales
FROM cte c1 LEFT JOIN cte c2
ON c2.Year = c1.Year - 1 AND c2.Month = c1.Month
【讨论】:
为什么不OVER (PARTITION BY MONTH(trandte) ORDER BY YEAR(trandte))
? 上年同月
当然,在 LAG() 中没有12
也可以这样写。【参考方案2】:
您可以在单个表扫描中执行此操作(无需连接或 CTE),并考虑可能缺少的月份。为此,请使用 lag()
和 range
子句,该子句正好针对去年的同一个月,如下所示:
select
year(trandte) as `year`,
month(trandte) as `yearmonth`,
lag(sum(totamount)) over(
order by concat(year(trandte), '-', month(trandte), '-01')
range between interval 1 year preceding and interval 1 year preceding
) as lastyearsales,
sum(totamount) as thisyearsales
from sync_invoice_lines
where type = 'IN' and sync_active = 1
group by year(trandte), month(trandte)
order by year(trandte), month(trandte)
【讨论】:
这看起来是一个不错的解决方案,但是会产生错误:Window '<unnamed window>' with RANGE N PRECEDING/FOLLOWING frame requires exactly one ORDER BY expression, of numeric or temporal type
。我会尝试解决它 - 只是 LAG 和 OVER 函数的新手。【参考方案3】:
您可以使用 CASE 表达式分别计算去年销售额和今年销售额的总额。 这很简单。
查询如下:
SELECT
YEAR(CURRENT_DATE) AS `Year`,
MONTH(trandte) AS `YearMonth`,
SUM(CASE YEAR(trandte) WHEN YEAR(CURRENT_DATE)-1 THEN totamount END) AS LastYearSales,
SUM(CASE YEAR(trandte) WHEN YEAR(CURRENT_DATE) THEN totamount END) AS ThisYearSales
FROM
sync_invoice_lines
WHERE type = 'IN' AND sync_active = 1
GROUP BY `YearMonth`
ORDER BY `YearMonth`;
DB Fiddle
您可以在 YEAR(CURRENT_DATE) 部分中指定任何年份。
【讨论】:
【参考方案4】:您可以使用数据透视表逐年显示每个月的销售额。
with monthly_sales as
(SELECT
YEAR(trandte) AS year,
MONTH(trandte) AS month,
SUM(totamount) AS sales
FROM
sync_invoice_lines
WHERE
type = 'IN' AND
sync_active = 1
GROUP BY YEAR(trandte), MONTH(trandte))
Select * from
(select month, year from monthly_sales)
pivot
(sum(sales)
for month in (2013, 2014, 2015)
)
order by month
【讨论】:
【参考方案5】:第 1 步:计算所有每月小计,没有逐年对比还:
SELECT LEFT(trandte, 7) AS yyyy_mm,
SUM(totamount) AS sales
FROM sync_invoice_lines
WHERE ...
GROUP BY 1;
首先,看看这是否得到了正确的数字,尽管不是按照所需的顺序。看看它跑得有多快。
这可能就是你所需要的。
第 2 步:这将使用大约 30 行,因此效率不是问题。将上述内容放在另一个表中,或者,因为您有 mysql 8.0(或 MariaDB 10.2),所以在 WITH
中使用它来完成其余的工作。第 2 步可能是使用自联接计算年复一年。
第三步:输出的顺序——还是图形包重新排列数据得到12组多年?
从长远来看,考虑建立和维护一个“汇总表”,也许是每日小计。这就像第 1 步,但有数千行,而不是数百万或数十行。有了它,您可以非常快速地计算每月的金额。或每周。或其他范围。这样,庞大的任务(第 1 步)以每日块的形式构建,速度将提高一千倍。
关于小计的更多信息:http://mysql.rjweb.org/doc.php/summarytables
【讨论】:
以上是关于MySQL 8 - 计算年度指标的主要内容,如果未能解决你的问题,请参考以下文章