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

Como você faz matemática de datas que ignora o ano?


Se você não se importa com explicações e detalhes, use a "Versão de magia negra" abaixo de.

Todas as consultas apresentadas em outras respostas até agora operam com condições que não podem ser sargáveis - eles não podem usar um índice e precisam calcular uma expressão para cada linha na tabela base para encontrar as linhas correspondentes. Não importa muito com mesas pequenas. Importa (muito ) com mesas grandes.

Dada a seguinte tabela simples:
CREATE TABLE event (
  event_id   serial PRIMARY KEY
, event_date date
);

Consulta


As versões 1. e 2. abaixo podem usar um índice simples do formulário:
CREATE INDEX event_event_date_idx ON event(event_date);

Mas todas as soluções a seguir são ainda mais rápidas sem índice .

1. Versão simples

SELECT *
FROM  (
   SELECT ((current_date + d) - interval '1 year' * y)::date AS event_date
   FROM       generate_series( 0,  14) d
   CROSS JOIN generate_series(13, 113) y
   ) x
JOIN  event USING (event_date);

Subconsulta x calcula todas as datas possíveis em um determinado intervalo de anos a partir de um CROSS JOIN de dois generate_series() chamadas. A seleção é feita com a junção simples final.

2. Versão avançada

WITH val AS (
   SELECT extract(year FROM age(current_date + 14, min(event_date)))::int AS max_y
        , extract(year FROM age(current_date,      max(event_date)))::int AS min_y
   FROM   event
   )
SELECT e.*
FROM  (
   SELECT ((current_date + d.d) - interval '1 year' * y.y)::date AS event_date
   FROM   generate_series(0, 14) d
        ,(SELECT generate_series(min_y, max_y) AS y FROM val) y
   ) x
JOIN  event e USING (event_date);

O intervalo de anos é deduzido da tabela automaticamente - minimizando assim os anos gerados.
Você poderia ir um passo adiante e destilar uma lista de anos existentes se houver lacunas.

A eficácia co-depende da distribuição das datas. Poucos anos com muitas linhas cada tornam esta solução mais útil. Muitos anos com poucas linhas cada tornam menos útil.

Fiddle SQL simples Brincar com.

3. Versão de magia negra


Atualizado em 2016 para remover uma "coluna gerada", que bloquearia o H.O.T. atualizações; função mais simples e rápida.
Atualizado em 2018 para calcular MMDD com IMMUTABLE expressões para permitir a função embutida.


Crie uma função SQL simples para calcular um integer do padrão 'MMDD' :
CREATE FUNCTION f_mmdd(date) RETURNS int LANGUAGE sql IMMUTABLE AS
'SELECT (EXTRACT(month FROM $1) * 100 + EXTRACT(day FROM $1))::int';

Eu tinha to_char(time, 'MMDD') no início, mas mudou para a expressão acima que se mostrou mais rápida em novos testes no Postgres 9.6 e 10:

db<>mexa aqui

Ele permite a função embutida porque EXTRACT (xyz FROM date) é implementado com o IMMUTABLE função date_part(text, date) internamente. E tem que ser IMMUTABLE para permitir seu uso no seguinte índice de expressão multicoluna essencial:
CREATE INDEX event_mmdd_event_date_idx ON event(f_mmdd(event_date), event_date);

Múltiplas colunas por vários motivos:
Pode ajudar com ORDER BY ou com a seleção de determinados anos. Leia aqui. Quase sem custo adicional para o índice. Uma date cabe nos 4 bytes que de outra forma seriam perdidos no preenchimento devido ao alinhamento de dados. Leia aqui.
Além disso, como ambas as colunas de índice fazem referência à mesma coluna da tabela, não há inconveniente em relação ao H.O.T. atualizações. Leia aqui.

Uma função de tabela PL/pgSQL para governá-los todos


Fork para uma das duas consultas para cobrir a virada do ano:
CREATE OR REPLACE FUNCTION f_anniversary(date = current_date, int = 14)
  RETURNS SETOF event AS
$func$
DECLARE
   d  int := f_mmdd($1);
   d1 int := f_mmdd($1 + $2 - 1);  -- fix off-by-1 from upper bound
