Encontre a duração total de cada série consecutiva de linhas

11

Versão do MySQL

O código será executado no MySQL 5.5

fundo

Eu tenho uma tabela como a seguinte

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;

Esta tabela é sobre pacientes em um hospital e armazena as camas em que cada paciente passou algum tempo enquanto estava hospitalizado.

Cada ala pode ter várias camas e cada paciente pode se mudar para uma cama diferente dentro da mesma ala.

Objetivo

O que eu quero fazer é descobrir quanto tempo cada paciente passou em uma ala específica sem ter se mudado para uma ala diferente. Ou seja, quero descobrir a duração total do tempo consecutivo que ele passou na mesma ala.

Caso de teste

-- 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);

Na tabela real, as linhas não são consecutivas, mas para cada paciente o registro de data e hora de alta de uma linha == o registro de data e hora de admissão da próxima linha.

SQLFiddle

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

resultado esperado

Eu gostaria de escrever algo como o seguinte:

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);

Por favor, observe que não podemos agrupar por Patient_id. Devemos recuperar um registro separado para cada visita à UTI.

Em outras palavras, se um paciente passa um tempo na UTI, depois se muda e volta para lá, preciso recuperar o tempo total que ele passou em cada visita à UTI (ou seja, dois registros)

pmav99
fonte
1
+1 para uma pergunta eloquente, explicando claramente um problema complexo (e interessante). Se eu pudesse votar duas vezes no bônus adicionado de um SQLFiddle, eu o faria. No entanto, meu instinto é que, sem CTEs (expressões comuns de tabela) ou funções de janelas, isso não será possível no MySQL. Que ambiente de desenvolvimento você está usando, ou seja, você pode ser obrigado a fazer isso através do código.
Vérace
@ Vérace Declaro escrever código que recupera todas as linhas que correspondem aos leitos de UTI e estou agrupando-os em Python.
Pmav99
Obviamente, se isso puder ser feito de uma maneira relativamente limpa no SQL, preferirei.
Pmav99
Conforme as linguagens, o Python é bastante limpo! :-) Se você não está preso ao MySQL e precisa de um banco de dados F / LOSS, recomendo o PostgreSQL (de muitas maneiras amplamente superior ao MySQL IMHO), que possui CTEs e funções de janelas.
Vérace

Respostas:

4

Consulta 1, testada no 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 ;

Consulta 2, que é igual a 1, mas sem as tabelas derivadas. Provavelmente, este terá um melhor plano de execução, com índices adequados. Teste no 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 ;

Ambas as consultas assumem que existe uma restrição exclusiva (patient_id, admitted). Se o servidor for executado com configurações ANSI estritas, ele bed_iddeverá ser adicionado na GROUP BYlista.

ypercubeᵀᴹ
fonte
Note que eu modifiquei os valores de inserção no violino, porque seus descarregadas / datas admitidos não se encontraram para ids paciente 1 e 2.
ypercubeᵀᴹ
2
Surpresa - eu realmente pensei que era impossível, dada a falta de CTEs. Estranhamente, a primeira consulta não funcionaria para mim no SQLFiddle - uma falha? O segundo foi embora, mas posso sugerir que o st.bed_id seja removido, pois é enganoso. O paciente 1 não passou toda a sua primeira estadia na enfermaria 1 na mesma cama.
Vérace
@ Verace, thnx. No começo, eu também pensei que precisávamos de uma CTE recursiva. Corrigi uma junção ausente no Patient_id (que ninguém notou;) e acrescentei seu ponto de vista sobre a cama.
precisa saber é o seguinte
@ypercube Muito obrigado pela sua resposta! Isso é realmente útil. Vou estudar isso em detalhes :)
pmav99
0

CONSULTA PROPOSTA

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;

Carreguei seus dados de amostra em um banco de dados local no meu laptop. Então, eu executei a consulta

CONSULTA PROPOSTA EXECUTADA

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>

CONSULTA PROPOSTA EXPLICADA

Na subconsulta AA, calculo o número de segundos decorridos usando UNIX_TIMESTAMP () subtraindo UNIX_TIMESTAMP(discharged)FROM UNIX_TIMESTAMP(admitted). Se o paciente ainda estiver na cama (como indicado por alta NULL), atribuo a hora atual NOW () . Então, eu faço a subtração. Isso fornecerá uma duração atualizada para qualquer paciente ainda na enfermaria.

Então, agrego a soma dos segundos por patient_id. Por fim, tomo os segundos para cada paciente e uso SEC_TO_TIME () para exibir horas, minutos e segundos da permanência do paciente.

DE UMA CHANCE !!!

RolandoMySQLDBA
fonte
Para o registro, eu executei isso no MySQL 5.6.22 no meu laptop Windows 7. Dá um erro no SQL Fiddle.
RolandoMySQLDBA 16/01
1
Muito obrigado pela sua resposta. Receio, porém, que isso não responda à minha pergunta; provavelmente não fui suficientemente claro na minha descrição. O que eu quero recuperar é o tempo total gasto em cada estadia na UTI. Não quero agrupar por paciente. Se um paciente passa um tempo na UTI, depois se muda e volta para lá, preciso recuperar o tempo total que ele passou em cada visita (ou seja, dois registros).
Pmav99
em um tópico diferente, escreva sua resposta (original). Acho que o uso de duas subconsultas não é realmente necessário (por exemplo, tabela Ae AA). Eu acho que um deles é suficiente.
Pmav99