【问题标题】:Cumulative sum over a set of rows in mysqlmysql中一组行的累积和
【发布时间】:2013-07-13 22:03:51
【问题描述】:

我有一个复杂的查询(包含多个联接、联合),它返回一组包含 id、day、hr、amount 的行。查询的输出如下所示:

id day    hr  amount 
1   1      1   10       
1   1      2   25       
1   1      3   30        
1   2      1   10       
1   2      2   40       
1   2      2   30        
2   1      1   10       
2   1      2   15        
2   1      3   30       
2   2      1   10       
2   2      2   20      
2   2      2   30  

我需要为一天中的每个小时找到每个 ID 的累积总数。输出应该是这样的:

id day    hr  amount cumulative total
1   1      1   10       10
1   1      2   25       35
1   1      3   30       65 
1   2      1   10       10
1   2      2   40       50
1   2      2   30       80 
2   1      1   10       10
2   1      2   15       25 
2   1      3   30       55
2   2      1   10       10
2   2      2   20       30
2   2      2   30       60

产生第一个输出的初始查询如下所示:

select id, day, hr, amount from
( //multiple joins on multiple tables)a
left join
(//unions on multiple tables)b
on a.id=b.id;

获取第二个输出中描述的累积总和的 sql 查询是什么?解决方案中不应使用 SET。

谢谢。

【问题讨论】:

  • 在更复杂(但可能)的解决方案之前:您是否考虑过 (a) WITH ROLLUP 是否足以满足您的需求,以及 (b) 在应用程序代码中而不是 SQL 中更容易实现这一点?
  • @Wrikken: a) WITH ROLLUP 在输出中添加了额外的行,我需要以列的形式,是否可以使用 WITH ROLLUP 来实现? b) 我同意它在应用程序代码中会更简单,但我面临必须在 SQL 本身中完成的情况。
  • 我会使用这样的存储过程:1. 使用查询结果创建一个临时表,2. 向临时表添加一列,3. 更新每一行带有累积总和的临时表。
  • 您以这种方式提出的问题“我需要为一天中的每个小时查找每个 id 的累积总数”不是表格显示的内容。您的样本输出中有两次 ID 1、DAY 2、HR 2。在不使用变量的情况下似乎无法以这种方式解决它,因为您实际上并没有在该表中使用主键。
  • “重复问题”提供的答案不足;这些答案中的查询不会产生此问题中指定的结果集。 (一个大问题是这些查询不会为每个组生成一个运行总计,而是为下一个组重置它,如本问题中所述。这些查询只为整个集合生成一个运行总计。这是一个明显不同的问题。

标签: mysql sql join cumulative-sum


【解决方案1】:

更新

MySQL 8.0 引入了“窗口函数”,相当于 SQL Server“窗口函数”(Transact-SQL OVER 语法提供分区和排序)和 Oracle“分析函数”的功能。

MySQL 参考手册 12.21 窗口函数https://dev.mysql.com/doc/refman/8.0/en/window-functions.html

这里提供的答案是 MySQL 8.0 之前版本的一种方法。


原始答案

MySQL 不提供用于获取运行“累积和”的类型分析函数,就像其他 DBMS(如 Oracle 或 SQL Server)中可用的分析函数。

但是,可以使用 MySQL 模拟一些分析函数。

有(至少)两种可行的方法:

一种是使用相关子查询来获取小计。这种方法在大型集合上可能很昂贵,并且如果外部查询上的谓词很复杂,则很复杂。这实际上取决于“多个表上的多个连接”有多复杂。 (不幸的是,MySQL 也不支持 CTE。)

另一种方法是利用MySQL用户变量,做一些控制中断处理。这里的“技巧”是对查询的结果进行排序(使用 ORDER BY),然后将查询包装在另一个查询中。

我将举例说明后一种方法。

由于 MySQL 执行操作的顺序,需要先计算 cumulative_total 列,然后将来自当前行的 idday 的值保存到用户变量中。最简单的方法就是将此列放在首位。

别名为 i 的内联视图(在下面的查询中)只是用于初始化用户变量,以防万一这些已在会话中设置。如果它们已经分配了值,我们想忽略它们的当前值,最简单的方法是初始化它们。

您的原始查询被括在括号中,并在下面的示例中被赋予别名c。对原始查询的唯一更改是添加了 ORDER BY 子句,因此我们可以确保按顺序处理查询中的行。

外部选择检查当前行中的idday 值是否“匹配”前一行。如果有,我们将当前行中的amount 添加到累计小计中。如果它们不匹配,那么我们将累积小计重置为零,并添加当前行的金额(或者更简单地说,只需分配当前行的金额)。

在完成累计总数的计算后,我们将当前行中的idday 值保存到用户变量中,以便在处理下一行时可用。

例如:

SELECT IF(@prev_id = c.id AND @prev_day = c.day
         ,@cumtotal := @cumtotal + c.amount
         ,@cumtotal := c.amount) AS cumulative_total
     , @prev_id  := c.id  AS `id`
     , @prev_day := c.day AS `day`
     , c.hr
     , c.amount AS `amount'
  FROM ( SELECT @prev_id  := NULL
              , @prev_day := NULL
              , @subtotal := 0
       ) i
  JOIN (

         select id, day, hr, amount from
         ( //multiple joins on multiple tables)a
         left join
         (//unions on multiple tables)b
         on a.id=b.id

         ORDER BY 1,2,3
       ) c

如果需要以不同的顺序返回列,并将累积总计作为最后一列,那么一个选择是将整个语句包装在一组括号中,并将该查询用作内联视图:

SELECT d.id
     , d.day
     , d.hr
     , d.amount
     , d.cumulative_total
FROM (
       // query from above
     ) d

【讨论】:

  • 你可以写一个简单的查询...看看下面的答案,这是一个简单的查询就可以了。
  • 在简单查询中,OP 原始查询需要指定两次,代替“foo”。 (如果对原始查询进行任何更改,则需要在两个地方进行修改。)简单查询的输出不符合规范,至少在 OP 示例中的第五行的情况下,因为有是两行id=1 day=2 hr=2。根据规范,第五行的小计不应包括第六行的金额。
  • 注:标记为“重复”的问题中提供的答案不会产生您指定的结果集。这些将产生整个集合的运行总数,而不是每个组。这些查询也不处理分组键的重复实例。
  • 这种方法也非常快。我很难在几百万行中获得大约 15 列的累积总和。我尝试了许多不同的方法。 @lukas-eder 的解决方案很好,但是如果行数(总和)增加,则需要的时间会越来越长。 @spencer7593 的这个解决方案并不关心你的集合有多大,它会保持快速。 25000 rows in 0.19 sec4.43 sec
【解决方案2】:

给你,这是你的最高总分......

select f1.id, f1.day, f1.hr, f1.amount, sum(f2.amount) as culminative_total from foo f1
 inner join foo f2 on (f1.day = f2.day and f1.id=f2.id)
 where f2.hr <= f1.hr
 group by f1.id, f1.day, f1.hour;

【讨论】:

  • 如果(id, day, hr) 有重复项,则不会返回指定的结果集,如 OP 示例中的第五行和第六行。在 OP 的情况下, foo 不是一个简单的表,而是一个涉及多个表的查询。该查询需要指定两次(代替 foo),这意味着 MySQL 将实现该查询两次。不幸的是,MySQL 还不支持公用表表达式 (CTE),这是避免重复子查询的一种方法。
  • 确实非常尖锐 :) 没有注意到初始数据中的细节
  • 如果我们确实保证了唯一性(在我们需要连接的列上),那么这个答案中查询使用的半连接方法将返回指定的结果。
  • 是的,我知道。当我对问题进行初步检查时,我没有注意到重复...
【解决方案3】:

如果您使用的是 MySQL 8 或更高版本,则应为此使用 window functions。您的查询将显示为:

SELECT
  id, day, hr, amount,
  SUM (amount) OVER (PARTITION BY id, day ORDER BY hr) AS `cumulative total`
FROM t

t 是您的表 b 左连接到 a。一些注意事项:

  • PARTITION BY 子句保证您获得每个 idday 的累积总和,因此我们每天都重新开始求和
  • ORDER BY 子句定义了累积发生的顺序

【讨论】:

  • 很高兴看到越来越多的 RDBMS 支持这种结构。我想实施的下一件大事是MATCH_RECOGNIZE - 顺便说一句。你的博客真的很棒(10 SQL Tricks That You Didn’t Think Were Possible启发了我:)
  • @LukaszSzozda:谢谢你的好话。我不确定MATCH_RECOGNIZE 是否会很快成为 MySQL 的优先事项,但你永远不知道!
  • 如果我们不添加 order by 子句,为什么我们没有累积一些?即使我不下订单也不应该将每一行添加到新总和中吗?
  • @Ambleu:如果你使用ORDER BY,那么RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW是隐式的(累积和语义)。如果你不使用ORDER BY,那么RANGE BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING 是隐式的(整个分区语义的聚合)。这实际上是一个非常有用的默认行为。
猜你喜欢
  • 2023-01-20
  • 1970-01-01
  • 2014-04-12
  • 1970-01-01
  • 2021-01-18
  • 2021-11-27
  • 1970-01-01
  • 1970-01-01
  • 2011-11-29
相关资源
最近更新 更多