【问题标题】:Query with dynamic date intervals使用动态日期间隔查询
【发布时间】:2018-10-09 13:29:22
【问题描述】:

给定一个包含有关产品可用性信息的状态表,我如何选择与产品有效的最近 20 天内的第 1 天相对应的日期?

是的,我知道这个问题很难理解。我认为另一种说法是:我想知道每种产品在其活跃的过去 20 天内销售了多少次,这意味着该产品可能已经活跃多年,但我只想要销售额从最近 20 天的“活跃”状态开始计算。

这在服务器端很容易实现(即从数据库中获取任何产品集合、迭代它们、对状态表执行 n+1 查询等),但我有数十万个项目,所以这是必要的出于性能原因在 SQL 中执行此操作。

表格:产品

+-------+-----------+
|   id  |   name    |
+-------+-----------+
|   1   |   Apple   |
|   2   |   Banana  |
|   3   |   Grape   |
+-------+-----------+

表格:状态

+-------+-------------+---------------+---------------+
|   id  |     name    |   product_id  |   created_at  |
+-------+-------------+---------------+---------------+
|   1   |   active    |            1  |   2018-01-01  |
|   2   |   inactive  |            1  |   2018-02-01  |
|   3   |   active    |            1  |   2018-03-01  |
|   4   |   inactive  |            1  |   2018-03-15  |
|   6   |   active    |            1  |   2018-04-25  |
|   7   |   active    |            2  |   2018-03-01  |
|   8   |   active    |            3  |   2018-03-10  |
|   9   |   inactive  |            3  |   2018-03-15  |
+-------+-------------+---------------+---------------+

表:项目(订购的产品)

+-------+---------------+-------------+
|   id  |   product_id  |   order_id  |
+-------+---------------+-------------+
|   1   |            1  |          1  |
|   2   |            1  |          2  |
|   3   |            1  |          3  |
|   4   |            1  |          4  |
|   5   |            1  |          5  |
|   6   |            2  |          3  |
|   7   |            2  |          4  |
|   8   |            2  |          5  |
|   9   |            3  |          5  |
+-------+---------------+-------------+

表格:订单

+-------+---------------+
|   id  |   created_at  |
+-------+---------------+
|   1   |   2018-01-02  |
|   2   |   2018-01-15  |
|   3   |   2018-03-02  |
|   4   |   2018-03-10  |
|   5   |   2018-03-13  |
+-------+---------------+

我希望我的最终结果如下所示:

+-------+-----------+----------------------+--------------------------------+
|   id  |   name    |  recent_sales_count  |  date_to_start_counting_sales  |
+-------+-----------+----------------------+--------------------------------+
|   1   |   Apple   |                   3  |                    2018-01-30  |
|   2   |   Banana  |                   0  |                    2018-04-09  |
|   3   |   Grape   |                   1  |                    2018-03-10  |
+-------+-----------+----------------------+--------------------------------+

这就是我所说的最近 20 天的活跃天数,例如苹果:

  • 最后一次激活是在“2018-04-25”。那是 4 天 前。

  • 在此之前,它自 '2018-03-15' 以来一直处于非活动状态,因此直到 '2018-04-25' 之前的所有这些天都不算在内。

  • 在此之前,它自“2018-03-01”以来一直处于活动状态。距离“2018-03-15”还有 14 天

  • 在此之前,自“2018 年 2 月 1 日”以来处于非活动状态。

  • 最后,它自 '2018-01-01' 以来一直处于活动状态,因此它应该只从 '2018- 向后计算缺少的 2 天 (4 + 14 + 2 = 20) 02-01',导致 date_to_start_counting_sales = '2018-01-30'。

  • 有了“2018-01-30”日期,我就可以计算过去 20 天的 Apple 订单:3。

希望这是有道理的。

这是一个fiddle,上面提供了数据。

【问题讨论】:

  • 您能否仔细检查您想要的结果中的date_to_start_counting_sales(假设您使用的是 YYYY-MM-DD)?请提供一个示例,说明您对其中一种产品的“过去 20 天处于活跃状态”的含义。例如。 Apple 最后一次活跃是 '2018-04-25',所以从 '2018-04-25' 到 '2018-05-15' 的所有销售额?或者您想要存在完整 20 天间隔的最后一个活动时间?我无法从您提供的信息中追溯您如何获得所需结果的数据。
  • @krokodilko 正如我所提到的,我已经尝试用服务器端语言解决问题,但在 SQL 中完成它时遇到了问题。感谢您的精彩合作。
  • 伙计,你在星期天早上让我头疼......好吧,我想再次理解这一点。你能准备一下你的结构和数据,这样我们就可以有一个操场了吗?
  • 这已经是一个很大的帮助:sqlfiddle.com/#!9/b7817f/3 其余部分我仍然很难理解,但从我在这里的查询来看,这肯定是花生。我喝了杯咖啡,我完成了这个
  • plsql 仍然需要很多时间来编写,而且几乎不可能转换为 mysql ......所以我改用 mysql 。反正一直想学。希望能帮助到你。现在让我着迷于这个问题!哈

