sql >> Database >  >> RDS >> PostgreSQL

Hoe doe je dating wiskunde die het jaar negeert?

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's this_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() en max() 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.



  1. SET NULL:geef een tekenreeks op die moet worden geretourneerd wanneer een null-waarde voorkomt in SQLcl / SQL*Plus

  2. Hoe orakelindexen kiezen en optimaliseren?

  3. INSERT INTO vs SELECT INTO

  4. Nieuwe AMD-processorfamilies zijn goed te vergelijken met nieuwe Intel-processors