Pisahkan kolom bulan dan tahun, atau tanggal dengan hari selalu diatur ke 1?

15

Saya sedang membangun database dengan Postgres di mana akan ada banyak pengelompokan hal oleh monthdan year, tetapi tidak pernah oleh date.

  • Saya bisa membuat bilangan bulat monthdan yearkolom dan menggunakannya.
  • Atau saya dapat memiliki month_yearkolom dan selalu mengatur dayke 1.

Yang pertama tampak sedikit lebih sederhana dan lebih jelas jika seseorang melihat data, tetapi yang terakhir bagus karena menggunakan tipe yang tepat.

David N. Welton
sumber
1
Atau Anda bisa membuat tipe data Anda sendiri monthyang berisi dua bilangan bulat. Tapi saya pikir jika Anda tidak pernah membutuhkan hari dalam sebulan, menggunakan dua bilangan bulat mungkin lebih mudah
a_horse_with_no_name
1
Anda harus mendeklarasikan kisaran tanggal yang memungkinkan, jumlah baris yang mungkin, apa yang ingin Anda optimalkan (penyimpanan, kinerja, keamanan, kesederhanaan?) Dan (seperti biasa) versi Postgres Anda.
Erwin Brandstetter

Jawaban:

17

Secara pribadi jika ini adalah kencan, atau dapat berupa kencan, saya sarankan untuk selalu menyimpannya sebagai satu. Ini lebih mudah untuk dikerjakan sebagai aturan praktis.

  • Tanggal adalah 4 byte.
  • Smallint adalah 2 byte (kita butuh dua)
    • ... 2 byte: satu smallint untuk tahun
    • ... 2 byte: satu smallint selama sebulan

Anda dapat memiliki satu tanggal yang akan mendukung hari jika Anda membutuhkannya, atau satu smallintuntuk tahun dan bulan yang tidak akan pernah mendukung ketepatan ekstra.

Contoh data

Mari kita lihat contoh sekarang .. Mari kita buat 1 juta tanggal untuk sampel kita. Ini sekitar 5.000 baris selama 200 tahun antara 1901, dan 2100. Setiap tahun harus memiliki sesuatu untuk setiap bulan.

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;

Pengujian

Sederhana WHERE

Sekarang kita dapat menguji teori-teori ini tentang tidak menggunakan tanggal. Saya menjalankan masing-masing beberapa kali untuk menghangatkannya.

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

Sekarang, mari kita coba metode lain yang terpisah

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)

Dalam keadilan, mereka tidak semua 0,749 .. ada yang sedikit lebih atau kurang, tetapi tidak masalah. Semuanya relatif sama. Itu tidak diperlukan.

Dalam satu bulan

Sekarang, mari bersenang-senang dengannya .. Katakanlah Anda ingin mencari semua interval dalam 1 bulan Januari 2014 (bulan yang sama dengan yang kami gunakan di atas).

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)

Bandingkan dengan metode gabungan

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)

Keduanya lebih lambat, dan lebih jelek.

GROUP BY/ORDER BY

Metode gabungan,

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)

Dan lagi dengan metode komposit

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)

Kesimpulan

Secara umum, biarkan orang pintar melakukan kerja keras. Datemath sulit, klien saya tidak membayar saya cukup. Saya biasa melakukan tes ini. Saya sulit sekali menyimpulkan bahwa saya bisa mendapatkan hasil yang lebih baik daripada date. Saya berhenti berusaha.

PEMBARUAN

@a_horse_with_no_name disarankan untuk pengujian saya dalam satu bulanWHERE (year, month) between (2013, 12) and (2014,2) . Menurut pendapat saya, walaupun keren itu permintaan yang lebih kompleks dan saya lebih suka menghindarinya kecuali ada keuntungan. Sayangnya, itu masih lebih lambat meskipun sudah dekat - yang lebih mengambil dari tes ini. Itu tidak masalah.

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)
Evan Carroll
sumber
4
Tidak seperti RDBMS lainnya (Lihat halaman 45 of use-the-index-luke.com/blog/2013-07/... ), Postgres juga sepenuhnya mendukung akses indeks dengan nilai baris: stackoverflow.com/a/34291099/939860 Tapi itu adalah selain itu, saya sepenuhnya setuju: dateadalah cara untuk pergi dalam banyak kasus.
Erwin Brandstetter
5

Sebagai alternatif untuk metode yang diusulkan Evan Carroll, yang saya anggap mungkin pilihan terbaik, saya telah menggunakan dalam beberapa kesempatan (dan tidak khusus ketika menggunakan PostgreSQL) hanya year_monthkolom, tipe INTEGER(4 byte), dihitung sebagai

 year_month = year * 100 + month

Artinya, Anda menyandikan bulan pada dua digit desimal paling kanan (digit 0, dan digit 1) dari angka integer, dan tahun pada digit 2 hingga 5 (atau lebih, jika diperlukan).