标签: mysql sql join


【解决方案1】:

我有一个标准的 SQL 解决方案,它不像你在 MySQL 5 上那样使用任何窗口函数

我的解决方案需要 3 个堆叠视图。

使用 CTE 会更好,但您的版本不支持它。堆叠视图也是如此......我不喜欢堆叠视图并且总是尽量避免它,但有时你别无选择,因为 MySQL 不接受视图的 FROM 子句中的子查询。

CREATE VIEW VIEW_product_dates AS
(
        SELECT product_id, created_at AS active_date,
                (
                    SELECT created_at
                    FROM statuses ti
                    WHERE name = 'inactive' AND ta.created_at < ti.created_at AND ti.product_id=ta.product_id
                    GROUP BY product_id
                ) AS inactive_date
        FROM statuses ta
        WHERE name = 'active'
);

CREATE VIEW VIEW_product_dates_days AS
(
    SELECT product_id, active_date, inactive_date, datediff(IFNULL(inactive_date, SYSDATE()),active_date) AS nb_days
    FROM VIEW_product_dates
);

CREATE VIEW VIEW_product_dates_days_cumul AS
(
    SELECT product_id, active_date, ifnull(inactive_date,sysdate()) AS inactive_date,  nb_days,
         IFNULL((SELECT SUM(V2.nb_days) + V1.nb_days
                 FROM VIEW_product_dates_days V2
                 WHERE V2.active_date >= IFNULL(V1.inactive_date, SYSDATE()) AND V1.product_id=V2.product_id
                ),V1.nb_days) AS cumul_days
    FROM  VIEW_product_dates_days V1
);  

最终视图产生这个:

| product_id |          active_date |        inactive_date | nb_days | cumul_days |
|------------|----------------------|----------------------|---------|------------|
|          1 | 2018-01-01T00:00:00Z | 2018-02-01T00:00:00Z |      31 |         49 |
|          1 | 2018-03-01T00:00:00Z | 2018-03-15T00:00:00Z |      14 |         18 |
|          1 | 2018-04-25T00:00:00Z | 2018-04-29T11:28:39Z |       4 |          4 |
|          2 | 2018-03-01T00:00:00Z | 2018-04-29T11:28:39Z |      59 |         59 |
|          3 | 2018-03-10T00:00:00Z | 2018-03-15T00:00:00Z |       5 |          5 |

因此它汇总了所有产品的所有活跃期,计算每个期的天数,以及自当前日期以来所有过去活跃期的累计天数。

然后我们可以查询这个最终视图以获取每个产品所需的日期。我为您的 20 天设置了一个变量,因此您可以根据需要轻松更改该数字。

SET @cap_days = 20 ;

SELECT PD.id, Pd.name, 
       SUM(CASE WHEN o.created_at > PD.date_to_start_counting_sales THEN 1 ELSE 0 END) AS recent_sales_count  ,
       PD.date_to_start_counting_sales
FROM
(
    SELECT p.*,
           (CASE WHEN LowerCap.max_cumul_days IS NULL 
                 THEN ADDDATE(ifnull(HigherCap.min_inactive_date,sysdate()),(-@cap_days))
                 ELSE 
                 CASE WHEN LowerCap.max_cumul_days < @cap_days AND  HigherCap.min_inactive_date IS NULL
                      THEN ADDDATE(ifnull(LowerCap.max_inactive_date,sysdate()),(-LowerCap.max_cumul_days))
                      ELSE ADDDATE(ifnull(HigherCap.min_inactive_date,sysdate()),(LowerCap.max_cumul_days-@cap_days))
                 END
            END) as date_to_start_counting_sales
    FROM products P
    LEFT JOIN
    (
        SELECT product_id, MAX(cumul_days) AS max_cumul_days, MAX(inactive_date) AS max_inactive_date
        FROM VIEW_product_dates_days_cumul
        WHERE cumul_days <= @cap_days
        GROUP BY product_id
    ) LowerCap ON P.id=LowerCap.product_id
    LEFT JOIN 
    (
        SELECT product_id, MIN(cumul_days) AS min_cumul_days, MIN(inactive_date) AS min_inactive_date
        FROM VIEW_product_dates_days_cumul
        WHERE cumul_days > @cap_days
        GROUP BY product_id
    ) HigherCap ON P.id=HigherCap.product_id
) PD
LEFT JOIN items i ON PD.id =  i.product_id
LEFT JOIN orders o ON o.id = i.order_id 
GROUP BY PD.id, Pd.name, PD.date_to_start_counting_sales

返回

