Separare le colonne di mese e anno o la data con il giorno sempre impostata su 1?


15

Sto costruendo un database con Postgres in cui ci saranno molti raggruppamenti di cose per monthe year, ma mai per date.

  • Potrei creare numeri interi monthe yearcolonne e usarli.
  • Oppure potrei avere una month_yearcolonna e impostare sempre daysu 1.

Il primo sembra un po 'più semplice e chiaro se qualcuno sta guardando i dati, ma il secondo è carino in quanto utilizza un tipo corretto.


1
Oppure potresti creare il tuo tipo di dati monthche contiene due numeri interi. Ma penso che se non hai mai, mai bisogno del giorno del mese, usare due numeri interi è probabilmente più facile
a_horse_with_no_name

1
Dovresti dichiarare il possibile intervallo di date, il possibile numero di righe, ciò che stai cercando di ottimizzare (archiviazione, prestazioni, sicurezza, semplicità?) E (come sempre) la tua versione di Postgres.
Erwin Brandstetter

Risposte:


17

Personalmente se è una data, o può essere una data, suggerisco di conservarla sempre come una data. È semplicemente più facile lavorare con una regola empirica.

  • Una data è di 4 byte.
  • Un smallint è di 2 byte (ne abbiamo bisogno di due)
    • ... 2 byte: un piccolo per anno
    • ... 2 byte: un piccolo per mese

Puoi avere una data che supporterà il giorno se mai ne avrai bisogno, o una smallintper anno e mese che non supporteranno mai la precisione in più.

Dati di esempio

Vediamo ora un esempio. Creiamo 1 milione di date per il nostro campione. Sono circa 5.000 file per 200 anni tra il 1901 e il 2100. Ogni anno dovrebbe avere qualcosa per ogni mese.

CREATE TABLE foo
AS
  SELECT
    x,
    make_date(year,month,1)::date AS date,
    year::smallint,
    month::smallint
  FROM generate_series(1,1e6) AS gs(x)
  CROSS JOIN LATERAL CAST(trunc(random()*12+1+x-x) AS int) AS month
  CROSS JOIN LATERAL CAST(trunc(random()*200+1901+x-x) AS int) AS year
;
CREATE INDEX ON foo(date);
CREATE INDEX ON foo (year,month);
VACUUM FULL ANALYZE foo;

analisi

Semplice WHERE

Ora possiamo testare queste teorie di non usare la data .. Ho eseguito ognuna di queste alcune volte per riscaldare le cose.

EXPLAIN ANALYZE SELECT * FROM foo WHERE date = '2014-1-1'
                                                        QUERY PLAN                                                        
--------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on foo  (cost=11.56..1265.16 rows=405 width=14) (actual time=0.164..0.751 rows=454 loops=1)
   Recheck Cond: (date = '2014-04-01'::date)
   Heap Blocks: exact=439
   ->  Bitmap Index Scan on foo_date_idx  (cost=0.00..11.46 rows=405 width=0) (actual time=0.090..0.090 rows=454 loops=1)
         Index Cond: (date = '2014-04-01'::date)
 Planning time: 0.090 ms
 Execution time: 0.795 ms

Ora proviamo l'altro metodo con loro separati

EXPLAIN ANALYZE SELECT * FROM foo WHERE year = 2014 AND month = 1;
                                                           QUERY PLAN                                                           
--------------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on foo  (cost=12.75..1312.06 rows=422 width=14) (actual time=0.139..0.707 rows=379 loops=1)
   Recheck Cond: ((year = 2014) AND (month = 1))
   Heap Blocks: exact=362
   ->  Bitmap Index Scan on foo_year_month_idx  (cost=0.00..12.64 rows=422 width=0) (actual time=0.079..0.079 rows=379 loops=1)
         Index Cond: ((year = 2014) AND (month = 1))
 Planning time: 0.086 ms
 Execution time: 0.749 ms
(7 rows)

In tutta onestà, non sono tutti 0.749 .. alcuni sono un po 'più o meno, ma non importa. Sono tutti relativamente uguali. Semplicemente non è necessario.

Entro un mese

Ora divertiamoci con questo. Supponiamo che tu voglia trovare tutti gli intervalli entro 1 mese da gennaio 2014 (lo stesso mese che abbiamo usato sopra).

