PostgreSQL
 sql >> Datenbank >  >> RDS >> PostgreSQL

Wie macht man Datumsmathematik, die das Jahr ignoriert?

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 @wildplasssers this_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() und max() 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.