Encuentra la duración total de cada serie consecutiva de filas

11

Versión MySQL

El código se ejecutará en MySQL 5.5

Antecedentes

Tengo una mesa como la siguiente

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 tabla trata sobre pacientes en un hospital y almacena las camas donde cada paciente pasó algún tiempo mientras estaba hospitalizado.

Cada sala puede tener varias camas y cada paciente puede moverse a una cama diferente dentro de la misma sala.

Objetivo

Lo que quiero hacer es averiguar cuánto tiempo pasó cada paciente en una sala específica sin haberse mudado a una sala diferente. Es decir, quiero encontrar la duración total del tiempo consecutivo que pasó dentro de la misma sala.

Caso de prueba

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

En la tabla real, las filas no son consecutivas, pero para cada paciente la marca de tiempo del alta de una fila == la marca de tiempo de admisión de la siguiente fila.

SQLFiddle

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

Resultado Esperado

Me gustaría escribir algo como lo siguiente:

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

Tenga en cuenta que no podemos agrupar por ID_paciente. Debemos recuperar un registro separado para cada visita a la UCI.

Para decirlo más claramente, si un paciente pasa tiempo en la UCI, luego sale de él y luego regresa allí, necesito recuperar el tiempo total que pasó en cada visita a la UCI (es decir, dos registros)

pmav99
fuente
1
+1 para una pregunta elocuente, que explica claramente un problema complejo (e interesante). Si pudiera votarlo dos veces por la bonificación adicional de un SQLFiddle, lo haría. Sin embargo, mi instinto es que sin CTE (expresiones de tabla comunes) o funciones de ventanas, esto no será posible en MySQL. ¿Qué entorno de desarrollo está utilizando? Es decir, podría estar obligado a hacerlo a través del código.
Vérace
@ Vérace He declarado escribir código que recupera todas las filas que corresponden a las camas de la UCI y las estoy agrupando en Python.
pmav99
Por supuesto, si esto se puede hacer de una manera relativamente limpia en SQL, lo preferiré.
pmav99
A medida que avanzan los idiomas, Python es bastante limpio. :-) Si no está atascado en MySQL y necesita una base de datos F / LOSS, ¿puedo recomendar PostgreSQL (en muchos aspectos muy superior a MySQL IMHO) que tiene funciones CTE y Windowing?
Vérace

Respuestas:

4

Consulta 1, probada en 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 es igual a 1 pero sin las tablas derivadas. Esto probablemente tendrá un mejor plan de ejecución, con índices adecuados. Prueba en 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 consultas asumen que hay una restricción única en (patient_id, admitted). Si el servidor se ejecuta con una configuración ANSI estricta, bed_iddebe agregarse en la GROUP BYlista.

ypercubeᵀᴹ
fuente
Tenga en cuenta que modifiqué los valores de inserción en el violín, porque sus fechas de alta / admisión no coincidían con los identificadores de paciente 1 y 2.
ypercubeᵀᴹ
2
Con asombro, realmente pensé que era imposible dada la falta de CTE. Curiosamente, la primera consulta no se ejecutará para mí en SQLFiddle: ¿un problema técnico? Sin embargo, el segundo sí, pero puedo sugerir que se elimine st.bed_id, ya que es engañoso. El paciente 1 no pasó toda su primera estancia en la sala 1 en la misma cama.
Vérace
@ Vérace, gracias. Al principio, también pensé que necesitábamos un CTE recursivo. He corregido una unión faltante en patient_id (que nadie notó;) y agregué tu punto sobre la cama.
ypercubeᵀᴹ
@ypercube ¡Muchas gracias por tu respuesta! Esto es realmente útil. Voy a estudiar esto en detalle :)
pmav99
0

CONSULTA PROPUESTA

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;

Cargué sus datos de muestra en una base de datos local en mi computadora portátil. Entonces, ejecuté la consulta

CONSULTA PROPUESTA EJECUTADA

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 PROPUESTA EXPLICADA

En la subconsulta AA, calculo el número de segundos transcurridos usando UNIX_TIMESTAMP () restando UNIX_TIMESTAMP(discharged)FROM UNIX_TIMESTAMP(admitted). Si el paciente todavía está en la cama (como lo indica el ser dado de alta NULL), le asigno la hora actual AHORA () . Entonces, hago la resta. Esto le dará una duración actualizada para cualquier paciente que aún esté en la sala.

Luego, agrego la suma de los segundos por patient_id. Finalmente, tomo los segundos para cada paciente y uso SEC_TO_TIME () para mostrar las horas, minutos y segundos de la estadía del paciente.

DARLE UNA OPORTUNIDAD !!!

RolandoMySQLDBA
fuente
Para el registro, ejecuté esto en MySQL 5.6.22 en mi computadora portátil con Windows 7. Da un error en SQL Fiddle.
RolandoMySQLDBA
1
Muchas gracias por su respuesta. Sin embargo, me temo que esto no responde a mi pregunta; probablemente no fui lo suficientemente claro en mi descripción. Lo que quiero recuperar es el tiempo total dedicado a cada estadía en la UCI. No quiero agrupar por paciente. Si un paciente pasa tiempo en la UCI, luego sale de él y luego regresa allí, necesito recuperar el tiempo total que pasó en cada visita (es decir, dos registros).
pmav99
En un tema diferente, wrt a su respuesta (original) Creo que el uso de dos subconsultas no es realmente necesario (es decir, tabla Ay AA). Creo que uno de ellos es suficiente.
pmav99