| id |   name | recent_sales_count | date_to_start_counting_sales |
|----|--------|--------------------|------------------------------|
|  1 |  Apple |                  3 |         2018-01-30T00:00:00Z |
|  2 | Banana |                  0 |         2018-04-09T20:43:23Z |
|  3 |  Grape |                  1 |         2018-03-10T00:00:00Z |

小提琴:http://sqlfiddle.com/#!9/804f52/24

【讨论】:

  • 太棒了!你让它看起来很简单,非常。但有一件事是否可以显示当前非活动产品的结果而不是 null?我会尽力解决这个问题并调整您的查询。
  • 是的,但非活动产品的逻辑是什么?
  • 同样的逻辑:最后 20 天。例如如果同一 Apple 产品今天被停用,它应该仍然会永远给出相同的结果,直到它再次被启用。
  • 首先尝试我更新的解决方案(小提琴链接也更新了),我们会看到之后。 recent_sales_count 将很容易计算
  • 我放弃了。在欧洲已经很晚了,我太累了,无法正确地集思广益。我知道最后的触摸很接近,但我看不到它,因为我必须睡觉。我仍然用recent_sales_count 更新了小提琴和答案。如果其他人想分叉我的答案来完成它,请为@GordonLinoff 做一份工作:)
【解决方案2】:

不确定您使用的是哪个版本的 MySql,但如果您可以使用 8.0,那么该版本提供了许多功能,使事情变得更加可行(CTE、row_number()、分区等)。

我的建议是创建一个类似DB-Fiddle Example 的视图,在服务器端调用该视图并以编程方式进行迭代。在 SQL 中有很多方法可以做到这一点,但是编写、测试会很麻烦,而且效率可能会降低。

假设:

  1. 在非活动日期范围内不能销售产品
  2. Statuses 表将始终为每个产品交替状态活动/非活动/活动。 IE。没有特定产品既处于活动状态又处于非活动状态的日期范围。

查看结果:

+------------+-------------+------------+-------------+
| product_id | active_date | end_date   | days_active |
+------------+-------------+------------+-------------+
| 1          | 2018-01-01  | 2018-02-01 | 31          |
+------------+-------------+------------+-------------+
| 1          | 2018-03-01  | 2018-03-15 | 14          |
+------------+-------------+------------+-------------+
| 1          | 2018-04-25  | 2018-04-29 | 4           |
+------------+-------------+------------+-------------+
| 2          | 2018-03-01  | 2018-04-29 | 59          |
+------------+-------------+------------+-------------+
| 3          | 2018-03-10  | 2018-03-15 | 5           |
+------------+-------------+------------+-------------+

查看:

CREATE OR REPLACE VIEW days_active AS (
WITH active_rn 
     AS (SELECT *, Row_number() 
                    OVER ( partition BY NAME, product_id 
                    ORDER BY created_at) AS rownum 
         FROM   statuses
         WHERE name = 'active'),
     inactive_rn 
     AS (SELECT *, Row_number() 
                    OVER ( partition BY NAME, product_id 
                    ORDER BY created_at) AS rownum 
         FROM   statuses
         WHERE name = 'inactive') 
SELECT x1.product_id, 
       x1.created_at AS active_date, 
       CASE WHEN x2.created_at IS NULL 
            THEN Curdate()
            ELSE x2.created_at 
       END AS end_date, 
       CASE WHEN x2.created_at IS NULL 
             THEN Datediff(Curdate(), x1.created_at) 
            ELSE  Datediff(x2.created_at,x1.created_at) 
        END AS days_active 
FROM   active_rn x1 
       LEFT OUTER JOIN inactive_rn x2 
                    ON x1.rownum = x2.rownum 
                       AND x1.product_id = x2.product_id ORDER  BY 
x1.product_id);

【讨论】:

  • 谢谢!我在 MySQL 5.7 上,但我喜欢这个视图的想法。但问题是,如果我在服务器端进行计算,则需要 N+1 次查询来计算每个产品的项目。我有数百种产品和数十万个订单/项目来计算最近的销售额。只需几分钟/超时。我可以尝试更改超时限制、在后台进程上运行脚本、缓存结果等;但由于所需的架构,这将是我最后的选择。
  • 澄清一下,我的意思是使用视图结果以编程方式获取date_to_start_counting_sales。那是您可能不应该在 SQL 中尝试的部分。一旦你有了它,只需将日期传递到select 语句中。或者再次查看您的迭代,因为几乎任何 ORM 都有解决 n+1 问题的方法。很多堆栈溢出的答案。祝你好运!
猜你喜欢
  • 2018-12-28
  • 2011-09-02
  • 1970-01-01
  • 2011-07-20
  • 1970-01-01
  • 1970-01-01
  • 2014-06-01
  • 2015-08-10
  • 2017-05-25
相关资源
最近更新 更多