Separate Monats- und Jahresspalten oder Datum mit immer 1 gesetztem Tag?

15

Ich baue mit Postgres eine Datenbank auf, in der viele Dinge nach monthund gruppiert werden year, aber niemals nach date.

  • Ich könnte Integer monthund yearSpalten erstellen und diese verwenden.
  • Oder ich könnte eine month_yearSpalte haben und die immer dayauf 1 setzen.

Ersteres scheint ein bisschen einfacher und klarer zu sein, wenn jemand die Daten betrachtet. Letzteres ist jedoch insofern von Vorteil, als es einen geeigneten Typ verwendet.

David N. Welton
quelle
1
Sie können auch einen eigenen Datentyp erstellen month, der zwei Ganzzahlen enthält. Aber ich denke, wenn Sie niemals den Tag des Monats brauchen, ist es wahrscheinlich einfacher, zwei ganze Zahlen zu verwenden
a_horse_with_no_name
1
Sie sollten den möglichen Datumsbereich, die mögliche Anzahl der Zeilen, das, was Sie optimieren möchten (Speicher, Leistung, Sicherheit, Einfachheit?) Und (wie immer) Ihre Version von Postgres angeben.
Erwin Brandstetter

Antworten:

17

Persönlich, wenn es ein Datum ist oder ein Datum sein kann, empfehle ich, es immer als eines zu speichern. Als Faustregel ist es einfach einfacher, damit zu arbeiten.

  • Ein Datum ist 4 Bytes.
  • Eine Smallint ist 2 Bytes (wir brauchen zwei)
    • ... 2 Bytes: ein Smallint für das Jahr
    • ... 2 Bytes: eine Smallint pro Monat

Sie können ein Datum haben, das den Tag unterstützt, wenn Sie es jemals benötigen, oder ein Datum smallintfür Jahr und Monat, das niemals die zusätzliche Genauigkeit unterstützt.

Beispieldaten

Sehen wir uns jetzt ein Beispiel an. Erstellen wir eine Million Daten für unser Beispiel. Dies sind ungefähr 5.000 Zeilen für 200 Jahre zwischen 1901 und 2100. Jedes Jahr sollte für jeden Monat etwas dabei sein.

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;

Testen

Einfach WHERE

Jetzt können wir diese Theorien der Nichtverwendung von Datumsangaben testen. Ich habe sie jeweils einige Male ausgeführt, um die Dinge aufzuwärmen.

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

Versuchen wir nun die andere Methode mit ihnen getrennt

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)

Fairerweise sind sie nicht alle 0.749. Einige sind ein bisschen mehr oder weniger, aber es spielt keine Rolle. Sie sind alle relativ gleich. Es wird einfach nicht benötigt.

Innerhalb eines Monats

Jetzt lass uns Spaß damit haben. Angenommen, du möchtest alle Intervalle innerhalb eines Monats ab Januar 2014 finden (der gleiche Monat, den wir oben verwendet haben).

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)

Vergleichen Sie das mit der kombinierten Methode

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)

Es ist sowohl langsamer als auch hässlicher.

GROUP BY/ORDER BY

Kombinierte Methode,

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)

Und wieder mit der Composite-Methode

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)

Fazit

Lassen Sie im Allgemeinen die klugen Leute die harte Arbeit erledigen. Datemath ist schwer, meine Kunden zahlen mir nicht genug. Ich habe diese Tests gemacht. Es fiel mir schwer, jemals zu dem Schluss zu kommen, dass ich bessere Ergebnisse erzielen könnte als date. Ich habe aufgehört, es zu versuchen.

AKTUALISIERUNG

@a_horse_with_no_name empfohlen für meinen Test innerhalb eines MonatsWHERE (year, month) between (2013, 12) and (2014,2) . Meiner Meinung nach ist das zwar cool, aber eine komplexere Abfrage, und ich würde sie lieber vermeiden, wenn es keinen Gewinn gäbe. Leider war es immer noch langsamer, obwohl es in der Nähe ist - das ist eher der Nachteil dieses Tests. Es macht einfach nicht viel aus.

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)
Evan Carroll
quelle
4
Im Gegensatz zu einigen anderen RDBMS (Siehe Seite 45 von use-the-index-luke.com/blog/2013-07/... ), Postgres unterstützt auch voll Indexzugriff mit Zeilenwerten: stackoverflow.com/a/34291099/939860 Aber das ist eine Abgesehen davon stimme ich voll und ganz zu: dateIst in den meisten Fällen der richtige Weg.
Erwin Brandstetter
5

Als Alternative zu der von Evan Carroll vorgeschlagenen Methode, die ich für die wahrscheinlich beste Option halte, habe ich in einigen Fällen (und nicht speziell bei Verwendung von PostgreSQL) nur eine year_monthSpalte vom Typ INTEGER(4 Byte) verwendet, die als berechnet wurde

 year_month = year * 100 + month

