Colonnes de mois et d'année séparées, ou date avec jour toujours défini sur 1?


15

Je construis une base de données avec Postgres où il y aura beaucoup de regroupements de choses par monthet year, mais jamais par date.

  • Je pourrais créer des entiers monthet des yearcolonnes et les utiliser.
  • Ou je pourrais avoir une month_yearcolonne et toujours mettre le dayà 1.

Le premier semble un peu plus simple et plus clair si quelqu'un regarde les données, mais le second est agréable en ce qu'il utilise un type approprié.


1
Ou vous pouvez créer votre propre type de données monthqui contient deux entiers. Mais je pense que si vous n'avez jamais, jamais besoin du jour du mois, utiliser deux entiers est probablement plus facile
a_horse_with_no_name

1
Vous devez déclarer la plage de dates possible, le nombre possible de lignes, ce que vous essayez d'optimiser (stockage, performances, sécurité, simplicité?) Et (comme toujours) votre version de Postgres.
Erwin Brandstetter

Réponses:


17

Personnellement, si c'est une date, ou peut être une date, je suggère de toujours la stocker comme une seule. C'est juste plus facile de travailler avec en règle générale.

  • Une date est de 4 octets.
  • Un smallint fait 2 octets (nous en avons besoin de deux)
    • ... 2 octets: un petit entier par an
    • ... 2 octets: un petit entier par mois

Vous pouvez avoir une date qui prendra en charge le jour si vous en avez besoin, ou une smallintpour l'année et le mois qui ne prendra jamais en charge la précision supplémentaire.

Exemples de données

Regardons maintenant un exemple. Créons 1 million de dates pour notre échantillon. Cela représente environ 5 000 lignes pendant 200 ans entre 1901 et 2100. Chaque année devrait avoir quelque chose pour chaque mois.

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;

Essai

Facile WHERE

Maintenant, nous pouvons tester ces théories de ne pas utiliser de date .. J'ai exécuté chacune de ces quelques fois afin de réchauffer les choses.

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

Maintenant, essayons l'autre méthode avec eux séparément

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)

En toute honnêteté, ils ne sont pas tous 0,749 ... certains sont un peu plus ou moins, mais cela n'a pas d'importance. Ils sont tous relativement les mêmes. Ce n'est tout simplement pas nécessaire.

Dans un mois

Maintenant, amusons-nous avec cela. Disons que vous voulez trouver tous les intervalles dans un délai d'un mois à partir de janvier 2014 (le même mois que nous avons utilisé ci-dessus).

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)

Comparez cela à la méthode combinée

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)

C'est à la fois plus lent et plus laid.

GROUP BY/ORDER BY

Méthode combinée,

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)

Et encore une fois avec la méthode composite

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)

Conclusion

En règle générale, laissez les gens intelligents faire le travail difficile. Datemath est difficile, mes clients ne me paient pas assez. J'avais l'habitude de faire ces tests. J'avais du mal à conclure que je pouvais obtenir de meilleurs résultats que date. J'ai arrêté d'essayer.

MISES À JOUR

@a_horse_with_no_name suggéré pour mon dans un délai d'un mois testWHERE (year, month) between (2013, 12) and (2014,2) . À mon avis, bien que cool, c'est une requête plus complexe et je préfère l'éviter sauf s'il y a un gain. Hélas, il était encore plus lent bien qu'il soit proche - ce qui est plus à retenir de ce test. Cela n'a pas beaucoup d'importance.

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
Contrairement à certains autres SGBDR (voir page 45 de use-the-index-luke.com/blog/2013-07/… ), Postgres prend également entièrement en charge l'accès aux index avec des valeurs de ligne: stackoverflow.com/a/34291099/939860 Mais c'est un à part, je suis entièrement d'accord: datec'est la voie à suivre dans la plupart des cas.
Erwin Brandstetter

5

Comme alternative à la méthode proposée par Evan Carroll, que je considère probablement la meilleure option, j'ai utilisé à certaines occasions (et pas spécialement lors de l'utilisation de PostgreSQL) juste une year_monthcolonne, de typeINTEGER (4 octets), calculée comme

 year_month = year * 100 + month

Autrement dit, vous codez le mois sur les deux chiffres décimaux les plus à droite (chiffre 0 et chiffre 1) du nombre entier et l'année sur les chiffres 2 à 5 (ou plus, si nécessaire).

