SQL запрос для получения пар дат внутри идентификатора - PullRequest
0 голосов
/ 18 марта 2020

У меня есть таблица со следующими строками:

    | item_id | change_type | change_date | change_id | other columns...
    | :------ | :---------- | :---------- | :-------- |
    |     123 |         off |  2019-06-04 |       321 |
    |     123 |          on |  2019-07-11 |       741 |
    |     123 |         off |  2019-07-13 |       987 |
    |     123 |          on |  2019-08-01 |       951 |
    |     123 |         off |  2019-08-07 |       357 |
    |     456 |         off |  2019-08-01 |       125 |
    |     456 |          on |  2019-11-18 |       878 |
    |     789 |          on |  2019-12-18 |       373 |
    |     012 |         off |  2019-12-25 |       654 |
    |     698 |         off |  2019-08-01 |       741 |
    |     698 |          on |  2018-01-03 |       147 |

Я пытаюсь выполнить запрос, который дает следующий результат:

    | item_id | on_date    | off_date   | on_id | off_id | other columns...
    | :------ | :--------- | :--------- | :---- | :----- |
    |     123 |            | 2019-06-04 |       |    321 |
    |     123 | 2019-07-11 | 2019-07-13 |   741 |    987 |
    |     123 | 2019-08-01 | 2019-08-07 |   951 |    357 |
    |     456 |            | 2019-08-01 |       |    125 |
    |     456 | 2019-11-18 |            |   878 |        |
    |     789 | 2019-12-18 |            |   373 |        |
    |     012 |            | 2019-12-25 |       |    654 |
    |     698 | 2018-01-03 | 2019-08-01 |   147 |    741 |

Нужный результат представляет собой таблицу, в которой даты «включено» и даты «выключено» отмечены в порядке убывания (сгруппированы по item_id), причем даты «выключено» в той же строке, что и предыдущая (по времени) дата «вкл.»

Наиболее близкими мне были следующие варианты:

Попытка первая:

SELECT
    changes_main.item_id,
    `on_date`,
    `off_date`,
    `on_id`,
    `off_id`
FROM (
    SELECT DISTINCT `item_id`
    FROM item_changes
) AS changes_main
LEFT OUTER JOIN (
    SELECT
        `item_id`, -- for joining purposes only
        `change_date` AS `on_date`,
        `change_id` AS `on_id`
    FROM item_changes
    WHERE `change_type` = 'on'
) AS changes_ons ON changes_ons.item_id = changes_main.item_id
RIGHT OUTER JOIN ( -- although LEFT or RIGHT doesn't seem to matter
    SELECT
        `item_id`, -- for joining purposes only
        `change_date` AS `off_date`,
        `change_id` AS `off_id`
    FROM item_changes
    WHERE `change_type` = 'off'
) AS changes_offs ON changes_offs.item_id = changes_main.item_id
;

Однако это, по сути, приводит к CROSS JOIN между on_date и off_date.

Единственное изменение во второй попытке - добавить предложение WHERE. Это была идея, которую я получил от этого вопроса .

Попытка вторая:

-- Same exact query as the above, however with the following
-- WHERE statement placed where the semicolon is above:
WHERE
    `off_date` = (
        SELECT MIN(offs2.change_date)
        FROM item_changes AS offs2
        WHERE offs2.change_type = 'off' AND
        offs2.change_date > changes_ons.on_date
    )
;

Проблема в том, что там, где есть четное число " on / off »в item_id, эти дополнительные« on »или« off »отфильтровываются.

Я пробовал вариант вышеуказанного WHERE предложения, включая OR off_date IS NULL, OR on_date IS NULL, et c.

Обновление:

Третьей попыткой было использование UNION и некоторых SCALAR SUBQUERIES. Это был самый близкий мне результат, который мне нужен. Тем не менее, по-прежнему не хватает (например, он не включает change_id, а также не создает идеального соответствия).

SELECT
    changes_on.item_id,
    changes_on.change_date AS `on_date`,
    (SELECT MIN(offs2.change_date)
        FROM item_changes AS offs2
        WHERE offs2.change_type = 'off' AND
        offs2.change_date > changes_ons.change_date
    ) AS `off_date`,
    changes_on.change_id AS `on_id`,
    NULL AS `off_id` -- odd
FROM item_changes AS changes_on
WHERE `change_type` = 'on'

UNION

SELECT
    changes_offs.item_id,
    changes_offs.change_date AS `off_date`,
    (SELECT MIN(ons2.change_date)
        FROM item_changes AS ons2
        WHERE ons2.change_type = 'on' AND
        ons2.change_date < changes_offs.on_date
    ) AS `off_date`,
    NULL AS `on_id`, -- odd
    changes_offs.change_id AS `off_id`
FROM item_changes AS changes_offs
WHERE `change_type` = 'off'
;

Ассистент / ввод / руководство приветствуются.

1 Ответ

1 голос
/ 18 марта 2020

Назначьте группу на основе количества включенных символов перед каждой строкой. Затем используйте условное агрегирование:

select item_id,
       max(case when change_type = 'on' then date end) as on_date,
       max(case when change_type = 'on' then change_id end) as on_change_id,
       max(case when change_type = 'off' then date end) as off_date,
       max(case when change_type = 'off' then change_id end) as off_change_id
from (select t.*,
             sum(case when change_type = 'on' then 1 else 0 end) over (partition by item_id order by change_date) as grp
      from t
     ) t
group by item_id, grp;

РЕДАКТИРОВАТЬ:

В более ранних версиях MySQL, вы можете express это как:

select item_id,
       max(case when change_type = 'on' then date end) as on_date,
       max(case when change_type = 'on' then change_id end) as on_change_id,
       max(case when change_type = 'off' then date end) as off_date,
       max(case when change_type = 'off' then change_id end) as off_change_id
from (select t.*,
             (select count(*)
              from t t2
              where t2.item_id = t.item_id and
                    t2.change_date <= t.change_date and
                    t2.change_type = 'on'
            ) as grp
      from t
     ) t
group by item_id, grp;

Производительность будет не так хорошо, как использование оконных функций, но поможет индекс (item_id, change_type, change_date).

...