BEGIN
   IF d1 > d THEN
      RETURN QUERY
      SELECT *
      FROM   event e
      WHERE  f_mmdd(e.event_date) BETWEEN d AND d1
      ORDER  BY f_mmdd(e.event_date), e.event_date;

   ELSE  -- wrap around end of year
      RETURN QUERY
      SELECT *
      FROM   event e
      WHERE  f_mmdd(e.event_date) >= d OR
             f_mmdd(e.event_date) <= d1
      ORDER  BY (f_mmdd(e.event_date) >= d) DESC, f_mmdd(e.event_date), event_date;
      -- chronological across turn of the year
   END IF;
END
$func$  LANGUAGE plpgsql;

Ligar usando padrões:14 dias começando "hoje":
SELECT * FROM f_anniversary();

Ligue para 7 dias a partir de '2014-08-23':
SELECT * FROM f_anniversary(date '2014-08-23', 7);

Fiddle SQL comparando EXPLAIN ANALYZE .

29 de fevereiro


Ao lidar com aniversários ou "aniversários", você precisa definir como lidar com o caso especial "29 de fevereiro" em anos bissextos.

Ao testar intervalos de datas, Feb 29 geralmente é incluído automaticamente, mesmo que o ano atual não seja um ano bissexto . O intervalo de dias é estendido em 1 retroativamente quando abrange este dia.
Por outro lado, se o ano atual for bissexto e você quiser procurar 15 dias, poderá obter resultados para 14 dias em anos bissextos se seus dados forem de anos não bissextos.

Digamos que Bob nasceu em 29 de fevereiro:
Minha consulta 1. e 2. incluem 29 de fevereiro apenas em anos bissextos. Bob faz aniversário apenas a cada ~ 4 anos.
Minha consulta 3. inclui 29 de fevereiro no intervalo. Bob faz aniversário todos os anos.

Não há solução mágica. Você precisa definir o que deseja para cada caso.

Teste


Para fundamentar meu ponto, fiz um extenso teste com todas as soluções apresentadas. Adaptei cada uma das consultas à tabela fornecida e produzi resultados idênticos sem ORDER BY .

A boa notícia:todas elas estão corretas e produz o mesmo resultado - exceto para a consulta de Gordon que teve erros de sintaxe e a consulta de @wildplasser que falha quando o ano termina (fácil de corrigir).

Insira 108.000 linhas com datas aleatórias do século 20, que é semelhante a uma tabela de pessoas vivas (13 anos ou mais).
INSERT INTO  event (event_date)
SELECT '2000-1-1'::date - (random() * 36525)::int
FROM   generate_series (1, 108000);

Delete ~ 8% para criar algumas tuplas mortas e deixar a tabela mais "vida real".
DELETE FROM event WHERE random() < 0.08;
ANALYZE event;

Meu caso de teste tinha 99.289 linhas, 4.012 acertos.

C - Chamada

WITH anniversaries as (
   SELECT event_id, event_date
         ,(event_date + (n || ' years')::interval)::date anniversary
   FROM   event, generate_series(13, 113) n
   )
SELECT event_id, event_date -- count(*)   --
FROM   anniversaries
WHERE  anniversary BETWEEN current_date AND current_date + interval '14' day;

C1 - Ideia de Catcall reescrita


Além de pequenas otimizações, a principal diferença é adicionar somente a quantidade exata de anos date_trunc('year', age(current_date + 14, event_date)) para obter o aniversário deste ano, o que evita totalmente a necessidade de um CTE:
SELECT event_id, event_date
FROM   event
WHERE (event_date + date_trunc('year', age(current_date + 14, event_date)))::date
       BETWEEN current_date AND current_date + 14;

D - Daniel

SELECT *   -- count(*)   -- 
FROM   event
WHERE  extract(month FROM age(current_date + 14, event_date))  = 0
AND    extract(day   FROM age(current_date + 14, event_date)) <= 14;

E1 - Erwin 1


Consulte "1. Versão simples" acima.

E2 - Erwin 2


Consulte "2. Versão avançada" acima.

E3 - Erwin 3


Veja "3. Versão de magia negra" acima.

