Eu tenho uma situação que acho que pode ser resolvida usando a função de janela, mas não tenho certeza.
Imagine a seguinte tabela
CREATE TABLE tmp (
date timestamp
, id_type integer
) ;
INSERT INTO tmp (date, id_type)
VALUES
( '2017-01-10 07:19:21.0', 3 ),
( '2017-01-10 07:19:22.0', 3 ),
( '2017-01-10 07:19:23.1', 3 ),
( '2017-01-10 07:19:24.1', 3 ),
( '2017-01-10 07:19:25.0', 3 ),
( '2017-01-10 07:19:26.0', 5 ),
( '2017-01-10 07:19:27.1', 3 ),
( '2017-01-10 07:19:28.0', 5 ),
( '2017-01-10 07:19:29.0', 5 ),
( '2017-01-10 07:19:30.1', 3 ),
( '2017-01-10 07:19:31.0', 5 ),
( '2017-01-10 07:19:32.0', 3 ),
( '2017-01-10 07:19:33.1', 5 ),
( '2017-01-10 07:19:35.0', 5 ),
( '2017-01-10 07:19:36.1', 5 ),
( '2017-01-10 07:19:37.1', 5 );
Eu gostaria de ter um novo grupo a cada mudança de valor na coluna id_type
. EG 1º grupo das 7:19:21 às 7:19:25, 2º largada e chegada às 7:19:26, e assim sucessivamente.
Neste momento, usando a consulta abaixo ...
SELECT distinct
min(min(date)) over w as begin,
max(max(date)) over w as end,
id_type
FROM tmp
GROUP BY id_type
WINDOW w AS (PARTITION BY id_type)
ORDER BY begin;
Obtenho o seguinte resultado:
begin end id_type
2017-01-10 07:19:21.0 2017-01-10 07:19:32.0 3
2017-01-10 07:19:26.0 2017-01-10 07:19:37.1 5
Enquanto eu gostaria:
begin end id_type
2017-01-10 07:19:21.0 2017-01-10 07:19:25.0 3
2017-01-10 07:19:26.0 2017-01-10 07:19:26.0 5
2017-01-10 07:19:27.1 2017-01-10 07:19:27.1 3
2017-01-10 07:19:28.0 2017-01-10 07:19:29.0 5
2017-01-10 07:19:30.1 2017-01-10 07:19:30.1 3
2017-01-10 07:19:31.0 2017-01-10 07:19:31.0 5
2017-01-10 07:19:32.0 2017-01-10 07:19:32.0 3
2017-01-10 07:19:33.1 2017-01-10 07:19:37.1 5
Quando isso funcionar, quero incluir mais critérios para definir grupos, e esses outros serão anuláveis.
Versão do Postgres: 8.4. Temos Postgres com PostGis, então não é fácil atualizar. As funções do PostGis mudam de nome e há outros problemas, mas já estamos reescrevendo tudo e a nova versão usará uma versão mais recente 9.X com PostGis 2.x.
1. Funções de janela mais subconsultas
Conte os passos para formar grupos, semelhante à ideia de Evan , com modificações e correções:
Isso pressupõe que as colunas envolvidas sejam
NOT NULL
. Caso contrário, você precisa fazer mais.Também supondo
date
ser definidoUNIQUE
, caso contrário você precisa adicionar um desempate àsORDER BY
cláusulas para obter resultados determinísticos. Como:ORDER BY date, id
.Explicação detalhada (resposta a pergunta muito semelhante):
Observe em particular:
Em casos relacionados,
lag()
com 3 parâmetros pode ser essencial cobrir o canto da primeira (ou última) linha de forma elegante. (O 3º parâmetro é usado como padrão se não houver nenhuma linha anterior (próxima).Como estamos interessados apenas em uma mudança real de
id_type
(TRUE
), isso não importa neste caso em particular.NULL
eFALSE
ambos não contam comostep
.count(step OR NULL) OVER (ORDER BY date)
é a sintaxe mais curta que também funciona no Postgres 9.3 ou anterior.count()
conta apenas valores não nulos ...No Postgres moderno, a sintaxe mais limpa e equivalente seria:
Detalhes:
2. Subtraia duas funções de janela, uma subconsulta
Semelhante à ideia de Erik com modificações:
Se
date
for definidoUNIQUE
, como mencionei acima,dense_rank()
seria inútil, pois o resultado é o mesmo que forrow_number()
e este último é substancialmente mais barato.Se não
date
estiver definido (e não sabemos se as únicas duplicatas estão em ), todas essas consultas são inúteis, pois o resultado é arbitrário.UNIQUE
(date, id_type)
Além disso, uma subconsulta geralmente é mais barata que um CTE no Postgres. Use CTEs apenas quando precisar deles.
Respostas relacionadas com mais explicações:
Em casos relacionados em que já temos um número em execução na tabela, podemos nos contentar com uma única função de janela:
3. Desempenho superior com a função plpgsql
Como essa pergunta se tornou inesperadamente popular, adicionarei outra solução para demonstrar o melhor desempenho.
SQL tem muitas ferramentas sofisticadas para criar soluções com sintaxe curta e elegante. Mas uma linguagem declarativa tem seus limites para requisitos mais complexos que envolvem elementos procedimentais.
Uma solução procedural com uma função do lado do servidor é mais rápida para isso do que qualquer coisa postada até agora, porque precisa apenas de uma única varredura sequencial sobre a tabela e uma única operação de classificação . Se um índice de ajuste estiver disponível, mesmo que seja apenas uma varredura de índice único.
Ligar:
Teste com:
Você pode tornar a função genérica com tipos polimórficos e passar o tipo de tabela e os nomes das colunas. Detalhes:
Se você não quiser ou não puder persistir uma função para isso, valeria a pena criar uma função temporária na hora. Custa alguns ms.
db<>fiddle here - comparing performance for all three. (Building on Jack's test case, modified.)
dbfiddle for Postgres 8.4, where performance differences are even bigger. (Not operational any more.)
You can do this as a simple subtraction of
ROW_NUMBER()
operations (or if your dates are not unique, though still unique perid_type
, then you can useDENSE_RANK()
instead, though it will be a more expensive query):See this work at DB Fiddle (or see the DENSE_RANK version)
Result:
Logically, you can think of this as a simple
DENSE_RANK()
with aPREORDER BY
, that is, you want theDENSE_RANK
of all the items that are ranked together, and you want them ordered by the dates, you just have to deal with the pesky problem of the fact that at each change in the date,DENSE_RANK
will increment. You do that by using the expression as I showed you above. Imagine if you had this syntax:DENSE_RANK() OVER (PREORDER BY date, ORDER BY id_type)
where thePREORDER
is excluded from the ranking calculation and only theORDER BY
is counted.Note that it's important to
GROUP BY
both the generatedSeq
column as well as theid_type
column.Seq
is NOT unique by itself, there can be overlaps--you must also group byid_type
.For further reading on this topic:
That first link gives you some code you can use if you wanted the begin or end date to be the same as the previous or next period's end/begin date (so there are no gaps). Plus other versions that could assist you in your query. Though they have to be translated from SQL Server syntax...
No Postgres 8.4 você pode usar uma função RECURSIVA .
Como eles fazem isso
A função recursiva adiciona um nível a cada id_type diferente, selecionando as datas uma a uma em ordem decrescente.
Em seguida, use MAX(date), MIN(date) agrupando por nível, id_type para obter o resultado desejado.
Confira: http://rextester.com/WCOYFP6623
Por alguns pontos,
tmp
que fica confusa..0
)date
. Se tiver data e hora, é um carimbo de data/hora (e armazene-o como um)Melhor usar uma função de janela ..
Saídas
Explicação
Primeiro precisamos de resets.. Nós os geramos com
lag()
Então contamos para obter grupos.
Em seguida, envolvemos em uma subseleção
GROUP BY
eORDER
selecionamos o min max (range)Here is another method, which is similar to Evan's and Erwin's in that it uses LAG to determine islands. It differs from those solutions in that it uses only one level of nesting, no grouping, and considerably more window functions:
The
is_start
computed column in the nested SELECT marks the beginning of each island. Additionally, the nested SELECT exposes each row's previous date and the dataset's last date.For rows that are the beginnings of their respective islands, the previous date effectively is the previous island's ending date. That is what the main SELECT uses it as. It picks only the rows matching the
is_start = 1
condition, and for each returned row it shows the row's owndate
asbegin
and the following row'sprev_date
asend
. As the last row does not have a following row,LEAD(prev_date)
returns a null for it, for which the COALESCE function substitutes the dataset's last date.You can play with this solution at dbfiddle.
When introducing additional columns identifying the islands, you will probably want to introduce a PARTITION BY subclause to each window function's OVER clause. For instance, if you want to detect the islands within groups defined by a
parent_id
, the above query will probably need to look like this:And if you decide to go with either Erwin's or Evan's solution, I believe a similar change will need to be added to it as well.
More out of academic interest than as a practical solution, you can also achieve this with a user-defined aggregate. Like the other solutions, this will work even on Postgres 8.4, but as others have commented, please upgrade if you can.
The aggregate handles
null
as if it is a differentfoo_type
, so runs of nulls would be given the samegrp
— that may or may not be what you want.dbfiddle here
Isso pode ser feito
RECURSIVE CTE
para passar o "tempo de início" de uma linha para a próxima, e algumas preparações extras (de conveniência).Esta consulta retorna o resultado que você deseja:
após a preparação... parte recursiva
Você pode verificar isso em http://rextester.com/POYM83542
Este método não escala bem. Para uma tabela de 8_641 linhas, são necessários 7 segundos, para uma tabela com o dobro desse tamanho, são necessários 28 segundos. Mais algumas amostras mostram tempos de execução parecidos com O(n^2).
O método de Evan Carrol leva menos de 1s (ou seja: vá em frente!), e se parece com O(n). As consultas recursivas são absolutamente ineficientes e devem ser consideradas o último recurso.