C'est, dans une certaine mesure, l' alternative d' un pauvre homme à la construction de votre propre year_monthtype et opérateurs. Il a certains avantages, principalement la «clarté de l'intention», et quelques économies d'espace (pas dans PostgreSQL, je pense), ainsi que quelques inconvénients, par rapport à deux colonnes distinctes.

Vous pouvez garantir que les valeurs sont valides en ajoutant simplement un

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

Vous pouvez avoir une WHEREclause ressemblant à ceci:

year_month BETWEEN 201610 and 201702 

et cela fonctionne efficacement (si la year_monthcolonne est correctement indexée, bien sûr).

Vous pouvez regrouper par year_month la même manière que vous pourriez le faire avec une date et avec la même efficacité (au moins).

Si vous devez séparer yearet month, le calcul est simple:

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

Ce qui est gênant : si vous voulez ajouter 15 mois à un, year_monthvous devez calculer (si je n'ai pas fait d'erreur ou de surveillance):

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

Si vous ne faites pas attention, cela peut être sujet à des erreurs.

Si vous souhaitez obtenir le nombre de mois entre deux year_months, vous devez effectuer des calculs similaires. C'est (avec beaucoup de simplifications) ce qui se passe vraiment sous le capot avec l'arithmétique des dates, qui nous est heureusement caché par des fonctions et des opérateurs déjà définis.

Si vous avez besoin de beaucoup de ces opérations, l'utilisation year_monthn'est pas trop pratique. Si vous ne le faites pas, c'est une façon très claire de clarifier votre intention.


Alternativement, vous pouvez définir un year_monthtype, définir un opérateur year_month+ interval, ainsi qu'un autre year_month- year_month... et masquer les calculs. En fait, je n'ai jamais fait une telle utilisation au point d'en ressentir le besoin dans la pratique. A date- datevous cache en fait quelque chose de similaire.


1
J'ai écrit une autre façon de le faire =) en profiter.
Evan Carroll

J'apprécie le mode d'emploi ainsi que les avantages et les inconvénients.
phunehehe

4

Comme alternative à la méthode de joanolo =) (désolé j'étais occupé mais je voulais écrire ceci)

BIT JOY

Nous allons faire la même chose, mais avec des bits. Unint4 dans PostgreSQL est un entier signé, allant de -2147483648 à +2147483647

Voici un aperçu de notre structure.

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

Mois de stockage.

  • Un mois nécessite 12 options pow(2,4)soit 4 bits .
  • Le reste que nous consacrons à l'année, 32-4 = 28 bits .

Voici notre bitmap de l'endroit où les mois sont stockés.

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

Mois, 1er janvier - 12 décembre

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

Ans. Les 28 bits restants nous permettent de stocker nos informations sur l'année

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

À ce stade, nous devons décider comment nous voulons procéder. Pour nos besoins, nous pourrions utiliser un décalage statique, si nous avons seulement besoin de couvrir 5 000 AD, nous pourrions revenir à 268,430,455 BCce qui couvre à peu près l'intégralité du Mésozoïque et tout ce qui est utile pour aller de l'avant.

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

Et, maintenant, nous avons les rudiments de notre type, qui expireront dans 2 700 ans.

Commençons donc à créer certaines fonctions.

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 rapide montre que cela fonctionne ..

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

Nous avons maintenant des fonctions que nous pouvons utiliser sur nos types binaires.

Nous aurions pu couper un bit de plus de la partie signée, enregistrer l'année comme positive, puis la faire trier naturellement comme un entier signé. Si la vitesse était une priorité plus élevée que l'espace de stockage, cela aurait été la voie que nous empruntons. Mais pour l'instant, nous avons une date qui fonctionne avec le Mésozoïque.

Je peux mettre à jour plus tard avec ça, juste pour le plaisir.


Les plages ne sont pas encore possibles, j'y reviendrai plus tard.
Evan Carroll

Je pense que "l'optimisation du bit" aurait tout son sens lorsque vous feriez également toutes les fonctions en "bas niveau C". Vous économisez jusqu'au dernier bit et jusqu'à la dernière nanoseconde ;-) Quoi qu'il en soit, joyeux! (Je me souviens encore de BCD. Pas nécessairement avec joie.)
joanolo
En utilisant notre site, vous reconnaissez avoir lu et compris notre politique liée aux cookies et notre politique de confidentialité.
Licensed under cc by-sa 3.0 with attribution required.