G - Gordon

SELECT * -- count(*)   
FROM  (SELECT *, to_char(event_date, 'MM-DD') AS mmdd FROM event) e
WHERE  to_date(to_char(now(), 'YYYY') || '-'
                 || (CASE WHEN mmdd = '02-29' THEN '02-28' ELSE mmdd END)
              ,'YYYY-MM-DD') BETWEEN date(now()) and date(now()) + 14;

H - a_horse_with_no_name

WITH upcoming as (
   SELECT event_id, event_date
         ,CASE 
            WHEN date_trunc('year', age(event_date)) = age(event_date)
                 THEN current_date
            ELSE cast(event_date + ((extract(year FROM age(event_date)) + 1)
                      * interval '1' year) AS date) 
          END AS next_event
   FROM event
   )
SELECT event_id, event_date
FROM   upcoming
WHERE  next_event - current_date  <= 14;

W - plasser selvagem

CREATE OR REPLACE FUNCTION this_years_birthday(_dut date) RETURNS date AS
$func$
DECLARE
    ret date;
BEGIN
    ret :=
    date_trunc( 'year' , current_timestamp)
        + (date_trunc( 'day' , _dut)
         - date_trunc( 'year' , _dut));
    RETURN ret;
END
$func$ LANGUAGE plpgsql;

Simplificado para retornar o mesmo que todos os outros:
SELECT *
FROM   event e
WHERE  this_years_birthday( e.event_date::date )
        BETWEEN current_date
        AND     current_date + '2weeks'::interval;

W1 - consulta do wildplasser reescrita


O acima sofre de vários detalhes ineficientes (além do escopo deste post já considerável). A versão reescrita é muito mais rápido:
CREATE OR REPLACE FUNCTION this_years_birthday(_dut INOUT date) AS
$func$
SELECT (date_trunc('year', now()) + ($1 - date_trunc('year', $1)))::date
$func$ LANGUAGE sql;

SELECT *
FROM   event e
WHERE  this_years_birthday(e.event_date)
        BETWEEN current_date
        AND    (current_date + 14);

Resultados do teste


Executei este teste com uma tabela temporária no PostgreSQL 9.1.7. Os resultados foram coletados com EXPLAIN ANALYZE , melhor de 5.

Resultados

Without index
C:  Total runtime: 76714.723 ms
C1: Total runtime:   307.987 ms  -- !
D:  Total runtime:   325.549 ms
E1: Total runtime:   253.671 ms  -- !
E2: Total runtime:   484.698 ms  -- min() & max() expensive without index
E3: Total runtime:   213.805 ms  -- !
G:  Total runtime:   984.788 ms
H:  Total runtime:   977.297 ms
W:  Total runtime:  2668.092 ms
W1: Total runtime:   596.849 ms  -- !

With index
E1: Total runtime:    37.939 ms  --!!
E2: Total runtime:    38.097 ms  --!!

With index on expression
E3: Total runtime:    11.837 ms  --!!

Todas as outras consultas têm o mesmo desempenho com ou sem índice porque usam não sargable expressões.

Conclusão


  • Até agora, a consulta de @Daniel foi a mais rápida.

  • A abordagem @wildplassers (reescrita) também funciona de maneira aceitável.

  • A versão do @Catcall é algo como a abordagem inversa da minha. O desempenho fica fora de controle rapidamente com tabelas maiores.
    A versão reescrita funciona muito bem, no entanto. A expressão que eu uso é algo como uma versão mais simples do this_years_birthday() do @wildplassser função.

  • Minha "versão simples" é mais rápida mesmo sem índice , porque precisa de menos cálculos.

  • Com o índice, a "versão avançada" é tão rápida quanto a "versão simples", porque min() e max() tornar-se muito barato com um índice. Ambos são substancialmente mais rápidos do que o resto que não pode usar o índice.

  • Minha "versão de magia negra" é mais rápida com ou sem índice . E é muito simples de chamar.


  • Com uma tabela da vida real, um índice tornará ainda maior diferença. Mais colunas tornam a tabela maior e a varredura sequencial mais cara, enquanto o tamanho do índice permanece o mesmo.