Cột tháng và năm riêng biệt, hoặc ngày với ngày luôn được đặt thành 1?


15

Tôi đang xây dựng một cơ sở dữ liệu với Postgres, nơi sẽ có rất nhiều nhóm các thứ monthyearkhông bao giờ bằng date.

  • Tôi có thể tạo số nguyên monthyearcột và sử dụng chúng.
  • Hoặc tôi có thể có một month_yearcột và luôn đặt thành day1.

Cái trước có vẻ đơn giản và rõ ràng hơn nếu ai đó đang xem dữ liệu, nhưng cái sau thì tốt ở chỗ nó sử dụng một loại thích hợp.


1
Hoặc bạn có thể tạo kiểu dữ liệu của riêng mình monthcó chứa hai số nguyên. Nhưng tôi nghĩ rằng nếu bạn không bao giờ, bao giờ cần đến ngày trong tháng, sử dụng hai số nguyên có lẽ dễ dàng hơn
a_horse_with_no_name

1
Bạn nên khai báo phạm vi ngày có thể, số lượng hàng có thể, những gì bạn đang cố gắng tối ưu hóa (lưu trữ, hiệu suất, an toàn, đơn giản?) Và (như mọi khi) phiên bản Postgres của bạn.
Erwin Brandstetter ngày

Câu trả lời:


17

Cá nhân nếu đó là một ngày, hoặc có thể là một ngày, tôi đề nghị luôn luôn lưu trữ nó như một ngày. Nó chỉ dễ dàng hơn để làm việc như là một quy tắc của ngón tay cái.

  • Một ngày là 4 byte.
  • Một smallint là 2 byte (chúng ta cần hai)
    • ... 2 byte: một smallint cho năm
    • ... 2 byte: một smallint cho tháng

Bạn có thể có một ngày sẽ hỗ trợ ngày nếu bạn cần, hoặc một ngày smallinttrong năm và tháng sẽ không bao giờ hỗ trợ thêm độ chính xác.

Dữ liệu mẫu

Bây giờ chúng ta hãy xem một ví dụ .. Hãy tạo 1 triệu ngày cho mẫu của chúng tôi. Con số này xấp xỉ 5.000 hàng trong 200 năm từ 1901 đến 2100. Mỗi năm nên có thứ gì đó cho mỗi tháng.

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;

Kiểm tra

Đơn giản WHERE

Bây giờ chúng ta có thể kiểm tra những lý thuyết về việc không sử dụng ngày này .. Tôi đã chạy từng thứ một vài lần để làm nóng mọi thứ.

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

Bây giờ, chúng ta hãy thử phương pháp khác với chúng riêng biệt

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)

Công bằng mà nói, chúng không phải là tất cả 0,749 .. một số ít nhiều hoặc ít hơn, nhưng nó không thành vấn đề. Tất cả đều tương đối giống nhau. Nó đơn giản là không cần thiết.

Trong vòng một tháng

Bây giờ, hãy vui vẻ với nó .. Giả sử bạn muốn tìm tất cả các khoảng trong vòng 1 tháng của tháng 1 năm 2014 (cùng tháng chúng tôi đã sử dụng ở trên).

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)

So sánh với phương pháp kết hợp

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)

Nó chậm hơn và xấu hơn.

GROUP BY/ORDER BY

Phương pháp kết hợp,

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)

Và một lần nữa với phương pháp tổng hợp

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)

Phần kết luận

Nói chung, hãy để những người thông minh làm công việc khó khăn. DHRath rất khó, khách hàng của tôi không trả đủ tiền cho tôi. Tôi đã từng làm những bài kiểm tra này. Tôi đã rất khó khăn để bao giờ kết luận rằng tôi có thể nhận được kết quả tốt hơn date. Tôi đã ngừng cố gắng.

CẬP NHẬT

@a_horse_with_no_name đề xuất cho thử nghiệm của tôi trong vòng một thángWHERE (year, month) between (2013, 12) and (2014,2) . Theo ý kiến ​​của tôi, trong khi tuyệt vời đó là một truy vấn phức tạp hơn và tôi muốn tránh nó trừ khi có lợi ích. Than ôi, nó vẫn chậm hơn mặc dù nó gần - đó là phần lớn lấy đi từ bài kiểm tra này. Nó đơn giản là không quan trọng lắm.

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
Không giống như một số RDBMS khác (Xem trang 45 của use-the-index-luke.com/blog/2013-07/ ,), Postgres cũng hỗ trợ đầy đủ quyền truy cập chỉ mục với các giá trị hàng: stackoverflow.com/a/34291099/939860 Nhưng đó là một sang một bên, tôi hoàn toàn đồng ý: datelà cách để đi trong hầu hết các trường hợp.
Erwin Brandstetter ngày

5

Thay thế cho phương pháp được đề xuất của Evan Carroll, mà tôi cho là có thể là lựa chọn tốt nhất, tôi đã sử dụng trong một số trường hợp (và không đặc biệt khi sử dụng PostgreQuery) chỉ là một year_monthcột, loại INTEGER(4 byte), được tính như

 year_month = year * 100 + month

