【问题标题】:Get rows from table whose sum is less than value从表中获取总和小于值的行
【发布时间】:2020-01-31 16:17:38
【问题描述】:

考虑我在表中有以下行

|  id |   user_id   |  amount |      date      |
------------------------------------------------
|  1  |     1       |  100    |   2019-09-30   |
------------------------------------------------
|  2  |     2       |  100    |   2019-09-30   |
------------------------------------------------
|  3  |     1       |  100    |   2019-09-30   |
------------------------------------------------
|  4  |     3       |  100    |   2019-10-01   |
------------------------------------------------
|  5  |     1       |  75     |   2019-10-01   |
------------------------------------------------
|  6  |     3       |  100    |   2019-10-01   |
------------------------------------------------
|  7  |     1       |  35     |   2019-10-01   |
------------------------------------------------

我正在尝试找到一种方法来获取带有user_id = 1 的所有行,其中sum(amount) < 300 和date <= '2019-10-01'。

我想要做的是只处理满足某个阈值总和的记录。我不太确定从哪里开始。

预期结果

|  id |   user_id   |  amount |      date      |
------------------------------------------------
|  1  |     1       |  100    |   2019-09-30   |
------------------------------------------------
|  3  |     1       |  100    |   2019-09-30   |
------------------------------------------------
|  5  |     1       |   75    |   2019-10-01   |
------------------------------------------------

这是我目前尝试过的

SELECT id, SUM(amount) as total_sum 
   FROM table 
   WHERE date <= '2019-10-01' AND user_id = 1
     ORDER BY date ASC
   HAVING total_sum <= 300

根据上述查询,我​​没有得到所需的输出。

当前使用的 MySQL 版本:5.7.25

我确实看过这个问题MySQL select records with sum greater than threshold 假设他们正在尝试做同样的事情,但这不是我正在看的

【问题讨论】:

  • 在您的预期输出中,您将获得对应于 10 月 1 日的行,但您的条件是日期 date < '2019-09-31' - 没有9月31日这样的事情。请修复问题声明。另外,您的 MySQL 服务器版本是什么?运行SELECT Version();
  • @MadhurBhaiya,感谢您的关注。我确实向该数据添加了更多行,但错过了更新问题陈述。我也添加了版本。
  • 谢谢,我去看看。

标签: mysql sum mariadb cumulative-sum mysql-5.7


【解决方案1】:

如果您不喜欢使用会话变量(一些有经验的 SO 用户非常不喜欢它们),您可以使用基于“Self-Join”的技术,然后使用 GROUP BY 和 HAVING 进行过滤。

一般的思路是,我们left join 获取特定user_id 的前几行,然后聚合得到rolling sum,然后使用Having 子句过滤。

查询

SELECT 
  t1.* 
FROM 
  your_table AS t1 
LEFT JOIN your_table AS t2 
  ON t2.user_id = t1.user_id 
     AND t2.date <= t1.date 
     AND t2.id <= t1.id 
WHERE t1.user_id = 1 
  AND t1.date <= '2019-10-31'
GROUP BY t1.user_id, t1.date, t1.id, t1.amount
HAVING COALESCE(SUM(t2.amount),0) < 300;

结果

| id  | user_id | amount | date       |
| --- | ------- | ------ | ---------- |
| 1   | 1       | 100    | 2019-09-30 |
| 3   | 1       | 100    | 2019-09-30 |
| 5   | 1       | 75     | 2019-10-01 |

View on DB Fiddle

您可以对这两种方法进行基准测试,然后决定哪种方法合适。

对于此查询,您将需要复合索引:(user_id, date)

