使用动态日期间隔查询

Posted

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了使用动态日期间隔查询相关的知识,希望对你有一定的参考价值。

如果状态表包含有关产品可用性的信息,如何选择与产品处于活动状态的最近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-02-01以来不活跃。
  • 最后,它自'2018-01-01'以来一直处于活动状态,因此它应该只计算从'2018-02-01'向后的2天(4 + 14 + 2 = 20),导致date_to_start_counting_sales ='2018-01 -30' 。
  • 随着'2018-01-30'日期的到来,我可以在最近20个活跃的日子里计算Apple订单:3。

希望有道理。

这是一个fiddle与上面提供的数据。

答案

我有一个标准的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 |

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

另一答案

不确定你正在使用哪个版本的MySql,但是如果你可以使用8.0,那个版本就会出现很多功能,这些功能会让事情变得更加可行(CTE,row_number(),分区等)。

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

假设:

  1. 在非活动日期范围内不能销售产品
  2. Statuses表将始终为每个产品交替显示活动/非活动/活动状态。即没有某个产品既有效又无效的日期范围。

查看结果:

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

以上是关于使用动态日期间隔查询的主要内容,如果未能解决你的问题,请参考以下文章

使用单个查询获取每个多个日期间隔的条目数

用于显示多个日期范围之间的间隔的 SQL 查询

Mysql查询检索帖子并根据特定日期间隔进行过滤

js获两个时间间隔天数 除星期天

php间隔和时区

如何在特定日期间隔内查找