Учитывая таблицу статусов, которая содержит информацию о доступности продуктов, как мне выбрать дату, соответствующую первому дню из последних 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.
Надеюсь, это имеет смысл.
Вот рабочий пример с данными, предоставленными выше.
@krokodilko Как я уже упоминал, я пытался решить проблему на серверном языке, и у меня возникла проблема с выполнением этого в SQL. Спасибо за отличное сотрудничество.
@Mallorie Спасибо! Я установил даты и добавил объяснение, как я получу желаемый результат для Apple. Пожалуйста, дайте мне знать, если это все еще не имеет смысла.
Я прочитал ваш «наконец» пункт 5 раз: я понимаю, как вы получаете свои 20 дней активных яблок, но я не понимаю, как вы получаете из этого дату 2018-01-30. Кроме того, поскольку MySQL не имеет оконных функций, этого будет сложно достичь в прямом SQL (без процедуры).
Спасибо rzb, теперь это имеет смысл. Думая о том же, что и @Thomas G ... мог бы что-то понять в PL / SQL (хотя и некрасиво), но mysql кажется ограниченным. И mysql - не моя сильная сторона. Я подумаю и вернусь, если что-нибудь придет в голову.
@ThomasG Я просто считаю 20 активных дней назад, игнорируя неактивные интервалы. Общее количество активных дней составит 49 (поскольку он был впервые активирован в «2018-01-01»), но поскольку мне нужно всего 20 дней, мне нужно еще только 2 на тот момент. Итак '2018-02-01' -2 дн. Довольно сложно объяснить и, наверное, еще сложнее понять = (
@Mallorie Если бы вы могли опубликовать то, что получили с помощью PL / SQL, это, вероятно, стало бы для меня отличной отправной точкой. Спасибо!
чувак, от тебя у меня болит голова воскресным утром ... хорошо, пытаюсь понять это снова. Не могли бы вы подготовить скрипку со своей структурой и данными, чтобы у нас была игровая площадка?
@ThomasG Обновил вопрос ссылкой на скрипку окончательный.
это уже будет большим подспорьем: sqlfiddle.com/#!9/b7817f/3 Мне все еще трудно понять остальное, но, судя по моему запросу здесь, это наверняка будет мелочью. Я беру кофе и допиваю
На написание plsql все равно уйдет много времени, и его практически невозможно перевести на mysql ... поэтому вместо этого я испортил mysql. Так или иначе, я хотел выучить это. Надеюсь, поможет. Теперь я одержим этой проблемой! ха
Можно ли заказывать товары, когда они неактивны? Заказывают ли товар обычно один раз в день? В таком случае используйте заказы за последние 20 дней. И почему вопрос с меткой MySQL, если вы пишете код PL / SQL. Oracle и MySQL имеют очень разные возможности.
@GordonLinoff Нет, их нельзя заказать, когда они не активны. И их можно заказывать от 0 до N раз в день. Некоторые продукты обычно продаются только в определенные даты. Я получил то, что вы предлагаете, но, к сожалению, нет конкретного шаблона. И нет, я не пишу код PL / SQL, это MySQL.






Не уверен, с какой версией MySql вы работаете, но если вы можете использовать 8.0, эта версия имеет множество функций, которые делают вещи немного более выполнимыми (CTE, row_number (), раздел и т. д.).
Я бы порекомендовал создать представление, подобное этому Пример DB-Fiddle, вызвать представление на стороне сервера и выполнить итерацию программно. Есть способы сделать это в SQL, но было бы сложно писать, тестировать и, вероятно, было бы менее эффективно.
Предположения:
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, необходимых для подсчета элементов для каждого продукта, если я делаю это на стороне сервера. У меня есть сотни продуктов и сотни тысяч заказов / позиций, по которым я могу рассчитать недавние продажи. Это занимает всего несколько минут / тайм-аут. Я мог бы попробовать изменить лимит тайм-аута, запустить скрипт в фоновом режиме, кэшировать результаты и т. Д .; но это было бы моим последним средством из-за необходимой архитектуры.
Чтобы прояснить, я имею в виду использование результатов просмотра, чтобы получить date_to_start_counting_sales программно. Это та часть, которую вы, вероятно, не должны пытаться использовать в SQL. Как только вы его получите, просто передайте дату в оператор select. Или посмотрите на свою итерацию еще раз, потому что практически любой ORM может решить проблему n + 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 |
FIDDLE: http://sqlfiddle.com/#!9/804f52/24
Потрясающие! Вы сделали это очень просто. Одна вещь, хотя можно ли вместо null показывать результаты для неактивных в настоящее время продуктов? Я постараюсь обойти это и настроить ваши запросы.
да, но какова логика для неактивных продуктов?
Та же логика: 20 последних активных дней. например если тот же продукт Apple был отключен сегодня, он все равно должен давать те же результаты навсегда, пока он снова не станет активным.
Кроме того, действительно важный столбец - это Recent_sales_count (см. Мою таблицу «желаемых результатов»). Date_to_start_counting_sales (ваша target_date) будет там, чтобы я мог использовать ее как дату начала для подсчета заказов.
Изменение переменной примерно на 15 дает неверные результаты. Наверное, потому, что он только считает дни и игнорирует месяцы.
Сначала попробуйте мое обновленное решение (ссылка на скрипт тоже обновлена), после этого мы увидим. Recent_sales_count будет легко вычислить
Хороший! Это верно для неактивных продуктов. Но если вы измените cap_days на 10, Apple должна иметь date_to_start_counting_sales «2018-03-09», а не «2018-01-26».
Я сдаюсь. В Европе уже поздно, и я слишком устал, чтобы правильно это продумать. Я знаю, что последний штрих близок, но я не вижу этого, потому что мне нужно спать. Я все еще обновил скрипку и отвечаю с помощью Recent_sales_count. Если кто-то еще хочет доработать мой ответ, чтобы закончить его, пожалуйста, сделайте ... работу для @GordonLinoff :)
Можете ли вы дважды проверить
date_to_start_counting_salesв желаемых результатах (при условии, что вы используете ГГГГ-ММ-ДД)? И, пожалуйста, приведите пример того, что вы подразумеваете под «последние 20 дней, когда он был активен» для одного из продуктов. Например. Последний раз Apple была активна 25 апреля 2018, поэтому все продажи с 25 апреля 2018 по 15 мая 2018? Или вам нужно время последней активности, когда существует полный 20-дневный интервал? Я не могу работать в обратном направлении, исходя из ваших данных, о том, как вы пришли к желаемым результатам с помощью предоставленной информации.