Отдельные столбцы месяца и года или дата с днем ​​всегда установлена ​​в 1?

15

Я строй базы данных Postgres , где будет много группирования вещей по monthи year, но никогда за date.

  • Я мог бы создать целое число monthи yearстолбцы и использовать их.
  • Или я мог бы иметь month_yearстолбец и всегда устанавливать в day1.

Первый выглядит немного проще и понятнее, если кто-то смотрит на данные, но последний приятен тем, что использует правильный тип.

Дэвид Н. Уэлтон
источник
1
Или вы можете создать свой собственный тип данных, monthкоторый содержит два целых числа. Но я думаю, что если вам никогда не понадобится день месяца, возможно, проще использовать два целых числа
a_horse_with_no_name
1
Вы должны указать возможный диапазон дат, возможное количество строк, то, что вы пытаетесь оптимизировать (хранение, производительность, безопасность, простота?) И (как всегда) вашу версию Postgres.
Эрвин Брандштеттер

Ответы:

17

Лично, если это дата или может быть дата, я предлагаю всегда хранить ее как единое целое. С этим просто легче работать, как правило.

  • Дата составляет 4 байта.
  • Smallint - 2 байта (нам нужно два)
    • ... 2 байта: одна строчка на год
    • ... 2 байта: одна строчная буква за месяц

У вас может быть одна дата, которая будет поддерживать день, если она вам когда-либо понадобится, или одна smallintдля года и месяца, которая никогда не будет поддерживать дополнительную точность.

Образец данных

Давайте посмотрим на пример сейчас. Давайте создадим 1 миллион дат для нашего образца. Это примерно 5000 строк за 200 лет между 1901 и 2100 годами. Каждый год должно быть что-то на каждый месяц.

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;

тестирование

просто WHERE

Теперь мы можем проверить эти теории неиспользования даты. Я запускал каждую из них несколько раз, чтобы согреться.

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

Теперь давайте попробуем другой метод с ними отдельно

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)

Справедливости ради, они не все 0,749 .. некоторые немного более или менее, но это не имеет значения. Они все относительно одинаковы. Это просто не нужно.

В течение одного месяца

Теперь давайте повеселимся. Допустим, вы хотите найти все интервалы в течение 1 месяца с января 2014 года (того же месяца, который мы использовали выше).

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)

Сравните это с комбинированным методом

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)

Это и медленнее, и страшнее.

GROUP BY/ORDER BY

Комбинированный метод,

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)

И снова с композитным методом

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)

Вывод

В общем, пусть умные люди делают тяжелую работу. Свидания трудны, мои клиенты не платят мне достаточно. Я имел обыкновение делать эти тесты. Мне было трудно когда-либо прийти к выводу, что я могу получить лучшие результаты, чем date. Я перестал пытаться

ОБНОВЛЕНИЕ

@a_horse_with_no_name предлагается для моего теста в течение одного месяцаWHERE (year, month) between (2013, 12) and (2014,2) . На мой взгляд, хотя это круто, это более сложный запрос, и я бы предпочел его избегать, если бы не было выигрыша. Увы, это было все еще медленнее, хотя это близко - что является большим количеством взятия от этого теста. Это просто не имеет большого значения.

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
В отличие от некоторых других СУБД (см. Стр. 45 в use-the-index-luke.com/blog/2013-07/… ), Postgres также полностью поддерживает доступ к индексам со значениями строк: stackoverflow.com/a/34291099/939860 Но это Кроме того, я полностью согласен: dateэто путь в большинстве случаев.
Эрвин Брандштеттер
5

В качестве альтернативы предложенному Эвану Кэрроллу методу, который я считаю, вероятно, лучшим вариантом, я использовал в некоторых случаях (и не особенно при использовании PostgreSQL) только year_monthстолбец типа INTEGER(4 байта), вычисляемый как

 year_month = year * 100 + month

То есть вы кодируете месяц двумя правыми десятичными цифрами (цифра 0 и цифра 1) целого числа, а год - цифрами от 2 до 5 (или более, если необходимо).

