Запрос с динамическими интервалами дат

Учитывая таблицу статусов, которая содержит информацию о доступности продуктов, как мне выбрать дату, соответствующую первому дню из последних 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  |
+-------+-------------+---------------+---------------+

table: items (заказанные товары)

+-------+---------------+-------------+
|   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 активными днями, например, Яблоко:

  • Последний раз он был активирован 25 апреля 2018 г. Это 4 дня назад.

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

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

  • До этого неактивен с 01.02.2018.

  • Наконец, он был активен с '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.

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

Вот рабочий пример с данными, предоставленными выше.

Можете ли вы дважды проверить date_to_start_counting_sales в желаемых результатах (при условии, что вы используете ГГГГ-ММ-ДД)? И, пожалуйста, приведите пример того, что вы подразумеваете под «последние 20 дней, когда он был активен» для одного из продуктов. Например. Последний раз Apple была активна 25 апреля 2018, поэтому все продажи с 25 апреля 2018 по 15 мая 2018? Или вам нужно время последней активности, когда существует полный 20-дневный интервал? Я не могу работать в обратном направлении, исходя из ваших данных, о том, как вы пришли к желаемым результатам с помощью предоставленной информации.

var_missmal 29.04.2018 09:23

@krokodilko Как я уже упоминал, я пытался решить проблему на серверном языке, и у меня возникла проблема с выполнением этого в SQL. Спасибо за отличное сотрудничество.

rzb 29.04.2018 09:34

@Mallorie Спасибо! Я установил даты и добавил объяснение, как я получу желаемый результат для Apple. Пожалуйста, дайте мне знать, если это все еще не имеет смысла.

rzb 29.04.2018 09:54

Я прочитал ваш «наконец» пункт 5 раз: я понимаю, как вы получаете свои 20 дней активных яблок, но я не понимаю, как вы получаете из этого дату 2018-01-30. Кроме того, поскольку MySQL не имеет оконных функций, этого будет сложно достичь в прямом SQL (без процедуры).

Thomas G 29.04.2018 10:04

Спасибо rzb, теперь это имеет смысл. Думая о том же, что и @Thomas G ... мог бы что-то понять в PL / SQL (хотя и некрасиво), но mysql кажется ограниченным. И mysql - не моя сильная сторона. Я подумаю и вернусь, если что-нибудь придет в голову.

var_missmal 29.04.2018 10:10

@ThomasG Я просто считаю 20 активных дней назад, игнорируя неактивные интервалы. Общее количество активных дней составит 49 (поскольку он был впервые активирован в «2018-01-01»), но поскольку мне нужно всего 20 дней, мне нужно еще только 2 на тот момент. Итак '2018-02-01' -2 дн. Довольно сложно объяснить и, наверное, еще сложнее понять = (

rzb 29.04.2018 10:12

@Mallorie Если бы вы могли опубликовать то, что получили с помощью PL / SQL, это, вероятно, стало бы для меня отличной отправной точкой. Спасибо!

rzb 29.04.2018 10:15

чувак, от тебя у меня болит голова воскресным утром ... хорошо, пытаюсь понять это снова. Не могли бы вы подготовить скрипку со своей структурой и данными, чтобы у нас была игровая площадка?

Thomas G 29.04.2018 10:16

@ThomasG Обновил вопрос ссылкой на скрипку окончательный.

rzb 29.04.2018 10:49

это уже будет большим подспорьем: sqlfiddle.com/#!9/b7817f/3 Мне все еще трудно понять остальное, но, судя по моему запросу здесь, это наверняка будет мелочью. Я беру кофе и допиваю

Thomas G 29.04.2018 11:01

На написание plsql все равно уйдет много времени, и его практически невозможно перевести на mysql ... поэтому вместо этого я испортил mysql. Так или иначе, я хотел выучить это. Надеюсь, поможет. Теперь я одержим этой проблемой! ха

var_missmal 29.04.2018 12:45

Можно ли заказывать товары, когда они неактивны? Заказывают ли товар обычно один раз в день? В таком случае используйте заказы за последние 20 дней. И почему вопрос с меткой MySQL, если вы пишете код PL / SQL. Oracle и MySQL имеют очень разные возможности.

Gordon Linoff 29.04.2018 12:55

@GordonLinoff Нет, их нельзя заказать, когда они не активны. И их можно заказывать от 0 до N раз в день. Некоторые продукты обычно продаются только в определенные даты. Я получил то, что вы предлагаете, но, к сожалению, нет конкретного шаблона. И нет, я не пишу код PL / SQL, это MySQL.

rzb 29.04.2018 13:00
Освоение архитектуры микросервисов с Laravel: Лучшие практики, преимущества и советы для разработчиков
Освоение архитектуры микросервисов с Laravel: Лучшие практики, преимущества и советы для разработчиков
В последние годы архитектура микросервисов приобрела популярность как способ построения масштабируемых и гибких приложений. Laravel , популярный PHP...
Как построить CRUD-приложение в Laravel
Как построить CRUD-приложение в Laravel
Laravel - это популярный PHP-фреймворк, который позволяет быстро и легко создавать веб-приложения. Одной из наиболее распространенных задач в...
Освоение PHP и управление базами данных: Создание собственной СУБД - часть II
Освоение PHP и управление базами данных: Создание собственной СУБД - часть II
В предыдущем посте мы создали функциональность вставки и чтения для нашей динамической СУБД. В этом посте мы собираемся реализовать функции обновления...
Документирование API с помощью Swagger на Springboot
Документирование API с помощью Swagger на Springboot
В предыдущей статье мы уже узнали, как создать Rest API с помощью Springboot и MySql .
Роли и разрешения пользователей без пакета Laravel 9
Роли и разрешения пользователей без пакета Laravel 9
Этот пост изначально был опубликован на techsolutionstuff.com .
Как установить LAMP Stack - Security 5/5 на виртуальную машину Azure Linux VM
Как установить LAMP Stack - Security 5/5 на виртуальную машину Azure Linux VM
В предыдущей статье мы завершили установку базы данных, для тех, кто не знает.
2
13
214
2

Ответы 2

Не уверен, с какой версией 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);

