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

Come si fa a datare la matematica che ignora l'anno?

Se non ti interessano spiegazioni e dettagli, usa la "versione Black magic" sotto.

Tutte le domande presentate finora in altre risposte operano con condizioni non selezionabili - non possono utilizzare un indice e devono calcolare un'espressione per ogni singola riga nella tabella di base per trovare le righe corrispondenti. Non importa molto con i tavolini. Conta (molto ) con grandi tavoli.

Data la seguente semplice tabella:

CREATE TABLE event (
  event_id   serial PRIMARY KEY
, event_date date
);

Interrogazione

Le versioni 1. e 2. seguenti possono utilizzare un semplice indice del modulo:

CREATE INDEX event_event_date_idx ON event(event_date);

Ma tutte le seguenti soluzioni sono ancora più veloci senza indice .

1. Versione semplice

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);

Sottoquery x calcola tutte le date possibili in un determinato intervallo di anni da un CROSS JOIN di due generate_series() chiamate. La selezione avviene con il join semplice finale.

2. Versione avanzata

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);

L'intervallo di anni viene dedotto automaticamente dalla tabella, riducendo così al minimo gli anni generati.
Potresti potere fai un passo avanti e distilla un elenco di anni esistenti se ci sono lacune.

L'efficacia dipende dalla distribuzione delle date. Pochi anni con molte righe ciascuno rendono questa soluzione più utile. Molti anni con poche righe ciascuno lo rendono meno utile.

Semplice Fiddle SQL con cui giocare.

3. Versione magia nera

Aggiornato 2016 per rimuovere una "colonna generata", che bloccherebbe H.O.T. aggiornamenti; funzione più semplice e veloce.
Aggiornato 2018 per calcolare MMDD con IMMUTABLE espressioni per consentire l'integrazione delle funzioni.

Crea una semplice funzione SQL per calcolare un integer dal modello 'MMDD' :

CREATE FUNCTION f_mmdd(date) RETURNS int LANGUAGE sql IMMUTABLE AS
'SELECT (EXTRACT(month FROM $1) * 100 + EXTRACT(day FROM $1))::int';

Avevo to_char(time, 'MMDD') all'inizio, ma è passato all'espressione sopra che si è rivelata più veloce nei nuovi test su Postgres 9.6 e 10:

db<>gioca qui

Consente l'integrazione delle funzioni perché EXTRACT (xyz FROM date) è implementato con IMMUTABLE funzione date_part(text, date) internamente. E deve essere IMMUTABLE per consentirne l'uso nel seguente indice di espressione multicolonna essenziale:

CREATE INDEX event_mmdd_event_date_idx ON event(f_mmdd(event_date), event_date);

Multicolonna per una serie di motivi:
Può aiutare con ORDER BY o con la selezione da determinati anni. Leggi qui. Quasi senza costi aggiuntivi per l'indice. Una date si inserisce nei 4 byte che altrimenti andrebbero persi per il riempimento a causa dell'allineamento dei dati. Leggi qui.
Inoltre, poiché entrambe le colonne dell'indice fanno riferimento alla stessa colonna della tabella, nessun inconveniente per quanto riguarda H.O.T. aggiornamenti. Leggi qui.

Una funzione tabella PL/pgSQL per controllarli tutti

Passa a una delle due query per coprire il volgere dell'anno:

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;

Chiama utilizzando i valori predefiniti:14 giorni a partire da "oggi":

SELECT * FROM f_anniversary();

Chiama per 7 giorni a partire dal "23-08-2014":

SELECT * FROM f_anniversary(date '2014-08-23', 7);

SQL Fiddle confrontando EXPLAIN ANALYZE .

29 febbraio

Quando si tratta di anniversari o "compleanni", è necessario definire come affrontare il caso speciale "29 febbraio" negli anni bisestili.

