Colunas separadas de mês e ano ou data com dia sempre definida como 1?


15

Estou construindo um banco de dados com o Postgres, onde haverá um monte de coisas agrupadas por monthe year, mas nunca pelo date.

  • Eu poderia criar números inteiros monthe yearcolunas e usá-los.
  • Ou eu poderia ter uma month_yearcoluna e sempre definir o day1.

O primeiro parece um pouco mais simples e mais claro se alguém estiver olhando para os dados, mas o segundo é bom porque usa um tipo adequado.


1
Ou você pode criar seu próprio tipo de dados monthque contém dois números inteiros. Mas eu acho que se você nunca, nunca precisar o dia do mês, usando dois inteiros é provavelmente mais fácil
a_horse_with_no_name

1
Você deve declarar o intervalo possível de datas, o número possível de linhas, o que está tentando otimizar (armazenamento, desempenho, segurança, simplicidade?) E (como sempre) sua versão do Postgres.
Erwin Brandstetter 03/03

Respostas:


17

Pessoalmente, se é uma data, ou pode ser uma data, sugiro sempre armazená-lo como um. É mais fácil trabalhar com isso como regra geral.

  • Uma data é de 4 bytes.
  • Um smallint é de 2 bytes (precisamos de dois)
    • ... 2 bytes: um pequeno por ano
    • ... 2 bytes: um pequeno por mês

Você pode ter uma data que suportará o dia, se você precisar, ou uma smallintpara o ano e o mês, que nunca suportará a precisão extra.

Dados de amostra

Vamos ver um exemplo agora. Vamos criar 1 milhão de datas para nossa amostra. São aproximadamente 5.000 linhas por 200 anos entre 1901 e 2100. Todo ano deve ter algo para todo mês.

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;

Teste

Simples WHERE

Agora podemos testar essas teorias de não usar data. Corri cada uma delas algumas vezes para aquecer as coisas.

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

Agora, vamos tentar o outro método com eles 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 justo, nem todos são 0,749. Alguns são um pouco mais ou menos, mas isso não importa. Eles são todos relativamente iguais. Simplesmente não é necessário.

Dentro de um mês

Agora, vamos nos divertir com isso. Digamos que você queira encontrar todos os intervalos dentro de 1 mês a partir de janeiro de 2014 (o mesmo mês que usamos acima).

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 isso com o 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)

É mais lento e mais feio.

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)

E novamente com o método composto

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)

Conclusão

Geralmente, deixe as pessoas inteligentes fazerem o trabalho duro. Datemath é difícil, meus clientes não me pagam o suficiente. Eu costumava fazer esses testes. Eu estava duro para concluir que poderia obter melhores resultados do que date. Eu parei de tentar.

ATUALIZAÇÕES

@a_horse_with_no_name sugerido para o meu teste dentro de um mêsWHERE (year, month) between (2013, 12) and (2014,2) . Na minha opinião, apesar de legal, é uma consulta mais complexa e prefiro evitá-la, a menos que haja um ganho. Infelizmente, ainda era mais lento, apesar de estar próximo - o que é mais difícil de tirar deste teste. Simplesmente não importa muito.

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
Ao contrário de outros RDBMS (consulte a página 45 de use-the-index-luke.com/blog/2013-07/… ), o Postgres também suporta totalmente o acesso ao índice com valores de linha: stackoverflow.com/a/34291099/939860 Mas isso é um aparte, concordo plenamente: dateé o caminho a percorrer na maioria dos casos.
Erwin Brandstetter 03/03

5

Como alternativa ao método proposto por Evan Carroll, que considero provavelmente a melhor opção, usei em algumas ocasiões (e não especialmente ao usar o PostgreSQL) apenas uma year_monthcoluna do tipo INTEGER(4 bytes), calculada como

 year_month = year * 100 + month

Ou seja, você codifica o mês nos dois dígitos decimais mais à direita (dígito 0 e dígito 1) do número inteiro e o ano nos dígitos 2 a 5 (ou mais, se necessário).