Это, в некоторой степени, альтернатива бедняку для создания вашего собственного year_monthтипа и операторов. У него есть некоторые преимущества, в основном «ясность намерений» и некоторая экономия места (я думаю, не в PostgreSQL), а также некоторые неудобства по сравнению с наличием двух отдельных столбцов.

Вы можете гарантировать, что значения действительны, просто добавив

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

Вы можете иметь WHEREпункт, похожий на:

year_month BETWEEN 201610 and 201702 

и это работает эффективно (если year_monthстолбец, конечно, правильно проиндексирован).

Вы можете группировать по year_monthтому же принципу, что и по дате, и с той же эффективностью (по крайней мере).

Если вам нужно разделить yearи month, вычисление является простым:

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

Что неудобно : если вы хотите добавить 15 месяцев к year_monthвычислению, вы должны вычислить (если я не сделал ошибку или упущение):

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

Если вы не будете осторожны, это может привести к ошибкам.

Если вы хотите узнать количество месяцев между двумя year_months, вам нужно сделать несколько похожих вычислений. Это (с большим количеством упрощений) то, что действительно происходит в тайне с арифметикой дат, к счастью, скрыто от нас через уже определенные функции и операторы.

Если вам нужно много этих операций, использование year_monthне слишком практично. Если вы этого не сделаете, это очень четкий способ прояснить ваши намерения.


В качестве альтернативы вы можете определить year_monthтип, определить оператор year_month+ interval, а также другой year_month- year_month... и скрыть вычисления. Я на самом деле никогда не использовал так много, чтобы чувствовать необходимость на практике. А date- dateна самом деле скрывает что-то подобное.

joanolo
источник
1
Я написал еще один способ сделать это =) наслаждайтесь.
Эван Кэрролл
Я ценю инструкции, а также плюсы и минусы.
phunehehe
4

Как альтернатива методу Жоаноло =) (извините, я был занят, но хотел написать это)

БИТ РАДОСТЬ

Мы собираемся сделать то же самое, но с кусочками. Один int4в PostgreSQL является целым числом со знаком, в диапазоне от -2147483648 до +2147483647

Вот краткий обзор нашей структуры.

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

Запоминание месяца.

  • Месяц требует 12 вариантов, pow(2,4)это 4 бита .
  • Остальное мы посвящаем году, 32-4 = 28 бит .

Вот наша битовая карта, где хранятся месяцы.

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

Месяцы, 1 января - 12 декабря

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

Годы. Оставшиеся 28 бит позволяют нам хранить информацию о годе

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

На данный момент нам нужно решить, как мы хотим это сделать. Для наших целей мы могли бы использовать статическое смещение, если нам нужно покрыть только 5000 г. н.э., мы могли бы вернуться к тому, 268,430,455 BCчто в значительной степени охватывает весь мезозой и все полезное, двигаясь вперед.

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

И теперь у нас есть зачатки нашего типа, срок действия которых истекает через 2700 лет.

Итак, давайте приступим к созданию некоторых функций.

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;

Быстрый тест показывает, что это работает ..

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

Теперь у нас есть функции, которые мы можем использовать в наших двоичных типах.

Мы могли бы вырезать еще один бит из подписанной части, сохранить год как положительный, а затем, естественно, отсортировать его как подписанный int. Если бы скорость была более высоким приоритетом, чем пространство для хранения, это был бы путь, по которому мы шли. Но пока у нас есть свидание, которое работает с мезозой.

Я могу обновить это позже, просто для удовольствия.

Эван Кэрролл
источник
Диапазоны пока не возможны, я посмотрю на это позже.
Эван Кэрролл
Я думаю, что «оптимизация до мелочей» имела бы смысл, если бы вы делали все функции на «низком уровне C». Вы экономите до последнего бита и до последней наносекунды ;-) В любом случае, радостно! (Я до сих пор помню BCD. Не обязательно с радостью.)
Joanolo