Durante il test per intervalli di date, Feb 29 di solito viene incluso automaticamente, anche se l'anno in corso è non bisestile . L'intervallo di giorni viene esteso di 1 in modo retroattivo quando copre questo giorno.
D'altra parte, se l'anno in corso è bisestile e desideri cercare 15 giorni, potresti ottenere risultati per 14 giorni negli anni bisestili se i tuoi dati provengono da anni non bisestili.

Supponiamo che Bob sia nato il 29 febbraio:
Le mie query 1. e 2. includono il 29 febbraio solo negli anni bisestili. Bob compie gli anni solo ogni ~ 4 anni.
La mia query 3. include il 29 febbraio nell'intervallo. Bob compie gli anni ogni anno.

Non esiste una soluzione magica. Devi definire cosa vuoi per ogni caso.

Test

Per corroborare il mio punto ho eseguito un test approfondito con tutte le soluzioni presentate. Ho adattato ciascuna delle query alla tabella data e per ottenere risultati identici senza ORDER BY .

La buona notizia:sono tutti corretti e produce lo stesso risultato, ad eccezione della query di Gordon che presentava errori di sintassi e della query di @wildplasser che fallisce quando l'anno finisce (facile da risolvere).

Inserisci 108000 righe con date casuali del 20° secolo, che è simile a una tabella di persone viventi (13 o più).

INSERT INTO  event (event_date)
SELECT '2000-1-1'::date - (random() * 36525)::int
FROM   generate_series (1, 108000);

Elimina ~ 8% per creare alcune tuple morte e rendere il tavolo più "vita reale".

DELETE FROM event WHERE random() < 0.08;
ANALYZE event;

Il mio test case aveva 99289 righe, 4012 risultati.

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 - L'idea di Catcall riscritta

A parte piccole ottimizzazioni, la differenza principale consiste nell'aggiungere solo la quantità esatta di anni date_trunc('year', age(current_date + 14, event_date)) per ottenere l'anniversario di quest'anno, che evita del tutto la necessità di un 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 - Daniele

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

Vedi "1. Versione semplice" sopra.

E2 - Erwin 2

Vedi "2. Versione avanzata" sopra.

E3 - Erwin 3

Vedi "3. Versione magia nera" sopra.

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 - un_cavallo_senza_nome

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 - jolly

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;

Semplificato per restituire lo stesso di tutti gli altri:

SELECT *
FROM   event e
WHERE  this_years_birthday( e.event_date::date )
        BETWEEN current_date
        AND     current_date + '2weeks'::interval;

W1:riscritta la query del jolly

Quanto sopra soffre di una serie di dettagli inefficienti (oltre lo scopo di questo post già considerevole). La versione riscritta è molto più veloce:

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);

Risultati del test

Ho eseguito questo test con una tabella temporanea su PostgreSQL 9.1.7. I risultati sono stati raccolti con EXPLAIN ANALYZE , al meglio di 5.

Risultati

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  --!!

Tutte le altre query funzionano allo stesso modo con o senza indice perché utilizzano non sargable espressioni.

Conclusione

  • Finora, la query di @Daniel è stata la più veloce.

  • Anche l'approccio @wildplassers (riscritto) funziona in modo accettabile.

  • La versione di @Catcall è qualcosa come il mio approccio inverso. Le prestazioni sfuggono rapidamente di mano con tabelle più grandi.
    La versione riscritta si comporta comunque abbastanza bene. L'espressione che uso è qualcosa come una versione più semplice di this_years_birthday() di @wildplassser funzione.

  • La mia "versione semplice" è più veloce anche senza indice , perché richiede meno calcoli.

  • Con index, la "versione avanzata" è veloce quanto la "versione semplice", perché min() e max() diventa molto a buon mercato con un indice. Entrambi sono sostanzialmente più veloci degli altri che non possono utilizzare l'indice.

  • La mia "versione magia nera" è la più veloce con o senza indice . Ed è molto semplice da chiamare.

  • Con una tabella reale un indice renderà ancora più grande differenza. Più colonne rendono la tabella più grande e la scansione sequenziale più costosa, mentre la dimensione dell'indice rimane la stessa.