Nghĩa là, bạn mã hóa tháng theo hai chữ số thập phân ngoài cùng bên phải (chữ số 0 và chữ số 1) của số nguyên và năm trên các chữ số từ 2 đến 5 (hoặc nhiều hơn, nếu cần).

Ở một mức độ nào đó, đây cách thay thế của một người nghèo để xây dựng year_monthkiểu và nhà khai thác của riêng bạn . Nó có một số lợi thế, chủ yếu là "sự rõ ràng của ý định" và một số tiết kiệm không gian (không phải trong PostgreQuery, tôi nghĩ vậy), và cũng có một số bất tiện, khi có hai cột riêng biệt.

Bạn có thể đảm bảo rằng các giá trị là hợp lệ bằng cách chỉ cần thêm một

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

Bạn có thể có một WHEREmệnh đề trông như:

year_month BETWEEN 201610 and 201702 

và nó hoạt động hiệu quả (tất nhiên nếu year_monthcột được lập chỉ mục đúng).

Bạn có thể nhóm theo year_monthcùng một cách bạn có thể làm điều đó với một ngày và với cùng hiệu quả (ít nhất là).

Nếu bạn cần tách riêng yearmonth, việc tính toán rất đơn giản:

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

Điều gì bất tiện : nếu bạn muốn thêm 15 tháng cho một year_monthbạn phải tính toán (nếu tôi không phạm sai lầm hoặc giám sát):

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

Nếu bạn không cẩn thận, điều này có thể dễ bị lỗi.

Nếu bạn muốn lấy số tháng trong khoảng từ hai năm_months, bạn cần thực hiện một số tính toán tương tự. Đó là (với rất nhiều đơn giản hóa) những gì thực sự xảy ra trong số mũ với số học ngày, may mắn được ẩn khỏi chúng ta thông qua các hàm và toán tử đã được xác định.

Nếu bạn cần nhiều thao tác này, việc sử dụng year_monthkhông quá thực tế. Nếu bạn không, đó là một cách rất rõ ràng để làm cho ý định của bạn rõ ràng.


Thay vào đó, bạn có thể xác định một year_monthloại và xác định toán tử year_month+ interval, và một loại khác year_month- year_month... và ẩn các phép tính. Tôi thực sự chưa bao giờ sử dụng nhiều như vậy để cảm thấy sự cần thiết trong thực tế. A date- datethực sự đang che giấu bạn một cái gì đó tương tự.


1
Tôi đã viết một cách khác để làm điều này =) tận hưởng nó.
Evan Carroll

Tôi đánh giá cao cách làm cũng như ưu và nhược điểm.
phunehehe

4

Thay thế cho phương pháp của joanolo =) (xin lỗi tôi đã bận nhưng muốn viết bài này)

BIT JOY

Chúng ta sẽ làm điều tương tự, nhưng với bit. Một int4trong PostgreSQL là một số nguyên có chữ ký, từ -2147483648 đến +2147483647

Dưới đây là một cái nhìn tổng quan, về cấu trúc của chúng tôi.

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

Tháng dự trữ.

  • Một tháng yêu cầu 12 tùy chọn pow(2,4)4 bit .
  • Phần còn lại chúng tôi dành cho năm, 32-4 = 28 bit .

Đây là bản đồ bit của chúng tôi về nơi lưu trữ tháng.

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

Tháng, 1 tháng 1 - 12 tháng 12

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

Năm 28 bit còn lại cho phép chúng tôi lưu trữ thông tin năm của chúng tôi

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

Tại thời điểm này, chúng tôi cần phải quyết định cách chúng tôi muốn làm điều này. Đối với mục đích của chúng tôi, chúng tôi có thể sử dụng một phần bù tĩnh, nếu chúng tôi chỉ cần trang trải 5.000 AD, chúng tôi có thể quay trở lại với 268,430,455 BCphần lớn bao gồm toàn bộ Mesozoi và mọi thứ hữu ích tiến về phía trước.

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

Và, bây giờ chúng ta có những sơ hở của loại hình của chúng ta, sẽ hết hạn sau 2.700 năm.

Vì vậy, hãy bắt tay vào thực hiện một số chức năng.

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;

Một thử nghiệm nhanh cho thấy điều này làm việc ..

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

Bây giờ chúng ta có các hàm mà chúng ta có thể sử dụng trên các loại nhị phân của mình ..

Chúng tôi có thể đã cắt thêm một bit từ phần đã ký, lưu trữ năm là dương và sau đó sắp xếp nó một cách tự nhiên như một int đã ký. Nếu tốc độ là ưu tiên cao hơn không gian lưu trữ, thì đó sẽ là tuyến đường chúng tôi đi xuống. Nhưng bây giờ, chúng tôi có một ngày làm việc với Mesozoi.

Tôi có thể cập nhật sau đó, chỉ để cho vui.


Phạm vi chưa thể, tôi sẽ xem xét điều đó sau.
Evan Carroll

Tôi nghĩ rằng "tối ưu hóa bit" sẽ có ý nghĩa khi bạn cũng thực hiện tất cả các chức năng ở "mức C thấp". Bạn lưu bit xuống đến cuối cùng và xuống nano giây cuối cùng ;-) Dù sao đi nữa, rất vui! (Tôi vẫn nhớ BCD. Không nhất thiết phải có niềm vui.)
joanolo
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.