Найти общую продолжительность каждой последовательной серии строк

11

Версия MySQL

Код будет работать в MySQL 5.5

Фон

У меня есть таблица, подобная следующей

CREATE TABLE t
( id INT NOT NULL AUTO_INCREMENT
, patient_id INT NOT NULL
, bed_id INT NOT NULL
, ward_id INT NOT NULL
, admitted DATETIME NOT NULL
, discharged DATETIME
, PRIMARY KEY (id)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8;

Эта таблица о пациентах в больнице и хранит кровати, где каждый пациент провел некоторое время, находясь в больнице.

В каждой палате может быть несколько кроватей, и каждый пациент может перейти на другую кровать в одной и той же палате.

Задача

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

Прецедент

-- Let's assume that ward_id = 1 corresponds to ICU (Intensive Care Unit)
INSERT INTO t
  (patient_id, bed_id, ward_id, admitted, discharged)
VALUES

-- Patient 1 is in ICU, changes some beds, then he is moved 
-- out of ICU, back in and finally he is out.
(1, 1, 1, '2015-01-06 06:05:00', '2015-01-07 06:04:00'),
(1, 2, 1, '2015-01-07 06:04:00', '2015-01-07 07:08:00'),
(1, 1, 1, '2015-01-07 07:08:00', '2015-01-08 08:11:00'),
(1, 4, 2, '2015-01-08 08:11:00', '2015-01-08 09:11:00'),
(1, 1, 1, '2015-01-08 09:11:00', '2015-01-08 10:11:00'),
(1, 3, 1, '2015-01-08 10:11:00', '2015-01-08 11:11:00'),
(1, 1, 2, '2015-01-08 11:11:00', '2015-01-08 12:11:00'),

-- Patient 2 is out of ICU, he gets inserted in ICU, 
-- changes some beds and he is back out
(2, 1, 2, '2015-01-06 06:00:00', '2015-01-07 06:04:00'),
(2, 1, 1, '2015-01-07 06:04:00', '2015-01-07 07:08:00'),
(2, 3, 1, '2015-01-07 07:08:00', '2015-01-08 08:11:00'),
(2, 1, 2, '2015-01-08 08:11:00', '2015-01-08 09:11:00'),

-- Patient 3 is not inserted in ICU
(3, 1, 2, '2015-01-08 08:10:00', '2015-01-09 09:00:00'),
(3, 2, 2, '2015-01-09 09:00:00', '2015-01-10 10:01:00'),
(3, 3, 2, '2015-01-10 10:01:00', '2015-01-11 12:34:00'),
(3, 4, 2, '2015-01-11 12:34:00', NULL),

-- Patient 4 is out of ICU, he gets inserted in ICU without changing any beds
-- and goes back out.
(4, 1, 2, '2015-01-06 06:00:00', '2015-01-07 06:04:00'),
(4, 2, 1, '2015-01-07 06:04:00', '2015-01-07 07:08:00'),
(4, 1, 2, '2015-01-07 07:08:00', '2015-01-08 09:11:00'),

-- Patient 5 is out of ICU, he gets inserted in ICU without changing any beds
-- and he gets dismissed.
(5, 1, 2, '2015-01-06 06:00:00', '2015-01-07 06:04:00'),
(5, 3, 2, '2015-01-07 06:04:00', '2015-01-07 07:08:00'),
(5, 1, 1, '2015-01-07 07:08:00', '2015-01-08 09:11:00'),

-- Patient 6 is inserted in ICU and he is still there
(6, 1, 1, '2015-01-11 12:34:00', NULL);

В реальной таблице строки не являются последовательными, но для каждого пациента отметка времени выписки из одной строки == отметка времени приема следующей строки.

SQLFiddle

http://sqlfiddle.com/#!2/b5fe5

ожидаемый результат

Я хотел бы написать что-то вроде следующего:

SELECT pid, ward_id, admitted, discharged
FROM  (....)
WHERE ward_id = 1;

(1, 1, '2015-01-06 06:05:00', '2015-01-08 08:11:00'),
(1, 1, '2015-01-08 09:11:00', '2015-01-09 11:11:00'),
(2, 1, '2015-01-07 06:04:00', '2015-01-08 08:11:00'),
(4, 1, '2015-01-07 06:04:00', '2015-01-07 07:08:00'),
(5, 1, '2015-01-07 07:08:00', '2015-01-08 09:11:00'),
(6, 1, '2015-01-11 12:34:00', NULL);

Пожалуйста, обратите внимание, что мы не можем группировать по Patient_id. Мы должны получить отдельную запись для каждого посещения отделения интенсивной терапии.

Проще говоря, если пациент проводит время в отделении интенсивной терапии, затем выходит из него и затем возвращается туда, мне нужно получить общее время, которое он провел за каждое посещение отделения интенсивной терапии (то есть две записи)

pmav99
источник
1
+1 за красноречивый вопрос, четко объясняющий сложную (и интересную) проблему. Если бы я мог проголосовать дважды за дополнительный бонус SQLFiddle, я бы так и сделал. Тем не менее, мой инстинкт заключается в том, что без CTE (общих табличных выражений) или оконных функций это будет невозможно в MySQL. Какую среду разработки вы используете, т.е. вы можете быть обязаны делать это с помощью кода.
Верас
@ Vérace Я написал для написания кода, который извлекает все строки, которые соответствуют койкам ICU, и я группирую их в Python.
pmav99
Конечно, если это можно сделать относительно чистым способом в SQL, я предпочту это.
pmav99
Поскольку языки идут, Python довольно чист! :-) Если вы не привязаны к MySQL и вам требуется база данных F / LOSS, могу я порекомендовать PostgreSQL (во многих отношениях значительно превосходящий MySQL IMHO), который имеет функции CTE и Windowing.
Верас