Спасибо! Я использую MySQL 5.7, но мне нравится идея представления. Проблема, однако, заключается в количестве запросов N + 1, необходимых для подсчета элементов для каждого продукта, если я делаю это на стороне сервера. У меня есть сотни продуктов и сотни тысяч заказов / позиций, по которым я могу рассчитать недавние продажи. Это занимает всего несколько минут / тайм-аут. Я мог бы попробовать изменить лимит тайм-аута, запустить скрипт в фоновом режиме, кэшировать результаты и т. Д .; но это было бы моим последним средством из-за необходимой архитектуры.

rzb 29.04.2018 13:22

Чтобы прояснить, я имею в виду использование результатов просмотра, чтобы получить date_to_start_counting_sales программно. Это та часть, которую вы, вероятно, не должны пытаться использовать в SQL. Как только вы его получите, просто передайте дату в оператор select. Или посмотрите на свою итерацию еще раз, потому что практически любой ORM может решить проблему n + 1. На это много ответов о переполнении стека. Удачи вам!

var_missmal 29.04.2018 22:20

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

Потрясающие! Вы сделали это очень просто. Одна вещь, хотя можно ли вместо null показывать результаты для неактивных в настоящее время продуктов? Я постараюсь обойти это и настроить ваши запросы.

rzb 29.04.2018 20:34

да, но какова логика для неактивных продуктов?

Thomas G 29.04.2018 20:36

Та же логика: 20 последних активных дней. например если тот же продукт Apple был отключен сегодня, он все равно должен давать те же результаты навсегда, пока он снова не станет активным.

rzb 29.04.2018 20:44

Кроме того, действительно важный столбец - это Recent_sales_count (см. Мою таблицу «желаемых результатов»). Date_to_start_counting_sales (ваша target_date) будет там, чтобы я мог использовать ее как дату начала для подсчета заказов.

rzb 29.04.2018 20:47

Изменение переменной примерно на 15 дает неверные результаты. Наверное, потому, что он только считает дни и игнорирует месяцы.

rzb 29.04.2018 21:20

Сначала попробуйте мое обновленное решение (ссылка на скрипт тоже обновлена), после этого мы увидим. Recent_sales_count будет легко вычислить

Thomas G 29.04.2018 21:39

Хороший! Это верно для неактивных продуктов. Но если вы измените cap_days на 10, Apple должна иметь date_to_start_counting_sales «2018-03-09», а не «2018-01-26».

rzb 29.04.2018 21:52

Я сдаюсь. В Европе уже поздно, и я слишком устал, чтобы правильно это продумать. Я знаю, что последний штрих близок, но я не вижу этого, потому что мне нужно спать. Я все еще обновил скрипку и отвечаю с помощью Recent_sales_count. Если кто-то еще хочет доработать мой ответ, чтобы закончить его, пожалуйста, сделайте ... работу для @GordonLinoff :)

Thomas G 29.04.2018 22:47

Другие вопросы по теме