Om du inte bryr dig om förklaringar och detaljer, använd "Black magic version" nedan.
Alla frågor som presenterats i andra svar hittills fungerar med villkor som är inte sargable - de kan inte använda ett index och måste beräkna ett uttryck för varje enskild rad i bastabellen för att hitta matchande rader. Spelar inte så stor roll med små bord. Spelar roll (mycket ) med stora bord.
Med tanke på följande enkla tabell:
CREATE TABLE event (
event_id serial PRIMARY KEY
, event_date date
);
Fråga
Version 1. och 2. nedan kan använda ett enkelt index av formen:
CREATE INDEX event_event_date_idx ON event(event_date);
Men alla följande lösningar är ännu snabbare utan index .
1. Enkel version
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);
Underfråga x
beräknar alla möjliga datum över ett givet årtal från en CROSS JOIN
av två generate_series()
samtal. Valet görs med den sista enkla sammanfogningen.
2. Avancerad version
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);
Årsintervall härleds från tabellen automatiskt - och minimerar därmed genererade år.
Du kan gå ett steg längre och destillera en lista över befintliga år om det finns luckor.
Effektiviteten beror på fördelningen av datum. Några år med många rader vardera gör denna lösning mer användbar. Många år med få rader vardera gör det mindre användbart.
Enkel SQL-fiol att leka med.
3. Svart magisk version
Uppdaterad 2016 för att ta bort en "genererad kolumn", som skulle blockera H.O.T. uppdateringar; enklare och snabbare funktion.
Uppdaterad 2018 för att beräkna MMDD med IMMUTABLE
uttryck för att tillåta inlining av funktioner.
Skapa en enkel SQL-funktion för att beräkna ett integer
från mönstret 'MMDD'
:
CREATE FUNCTION f_mmdd(date) RETURNS int LANGUAGE sql IMMUTABLE AS
'SELECT (EXTRACT(month FROM $1) * 100 + EXTRACT(day FROM $1))::int';
Jag hade to_char(time, 'MMDD')
först, men bytte till uttrycket ovan som visade sig snabbast i nya tester på Postgres 9.6 och 10:
db<>spela här
Det tillåter funktion inlining eftersom EXTRACT (xyz FROM date)
implementeras med IMMUTABLE
funktion date_part(text, date)
internt. Och det måste vara IMMUTABLE
för att tillåta dess användning i följande viktiga uttrycksindex med flera kolumner:
CREATE INDEX event_mmdd_event_date_idx ON event(f_mmdd(event_date), event_date);
Flerkolumn av ett antal anledningar:
Kan hjälpa till med ORDER BY
eller med att välja från givna år. Läs här. Nästan utan extra kostnad för index. Ett date
passar in i de 4 byte som annars skulle gå förlorade till utfyllnad på grund av datajustering. Läs här.
Också, eftersom båda indexkolumnerna refererar till samma tabellkolumn, finns det ingen nackdel med avseende på H.O.T. uppdateringar. Läs här.
En PL/pgSQL-tabellfunktion för att styra dem alla
Dela till en av två frågor för att täcka årsskiftet:
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;
Ring använder standardvärden:14 dagar från och med "idag":
SELECT * FROM f_anniversary();
Ring i 7 dagar från och med '2014-08-23':
SELECT * FROM f_anniversary(date '2014-08-23', 7);
SQL Fiddle jämför EXPLAIN ANALYZE
.
29 februari
När du har att göra med årsdagar eller "födelsedagar" måste du definiera hur du ska hantera specialfallet "29 februari" under skottår.
När du testar för datumintervall, Feb 29
inkluderas vanligtvis automatiskt, även om innevarande år inte är ett skottår . Dagsintervallet utökas med 1 retroaktivt när det täcker denna dag.
Om det aktuella året å andra sidan är ett skottår och du vill leta efter 15 dagar, kan du få resultat för 14 dagar dagar under skottår om din data är från icke-skottår.
Säg, Bob är född den 29 februari:
Min fråga 1. och 2. inkluderar den 29 februari endast under skottår. Bob fyller bara år vart fjärde år.
Min fråga 3 inkluderar den 29 februari i sortimentet. Bob fyller år varje år.
Det finns ingen magisk lösning. Du måste definiera vad du vill för varje fall.
Testa
För att underbygga min poäng körde jag ett omfattande test med alla presenterade lösningar. Jag anpassade var och en av frågorna till den givna tabellen och för att ge identiska resultat utan ORDER BY
.
De goda nyheterna:alla är korrekta och ger samma resultat - förutom Gordons fråga som hade syntaxfel och @wildplassers fråga som misslyckas när året går runt (lätt att fixa).
Infoga 108000 rader med slumpmässiga datum från 1900-talet, vilket liknar en tabell över levande människor (13 eller äldre).
INSERT INTO event (event_date)
SELECT '2000-1-1'::date - (random() * 36525)::int
FROM generate_series (1, 108000);
Ta bort ~ 8 % för att skapa några döda tuplar och göra bordet mer "riktigt".
DELETE FROM event WHERE random() < 0.08;
ANALYZE event;
Mitt testfall hade 99289 rader, 4012 träffar.
C - Catcall
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 - Catcalls idé omskriven
Bortsett från mindre optimeringar är den stora skillnaden att lägga till bara det exakta antalet år date_trunc('year', age(current_date + 14, event_date))
för att få årets jubileum, vilket helt och hållet undviker behovet av en 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
Se "1. Enkel version" ovan.
E2 - Erwin 2
Se "2. Avancerad version" ovan.
E3 - Erwin 3
Se "3. Black magic version" ovan.
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 - vilda platser
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;
Förenklat för att returnera samma som alla andra:
SELECT *
FROM event e
WHERE this_years_birthday( e.event_date::date )
BETWEEN current_date
AND current_date + '2weeks'::interval;
W1 - wildplassers fråga har skrivits om
Ovanstående lider av ett antal ineffektiva detaljer (utöver omfattningen av detta redan betydande inlägg). Den omskrivna versionen är mycket snabbare:
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);
Testresultat
Jag körde det här testet med en tillfällig tabell på PostgreSQL 9.1.7. Resultaten samlades in med EXPLAIN ANALYZE
, bäst av 5.
Resultat
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 --!!
Alla andra frågor fungerar likadant med eller utan index eftersom de använder icke-sargable uttryck.
Slutsats
-
Hittills var @Daniels fråga den snabbaste.
-
@wildplassers (omskrivet) tillvägagångssätt fungerar också acceptabelt.
-
@Catcalls version är ungefär mitt omvända tillvägagångssätt. Prestanda blir snabbt ur hand med större bord.
Den omskrivna versionen presterar dock ganska bra. Uttrycket jag använder är ungefär en enklare version av @wildplasssersthis_years_birthday()
funktion. -
Min "enkla version" är snabbare även utan index , eftersom det kräver färre beräkningar.
-
Med index är den "avancerade versionen" ungefär lika snabb som den "enkla versionen", eftersom
min()
ochmax()
bli mycket billigt med index. Båda är betydligt snabbare än resten som inte kan använda indexet. -
Min "svarta magiska version" är snabbast med eller utan index . Och det är mycket enkelt att ringa.
-
Med en verklig tabell ett index kommer att göra ännu större skillnad. Fler kolumner gör tabellen större och sekventiell skanning dyrare, samtidigt som indexstorleken förblir densamma.