sql >> Database teknologi >  >> RDS >> PostgreSQL

Hvordan laver du date-matematik, der ignorerer året?

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



  1. Fejl ved indlæsning af MySQLdb-modul 'Har du installeret mysqlclient eller MySQL-python?'

  2. ResultSet#getDate() semantik

  3. Sådan opretter du en ikke-nul kolonne i en visning

  4. Opgrader MySQL til MariaDB 10 (del 1 – Installer MariaDB 5.5)