Una solución para este tipo de consulta implica dos partes:la generación de categorías seguida de la agregación en las categorías generadas.
Para los datos que proporcionó, el primer paso en este tipo de solución es agrupar los datos por hora (dado que los datos que proporcionó no tienen ningún evento en la hora 02:00 o la hora 04:00, para mostrar estas horas en el resultado final, se pueden generar en cambio).
La segunda parte es agregar en los cubos por hora a través de un pivot
, como lo menciona Jorge Campos en los comentarios.
A continuación se muestra un ejemplo.
Primero cree una tabla de prueba:
CREATE TABLE INSERT_TIME_STATUS(
INSERT_TIME TIMESTAMP,
STATUS VARCHAR2(128)
);
Y agregue los datos de prueba:
INSERT INTO INSERT_TIME_STATUS VALUES (TIMESTAMP '2017-01-01 00:00:00', 'AVAILABLE');
INSERT INTO INSERT_TIME_STATUS VALUES (TIMESTAMP '2017-01-01 00:15:00', 'BUSY');
INSERT INTO INSERT_TIME_STATUS VALUES (TIMESTAMP '2017-01-01 00:30:00', 'NOT AVAILABLE');
INSERT INTO INSERT_TIME_STATUS VALUES (TIMESTAMP '2017-01-01 01:30:00', 'AVAILABLE');
INSERT INTO INSERT_TIME_STATUS VALUES (TIMESTAMP '2017-01-01 03:10:00', 'BUSY');
INSERT INTO INSERT_TIME_STATUS VALUES (TIMESTAMP '2017-01-01 05:00:00', 'NOT AVAILABLE');
Luego crea la consulta. Esto utilizará la factorización de subconsultas para delinear la naturaleza de dos pasos de este proceso.
El CALENDAR
subfactor aquí generará cada hora del día, independientemente de si se produjeron registros durante esa hora.
Las HOUR_CALENDAR
El subfactor asignará cada registro de estado proporcionado a una hora específica y cortará en pedazos los estados que se cruzan con otra hora, de modo que todos los registros quepan dentro de un lapso de una hora.
El DURATION_IN_STATUS
El subfactor contará cuántos minutos estuvo activo cada estado durante cada hora.
La consulta final PIVOT
para agregar (SUM
) la cantidad de tiempo cada STATUS
estuvo activo durante cada hora.
WITH HOUR_OF_DAY AS (SELECT LEVEL - 1 AS THE_HOUR
FROM DUAL
CONNECT BY LEVEL < 25),
CALENDAR AS (SELECT DAY_START
FROM (
SELECT (TIMESTAMP '2017-01-01 00:00:00' + NUMTODSINTERVAL(DATE_INCREMENT.OFFSET, 'DAY')) AS DAY_START
FROM (SELECT LEVEL - 1 AS OFFSET
FROM DUAL
CONNECT BY LEVEL < 9999) DATE_INCREMENT)
WHERE DAY_START BETWEEN (SELECT MIN(TRUNC(INSERT_TIME_STATUS.INSERT_TIME))
FROM INSERT_TIME_STATUS)
AND (SELECT MAX(TRUNC(INSERT_TIME_STATUS.INSERT_TIME))
FROM INSERT_TIME_STATUS)),
HOUR_CALENDAR AS (
SELECT
TO_CHAR(CALENDAR.DAY_START, 'MM/DD/YYYY') AS THE_DAY,
HOUR_OF_DAY.THE_HOUR,
CALENDAR.DAY_START + NUMTODSINTERVAL(HOUR_OF_DAY.THE_HOUR, 'HOUR') AS HOUR_START,
(SELECT MAX(INSERT_TIME_STATUS.STATUS)
KEEP (DENSE_RANK LAST
ORDER BY INSERT_TIME_STATUS.INSERT_TIME ASC)
FROM INSERT_TIME_STATUS
WHERE INSERT_TIME_STATUS.INSERT_TIME <= DAY_START + NUMTODSINTERVAL(THE_HOUR, 'HOUR')) AS HOUR_START_STATUS
FROM CALENDAR
CROSS JOIN HOUR_OF_DAY),
ALL_HOUR_STATUS AS (
SELECT
HOUR_CALENDAR.THE_DAY,
HOUR_CALENDAR.THE_HOUR,
HOUR_CALENDAR.HOUR_START AS THE_TIME,
HOUR_CALENDAR.HOUR_START_STATUS AS THE_STATUS
FROM HOUR_CALENDAR
UNION ALL
SELECT
HOUR_CALENDAR.THE_DAY,
HOUR_CALENDAR.THE_HOUR,
INSERT_TIME_STATUS.INSERT_TIME AS THE_TIME,
INSERT_TIME_STATUS.STATUS AS THE_STATUS
FROM HOUR_CALENDAR
INNER JOIN INSERT_TIME_STATUS
ON HOUR_CALENDAR.HOUR_START < INSERT_TIME_STATUS.INSERT_TIME
AND HOUR_CALENDAR.THE_HOUR = EXTRACT(HOUR FROM INSERT_TIME_STATUS.INSERT_TIME)),
DURATION_IN_STATUS AS (
SELECT
ALL_HOUR_STATUS.THE_DAY,
ALL_HOUR_STATUS.THE_HOUR,
ALL_HOUR_STATUS.THE_STATUS,
(EXTRACT(HOUR FROM
(COALESCE(LEAD(THE_TIME)
OVER (
PARTITION BY NULL
ORDER BY THE_TIME ASC ), TO_TIMESTAMP(THE_DAY, 'MM/DD/YYYY') + NUMTODSINTERVAL(THE_HOUR + 1, 'HOUR')) - THE_TIME)) * 60)
+
EXTRACT(MINUTE FROM
(COALESCE(LEAD(THE_TIME)
OVER (
PARTITION BY NULL
ORDER BY THE_TIME ASC ), TO_TIMESTAMP(THE_DAY, 'MM/DD/YYYY') + NUMTODSINTERVAL(THE_HOUR + 1, 'HOUR')) - THE_TIME))
AS DURATION_IN_STATUS
FROM ALL_HOUR_STATUS)
SELECT
THE_DAY,
THE_HOUR,
COALESCE(AVAILABLE, 0) AS AVAILABLE,
COALESCE(NOT_AVAILABLE, 0) AS NOT_AVAILABLE,
COALESCE(BUSY, 0) AS BUSY
FROM DURATION_IN_STATUS
PIVOT (SUM(DURATION_IN_STATUS)
FOR THE_STATUS
IN ('AVAILABLE' AS AVAILABLE, 'NOT AVAILABLE' AS NOT_AVAILABLE, 'BUSY' AS BUSY)
)
ORDER BY THE_DAY ASC, THE_HOUR ASC;
Resultado:
THE_DAY THE_HOUR AVAILABLE NOT_AVAILABLE BUSY
01/01/2017 0 15 30 15
01/01/2017 1 30 30 0
01/01/2017 2 60 0 0
01/01/2017 3 10 0 50
01/01/2017 4 0 0 60
01/01/2017 5 0 60 0
01/01/2017 6 0 60 0
01/01/2017 7 0 60 0
01/01/2017 8 0 60 0
01/01/2017 9 0 60 0
01/01/2017 10 0 60 0
01/01/2017 11 0 60 0
01/01/2017 12 0 60 0
01/01/2017 13 0 60 0
01/01/2017 14 0 60 0
01/01/2017 15 0 60 0
01/01/2017 16 0 60 0
01/01/2017 17 0 60 0
01/01/2017 18 0 60 0
01/01/2017 19 0 60 0
01/01/2017 20 0 60 0
01/01/2017 21 0 60 0
01/01/2017 22 0 60 0
01/01/2017 23 0 60 0
24 rows selected.
Esta consulta de ejemplo genera registros para todo el día. Así que el último estado de NOT AVAILABLE
lleva a cabo. Si desea detenerse en el momento del último estado asignado, este comportamiento podría ajustarse según sea necesario.
EDITAR, en respuesta a su actualización para evaluar estos tiempos por channel_id
y user_id
, aquí hay otro ejemplo:
Primero cree la tabla de prueba:
CREATE TABLE INSERT_TIME_STATUS(
USER_ID NUMBER,
CHANNEL_ID NUMBER,
INSERT_TIME TIMESTAMP,
STATUS VARCHAR2(128)
);
Y cárguelo (aquí user_id=1 está en los canales 3 y 4 y user_id=2 está solo en el canal 3):
INSERT INTO INSERT_TIME_STATUS VALUES (1111,3,TO_TIMESTAMP('1/1/2017 0:00','MM/DD/YYYY HH24:MI'),'AVAILABLE');
INSERT INTO INSERT_TIME_STATUS VALUES (1111,3,TO_TIMESTAMP('1/1/2017 0:15','MM/DD/YYYY HH24:MI'),'BUSY');
INSERT INTO INSERT_TIME_STATUS VALUES (1111,3,TO_TIMESTAMP('1/1/2017 0:30','MM/DD/YYYY HH24:MI'),'NOT AVAILABLE');
INSERT INTO INSERT_TIME_STATUS VALUES (1111,3,TO_TIMESTAMP('1/1/2017 1:30','MM/DD/YYYY HH24:MI'),'AVAILABLE');
INSERT INTO INSERT_TIME_STATUS VALUES (1111,3,TO_TIMESTAMP('1/1/2017 3:10','MM/DD/YYYY HH24:MI'),'BUSY');
INSERT INTO INSERT_TIME_STATUS VALUES (1111,3,TO_TIMESTAMP('1/1/2017 5:00','MM/DD/YYYY HH24:MI'),'NOT AVAILABLE');
INSERT INTO INSERT_TIME_STATUS VALUES (1111,4,TO_TIMESTAMP('1/1/2017 0:00','MM/DD/YYYY HH24:MI'),'AVAILABLE');
INSERT INTO INSERT_TIME_STATUS VALUES (1111,4,TO_TIMESTAMP('1/1/2017 0:15','MM/DD/YYYY HH24:MI'),'BUSY');
INSERT INTO INSERT_TIME_STATUS VALUES (1111,4,TO_TIMESTAMP('1/1/2017 0:30','MM/DD/YYYY HH24:MI'),'NOT AVAILABLE');
INSERT INTO INSERT_TIME_STATUS VALUES (1111,4,TO_TIMESTAMP('1/1/2017 1:30','MM/DD/YYYY HH24:MI'),'AVAILABLE');
INSERT INTO INSERT_TIME_STATUS VALUES (1111,4,TO_TIMESTAMP('1/1/2017 3:10','MM/DD/YYYY HH24:MI'),'BUSY');
INSERT INTO INSERT_TIME_STATUS VALUES (1111,4,TO_TIMESTAMP('1/1/2017 5:00','MM/DD/YYYY HH24:MI'),'NOT AVAILABLE');
INSERT INTO INSERT_TIME_STATUS VALUES (2222,3,TO_TIMESTAMP('1/1/2017 0:00','MM/DD/YYYY HH24:MI'),'AVAILABLE');
INSERT INTO INSERT_TIME_STATUS VALUES (2222,3,TO_TIMESTAMP('1/1/2017 0:15','MM/DD/YYYY HH24:MI'),'BUSY');
INSERT INTO INSERT_TIME_STATUS VALUES (2222,3,TO_TIMESTAMP('1/1/2017 0:30','MM/DD/YYYY HH24:MI'),'NOT AVAILABLE');
INSERT INTO INSERT_TIME_STATUS VALUES (2222,3,TO_TIMESTAMP('1/1/2017 1:30','MM/DD/YYYY HH24:MI'),'AVAILABLE');
INSERT INTO INSERT_TIME_STATUS VALUES (2222,3,TO_TIMESTAMP('1/1/2017 3:10','MM/DD/YYYY HH24:MI'),'BUSY');
INSERT INTO INSERT_TIME_STATUS VALUES (2222,3,TO_TIMESTAMP('1/1/2017 5:00','MM/DD/YYYY HH24:MI'),'NOT AVAILABLE');
INSERT INTO INSERT_TIME_STATUS VALUES (2222,3,TO_TIMESTAMP('1/1/2017 5:00','MM/DD/YYYY HH24:MI'),'NOT AVAILABLE');
Luego actualice la consulta para generar datos por user_id
por channel_id
. En este ejemplo, los datos se incluyen para todos los tiempos, para todos los canales en los que participa cada usuario. el usuario 1 tendrá conteos para cada hora del día para los canales 3
y 4
mientras que el usuario 2 tendrá conteos para cada hora del día solo para el canal 3 (si tuviera registros en otro canal, ese canal también se incluirá).
WITH HOUR_OF_DAY AS (SELECT LEVEL - 1 AS THE_HOUR
FROM DUAL
CONNECT BY LEVEL < 25),
CALENDAR AS (SELECT DAY_START
FROM (
SELECT ((SELECT MIN(TRUNC(INSERT_TIME_STATUS.INSERT_TIME))
FROM INSERT_TIME_STATUS) + NUMTODSINTERVAL(DATE_INCREMENT.OFFSET, 'DAY')) AS DAY_START
FROM (SELECT LEVEL - 1 AS OFFSET
FROM DUAL
CONNECT BY LEVEL < 9999) DATE_INCREMENT)
WHERE DAY_START BETWEEN (SELECT MIN(TRUNC(INSERT_TIME_STATUS.INSERT_TIME))
FROM INSERT_TIME_STATUS)
AND (SELECT MAX(TRUNC(INSERT_TIME_STATUS.INSERT_TIME))
FROM INSERT_TIME_STATUS)),
USER_CHANNEL_HOUR_CALENDAR AS (
SELECT
USER_ID,
CHANNEL_ID,
CALENDAR.DAY_START,
TO_CHAR(CALENDAR.DAY_START, 'MM/DD/YYYY') AS THE_DAY,
HOUR_OF_DAY.THE_HOUR,
CALENDAR.DAY_START + NUMTODSINTERVAL(HOUR_OF_DAY.THE_HOUR, 'HOUR') AS HOUR_START
FROM CALENDAR
CROSS JOIN HOUR_OF_DAY
--
CROSS JOIN (SELECT UNIQUE USER_ID, CHANNEL_ID FROM INSERT_TIME_STATUS)
),
HOUR_CALENDAR AS (
SELECT USER_ID,
CHANNEL_ID,
THE_DAY,
THE_HOUR,
DAY_START,
HOUR_START,
(SELECT MAX(INSERT_TIME_STATUS.STATUS)
KEEP (DENSE_RANK LAST
ORDER BY INSERT_TIME_STATUS.INSERT_TIME ASC)
FROM INSERT_TIME_STATUS
WHERE INSERT_TIME_STATUS.INSERT_TIME <= DAY_START + NUMTODSINTERVAL(THE_HOUR, 'HOUR')
AND INSERT_TIME_STATUS.USER_ID = USER_ID
AND INSERT_TIME_STATUS.CHANNEL_ID = CHANNEL_ID) AS HOUR_START_STATUS
FROM USER_CHANNEL_HOUR_CALENDAR),
ALL_HOUR_STATUS AS (
SELECT
HOUR_CALENDAR.USER_ID,
HOUR_CALENDAR.CHANNEL_ID,
HOUR_CALENDAR.THE_DAY,
HOUR_CALENDAR.THE_HOUR,
HOUR_CALENDAR.HOUR_START AS THE_TIME,
HOUR_CALENDAR.HOUR_START_STATUS AS THE_STATUS
FROM HOUR_CALENDAR
UNION ALL
SELECT
INSERT_TIME_STATUS.USER_ID,
INSERT_TIME_STATUS.CHANNEL_ID,
HOUR_CALENDAR.THE_DAY,
HOUR_CALENDAR.THE_HOUR,
INSERT_TIME_STATUS.INSERT_TIME AS THE_TIME,
INSERT_TIME_STATUS.STATUS AS THE_STATUS
FROM HOUR_CALENDAR
INNER JOIN INSERT_TIME_STATUS
ON HOUR_CALENDAR.HOUR_START < INSERT_TIME_STATUS.INSERT_TIME
AND HOUR_CALENDAR.THE_HOUR = EXTRACT(HOUR FROM INSERT_TIME_STATUS.INSERT_TIME)
AND HOUR_CALENDAR.USER_ID = INSERT_TIME_STATUS.USER_ID
AND HOUR_CALENDAR.CHANNEL_ID = INSERT_TIME_STATUS.CHANNEL_ID),
DURATION_IN_STATUS AS (
SELECT
ALL_HOUR_STATUS.USER_ID,
ALL_HOUR_STATUS.CHANNEL_ID,
ALL_HOUR_STATUS.THE_DAY,
ALL_HOUR_STATUS.THE_HOUR,
ALL_HOUR_STATUS.THE_STATUS,
(EXTRACT(HOUR FROM
(COALESCE(LEAD(THE_TIME)
OVER (
PARTITION BY USER_ID, CHANNEL_ID
ORDER BY THE_TIME ASC ), TO_TIMESTAMP(THE_DAY, 'MM/DD/YYYY') + NUMTODSINTERVAL(THE_HOUR + 1, 'HOUR')) - THE_TIME)) * 60)
+
EXTRACT(MINUTE FROM
(COALESCE(LEAD(THE_TIME)
OVER (
PARTITION BY USER_ID, CHANNEL_ID
ORDER BY THE_TIME ASC ), TO_TIMESTAMP(THE_DAY, 'MM/DD/YYYY') + NUMTODSINTERVAL(THE_HOUR + 1, 'HOUR')) - THE_TIME))
AS DURATION_IN_STATUS
FROM ALL_HOUR_STATUS)
SELECT
USER_ID,
CHANNEL_ID,
THE_DAY,
THE_HOUR,
COALESCE(AVAILABLE, 0) AS AVAILABLE,
COALESCE(NOT_AVAILABLE, 0) AS NOT_AVAILABLE,
COALESCE(BUSY, 0) AS BUSY
FROM DURATION_IN_STATUS
PIVOT (SUM(DURATION_IN_STATUS)
FOR THE_STATUS
IN ('AVAILABLE' AS AVAILABLE, 'NOT AVAILABLE' AS NOT_AVAILABLE, 'BUSY' AS BUSY)
)
-- You can additionally filter the result
-- WHERE CHANNEL_ID IN (3,4)
-- WHERE USER_ID = 12345
-- WHERE THE_DAY > TO_CHAR(DATE '2017-01-01')
-- etc.
ORDER BY USER_ID ASC, CHANNEL_ID ASC, THE_DAY ASC, THE_HOUR ASC;
Entonces pruébalo:
USER_ID CHANNEL_ID THE_DAY THE_HOUR AVAILABLE NOT_AVAILABLE BUSY
1111 3 01/01/2017 0 15 30 15
1111 3 01/01/2017 1 30 30 0
1111 3 01/01/2017 2 60 0 0
1111 3 01/01/2017 3 10 0 50
1111 3 01/01/2017 4 0 0 60
1111 3 01/01/2017 5 0 60 0
1111 3 01/01/2017 6 0 60 0
...
1111 3 01/01/2017 23 0 60 0
1111 4 01/01/2017 0 15 30 15
1111 4 01/01/2017 1 30 30 0
1111 4 01/01/2017 2 60 0 0
1111 4 01/01/2017 3 10 0 50
1111 4 01/01/2017 4 0 0 60
1111 4 01/01/2017 5 0 60 0
1111 4 01/01/2017 6 0 60 0
...
1111 4 01/01/2017 23 0 60 0
2222 3 01/01/2017 0 15 30 15
2222 3 01/01/2017 1 30 30 0
2222 3 01/01/2017 2 60 0 0
2222 3 01/01/2017 3 10 0 50
2222 3 01/01/2017 4 0 0 60
2222 3 01/01/2017 5 0 60 0
2222 3 01/01/2017 6 0 60 0