EXPLAIN ANALYZE
  SELECT *
  FROM foo
  WHERE date
    BETWEEN
      ('2014-1-1'::date - '1 month'::interval)::date 
      AND ('2014-1-1'::date + '1 month'::interval)::date;
                                                        QUERY PLAN                                                         
---------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on foo  (cost=21.27..2310.97 rows=863 width=14) (actual time=0.384..1.644 rows=1226 loops=1)
   Recheck Cond: ((date >= '2013-12-01'::date) AND (date <= '2014-02-01'::date))
   Heap Blocks: exact=1083
   ->  Bitmap Index Scan on foo_date_idx  (cost=0.00..21.06 rows=863 width=0) (actual time=0.208..0.208 rows=1226 loops=1)
         Index Cond: ((date >= '2013-12-01'::date) AND (date <= '2014-02-01'::date))
 Planning time: 0.104 ms
 Execution time: 1.727 ms
(7 rows)

Confrontalo con il metodo combinato

EXPLAIN ANALYZE
  SELECT *
  FROM foo
  WHERE year = 2013 AND month = 12
    OR ( year = 2014 AND ( month = 1 OR month = 2) );

                                                                 QUERY PLAN                                                                 
--------------------------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on foo  (cost=38.79..2999.66 rows=1203 width=14) (actual time=0.664..2.291 rows=1226 loops=1)
   Recheck Cond: (((year = 2013) AND (month = 12)) OR (((year = 2014) AND (month = 1)) OR ((year = 2014) AND (month = 2))))
   Heap Blocks: exact=1083
   ->  BitmapOr  (cost=38.79..38.79 rows=1237 width=0) (actual time=0.479..0.479 rows=0 loops=1)
         ->  Bitmap Index Scan on foo_year_month_idx  (cost=0.00..12.64 rows=421 width=0) (actual time=0.112..0.112 rows=402 loops=1)
               Index Cond: ((year = 2013) AND (month = 12))
         ->  BitmapOr  (cost=25.60..25.60 rows=816 width=0) (actual time=0.218..0.218 rows=0 loops=1)
               ->  Bitmap Index Scan on foo_year_month_idx  (cost=0.00..12.62 rows=420 width=0) (actual time=0.108..0.108 rows=423 loops=1)
                     Index Cond: ((year = 2014) AND (month = 1))
               ->  Bitmap Index Scan on foo_year_month_idx  (cost=0.00..12.38 rows=395 width=0) (actual time=0.108..0.108 rows=401 loops=1)
                     Index Cond: ((year = 2014) AND (month = 2))
 Planning time: 0.256 ms
 Execution time: 2.421 ms
(13 rows)

È sia più lento, sia più brutto.

GROUP BY/ORDER BY

Metodo combinato,

EXPLAIN ANALYZE
  SELECT date, count(*)
  FROM foo
  GROUP BY date
  ORDER BY date;
                                                        QUERY PLAN                                                        
--------------------------------------------------------------------------------------------------------------------------
 Sort  (cost=20564.75..20570.75 rows=2400 width=4) (actual time=286.749..286.841 rows=2400 loops=1)
   Sort Key: date
   Sort Method: quicksort  Memory: 209kB
   ->  HashAggregate  (cost=20406.00..20430.00 rows=2400 width=4) (actual time=285.978..286.301 rows=2400 loops=1)
         Group Key: date
         ->  Seq Scan on foo  (cost=0.00..15406.00 rows=1000000 width=4) (actual time=0.012..70.582 rows=1000000 loops=1)
 Planning time: 0.094 ms
 Execution time: 286.971 ms
(8 rows)

E ancora con il metodo composito

EXPLAIN ANALYZE
  SELECT year, month, count(*)
  FROM foo
  GROUP BY year, month
  ORDER BY year, month;
                                                        QUERY PLAN                                                        
--------------------------------------------------------------------------------------------------------------------------
 Sort  (cost=23064.75..23070.75 rows=2400 width=4) (actual time=336.826..336.908 rows=2400 loops=1)
   Sort Key: year, month
   Sort Method: quicksort  Memory: 209kB
   ->  HashAggregate  (cost=22906.00..22930.00 rows=2400 width=4) (actual time=335.757..336.060 rows=2400 loops=1)
         Group Key: year, month
         ->  Seq Scan on foo  (cost=0.00..15406.00 rows=1000000 width=4) (actual time=0.010..70.468 rows=1000000 loops=1)
 Planning time: 0.098 ms
 Execution time: 337.027 ms