Ответы:

4

Запрос 1, протестирован в SQLFiddle-1

SET @ward_id_to_check = 1 ;

SELECT
    st.patient_id,
    st.bed_id AS starting_bed_id,          -- the first bed a patient uses
                                           -- can be omitted
    st.admitted,
    MIN(en.discharged) AS discharged
FROM
  ( SELECT patient_id, bed_id, admitted, discharged
    FROM t 
    WHERE t.ward_id = @ward_id_to_check
      AND NOT EXISTS
          ( SELECT * 
            FROM t AS prev 
            WHERE prev.ward_id = @ward_id_to_check
              AND prev.patient_id = t.patient_id
              AND prev.discharged = t.admitted
          )
  ) AS st
JOIN
  ( SELECT patient_id, admitted, discharged
    FROM t 
    WHERE t.ward_id = @ward_id_to_check
      AND NOT EXISTS
          ( SELECT * 
            FROM t AS next 
            WHERE next.ward_id = @ward_id_to_check
              AND next.patient_id = t.patient_id
              AND next.admitted = t.discharged
          )
  ) AS en
    ON  st.patient_id = en.patient_id
    AND st.admitted <= en.admitted
GROUP BY
    st.patient_id,
    st.admitted ;

Запрос 2, такой же, как 1, но без производных таблиц. Это, вероятно, будет иметь лучший план выполнения с надлежащими индексами. Тест в SQLFiddle-2 :

SET @ward_id_to_check = 1 ;

SELECT
    st.patient_id,
    st.bed_id AS starting_bed_id,
    st.admitted,
    MIN(en.discharged) AS discharged
FROM
    t AS st    -- starting period
  JOIN
    t AS en    -- ending period
      ON  en.ward_id = @ward_id_to_check
      AND st.patient_id = en.patient_id
      AND NOT EXISTS
          ( SELECT * 
            FROM t AS next 
            WHERE next.ward_id = @ward_id_to_check
              AND next.patient_id = en.patient_id
              AND next.admitted = en.discharged
          )
      AND st.admitted <= en.admitted
WHERE 
      st.ward_id = @ward_id_to_check
  AND NOT EXISTS
      ( SELECT * 
        FROM t AS prev 
        WHERE prev.ward_id = @ward_id_to_check
          AND prev.patient_id = st.patient_id
          AND prev.discharged = st.admitted
      )
GROUP BY
    st.patient_id,
    st.admitted ;

