sql >> Databasteknik >  >> RDS >> PostgreSQL

Hur gör man dejtmatematik som ignorerar årtalet?

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



  1. Bästa sättet att förkorta UTF8-strängen baserat på bytelängd

  2. Returnera slutet av månaden i SQLite

  3. MySQL CEILING() Funktion – Runda uppåt till närmaste heltal

  4. Hantera MySQL med phpMyAdmin på Ubuntu 9.10 (Karmic)