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 dothis_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", porquemin()
emax()
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.