Wenn Ihnen Erklärungen und Details nicht wichtig sind, verwenden Sie die "schwarzmagische Version" unten.
Alle Abfragen, die bisher in anderen Antworten präsentiert wurden, arbeiten mit Bedingungen, die nicht sargierbar sind - Sie können keinen Index verwenden und müssen für jede einzelne Zeile in der Basistabelle einen Ausdruck berechnen, um passende Zeilen zu finden. Bei kleinen Tischen ist das egal. Wichtig (sehr ) mit großen Tabellen.
Angesichts der folgenden einfachen Tabelle:
CREATE TABLE event (
event_id serial PRIMARY KEY
, event_date date
);
Abfrage
Version 1. und 2. unten können einen einfachen Index der Form verwenden:
CREATE INDEX event_event_date_idx ON event(event_date);
Aber alle folgenden Lösungen sind noch schneller ohne Index .
1. Einfache 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);
Unterabfrage x
berechnet alle möglichen Daten über einen gegebenen Bereich von Jahren aus einem CROSS JOIN
von zwei generate_series()
Anrufe. Die Auswahl erfolgt mit dem abschließenden einfachen Join.
2. Fortgeschrittene 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);
Der Jahresbereich wird automatisch aus der Tabelle abgeleitet - wodurch die generierten Jahre minimiert werden.
Sie könnten gehen Sie einen Schritt weiter und destillieren Sie eine Liste bestehender Jahre, falls es Lücken gibt.
Die Wirksamkeit hängt auch von der Verteilung der Daten ab. Wenige Jahre mit jeweils vielen Zeilen machen diese Lösung nützlicher. Viele Jahre mit jeweils wenigen Zeilen machen es weniger nützlich.
Einfache SQL-Fiddle zum Spielen.
3. Schwarzmagische Version
Aktualisiert 2016, um eine „generierte Spalte“ zu entfernen, die H.O.T. blockieren würde. Aktualisierung; einfachere und schnellere Funktion.
2018 aktualisiert, um MMDD mit IMMUTABLE
zu berechnen Ausdrücke, um das Inlining von Funktionen zu ermöglichen.
Erstellen Sie eine einfache SQL-Funktion, um eine integer
zu berechnen aus dem Muster 'MMDD'
:
CREATE FUNCTION f_mmdd(date) RETURNS int LANGUAGE sql IMMUTABLE AS
'SELECT (EXTRACT(month FROM $1) * 100 + EXTRACT(day FROM $1))::int';
Ich hatte to_char(time, 'MMDD')
zunächst, wechselte aber zu obigem Ausdruck, der sich in neuen Tests auf Postgres 9.6 und 10 als am schnellsten erwies:
db<>hier fummeln
Es erlaubt Funktions-Inlining, weil EXTRACT (xyz FROM date)
wird mit dem IMMUTABLE
implementiert Funktion date_part(text, date)
im Inneren. Und es muss IMMUTABLE
sein um seine Verwendung im folgenden wesentlichen mehrspaltigen Ausdrucksindex zu ermöglichen:
CREATE INDEX event_mmdd_event_date_idx ON event(f_mmdd(event_date), event_date);
Mehrspaltig aus mehreren Gründen:
Kann bei ORDER BY
helfen oder mit Auswahl aus vorgegebenen Jahren. Lies hier. Fast ohne zusätzliche Kosten für den Index. Ein date
passt in die 4 Bytes, die ansonsten durch das Auffüllen aufgrund der Datenausrichtung verloren gehen würden. Lesen Sie hier.
Außerdem, da beide Indexspalten auf dieselbe Tabellenspalte verweisen, kein Nachteil bezüglich H.O.T. Aktualisierung. Lesen Sie hier.
Eine PL/pgSQL-Tabellenfunktion, um sie alle zu beherrschen
Verzweigen Sie zu einer von zwei Abfragen, um den Jahreswechsel abzudecken:
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;
Anruf Standardwerte verwenden:14 Tage ab "heute":
SELECT * FROM f_anniversary();
Anruf für 7 Tage ab dem 23.08.2014:
SELECT * FROM f_anniversary(date '2014-08-23', 7);
SQL-Geige Vergleich von EXPLAIN ANALYZE
.
29. Februar
Bei Jubiläen oder "Geburtstagen" müssen Sie definieren, wie mit dem Sonderfall "29. Februar" in Schaltjahren umgegangen werden soll.
Beim Testen auf Datumsbereiche Feb 29
wird in der Regel automatisch übernommen, auch wenn das aktuelle Jahr kein Schaltjahr ist . Der Bereich der Tage wird rückwirkend um 1 erweitert, wenn er diesen Tag abdeckt.
Wenn andererseits das aktuelle Jahr ein Schaltjahr ist und Sie nach 15 Tagen suchen möchten, erhalten Sie möglicherweise Ergebnisse für 14 Tage in Schaltjahren, wenn Ihre Daten aus Nicht-Schaltjahren stammen.
Angenommen, Bob wird am 29. Februar geboren:
Meine Abfrage 1. und 2. beinhalten den 29. Februar nur in Schaltjahren. Bob hat nur alle ~4 Jahre Geburtstag.
Meine Abfrage 3. enthält den 29. Februar im Bereich. Bob hat jedes Jahr Geburtstag.
Es gibt keine magische Lösung. Sie müssen für jeden Fall definieren, was Sie wollen.
Test
Um meinen Standpunkt zu untermauern, habe ich einen ausführlichen Test mit allen vorgestellten Lösungen durchgeführt. Ich habe jede der Abfragen an die gegebene Tabelle angepasst und um identische Ergebnisse ohne ORDER BY
zu erhalten .
Die gute Nachricht:Alle sind richtig und liefern das gleiche Ergebnis - mit Ausnahme von Gordons Abfrage, die Syntaxfehler aufwies, und @wildplassers Abfrage, die fehlschlägt, wenn das Jahr umläuft (einfach zu beheben).
Fügen Sie 108000 Zeilen mit zufälligen Daten aus dem 20. Jahrhundert ein, was einer Tabelle mit lebenden Personen (13 oder älter) ähnelt.
INSERT INTO event (event_date)
SELECT '2000-1-1'::date - (random() * 36525)::int
FROM generate_series (1, 108000);
Löschen Sie ~ 8 %, um einige tote Tupel zu erstellen und die Tabelle "echter" zu machen.
DELETE FROM event WHERE random() < 0.08;
ANALYZE event;
Mein Testfall hatte 99289 Zeilen, 4012 Treffer.
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 Idee umgeschrieben
Abgesehen von geringfügigen Optimierungen besteht der Hauptunterschied darin, dass nur die genaue Anzahl von Jahren hinzugefügt wird date_trunc('year', age(current_date + 14, event_date))
um das diesjährige Jubiläum zu erhalten, wodurch die Notwendigkeit eines CTE vollständig vermieden wird:
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
Siehe "1. Einfache Version" oben.
E2 - Erwin 2
Siehe "2. Erweiterte Version" oben.
E3 - Erwin 3
Siehe "3. Schwarzmagische Version" oben.
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 - ein_Pferd_ohne_Namen
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 - Wildpässer
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;
Vereinfacht, um dasselbe wie alle anderen zurückzugeben:
SELECT *
FROM event e
WHERE this_years_birthday( e.event_date::date )
BETWEEN current_date
AND current_date + '2weeks'::interval;
W1 - Abfrage von Wildplasser umgeschrieben
Das Obige leidet unter einer Reihe ineffizienter Details (über den Rahmen dieses bereits beträchtlichen Beitrags hinaus). Die umgeschriebene Version ist viel schneller:
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);
Testergebnisse
Ich habe diesen Test mit einer temporären Tabelle auf PostgreSQL 9.1.7 durchgeführt. Die Ergebnisse wurden mit EXPLAIN ANALYZE
gesammelt , Best of 5.
Ergebnisse
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 anderen Abfragen verhalten sich mit oder ohne Index genauso, da sie non-sargable verwenden Ausdrücke.
Schlussfolgerung
-
Bisher war die Abfrage von @Daniel die schnellste.
-
Der (umgeschriebene) Ansatz von @wildplasser funktioniert ebenfalls akzeptabel.
-
Die Version von @Catcall ist so etwas wie der umgekehrte Ansatz von mir. Bei größeren Tabellen gerät die Performance schnell außer Kontrolle.
Die umgeschriebene Version performt aber ziemlich gut. Der Ausdruck, den ich verwende, ist so etwas wie eine einfachere Version von @wildplasssersthis_years_birthday()
Funktion. -
Meine "einfache Version" ist auch ohne Index schneller , weil es weniger Berechnungen benötigt.
-
Mit Index ist die "erweiterte Version" etwa so schnell wie die "einfache Version", weil
min()
undmax()
sehr werden günstig mit Index. Beide sind wesentlich schneller als die anderen, die den Index nicht verwenden können. -
Meine "schwarzmagische Version" ist mit oder ohne Index am schnellsten . Und es ist sehr einfach anzurufen.
-
Mit einem realen Tisch ein Index wird noch größer machen Unterschied. Mehr Spalten machen die Tabelle größer und sequentielles Scannen teurer, während die Indexgröße gleich bleibt.