Usando postgresql 9.6.
A tabela tem sessões de usuário e eu preciso de sessões distintas não sobrepostas impressas.
CREATE TABLE SESSIONS(
id serial NOT NULL PRIMARY KEY,
ctn INT NOT NULL,
day DATE NOT NULL,
f_time TIME(0) NOT NULL,
l_time TIME(0) NOT NULL
);
INSERT INTO SESSIONS(id, ctn, day, f_time, l_time)
VALUES
(1, 707, '2019-06-18', '10:48:25', '10:56:17'),
(2, 707, '2019-06-18', '10:48:33', '10:56:17'),
(3, 707, '2019-06-18', '10:53:17', '11:00:49'),
(4, 707, '2019-06-18', '10:54:31', '10:57:37'),
(5, 707, '2019-06-18', '11:03:59', '11:10:39'),
(6, 707, '2019-06-18', '11:04:41', '11:08:02'),
(7, 707, '2019-06-18', '11:11:04', '11:19:39');
id ctn day f_time l_time
1 707 2019-06-18 10:48:25 10:56:17
2 707 2019-06-18 10:48:33 10:56:17
3 707 2019-06-18 10:53:17 11:00:49
4 707 2019-06-18 10:54:31 10:57:37
5 707 2019-06-18 11:03:59 11:10:39
6 707 2019-06-18 11:04:41 11:08:02
7 707 2019-06-18 11:11:04 11:19:39
Agora eu preciso de sessões de usuário não sobrepostas distintas, então isso deve me dar:
1. start_time: 10:48:25 end_time: 11:00:49 duration: 12min,24 sec
2. start_time: 11:03:59 end_time: 11:10:39 duration: 6min,40 sec
3. start_time: 11:11:04 end_time: 11:19:39 duration: 8min,35 sec
Para resolver este problema fiz o seguinte:
Explicação "fácil":
Para esta parte, adicionei um pouco à definição da tabela fornecida pelo OP. Acredito firmemente que o DDL deve ser usado o máximo possível para "guiar" todo o processo de programação de banco de dados e pode ser muito mais poderoso - um exemplo disso seria SQL em
CHECK
restrições - até agora fornecido apenas pelo Firebird ( exemplo aqui ) e H2 (veja referência aqui ).No entanto, tudo isso é muito bom, mas temos que lidar com os recursos 9.6 do PostgreSQL - a versão do OP. Meu DDL ajustado para a explicação "simples" (veja o violino inteiro aqui ):
Índices:
Apenas um ponto a ser observado: não use palavras- chave SQL como nomes de tabelas ou colunas -
day
é uma palavra-chave! Pode ser confuso para depurar &c - simplesmente não é uma boa prática. Eu mudei seu nome de campo originalday
paraf_day
- observe todas as maiúsculas e minúsculas do python! Faça o que fizer, tenha um método padrão de nomear variáveis e cumpra-o - existem muitos documentos de padrões de codificação por aí.A mudança para 'f_day' não tem efeito no resto do SQL, pois não levamos em consideração as sessões que abrangem a meia-noite. Levar em conta isso pode ser feito com relativa facilidade fazendo o seguinte (veja o violino).
Agora com o advento das
GENERATED
colunas, você nem precisa se preocupar com isso - basta ter umGENERATED
campo como acima!Se uma restrição para o segundo é impraticável - logins ao mesmo tempo, você pode usar
TIME(2) (or 3..6)
para garantir a exclusividade. Se [você não quer | não pode ter]UNIQUE
restrições, você pode colocar emDISTINCT
seu SQL para tempos de login e logout idênticos - embora isso seja improvável.O fato é que alguns DDL simples como esse simplificam enormemente seu SQL subsequente (veja a discussão no final da explicação "complexa" abaixo).
Você também pode querer colocar
ctn
e/ou em suas restriçõesday
DDL como mostrado?UNIQUE
Eu também adicionei o que eu acho que podem ser bons índices! Você também pode querer investigar oOVERLAPS
operador?Quanto aos dados de exemplo, também adicionei alguns registros para testar minha solução da seguinte forma:
Vou percorrer minha lógica passo a passo - bom para você talvez, mas também para mim, pois me ajuda a esclarecer meu pensamento e garantirá que as lições que aprendi com este exercício permaneçam comigo - "Eu ouço e Eu esqueço. Eu vejo e lembro. Eu faço e eu entendo." - Confúcio .
Todos os itens a seguir estão incluídos no violino.
O primeiro passo é usar a
LAG
função ( documentation ) da seguinte forma:Resultado:
Portanto, sempre que houver um novo intervalo, haverá um 1 na
ovl
coluna (sobreposição).Em seguida, tomamos o cumulativo
SUM
desses 1s da seguinte forma:Resultado:
Então, agora "dividimos" e temos uma maneira de distinguir entre nossos intervalos - cada intervalo tem um valor diferente de
s
- 1..5.Então, agora queremos obter o menor valor de
f_time
e o maior valor del_time
para esses intervalos. Minha primeira tentativa usandoMAX()
eMIN()
ficou assim:Resultado:
Observe como temos que obter
rn
= 3 para o primeiro intervalo,rn
= 3 para o quarto e valores diferentes dern
para diferentes intervalos - se houvesse 7 subintervalos formando um intervalo, teríamos que recuperarrn
= 7 - isso me deixou perplexo por um tempo!Então o poder das funções do Windows veio em socorro - se você classificar o
MAX()
eMIN()
de maneira diferente, o resultado correto cai em nosso colo:Resultado:
Observe que agora,
rn
= 1 é sempre nosso registro desejado - este é o resultado de:Observe que para
MIN()
, a ordenação é porlt DESC
e paraMAX()
(particionado por intervalo - ou sejas
) é porft DESC
. Isso combina o menorft
com o maiorlt
, que é o que queremos.Este é essencialmente o nosso resultado desejado - basta adicionar um pouco de organização e formatação de acordo com os requisitos do OP e estamos prontos. Esta parte também demonstra o uso de outra função Window muito útil -
ROW_NUMBER()
.Resultado final:
Não posso dar garantias sobre o desempenho desta consulta se houver um grande número de registros, veja o resultado
EXPLAIN (ANALYZE, BUFFERS)
no final do violino. No entanto, estou assumindo que, como está em um formato de estilo de relatório, pode ser para um determinado valor dectn
e/ouday
- ou seja, não há muitos registros?Explicação "complexa":
Não mostrarei todas as etapas - depois de eliminar os
f_time
s e s duplicadosl_time
, as etapas são idênticas.Aqui, a definição e os dados da tabela são um pouco diferentes (fiddle disponível aqui ):
As únicas restrições que mantive são
CHECK (f_time < l_time)
(não poderia ser de outra forma) eUNIQUE f_time, l_time
(talvez adicionarday
e/ouctn
a isso - o conselhoTIME(2) or (3...6)
acima também se aplica.Vou deixar para o leitor aplicar
UNIQUE
a combinações dectn
ef_day
conforme aplicável!Eu adicionei alguns registros potencialmente "problemáticos" (2 e 4) com o mesmo
f_time
el_time
dentro do mesmo intervalo. Assim, no caso de um idênticof_time
, queremos o subintervalo com o maiorl_time
e vice-versa para o caso de um idênticol_time
(ou seja, o menorf_time
).So, what I did in this case was to eliminate duplicates by chaining
CTE
's (aka theWITH
clause) as follows:Result:
And then I treat
cte2
as the starting point for the process in the "easy" explanation.The final SQL is as follows:
Result:
As you can see, it's pretty hairy stuff - not having the
UNIQUE
constraints in the DDL has doubled the length of the SQL and the time taken for the planning and executions stages, and made it pretty horrible into the bargain.See the end of the fiddle for the plans for both queries! Lessons to be learnt there! As a rule of thumb, the longer the plan, the slower the query!
I'm not sure that the indexes can play any role here since we're selecting from the entire table and it's very small! If we were filtering a large table by
ctn
and/orf_day
and/orf_time
, I'm pretty sure that we would start to see differences in the plans (and timings!) if there were no indexes!I used the accepted answer for my own needs until I found a limit:
What if we have these rows ?
Both queries create a new group starting on row 10 instead of grouping these 3 rows together.
The answer is to tweak the core query
needs to be replace by
then you'll get: