使用动态日期间隔查询

时间:2018-04-29 05:52:46

标签: mysql sql join

如果状态表包含有关产品可用性的信息,如何选择与产品处于活动状态的最近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以来一直处于活跃状态。那是 14天直到'2018-03-15'。

  • 在此之前,自2018-02-01以来不活跃。

  • 最后,它自'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,其中包含上述数据。

2 个答案:

答案 0 :(得分:2)

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

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

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

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 |

FIDDLE:http://sqlfiddle.com/#!9/804f52/24

答案 1 :(得分:1)

不确定您正在使用哪个版本的MySql,但如果您可以使用8.0,则该版本提供了许多功能,使得事情稍微有些可行(CTE&#39; s,row_number(),分区等)。

我的建议是创建一个类似于DB-Fiddle Example的视图,在服务器端调用视图并以编程方式迭代。有一些方法可以在SQL中实现它,但它是一个写入,测试并且可能效率较低的熊。

假设:

  1. 在非活动日期范围内无法销售商品
  2. Statuses表将始终为每个产品交替显示活动/非活动/活动状态。即没有某个产品处于活动状态和非活动状态的日期范围。
  3. 查看结果:

    +------------+-------------+------------+-------------+
    | 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);