【讨论】:

  • 谢谢,感谢您花时间为这个问题添加另一个答案。很高兴知道我可以在没有会话变量的情况下做到这一点。我将研究两者,看看从性能角度看哪个效果更好。
  • @Abishek 欢迎您。一定要知道哪个效果更好;或两者都好;或两者都不好。如果两者都不好,也许我们可以研究出更多技术,但我将赌注押在会话变量上
  • 我确实看到了这个查询导致的结果问题。将HAVING 子句设置为&lt; 300,它会返回正确的结果。但是,如果我将 HAVING 子句更改为 &lt; 180 它会返回错误的结果。它应该显示 2 行,数量为 100、75,但它只返回 1 行。这是 DB Fiddle db-fiddle.com/f/svmyjqaqBuNECdny4u3EmB/4
  • @Abishek 查询没有问题。仔细检查输入数据。 2019-09-30 上的 user_id = 1 有两行,其值等于 100。检查 id 值: 1,2 。因此,它们的总数 = 100 + 100 = 200 在到达值为 75 的行之前已经越过了 180 的障碍。
  • 但对于满足日期条件的同一用户,还有第 6 行的金额为 75。这不应该也是它的一部分吗
【解决方案2】:

这是一个滚动求和问题。在 MySQL 8.0.2 及更高版本中,您可以使用Window functions with Frames 解决此问题。在旧版本中,我们可以使用用户定义的 Session 变量来做同样的事情。

  • 我们首先使用Session variables 计算滚动和。
  • 然后,使用Derived table中的结果集,找到总和越过“障碍”300的id。当新滚动总和大于300时达到障碍。我们将障碍值设置为此时为 1,之前的行为 0,之后的行为 2 或更多。
  • 我们将只考虑障碍为 0 的行。

尝试(适用于所有 MySQL 版本):

查询 #1

SELECT dt.id, 
       dt.user_id, 
       dt.amount, 
       dt.date
FROM 
(
  SELECT 
    t.id, 
    t.user_id, 
    t.amount, 
    t.date, 
    @barrier := CASE
                  WHEN 
                       (@tot_qty := @tot_qty + t.amount) > 300
                  THEN (@barrier + 1)
                  ELSE 0
                END AS barrier 
  FROM 
    your_table AS t 
  CROSS JOIN (SELECT @tot_qty := 0, 
                     @barrier := 0) AS user_init
    WHERE t.user_id = 1 
        AND t.date <= '2019-10-01' 
  ORDER BY t.user_id, t.date, t.id
) AS dt 
WHERE dt.barrier = 0 
ORDER BY dt.user_id, dt.date, dt.id;

结果

| id  | user_id | amount | date       |
| --- | ------- | ------ | ---------- |
| 1   | 1       | 100    | 2019-09-30 |
| 3   | 1       | 100    | 2019-09-30 |
| 5   | 1       | 75     | 2019-10-01 |

View on DB Fiddle

【讨论】:

  • 感谢您的回答,不胜感激。这个查询的性能如何?考虑到这个查询,我们是否需要在任何列上添加索引?此表每天接收近 50k 条记录,此查询检索到的实际行数可能约为每位用户 150+。
  • @Abishek 索引取决于您的WHERE 条件。例如,在当前查询中,您要过滤特定用户和日期范围,您将需要一个复合索引:(user_id, date)。列的顺序在这里很重要。关于一般性能,此查询将具有与窗口函数技术相似的性能;而且我认为它对于旧版本的 MySQL 来说非常好。
  • 同样的问题也适用于这个查询。如果我将阈值限制设置为 180,它只会返回第 1 行而不是第 1 行和第 5 行。
  • 不确定是否有误传。我的问题陈述是“我正在尝试找到一种方法来获取 user_id = ? 的所有行,其中 sum(amount) &lt; ? 和 date &lt;= '?'”
  • @Abishek 您可以在某些存储过程中执行此操作。但归根结底,这将是一个循环,然后一些 if else 检查哪个最适合。我会进一步考虑;如果我发现了什么,请及时通知您
猜你喜欢
  • 2016-11-23
  • 1970-01-01
  • 2021-05-10
  • 1970-01-01
  • 2022-01-06
  • 2013-09-20
  • 1970-01-01
  • 1970-01-01
  • 2020-08-16
相关资源
最近更新 更多