Uma solução para esse tipo de consulta envolve duas partes:geração de categoria seguida de agregação nas categorias geradas.
Para os dados que você forneceu, o primeiro passo neste tipo de solução é agrupar os dados por hora (já que os dados que você forneceu não têm nenhum evento nas 02:00 horas ou nas 04:00 horas, para mostrar essas horas no resultado final, eles podem ser gerados em vez de).
A segunda parte é agregar nos buckets por hora por meio de um
pivot
, conforme mencionado por Jorge Campos nos comentários. Abaixo está um exemplo.
Primeiro crie uma tabela de teste:
CREATE TABLE INSERT_TIME_STATUS(
INSERT_TIME TIMESTAMP,
STATUS VARCHAR2(128)
);
E adicione os dados de teste:
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');
Em seguida, crie a consulta. Isso usará fatoração de subconsulta para delinear a natureza de duas etapas desse processo.
O
CALENDAR
subfator aqui irá gerar cada hora do dia, independentemente de quaisquer registros terem ocorrido durante aquela hora. O
HOUR_CALENDAR
O subfactor atribuirá cada registro de status fornecido a uma hora específica e cortará os status que se cruzam em outra hora em pedaços, para que todos os registros caibam em um intervalo de uma hora. O
DURATION_IN_STATUS
subfator contará quantos minutos cada status esteve ativo durante cada hora. A consulta final será
PIVOT
para agregar (SUM
) a quantidade de tempo que cada STATUS
estava ativo 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 exemplo gera registros para o dia inteiro. Portanto, o último status de
NOT AVAILABLE
realiza. Se você deseja parar no momento do último status atribuído, esse comportamento pode ser ajustado conforme necessário. EDITAR, em resposta à sua atualização para avaliar esses tempos por
channel_id
e user_id
, aqui está outro exemplo: Primeiro crie a tabela de teste:
CREATE TABLE INSERT_TIME_STATUS(
USER_ID NUMBER,
CHANNEL_ID NUMBER,
INSERT_TIME TIMESTAMP,
STATUS VARCHAR2(128)
);
E carregue-o (aqui user_id=1 está nos canais 3 e 4 e user_id=2 está apenas no 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');
Em seguida, atualize a consulta para gerar dados por
user_id
por-channel_id
. Neste exemplo, os dados são incluídos para todos os tempos, para todos os canais com os quais cada usuário está envolvido. o usuário 1 terá contagens para cada hora do dia para os canais 3
e 4
enquanto o usuário-2 terá contagens para cada hora do dia apenas para o canal 3 (se tiver registros em outro canal, esse canal também será incluído). 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;
Então teste:
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