Hvis du er ligeglad med forklaringer og detaljer, så brug "Sort magisk version" nedenfor.
Alle forespørgsler præsenteret i andre svar hidtil fungerer under forhold, der ikke kan sarges - de kan ikke bruge et indeks og skal beregne et udtryk for hver enkelt række i basistabellen for at finde matchende rækker. Det betyder ikke meget med små borde. Betyder noget (meget ) med store borde.
Givet følgende enkle tabel:
CREATE TABLE event (
event_id serial PRIMARY KEY
, event_date date
);
Forespørgsel
Version 1. og 2. nedenfor kan bruge et simpelt indeks på formen:
CREATE INDEX event_event_date_idx ON event(event_date);
Men alle de følgende løsninger er endnu hurtigere uden indeks .
1. Simpel 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);
Underforespørgsel x
beregner alle mulige datoer over en given årrække fra en CROSS JOIN
af to generate_series()
opkald. Udvælgelsen foretages med den sidste simple join.
2. Avanceret 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);
Årstal udledes automatisk fra tabellen - derved minimeres genererede år.
Du kunne gå et skridt videre og destillerer en liste over eksisterende år, hvis der er huller.
Effektiviteten afhænger af fordelingen af datoer. Få år med mange rækker hver gør denne løsning mere anvendelig. Mange år med få rækker hver gør det mindre nyttigt.
Simpel SQL Fiddle at lege med.
3. Sort magisk version
Opdateret 2016 for at fjerne en "genereret kolonne", som ville blokere H.O.T. opdateringer; enklere og hurtigere funktion.
Opdateret 2018 for at beregne MMDD med IMMUTABLE
udtryk for at tillade funktion inlining.
Opret en simpel SQL-funktion til at beregne et integer
fra mønsteret 'MMDD'
:
CREATE FUNCTION f_mmdd(date) RETURNS int LANGUAGE sql IMMUTABLE AS
'SELECT (EXTRACT(month FROM $1) * 100 + EXTRACT(day FROM $1))::int';
Jeg havde to_char(time, 'MMDD')
først, men skiftede til ovenstående udtryk, som viste sig hurtigst i nye test på Postgres 9.6 og 10:
db<>spil her
Det tillader funktion inlining, fordi EXTRACT (xyz FROM date)
er implementeret med IMMUTABLE
funktion date_part(text, date)
internt. Og det skal være IMMUTABLE
for at tillade dets brug i følgende essentielle multikolonne-ekspressionsindeks:
CREATE INDEX event_mmdd_event_date_idx ON event(f_mmdd(event_date), event_date);
Flerkolonne af en række årsager:
Kan hjælpe med ORDER BY
eller med at vælge fra givne år. Læs her. Næsten uden ekstra omkostninger for indekset. En date
passer ind i de 4 bytes, der ellers ville gå tabt til polstring på grund af datajustering. Læs her.
Da begge indekskolonner refererer til den samme tabelkolonne, er der ingen ulempe med hensyn til H.O.T. opdateringer. Læs her.
Én PL/pgSQL-tabelfunktion til at styre dem alle
Fork til en af to forespørgsler for at dække å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 bruger standardindstillinger:14 dage fra "i dag":
SELECT * FROM f_anniversary();
Ring i 7 dage fra '2014-08-23':
SELECT * FROM f_anniversary(date '2014-08-23', 7);
SQL Fiddle sammenligner EXPLAIN ANALYZE
.
29. februar
Når du har at gøre med mærkedage eller "fødselsdage", skal du definere, hvordan du skal håndtere det særlige tilfælde "29. februar" i skudår.
Når du tester for datointervaller, Feb 29
medtages normalt automatisk, selvom det aktuelle år ikke er et skudår . Udvalget af dage forlænges med 1 med tilbagevirkende kraft, når det dækker denne dag.
På den anden side, hvis det aktuelle år er et skudår, og du vil kigge efter 15 dage, kan du ende med at få resultater for 14 dage i skudår, hvis dine data er fra ikke-skudår.
Lad os sige, Bob er født den 29. februar:
Min forespørgsel 1. og 2. omfatter kun den 29. februar i skudår. Bob har kun fødselsdag hvert ~ 4. år.
Min forespørgsel 3. omfatter den 29. februar i udvalget. Bob har fødselsdag hvert år.
Der er ingen magisk løsning. Du skal definere, hvad du ønsker for hver sag.
Test
For at underbygge min pointe kørte jeg en omfattende test med alle de præsenterede løsninger. Jeg tilpassede hver af forespørgslerne til den givne tabel og for at give identiske resultater uden ORDER BY
.
Den gode nyhed:alle er korrekte og giver det samme resultat - bortset fra Gordons forespørgsel, der havde syntaksfejl, og @wildplassers forespørgsel, der mislykkes, når året løber rundt (let at rette).
Indsæt 108000 rækker med tilfældige datoer fra det 20. århundrede, som ligner en tabel med levende mennesker (13 eller ældre).
INSERT INTO event (event_date)
SELECT '2000-1-1'::date - (random() * 36525)::int
FROM generate_series (1, 108000);
Slet ~ 8 % for at skabe nogle døde tupler og gøre bordet mere "virkeligt".
DELETE FROM event WHERE random() < 0.08;
ANALYZE event;
Min testcase havde 99289 rækker, 4012 hits.
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é omskrevet
Bortset fra mindre optimeringer er den største forskel at tilføje kun det nøjagtige antal år date_trunc('year', age(current_date + 14, event_date))
for at få dette års jubilæum, hvilket helt undgår behovet for 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" ovenfor.
E2 - Erwin 2
Se "2. Avanceret version" ovenfor.
E3 - Erwin 3
Se "3. Sort magisk version" ovenfor.
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 - en_hest_med_intet_navn
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 - vilde steder
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;
Forenklet for at returnere det samme som alle de andre:
SELECT *
FROM event e
WHERE this_years_birthday( e.event_date::date )
BETWEEN current_date
AND current_date + '2weeks'::interval;
W1 - wildplassers forespørgsel omskrevet
Ovenstående lider under en række ineffektive detaljer (ud over omfanget af dette allerede betydelige indlæg). Den omskrevne version er meget hurtigere:
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);
Testresultater
Jeg kørte denne test med en midlertidig tabel på PostgreSQL 9.1.7. Resultaterne blev indsamlet med EXPLAIN ANALYZE
, bedst af 5.
Resultater
Uden indeks C:Samlet kørselstid:76714.723 msC1:Samlet kørselstid:307.987 ms -- !D:Samlet kørselstid:325.549 msE1:Samlet kørselstid:253.671 ms -- ! E2:Samlet kørselstid:484.698 ms -- min() &max() dyrt uden indeksE3:Samlet kørselstid:213.805 ms -- ! G:Samlet kørselstid:984.788 msH:Samlet kørselstid:977.297 msW:Samlet kørselstid:2668.092 msW1:Samlet kørselstid:596.849 ms -- !Med indeks E1:Samlet køretid:37.939 ms --!! E2:Samlet køretid:38.097 ms --!! Med indeks på udtryk E3:Samlet køretid:11.837 ms --!!
Alle andre forespørgsler udfører det samme med eller uden indeks, fordi de bruger ikke-sargerbar udtryk.
Konklusion
-
Indtil videre var @Daniels forespørgsel den hurtigste.
-
@wildplassers (omskrevet) tilgang fungerer også acceptabelt.
-
@Catcalls version ligner min omvendte tilgang. Ydeevnen kommer hurtigt ud af hånden med større borde.
Den omskrevne version klarer sig dog ret godt. Det udtryk, jeg bruger, er noget i retning af en enklere version af @wildplasssersthis_years_birthday()
funktion. -
Min "simple version" er hurtigere selv uden indeks , fordi det kræver færre beregninger.
-
Med indeks er den "avancerede version" omtrent lige så hurtig som den "simple version", fordi
min()
ogmax()
blive meget billigt med et indeks. Begge er væsentligt hurtigere end resten, som ikke kan bruge indekset. -
Min "sort magiske version" er hurtigst med eller uden indeks . Og det er meget nemt at ringe til.
-
Med en virkelige tabel et indeks vil gøre endnu større forskel. Flere kolonner gør tabellen større og sekventiel scanning dyrere, mens indeksstørrelsen forbliver den samme.