(8 rows)

Conclusione

In generale, lascia che le persone intelligenti facciano il duro lavoro. Datemath è difficile, i miei clienti non mi pagano abbastanza. Facevo questi test. Mi è stato molto difficile concludere che avrei potuto ottenere risultati migliori rispetto a date. Ho smesso di provare.

AGGIORNAMENTI

@a_horse_with_no_name suggerito per il mio test entro un meseWHERE (year, month) between (2013, 12) and (2014,2) . Secondo me, benché sia ​​una domanda più complessa e preferirei evitarlo a meno che non ci fosse un guadagno. Purtroppo, è stato ancora più lento anche se è vicino - il che è più del take away di questo test. Semplicemente non importa molto.

EXPLAIN ANALYZE
  SELECT *
  FROM foo
  WHERE (year, month) between (2013, 12) and (2014,2);

                                                              QUERY PLAN                                                              
--------------------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on foo  (cost=5287.16..15670.20 rows=248852 width=14) (actual time=0.753..2.157 rows=1226 loops=1)
   Recheck Cond: ((ROW(year, month) >= ROW(2013, 12)) AND (ROW(year, month) <= ROW(2014, 2)))
   Heap Blocks: exact=1083
   ->  Bitmap Index Scan on foo_year_month_idx  (cost=0.00..5224.95 rows=248852 width=0) (actual time=0.550..0.550 rows=1226 loops=1)
         Index Cond: ((ROW(year, month) >= ROW(2013, 12)) AND (ROW(year, month) <= ROW(2014, 2)))
 Planning time: 0.099 ms
 Execution time: 2.249 ms
(7 rows)

4
A differenza di altri RDBMS (vedi pagina 45 di use-the-index-luke.com/blog/2013-07/… ), Postgres supporta anche completamente l'accesso all'indice con valori di riga: stackoverflow.com/a/34291099/939860 Ma questo è un a parte, sono pienamente d'accordo: dateè la strada da percorrere nella maggior parte dei casi.
Erwin Brandstetter,

5

In alternativa al metodo proposto da Evan Carroll, che considero probabilmente l'opzione migliore, ho usato in alcune occasioni (e non specialmente quando utilizzo PostgreSQL) solo una year_monthcolonna, di tipo INTEGER(4 byte), calcolata come

 year_month = year * 100 + month

Cioè, si codifica il mese sulle due cifre decimali più a destra (cifra 0 e cifra 1) del numero intero e l'anno sulle cifre da 2 a 5 (o più, se necessario).

Questa è, in una certa misura, l' alternativa di un uomo povero alla costruzione del proprio year_monthtipo e dei propri operatori. Ha alcuni vantaggi, principalmente "chiarezza di intenti" e alcuni risparmi di spazio (non in PostgreSQL, credo), e anche alcuni inconvenienti, rispetto al fatto di avere due colonne separate.

Puoi garantire che i valori siano validi semplicemente aggiungendo a

CHECK ((year_date % 100) BETWEEN 1 AND 12)   /*  % = modulus operator */

Puoi avere una WHEREclausola simile a:

year_month BETWEEN 201610 and 201702 

e funziona in modo efficiente (se la year_monthcolonna è correttamente indicizzata, ovviamente).

Puoi raggruppare year_monthnello stesso modo in cui potresti farlo con una data e con la stessa efficienza (almeno).

Se è necessario separare yeare month, il calcolo è semplice:

month = year_month % 100    -- % is modulus operator
year  = year_month / 100    -- / is integer division 

Che cosa è scomodo : se vuoi aggiungere 15 mesi a un year_monthdevi calcolare (se non ho fatto un errore o una svista):

year_month + delta (months) = ...

    /* intermediate calculations */
    year = year_month/100 + delta/12    /* years we had + new years */
           + (year_month % 100 + delta%12) / 12  /* extra months make 1 more year? */
    month = ((year_month%10) + (delta%12) - 1) % 12 + 1

/* final result */
... = year * 100 + month

Se non stai attento, questo può essere soggetto a errori.

Se si desidera ottenere il numero di mesi tra due year_months, è necessario eseguire calcoli simili. Questo è (con molte semplificazioni) ciò che realmente accade sotto il cofano con l'aritmetica della data, che per fortuna ci è nascosto attraverso funzioni e operatori già definiti.

