Запрос с динамическими интервалами дат - PullRequest
0 голосов
/ 29 апреля 2018

Учитывая таблицу статусов, которая содержит информацию о доступности продуктов, как выбрать дату, которая соответствует 1-му дню за последние 20 дней, в течение которых продукт был активен?

Да, я знаю, что за этим вопросом трудно следовать. Я думаю, что другой способ выразиться так: я хочу знать, сколько раз каждый продукт был продан за последние 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 активными днями, например. Apple:

  • Последний раз активировался в '2018-04-25'. Это 4 дня назад.

  • До этого он был неактивен с '2018-03-15', поэтому все эти дни до '2018-04-25' не учитываются.

  • До этого он был активен с '2018-03-01'. Это больше 14 дней до «2018-03-15».

  • До этого, неактивен с '2018-02-01'.

  • Наконец, он был активен с '2018-01-01', поэтому он должен считать только недостающие 2 дня (4 + 14 + 2 = 20) в обратном направлении от '2018-02 -01 ', в результате чего date_to_start_counting_sales =' 2018-01-30 '.

  • Имея дату «2018-01-30», я могу рассчитывать заказы Apple за последние 20 активных дней: 3.

Надеюсь, что это имеет смысл.

Вот скрипка с данными, указанными выше.

Ответы [ 2 ]

0 голосов
/ 29 апреля 2018

У меня есть стандартное решение 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

0 голосов
/ 29 апреля 2018

Не уверен, с какой версией MySql вы работаете, но если вы можете использовать 8.0, эта версия вышла с большим количеством функций, которые делают вещи немного более выполнимыми (CTE, row_number (), раздел и т. Д.).

Я бы порекомендовал создать представление, как в этом примере DB-Fiddle , вызвать представление на стороне сервера и выполнить программную итерацию. Существуют способы сделать это в 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);
Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...