Esta é, até certo ponto, a alternativa de um homem pobre para criar seu próprio year_monthtipo e operadores. Ele tem algumas vantagens, principalmente "clareza de intenção", e algumas economias de espaço (não no PostgreSQL, eu acho), e também alguns inconvenientes, por ter duas colunas separadas.

Você pode garantir que os valores sejam válidos apenas adicionando um

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

Você pode ter uma WHEREcláusula parecida com:

year_month BETWEEN 201610 and 201702 

e funciona de forma eficiente (se a year_monthcoluna estiver adequadamente indexada, é claro).

Você pode agrupar year_monthda mesma maneira que faria com uma data e com a mesma eficiência (pelo menos).

Se você precisar separar yeare month, o cálculo é direto:

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

O que é inconveniente : se você deseja adicionar 15 meses a um, year_monthprecisa calcular (se não cometi um erro ou supervisão):

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 você não tomar cuidado, isso pode ser propenso a erros.

Se você deseja obter o número de meses entre dois meses, precisa fazer alguns cálculos semelhantes. É isso (com muitas simplificações) o que realmente acontece nos bastidores da aritmética das datas, que felizmente está escondido de nós por meio de funções e operadores já definidos.

Se você precisar de muitas dessas operações, o uso year_monthnão é muito prático. Caso contrário, é uma maneira muito clara de deixar clara sua intenção.


Como alternativa, você pode definir um year_monthtipo e definir um operador year_month+ intervale também outro year_month- year_month... e ocultar os cálculos. Na verdade, nunca fiz um uso tão pesado que senti a necessidade na prática. A date- datena verdade está escondendo algo parecido.


1
Eu escrevi ainda outra maneira de fazer isso =) aproveite.
Evan Carroll

Eu aprecio o how-to, bem como os prós e contras.
phunehehe

4

Como alternativa ao método de joanolo =) (desculpe, eu estava ocupado, mas queria escrever isso)

ALEGRIA BIT

Nós vamos fazer a mesma coisa, mas com bits. Um int4no PostgreSQL é um número inteiro assinado, variando de -2147483648 a +2147483647

Aqui está uma visão geral da nossa estrutura.

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

Armazenando mês.

  • Um mês exige 12 opções pow(2,4)e 4 bits .
  • O resto que dedicamos ao ano, 32-4 = 28 bits .

Aqui está o nosso mapa de bits de onde os meses são armazenados.

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

Meses, 1 de janeiro a 12 de dezembro

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

Anos. Os 28 bits restantes nos permitem armazenar nossas informações do ano

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

Neste ponto, precisamos decidir como queremos fazer isso. Para nossos propósitos, poderíamos usar um deslocamento estático; se precisarmos cobrir apenas 5.000 dC, poderíamos voltar para o 268,430,455 BCque abrange praticamente todo o Mesozóico e tudo o que é útil para avançar.

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

E agora temos os rudimentos do nosso tipo, que devem expirar em 2.700 anos.

Então, vamos trabalhar para fazer algumas funções.

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;

Um teste rápido mostra esse funcionamento ..

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

Agora temos funções que podemos usar em nossos tipos binários.

Poderíamos ter cortado mais um pouco da parte assinada, armazenado o ano como positivo e, depois, classificado naturalmente como um int assinado. Se a velocidade fosse uma prioridade mais alta que o espaço de armazenamento, essa seria a rota que seguimos. Mas, por enquanto, temos uma data que funciona com o mesozóico.

Posso atualizar mais tarde com isso, apenas por diversão.


As faixas ainda não são possíveis, analisarei isso mais tarde.
Evan Carroll

Eu acho que "otimizar ao máximo" faria todo o sentido quando você também faria todas as funções no "nível C baixo". Você economiza o penúltimo bit e o penúltimo nanossegundo ;-) De qualquer forma, alegre! (Ainda me lembro BCD Não necessariamente com alegria..)
joanolo
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.