Окремі стовпці місяця та року або дата з днем ​​завжди встановлено на 1?


15

Я лад бази даних Postgres , де буде багато групування речей по monthі year, але ніколи за date.

  • Я міг би створити цілі monthчи yearстовпці і використовувати їх.
  • Або я міг би мати month_yearстовпчик і завжди встановлювати значення day1.

Перший здається трохи простішим і зрозумілішим, якщо хтось переглядає дані, але останній приємний тим, що використовує належний тип.


1
Або ви можете створити власний тип даних, monthякий містить два цілі числа. Але я думаю, якщо вам ніколи і ніколи не потрібен день місяця, скористатися двома цілими числами, мабуть, простіше
a_horse_with_no_name

1
Вам слід оголосити можливий діапазон дат, можливу кількість рядків, що ви намагаєтеся оптимізувати (зберігання, продуктивність, безпека, простота?) Та (як завжди) свою версію Postgres.
Erwin Brandstetter

Відповіді:


17

Особисто, якщо це дата або це може бути дата, я пропоную завжди зберігати її як одну. З цим просто легше працювати, як правило.

  • Дата - 4 байти.
  • Невеличкий байт - 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)

Висновок

Як правило, нехай розумні люди роблять важку роботу. Datemath важкий, мої клієнти мені не платять достатньо. Я раніше робив ці тести. Мені важко було прийти до висновку, що я можу отримати кращі результати, ніж 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
На відміну від деяких інших RDBMS (див. Стор. 45 використання use-the-index-luke.com/blog/2013-07/… ), Postgres також повністю підтримує доступ до індексу зі значеннями рядків: stackoverflow.com/a/34291099/939860 Але це осторонь я повністю згоден: dateце шлях у більшості випадків.
Erwin Brandstetter

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_monthне надто практично. Якщо ви цього не зробите, це ясно зрозуміти свій намір.


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


1
Я написав ще один спосіб зробити це =) насолоджуйтесь.
Еван Керролл

Я ціную як практичні, так і плюси та мінуси.
phunehehe

4

Як альтернатива методу joanolo =) (вибачте, що я був зайнятий, але хотів написати це)

БІТЬ РОБОТА

Ми будемо робити те ж саме, але з бітами. Один 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. Якби швидкість була вищим пріоритетом, ніж простір для зберігання, це був би маршрут, яким ми йдемо вниз. Але наразі у нас є дата, яка працює з мезозоєм.

Я можу пізніше оновити це, просто заради розваги.


Діапазони поки неможливі, я розгляну це пізніше.
Еван Керролл

Я думаю, що "оптимізація до біта" мала б сенс, коли ви також зробили б усі функції на "С низькому рівні". Ви економите до останнього шматочка і до останнього наносекунди ;-) Як би там не було, радісно! (Я все ще пам’ятаю БХД. Не обов’язково з радістю.)
joanolo
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.