PostgreSQL
 sql >> Base de Dados >  >> RDS >> PostgreSQL

Encontrar intervalos de datas sobrepostos no PostgreSQL


A resposta atualmente aceita não responde à pergunta. E é errado em princípio. a BETWEEN x AND y traduz para:
a >= x AND a <= y

Incluindo o limite superior, enquanto as pessoas normalmente precisam excluir isto:
a >= x AND a < y

Com datas você pode ajustar facilmente. Para o ano de 2009, use '2009-12-31' como limite superior.
Mas não é tão simples com timestamps que permitem dígitos fracionários. As versões modernas do Postgres usam um inteiro de 8 bytes internamente para armazenar até 6 segundos fracionários (resolução µs). Sabendo disso, poderíamos ainda faz funcionar, mas isso não é intuitivo e depende de um detalhe de implementação. Péssima ideia.

Além disso, a BETWEEN x AND y não encontra intervalos sobrepostos. Nós precisamos:
b >= x AND a < y

E jogadores que nunca saíram não são considerados, ainda.

Resposta adequada


Assumindo o ano 2009 , reformularei a pergunta sem alterar seu significado:

"Encontre todos os jogadores de um determinado time que entraram antes de 2010 e não saíram antes de 2009."

Consulta básica:
SELECT p.*
FROM   team     t
JOIN   contract c USING (name_team) 
JOIN   player   p USING (name_player) 
WHERE  t.name_team = ? 
AND    c.date_join  <  date '2010-01-01'
AND    c.date_leave >= date '2009-01-01';

Mas há mais:

Se a integridade referencial for aplicada com restrições FK, a tabela team em si é apenas ruído na consulta e pode ser removido.

Embora o mesmo jogador possa sair e voltar ao mesmo time, também precisamos dobrar possíveis duplicatas, por exemplo, com DISTINCT .

E nós podemos precisa prever um caso especial:jogadores que nunca saíram. Supondo que esses jogadores tenham NULL em date_leave .

"Assume-se que um jogador que não se sabe que saiu está jogando pelo time até hoje."

Consulta refinada:
SELECT DISTINCT p.* 
FROM   contract c
JOIN   player   p USING (name_player) 
WHERE  c.name_team = ? 
AND    c.date_join  <  date '2010-01-01'
AND   (c.date_leave >= date '2009-01-01' OR c.date_leave IS NULL);

A precedência do operador funciona contra nós, AND vincula antes de OR . Precisamos de parênteses.

Resposta relacionada com DISTINCT otimizado (se duplicatas são comuns):
  • Tabela de muitos para muitos - o desempenho é ruim

Normalmente, nomes das pessoas físicas não são únicas e uma chave primária substituta é usada. Mas, obviamente, name_player é a chave primária do player . Se tudo o que você precisa são nomes de jogadores, não precisamos da mesa player na consulta, ou:
SELECT DISTINCT name_player 
FROM   contract
WHERE  name_team = ? 
AND    date_join  <  date '2010-01-01'
AND   (date_leave >= date '2009-01-01' OR date_leave IS NULL);

SQL OVERLAPS operador


O manual:

OVERLAPS automaticamente toma o valor anterior do par como o início. Cada período de tempo é considerado para representar o intervalo de meio-aberto start <= time < end , a menos que start e end são iguais, caso em que representa aquele único instante de tempo.

Para cuidar de possíveis NULL valores, COALESCE parece mais fácil:
SELECT DISTINCT name_player 
FROM   contract
WHERE  name_team = ? 
AND    (date_join, COALESCE(date_leave, CURRENT_DATE)) OVERLAPS
       (date '2009-01-01', date '2010-01-01');  -- upper bound excluded

Tipo de intervalo com suporte a índice


No Postgres 9.2 ou posterior você também pode operar com tipos de intervalo reais :
SELECT DISTINCT name_player 
FROM   contract
WHERE  name_team = ? 
AND    daterange(date_join, date_leave) &&
       daterange '[2009-01-01,2010-01-01)';  -- upper bound excluded

Os tipos de intervalo adicionam alguma sobrecarga e ocupam mais espaço. 2 x date =8 bytes; 1 x daterange =14 bytes no disco ou 17 bytes na RAM. Mas em combinação com o operador de sobreposição && a consulta pode ser suportada com um índice GiST.

Além disso, não há necessidade de valores NULL de maiúsculas e minúsculas. NULL significa "range aberto" em um tipo de intervalo - exatamente o que precisamos. A definição da tabela nem precisa ser alterada:podemos criar o tipo de intervalo em tempo real - e oferecer suporte à consulta com um índice de expressão correspondente:
CREATE INDEX mv_stock_dr_idx ON mv_stock USING gist (daterange(date_join, date_leave));

Relacionado:
  • Tabela de histórico de estoque médio