Ini, sampai batas tertentu, adalah alternatif orang miskin untuk membangun year_monthtipe dan operator Anda sendiri . Itu punya beberapa keuntungan, sebagian besar "kejelasan niat", dan beberapa penghematan ruang (tidak dalam PostgreSQL, saya pikir), dan juga beberapa ketidaknyamanan, karena memiliki dua kolom terpisah.

Anda dapat menjamin bahwa nilai-nilai itu valid dengan hanya menambahkan a

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

Anda dapat memiliki WHEREklausa yang terlihat seperti:

year_month BETWEEN 201610 and 201702 

dan itu bekerja secara efisien (jika year_monthkolom diindeks dengan benar, tentu saja).

Anda dapat mengelompokkan dengan year_monthcara yang sama Anda bisa melakukannya dengan kencan, dan dengan efisiensi yang sama (setidaknya).

Jika Anda perlu memisahkan yeardan month, perhitungannya mudah:

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

Apa yang tidak nyaman : jika Anda ingin menambahkan 15 bulan ke year_monthAnda harus menghitung (jika saya tidak membuat kesalahan atau pengawasan):

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

Jika Anda tidak hati-hati, ini bisa menjadi kesalahan.

Jika Anda ingin mendapatkan jumlah bulan antara dua tahun_bulan, Anda perlu melakukan beberapa perhitungan serupa. Itulah (dengan banyak penyederhanaan) yang sebenarnya terjadi di bawah tenda dengan aritmatika tanggal, yang untungnya disembunyikan dari kami melalui fungsi dan operator yang telah ditentukan.

Jika Anda membutuhkan banyak operasi ini, penggunaannya year_monthtidak terlalu praktis. Jika tidak, itu adalah cara yang sangat jelas untuk memperjelas niat Anda.


Sebagai alternatif, Anda bisa mendefinisikan year_monthtipe, dan mendefinisikan operator year_month+ interval, dan juga yang lain year_month- year_month... dan menyembunyikan kalkulasi. Sebenarnya saya tidak pernah menggunakan sebanyak itu untuk merasakan kebutuhan dalam latihan. A date- datesebenarnya menyembunyikan sesuatu yang mirip padamu.

joanolo
sumber
1
Saya menulis cara lain untuk melakukan ini =) menikmatinya.
Evan Carroll
Saya menghargai bagaimana-untuk serta pro dan kontra.
phunehehe
4

Sebagai alternatif dari metode joanolo =) (maaf saya sibuk tetapi ingin menulis ini)

BIT JOY

Kita akan melakukan hal yang sama, tetapi dengan bit. Satu int4di PostgreSQL adalah bilangan bulat yang ditandatangani, mulai dari -2147483648 hingga +2147483647

Berikut ini gambaran umum dari struktur kami.

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

Menyimpan bulan.

  • Sebulan membutuhkan 12 opsi pow(2,4)adalah 4 bit .
  • Sisanya kami curahkan untuk tahun ini, 32-4 = 28 bit .

Berikut ini adalah peta bit tempat penyimpanan bulan.

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

Bulan, 1-Jan - 12 Des

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

Bertahun-tahun Sisa 28 bit memungkinkan kita untuk menyimpan informasi tahun kita

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

Pada titik ini kita perlu memutuskan bagaimana kita ingin melakukan ini. Untuk keperluan kita, kita dapat menggunakan offset statis, jika kita hanya perlu mencakup 5.000 AD, kita bisa kembali ke 268,430,455 BCyang cukup banyak mencakup keseluruhan Mesozoikum dan segala sesuatu yang berguna bergerak maju.

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

Dan, sekarang kami memiliki dasar-dasar dari tipe kami, yang akan kedaluwarsa dalam 2.700 tahun.

Jadi mari kita mulai bekerja membuat beberapa fungsi.

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;

Tes cepat menunjukkan ini berfungsi ..

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

Sekarang kita memiliki fungsi yang dapat kita gunakan pada tipe biner kita ..

Kita bisa memotong satu bit lagi dari bagian yang ditandatangani, menyimpan tahun sebagai positif, dan kemudian memilahnya secara alami sebagai int yang ditandatangani. Jika kecepatan adalah prioritas yang lebih tinggi daripada ruang penyimpanan, itu akan menjadi rute yang kita lewati. Tetapi untuk saat ini, kami memiliki tanggal yang berfungsi dengan Mesozoikum.

Saya dapat memperbarui nanti dengan itu, hanya untuk bersenang-senang.

Evan Carroll
sumber
Kisaran belum memungkinkan, saya akan melihatnya nanti.
Evan Carroll
Saya pikir "mengoptimalkan bit" akan masuk akal ketika Anda juga akan membuat semua fungsi dalam "level rendah C". Anda menghemat sedikit-ke-yang-terakhir dan nanosecond-ke-yang-terakhir ;-) Bagaimanapun, gembira! (Saya masih ingat BCD. Belum tentu dengan sukacita.)
joanolo