¿Separe las columnas de mes y año, o la fecha con el día siempre establecida en 1?


15

Estoy construyendo una base de datos con Postgres donde habrá muchas agrupaciones de cosas por monthy year, pero nunca por date.

  • Podría crear enteros monthy yearcolumnas y usarlos.
  • O podría tener una month_yearcolumna y siempre establecer el day1.

El primero parece un poco más simple y claro si alguien está mirando los datos, pero el segundo es bueno porque usa un tipo apropiado.


1
O podría crear su propio tipo de datos monthque contenga dos enteros. Pero creo que si nunca necesitas el día del mes, usar dos enteros probablemente sea más fácil
a_horse_with_no_name el

1
Debe declarar el posible rango de fechas, el número posible de filas, lo que está tratando de optimizar (almacenamiento, rendimiento, seguridad, simplicidad) y (como siempre) su versión de Postgres.
Erwin Brandstetter

Respuestas:


17

Personalmente, si es una fecha, o puede ser una fecha, sugiero que siempre la almacene como una. Es más fácil trabajar con él como regla general.

  • Una fecha es de 4 bytes.
  • Una letra pequeña es de 2 bytes (necesitamos dos)
    • ... 2 bytes: una letra pequeña por año
    • ... 2 bytes: una letra pequeña por mes

Puede tener una fecha que admitirá el día si alguna vez la necesita, o una smallintpara año y mes que nunca admitirá la precisión adicional.

Data de muestra

Veamos un ejemplo ahora ... Creemos 1 millón de fechas para nuestra muestra. Esto es aproximadamente 5,000 filas por 200 años entre 1901 y 2100. Cada año debería tener algo para cada mes.

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;

Pruebas

Sencillo WHERE

Ahora podemos probar estas teorías de no usar la fecha. Ejecuté cada una de estas veces para calentar las cosas.

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

Ahora, intentemos el otro método con ellos separados

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)

Para ser justos, no todos son 0.749 ... algunos son un poco más o menos, pero no importa. Todos son relativamente iguales. Simplemente no es necesario.

En un mes

Ahora, divirtámonos ... Digamos que desea encontrar todos los intervalos dentro de 1 mes de enero de 2014 (el mismo mes que utilizamos anteriormente).

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)

Compare eso con el método combinado

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 a la vez más lento y más feo.

GROUP BY/ /ORDER BY

Método combinado,

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)

Y nuevamente con el método compuesto

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)

Conclusión

En general, deje que las personas inteligentes hagan el trabajo duro. Datemath es difícil, mis clientes no me pagan lo suficiente. Solía ​​hacer estas pruebas. Me costaba mucho concluir que podría obtener mejores resultados que date. Dejé de intentarlo.

ACTUALIZACIONES

@a_horse_with_no_name sugerido para mi prueba dentro de un mesWHERE (year, month) between (2013, 12) and (2014,2) . En mi opinión, si bien es una consulta más compleja, prefiero evitarla a menos que haya una ganancia. Por desgracia, aún fue más lento, aunque está cerca, lo que es más fácil de quitar de esta prueba. Simplemente no importa mucho.

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)

44
A diferencia de otros RDBMS (consulte la página 45 de use-the-index-luke.com/blog/2013-07/… ), Postgres también es totalmente compatible con el acceso al índice con valores de fila: stackoverflow.com/a/34291099/939860 Pero eso es un aparte, estoy totalmente de acuerdo: datees el camino a seguir en la mayoría de los casos.
Erwin Brandstetter

5

Como alternativa al método propuesto por Evan Carroll, que considero probablemente la mejor opción, he usado en algunas ocasiones (y no especialmente cuando uso PostgreSQL) solo una year_monthcolumna, de tipo INTEGER(4 bytes), calculada como

 year_month = year * 100 + month

Es decir, codifica el mes en los dos dígitos decimales más a la derecha (dígito 0 y dígito 1) del número entero, y el año en los dígitos 2 a 5 (o más, si es necesario).