Das heißt, Sie codieren den Monat mit den beiden am weitesten rechts stehenden Dezimalstellen (Ziffer 0 und Ziffer 1) der Ganzzahl und das Jahr mit den Ziffern 2 bis 5 (oder, falls erforderlich, mehr).

Dies ist in gewissem Maße die Alternative eines armen Mannes zum Aufbau eines eigenen year_monthTyps und eigener Betreiber. Es hat einige Vorteile, vor allem "Klarheit der Absicht", und einige Platzersparnisse (nicht in PostgreSQL, denke ich) und auch einige Unannehmlichkeiten, über zwei separate Spalten.

Sie können die Gültigkeit der Werte garantieren, indem Sie einfach a hinzufügen

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

Sie können eine WHEREKlausel haben , die wie folgt aussieht:

year_month BETWEEN 201610 and 201702 

und es funktioniert effizient (wenn die year_monthSpalte natürlich richtig indiziert ist).

Sie können auf year_monthdie gleiche Weise wie mit einem Datum und (mindestens) mit der gleichen Effizienz gruppieren .

Wenn Sie trennen yearund monthist die Berechnung einfach:

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

Was ist unpraktisch : Wenn Sie einen Zeitraum von 15 Monaten verlängern möchten, year_monthmüssen Sie Folgendes berechnen (wenn ich keinen Fehler oder Fehler gemacht habe):

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

Wenn Sie nicht aufpassen, kann dies fehleranfällig sein.

Wenn Sie die Anzahl der Monate zwischen zwei year_months ermitteln möchten, müssen Sie einige ähnliche Berechnungen durchführen. Das ist (mit vielen Vereinfachungen), was wirklich unter der Haube mit Datumsarithmetik passiert, was uns zum Glück durch bereits definierte Funktionen und Operatoren verborgen bleibt.

Wenn Sie viele dieser Operationen benötigen, ist die Verwendung year_monthnicht allzu praktisch. Wenn Sie dies nicht tun, ist dies eine sehr klare Methode, um Ihre Absichten deutlich zu machen.


Alternativ können Sie einen year_monthTyp definieren und einen Operator year_month+ intervalsowie ein anderes year_month- year_month... definieren und die Berechnungen ausblenden. Ich habe eigentlich noch nie so viel gebraucht, um das Bedürfnis in der Praxis zu spüren. A date- dateversteckt Sie tatsächlich etwas Ähnliches.

joanolo
quelle
1
Ich habe noch einen anderen Weg dazu geschrieben =) genieße es.
Evan Carroll
Ich schätze das How-to sowie die Vor- und Nachteile.
Phunehehe
4

Als Alternative zu Joanolos Methode =) (Entschuldigung, ich war beschäftigt, wollte dies aber schreiben)

BIT JOY

Wir werden das Gleiche tun, aber mit Bits. Eine int4in PostgreSQL ist eine Ganzzahl mit Vorzeichen, die von -2147483648 bis +2147483647 reicht

Hier finden Sie eine Übersicht über unsere Struktur.

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

Speichermonat.

  • Ein Monat benötigt 12 Optionen pow(2,4)sind 4 Bits .
  • Den Rest widmen wir dem Jahr, 32-4 = 28 Bits .

Hier ist unsere Bitmap, wo Monate gespeichert sind.

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

Monate, 1. Januar - 12. Dezember

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

Jahre. Die verbleibenden 28 Bits ermöglichen es uns, unsere Jahresinformationen zu speichern

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

An diesem Punkt müssen wir entscheiden, wie wir das tun wollen. Für unsere Zwecke könnten wir einen statischen Offset verwenden, wenn wir nur 5.000 n. Chr. Abdecken müssten, könnten wir zurückgehen, 268,430,455 BCwas so ziemlich das gesamte Mesozoikum und alles Nützliche in der Zukunft abdeckt .

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

Und jetzt haben wir die Grundlagen unserer Art, die in 2.700 Jahren verfallen werden.

Machen wir uns also an die Arbeit, um einige Funktionen zu erstellen.

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;

Ein schneller Test zeigt, dass dies funktioniert.

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

Jetzt haben wir Funktionen, die wir für unsere Binärtypen verwenden können.

Wir hätten noch ein Bit von dem signierten Teil abschneiden können, das Jahr als positiv speichern und es dann natürlich als signiertes int sortieren lassen. Wenn Geschwindigkeit eine höhere Priorität als Speicherplatz gehabt hätte, wäre dies die Route gewesen, die wir eingeschlagen haben. Aber vorerst haben wir ein Datum, das mit dem Mesozoikum zusammenarbeitet.

Ich kann später damit aktualisieren, nur zum Spaß.

Evan Carroll
quelle
Reichweiten sind noch nicht möglich, das schaue ich mir später an.
Evan Carroll
Ich denke, "Optimierung auf das Bit" wäre sinnvoll, wenn Sie auch alle Funktionen in "Low Level C" ausführen würden. Du sparst bis zur letzten Sekunde und bis zur letzten Nanosekunde ;-) Wie auch immer, freudig! (Ich erinnere mich noch an BCD. Nicht unbedingt mit Freude.)
Joanolo