Se hai bisogno di molte di queste operazioni, l'utilizzo year_monthnon è troppo pratico. In caso contrario, è un modo molto chiaro per chiarire le tue intenzioni.


In alternativa, è possibile definire un year_monthtipo e definire un operatore year_month+ interval, e anche un altro year_month- year_month... e nascondere i calcoli. In realtà non ho mai fatto un uso così pesante da sentire il bisogno in pratica. A date- ti datesta effettivamente nascondendo qualcosa di simile.


1
Ho scritto ancora un altro modo per farlo =) divertitevi.
Evan Carroll,

Apprezzo il how-to così come i pro e i contro.
phunehehe,

4

In alternativa al metodo di joanolo =) (scusate se ero occupato ma volevo scrivere questo)

BIT JOY

Faremo la stessa cosa, ma a pezzi. Uno int4in PostgreSQL è un numero intero con segno, che va da -2147483648 a +2147483647

Ecco una panoramica della nostra struttura.

               bit                
----------------------------------
 YYYYYYYYYYYYYYYYYYYYYYYYYYYYMMMM

Mese di memorizzazione.

  • Un mese richiede 12 opzioni pow(2,4)è 4 bit .
  • Il resto lo dedichiamo all'anno, 32-4 = 28 bit .

Ecco la nostra bit map di dove sono memorizzati i mesi.

               bit                
----------------------------------
 00000000000000000000000000001111

Mesi, 1 gennaio - 12 dicembre

               bit                
----------------------------------
 00000000000000000000000000000001
               bit                
----------------------------------
 00000000000000000000000000001100

Anni. I restanti 28 bit ci consentono di memorizzare le informazioni dell'anno

SELECT (pow(2,28)-1)::int;
   int4    
-----------
 268435455
(1 row)

A questo punto dobbiamo decidere come vogliamo farlo. Per i nostri scopi, potremmo usare un offset statico, se solo avessimo bisogno di coprire il 5.000 d.C., potremmo tornare indietro a 268,430,455 BCcui praticamente copre l'intero mesozoico e tutto ciò che è utile andare avanti.

SELECT (pow(2,28)-1)::int4::bit(32) << 4;
               year               
----------------------------------
 11111111111111111111111111110000

E ora abbiamo i rudimenti del nostro tipo, che scadranno tra 2.700 anni.

Quindi mettiamoci al lavoro per creare alcune funzioni.

CREATE DOMAIN year_month AS int4;

CREATE OR REPLACE FUNCTION to_year_month (cstring text)
RETURNS year_month
AS $$
  SELECT (
    ( ((date[1]::int4 - 5000) * -1)::bit(32) << 4 )
    | date[2]::int4::bit(32)
  )::year_month
  FROM regexp_split_to_array(cstring,'-(?=\d{1,2}$)')
    AS t(date)
$$
LANGUAGE sql
IMMUTABLE;

CREATE OR REPLACE FUNCTION year_month_to_text (ym year_month)
RETURNS text
AS $$
  SELECT ((ym::bit(32) >>4)::int4 * -1 + 5000)::text ||
  '-' ||
  (ym::bit(32) <<28 >>28)::int4::text
$$ LANGUAGE sql
IMMUTABLE;

Un test rapido mostra che funziona ..

SELECT year_month_to_text( to_year_month('2014-12') );
SELECT year_month_to_text( to_year_month('-5000-10') );
SELECT year_month_to_text( to_year_month('-8000-10') );
SELECT year_month_to_text( to_year_month('-84398-10') );

Ora abbiamo funzioni che possiamo usare sui nostri tipi binari ..

Avremmo potuto tagliare ancora un po 'dalla parte firmata, archiviare l'anno come positivo, e quindi farlo in modo naturale come int firmato. Se la velocità fosse una priorità più alta dello spazio di archiviazione, quella sarebbe stata la strada da percorrere. Ma per ora, abbiamo una data che funziona con il mesozoico.

Potrei aggiornarlo in seguito, solo per divertimento.


Gli intervalli non sono ancora possibili, lo vedrò più avanti.
Evan Carroll,

Penso che "l'ottimizzazione al bit" avrebbe senso se avessi anche tutte le funzioni in "basso livello C". Risparmi fino all'ultimo bit e fino all'ultimo nanosecondo ;-) Comunque, gioioso! (Ricordo ancora BCD. Non necessariamente con gioia.)
joanolo
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.