Esta es, hasta cierto punto, la alternativa de un hombre pobre para construir su propio year_monthtipo y operadores. Tiene algunas ventajas, principalmente "claridad de intención", y algunos ahorros de espacio (no en PostgreSQL, creo), y también algunos inconvenientes, al tener dos columnas separadas.

Puede garantizar que los valores son válidos simplemente agregando un

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

Puede tener una WHEREcláusula que se vea así:

year_month BETWEEN 201610 and 201702 

y funciona de manera eficiente (si la year_monthcolumna está indexada correctamente, por supuesto).

Puede agrupar de year_monthla misma manera que podría hacerlo con una fecha y con la misma eficiencia (al menos).

Si necesita separarse yeary month, el cálculo es sencillo:

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

Lo que es inconveniente : si desea agregar 15 meses a uno year_month, debe calcular (si no he cometido un error o supervisión):

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 no tiene cuidado, esto puede ser propenso a errores.

Si desea obtener la cantidad de meses entre dos años-meses, debe hacer algunos cálculos similares. Eso es (con muchas simplificaciones) lo que realmente sucede debajo del capó con la aritmética de fechas, que afortunadamente se nos oculta a través de funciones y operadores ya definidos.

Si necesita muchas de estas operaciones, el uso year_monthno es demasiado práctico. Si no lo hace, es una forma muy clara de aclarar su intención.


Como alternativa, podría definir un year_monthtipo, y definir un operador year_month+ interval, y también otro year_month- year_month... y ocultar los cálculos. En realidad nunca he hecho un uso tan intenso como para sentir la necesidad en la práctica. A date- en daterealidad te está ocultando algo similar.


1
Escribí otra forma de hacer esto =) disfrútalo.
Evan Carroll

Aprecio el cómo, así como los pros y los contras.
phunehehe

4

Como alternativa al método de joanolo =) (lo siento, estaba ocupado pero quería escribir esto)

ALEGRÍA

Vamos a hacer lo mismo, pero con bits. Uno int4en PostgreSQL es un entero con signo, que va desde -2147483648 hasta +2147483647

Aquí hay una descripción general de nuestra estructura.

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

Almacenamiento mes.

  • Un mes requiere 12 opciones pow(2,4)es de 4 bits .
  • El resto lo dedicamos al año, 32-4 = 28 bits .

Aquí está nuestro mapa de bits de dónde se almacenan los meses.

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

Meses, 1 de enero - 12 de diciembre

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

Años. Los 28 bits restantes nos permiten almacenar nuestra información anual

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

En este punto, necesitamos decidir cómo queremos hacer esto. Para nuestros propósitos, podríamos usar un desplazamiento estático, si solo necesitamos cubrir 5,000 AD, podríamos volver a lo 268,430,455 BCque cubre casi todo Mesozoic y todo lo útil en el futuro.

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

Y, ahora tenemos los rudimentos de nuestro tipo, que expiran en 2.700 años.

Así que manos a la obra para hacer algunas funciones.

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;

Una prueba rápida muestra que esto funciona ...

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

Ahora tenemos funciones que podemos usar en nuestros tipos binarios.

Podríamos haber cortado un poco más de la parte firmada, almacenado el año como positivo, y luego haberlo ordenado naturalmente como un int firmado. Si la velocidad fuera una prioridad más alta que el espacio de almacenamiento, esa habría sido la ruta que seguimos. Pero por ahora, tenemos una fecha que funciona con el Mesozoico.

Puedo actualizar más tarde con eso, solo por diversión.


Los rangos aún no son posibles, lo veré más tarde.
Evan Carroll

Creo que "optimizar al bit" tendría todo el sentido cuando también haría todas las funciones en "bajo nivel C". Ahorras hasta el último bit y hasta el último nanosegundo ;-) De todos modos, ¡alegre! (Todavía recuerdo BCD. No necesariamente con alegría.)
joanolo
Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.