Оба запроса предполагают наличие уникального ограничения (patient_id, admitted). Если сервер работает со строгими настройками ANSI, его bed_idследует добавить в GROUP BYсписок.

ypercubeᵀᴹ
источник
Обратите внимание, что я изменил значения вставки в скрипте, потому что ваши выписанные / принятые даты не совпадали для идентификаторов пациентов 1 и 2.
ypercubeᵀᴹ
2
В страхе - я действительно думал, что это невозможно, учитывая отсутствие CTE. Как ни странно, первый запрос не будет работать для меня в SQLFiddle - глюк? Второй сделал, хотя, но я могу предложить удалить st.bed_id, поскольку это вводит в заблуждение. Пациент 1 не провел все свое первое пребывание в палате 1 в одной постели.
Верас
@ Vérace, спасибо. Сначала я тоже подумал, что нам нужен рекурсивный CTE. Я исправил отсутствующее объединение на Patient_id (который никто не заметил;) и добавил ваше замечание о кровати.
ypercubeᵀᴹ
@ypercube Большое спасибо за ваш ответ! Это действительно полезно. Я собираюсь изучить это подробно :)
pmav99
0

ПРЕДЛАГАЕМЫЙ ЗАПРОС

SELECT patient_id,SEC_TO_TIME(SUM(elapsed_time)) elapsed
FROM (SELECT * FROM (SELECT patient_id,
UNIX_TIMESTAMP(IFNULL(discharged,NOW())) -
UNIX_TIMESTAMP(admitted) elapsed_time
FROM t WHERE ward_id = 1) AA) A
GROUP BY patient_id;

Я загрузил ваши образцы данных в локальную базу данных на моем ноутбуке. Затем я запустил запрос

ПРЕДЛАГАЕМЫЙ ЗАПРОС ИСПОЛНЕНО

mysql> SELECT patient_id,SEC_TO_TIME(SUM(elapsed_time)) elapsed
    -> FROM (SELECT * FROM (SELECT patient_id,
    -> UNIX_TIMESTAMP(IFNULL(discharged,NOW())) -
    -> UNIX_TIMESTAMP(admitted) elapsed_time
    -> FROM t WHERE ward_id = 1) AA) A
    -> GROUP BY patient_id;
+------------+-----------+
| patient_id | elapsed   |
+------------+-----------+
|          1 | 76:06:00  |
|          2 | 26:07:00  |
|          4 | 01:04:00  |
|          5 | 26:03:00  |
|          6 | 118:55:48 |
+------------+-----------+
5 rows in set (0.00 sec)

mysql>

ПРЕДЛАГАЕМЫЙ ЗАПРОС ОБЪЯСНЕН

В подзапросе AA я вычисляю количество прошедших секунд, используя UNIX_TIMESTAMP () , вычитая UNIX_TIMESTAMP(discharged)FROM UNIX_TIMESTAMP(admitted). Если пациент все еще лежит в кровати (как указано разряженным существом NULL), я назначаю текущее время СЕЙЧАС () . Затем я делаю вычитание. Это даст вам самую последнюю продолжительность для любого пациента, который все еще находится в палате.

Затем я собираю сумму секунд по patient_id. Наконец, я беру секунды для каждого пациента и использую SEC_TO_TIME () для отображения часов, минут и секунд пребывания пациента.

Дайте ему попробовать!

RolandoMySQLDBA
источник
Для справки, я запустил это в MySQL 5.6.22 на моем ноутбуке с Windows 7. Это дает ошибку в SQL Fiddle.
RolandoMySQLDBA
1
Большое спасибо за ответ. Я боюсь, что это не отвечает на мой вопрос; вероятно, я не был достаточно ясен в своем описании. То, что я хочу получить, это общее время, потраченное на каждое пребывание в отделении интенсивной терапии. Я не хочу группировать по пациентам. Если пациент проводит время в отделении интенсивной терапии, затем выходит из него и затем возвращается туда, мне нужно получить общее время, которое он провел за каждое посещение (т.е. две записи).
pmav99
по другой теме, относительно вашего (оригинального) ответа Я думаю, что использование двух подзапросов не является действительно необходимым (т. е. таблица Aи AA). Я думаю, что одного из них достаточно.
pmav99