Als je niet om uitleg en details geeft, gebruik dan de "Black magic version" hieronder.
Alle vragen die tot nu toe in andere antwoorden zijn gepresenteerd, werken met voorwaarden die niet kunnen worden weergegeven - ze kunnen geen index gebruiken en moeten een uitdrukking berekenen voor elke afzonderlijke rij in de basistabel om overeenkomende rijen te vinden. Met kleine tafels maakt het niet veel uit. Is van belang (veel ) met grote tafels.
Gegeven de volgende eenvoudige tabel:
CREATE TABLE event (
event_id serial PRIMARY KEY
, event_date date
);
Zoekopdracht
Versie 1. en 2. hieronder kunnen een eenvoudige index van het formulier gebruiken:
CREATE INDEX event_event_date_idx ON event(event_date);
Maar alle volgende oplossingen zijn nog sneller zonder index .
1. Eenvoudige versie
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);
Subquery x
berekent alle mogelijke datums over een bepaald bereik van jaren vanaf een CROSS JOIN
van twee generate_series()
belt. De selectie wordt gedaan met de laatste eenvoudige join.
2. Geavanceerde versie
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);
Het bereik van jaren wordt automatisch uit de tabel afgeleid - waardoor het aantal gegenereerde jaren wordt geminimaliseerd.
U zou ga nog een stap verder en distilleer een lijst van bestaande jaren als er hiaten zijn.
Effectiviteit is mede afhankelijk van de verdeling van data. Enkele jaren met elk veel rijen maken deze oplossing nuttiger. Vele jaren met weinig rijen maken het minder bruikbaar.
Eenvoudige SQL Fiddle om mee te spelen.
3. Zwarte magie versie
2016 bijgewerkt om een "gegenereerde kolom" te verwijderen, die H.O.T. updates; eenvoudigere en snellere functie.
2018 bijgewerkt om MMDD te berekenen met IMMUTABLE
expressies om functie-inlining toe te staan.
Maak een eenvoudige SQL-functie om een integer
te berekenen uit het patroon 'MMDD'
:
CREATE FUNCTION f_mmdd(date) RETURNS int LANGUAGE sql IMMUTABLE AS
'SELECT (EXTRACT(month FROM $1) * 100 + EXTRACT(day FROM $1))::int';
Ik had to_char(time, 'MMDD')
eerst, maar schakelde over naar de bovenstaande uitdrukking die het snelst bleek in nieuwe tests op Postgres 9.6 en 10:
db<>viool hier
Het staat functie-inlining toe omdat EXTRACT (xyz FROM date)
is geïmplementeerd met de IMMUTABLE
functie date_part(text, date)
intern. En het moet IMMUTABLE
zijn om het gebruik ervan in de volgende essentiële expressie-index met meerdere kolommen toe te staan:
CREATE INDEX event_mmdd_event_date_idx ON event(f_mmdd(event_date), event_date);
Meerdere kolommen om een aantal redenen:
Kan helpen met ORDER BY
of met een keuze uit bepaalde jaren. Lees hier. Tegen bijna geen extra kosten voor de index. Een date
past in de 4 bytes die anders verloren zouden gaan aan opvulling als gevolg van gegevensuitlijning. Lees hier.
Ook, aangezien beide indexkolommen verwijzen naar dezelfde tabelkolom, geen nadeel met betrekking tot H.O.T. updates. Lees hier.
Eén PL/pgSQL-tabelfunctie om ze allemaal te regeren
Ga naar een van de twee vragen over de jaarwisseling:
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;
Bel met standaardwaarden:14 dagen vanaf "vandaag":
SELECT * FROM f_anniversary();
Bel voor 7 dagen vanaf '2014-08-23':
SELECT * FROM f_anniversary(date '2014-08-23', 7);
SQL Fiddle vergelijken EXPLAIN ANALYZE
.
29 februari
Bij jubilea of "verjaardagen" moet u definiëren hoe u in schrikkeljaren met het speciale geval "29 februari" omgaat.
Bij het testen op datumbereiken, Feb 29
wordt meestal automatisch opgenomen, zelfs als het huidige jaar geen schrikkeljaar is . Het aantal dagen wordt met terugwerkende kracht met 1 verlengd wanneer het deze dag omvat.
Aan de andere kant, als het huidige jaar een schrikkeljaar is en u wilt 15 dagen zoeken, krijgt u mogelijk resultaten voor 14 dagen in schrikkeljaren als uw gegevens uit niet-schrikkeljaren komen.
Stel, Bob is geboren op 29 februari:
Mijn vraag 1. en 2. bevatten 29 februari alleen in schrikkeljaren. Bob is slechts om de ~ 4 jaar jarig.
Mijn vraag 3. omvat 29 februari in het assortiment. Bob is elk jaar jarig.
Er is geen magische oplossing. Je moet voor elk geval definiëren wat je wilt.
Test
Om mijn punt te onderbouwen heb ik een uitgebreide test gedaan met alle gepresenteerde oplossingen. Ik heb elk van de vragen aangepast aan de gegeven tabel en identieke resultaten opgeleverd zonder ORDER BY
.
Het goede nieuws:ze zijn allemaal juist en levert hetzelfde resultaat op - behalve de zoekopdracht van Gordon die syntaxisfouten bevatte, en de zoekopdracht van @wildplasser die mislukt als het jaar voorbij is (eenvoudig op te lossen).
Voeg 108000 rijen in met willekeurige datums uit de 20e eeuw, wat vergelijkbaar is met een tabel met levende mensen (13 jaar of ouder).
INSERT INTO event (event_date)
SELECT '2000-1-1'::date - (random() * 36525)::int
FROM generate_series (1, 108000);
Verwijder ~ 8% om wat dode tupels te maken en de tafel meer "echt" te maken.
DELETE FROM event WHERE random() < 0.08;
ANALYZE event;
Mijn testcase had 99289 rijen, 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 - Idee van Catcall herschreven
Afgezien van kleine optimalisaties, is het grootste verschil het toevoegen van alleen het exacte aantal jaren date_trunc('year', age(current_date + 14, event_date))
om de verjaardag van dit jaar te krijgen, waardoor een CTE helemaal niet nodig is:
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 - Daniël
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
Zie "1. Eenvoudige versie" hierboven.
E2 - Erwin 2
Zie "2. Geavanceerde versie" hierboven.
E3 - Erwin 3
Zie "3. Zwarte magie-versie" hierboven.
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 - wildplasser
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;
Vereenvoudigd om hetzelfde terug te geven als alle andere:
SELECT *
FROM event e
WHERE this_years_birthday( e.event_date::date )
BETWEEN current_date
AND current_date + '2weeks'::interval;
W1 - de vraag van wildplasser herschreven
Het bovenstaande lijdt aan een aantal inefficiënte details (buiten het bestek van deze toch al omvangrijke post). De herschreven versie is veel sneller:
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);
Testresultaten
Ik heb deze test uitgevoerd met een tijdelijke tabel op PostgreSQL 9.1.7. De resultaten zijn verzameld met EXPLAIN ANALYZE
, beste van 5.
Resultaten
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 --!!
Alle andere zoekopdrachten presteren hetzelfde met of zonder index omdat ze niet-sargable . gebruiken uitdrukkingen.
Conclusie
-
Tot nu toe was de vraag van @Daniel de snelste.
-
@wildplassers (herschreven) aanpak werkt ook acceptabel.
-
@Catcall's versie is zoiets als de omgekeerde benadering van mij. Prestaties lopen snel uit de hand met grotere tabellen.
De herschreven versie presteert echter redelijk goed. De uitdrukking die ik gebruik is zoiets als een eenvoudigere versie van @wildplassser'sthis_years_birthday()
functie. -
Mijn "eenvoudige versie" is sneller zelfs zonder index , omdat er minder berekeningen nodig zijn.
-
Met index is de "geavanceerde versie" ongeveer net zo snel als de "eenvoudige versie", omdat
min()
enmax()
word heel goedkoop met een index. Beide zijn aanzienlijk sneller dan de rest die de index niet kunnen gebruiken. -
Mijn "zwarte magie-versie" is het snelst met of zonder index . En het is erg eenvoudig te bellen.
-
Met een real life tabel een index maakt nog groter verschil. Meer kolommen maken de tabel groter en sequentiële scan duurder, terwijl de indexgrootte hetzelfde blijft.