Ayrı ay ve yıl sütunları veya gün her zaman 1 olarak ayarlanmış tarih?


15

Postgres ile bir şeyler inşa ediyorum ve bir sürü şey olacak monthve yearasla date.

  • Tam sayı monthve yearsütunlar oluşturabilir ve bunları kullanabilirim.
  • Veya bir month_yearsütun alabilir ve her zaman day1 olarak ayarlayabilir .

Birisi verilere bakıyorsa, birincisi biraz daha basit ve net görünüyor, ancak ikincisi uygun bir tür kullanması açısından güzel.


1
Veya monthiki tamsayı içeren kendi veri türünüzü oluşturabilirsiniz. Ama bence asla, güne hiç ihtiyacınız yoksa, iki tamsayı kullanmak muhtemelen daha kolay
a_horse_with_no_name

1
Olası tarih aralığını, olası satır sayısını, neyi optimize etmeye çalıştığınızı (depolama, performans, güvenlik, basitlik?) Ve (her zamanki gibi) Postgres sürümünüzü bildirmelisiniz.
Erwin Brandstetter

Yanıtlar:


17

Şahsen bir tarih veya bir tarih olabilirse, her zaman bir olarak saklamanızı öneririm. Genel bir kural olarak çalışmak daha kolaydır.

  • Bir tarih 4 bayttır.
  • Bir smallint 2 bayttır (ikiye ihtiyacımız var)
    • ... 2 bayt: yıl için bir küçük
    • ... 2 bayt: ay için bir küçük

İhtiyacınız olursa günü destekleyecek bir tarihe veya smallintekstra hassasiyeti asla desteklemeyecek yıl ve ay için bir tarihe sahip olabilirsiniz .

Örnek veri

Şimdi bir örneğe bakalım .. Örneğimiz için 1 milyon tarih oluşturalım. Bu, 1901 ile 2100 arasında 200 yıl boyunca yaklaşık 5.000 satırdır. Her yıl her ay için bir şey olmalıdır.

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;

Test yapmak

Basit WHERE

Şimdi bu tarihi kullanmama teorilerini test edebiliriz .. Her şeyi ısıtmak için her birini birkaç kez çalıştırdım.

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

Şimdi diğer yöntemi ayrı ayrı deneyelim

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)

Adil olmak gerekirse, hepsi 0.749 değil .. bazıları biraz az ya da çok, ama önemli değil. Hepsi nispeten aynı. Sadece gerekli değil.

Bir ay içinde

Şimdi onunla eğlenelim .. Diyelim ki Ocak 2014'ün 1 ayı içinde (yukarıda kullandığımız ay) tüm aralıkları bulmak istiyorsunuz.

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)

Bunu kombine yöntemle karşılaştırın

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)

Hem daha yavaş hem de daha çirkin.

GROUP BY/ORDER BY

Kombine yöntem,

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)

Ve yine kompozit yöntemle

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)

Sonuç

Genellikle akıllı insanların zor işleri yapmasına izin verin. Datemath zor, müşterilerim bana yeterince ödeme yapmıyor. Bu testleri yapardım. Bundan daha iyi sonuçlar alabileceğime karar vermekte zorlandım date. Denemeyi bıraktım.

GÜNCEL

@a_horse_with_no_name bir ay içinde yaptığım test için önerildi WHERE (year, month) between (2013, 12) and (2014,2). Bence, bu serin iken bu daha karmaşık bir sorgu ve bir kazanç olmadıkça bunu önlemek istiyorum. Ne yazık ki, hala yakın olsa da daha yavaştı - ki bu testten daha fazla uzaklaşıyor. Çok önemli değil.

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
Diğer bazı RDBMS'den farklı olarak (bkz. Kullanım-the- index-luke.com / blog / 2013-07/… sayfa 45 ), Postgres satır değerleriyle dizin erişimini de tamamen destekler: stackoverflow.com/a/34291099/939860 Ancak bu bir kenara, tamamen katılıyorum: dateçoğu durumda gitmenin yolu budur.
Erwin Brandstetter

5

Muhtemelen en iyi seçenek olduğunu düşündüğüm Evan Carroll önerilen yönteme alternatif olarak, bazı durumlarda (ve özellikle PostgreSQL kullanırken değil) yalnızca (4 bayt) year_monthtüründe bir sütunINTEGER

 year_month = year * 100 + month

Yani, ayı , tamsayı sayısının en sağdaki iki ondalık basamağına (0 rakamı ve 1 rakamı) ve 2 ile 5 arasındaki rakamlara (veya gerekirse daha fazla) kodlarsınız.

Bu, bir dereceye kadar, fakir bir adamın kendi year_monthtipinizi ve operatörlerinizi inşa etmenin alternatifidir . Bazı avantajları, çoğunlukla "niyet netliği" ve bazı yer tasarrufu (PostgreSQL'de değil bence) ve ayrıca iki ayrı sütuna sahip olmaktan kaynaklanan bazı rahatsızlıklar var.

Yalnızca bir değer ekleyerek değerlerin geçerli olduğunu garanti edebilirsiniz.

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

WHEREŞunlara benzer bir cümleniz olabilir :

year_month BETWEEN 201610 and 201702 

ve verimli bir şekilde çalışır (eğer year_monthsütun düzgün bir şekilde dizine eklenmişse, elbette).

year_monthBir tarihle ve aynı verimlilikle (en azından) yapabileceğiniz şekilde gruplayabilirsiniz .

Ayırmanız gerekiyorsa yearve monthhesaplama basittir:

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

Ne var sakıncalı : Bir 15 ay eklemek istiyorsanız year_monthsize (ben bir hata veya gözetim yapmadığınızdan varsa) hesaplamak için vardır:

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

Dikkatli değilseniz, bu hataya eğilimli olabilir.

İki yıl_ay arasındaki ay sayısını almak istiyorsanız, benzer hesaplamalar yapmanız gerekir. Tarih aritmetiği ile kaputun altında gerçekten olan, zaten tanımlanmış fonksiyonlar ve operatörler aracılığıyla şansımızdan gizlenen şey (çok sayıda basitleştirme ile).

Bu işlemlerin çoğuna ihtiyacınız varsa, kullanmak year_monthçok pratik değildir. Eğer yapmazsanız, niyetinizi açıklığa kavuşturmanın çok açık bir yoludur.


Alternatif olarak, bir year_monthtür tanımlayabilir ve bir operatör year_month+ intervalve ayrıca başka bir year_month- year_month... tanımlayabilir ve hesaplamaları gizleyebilirsiniz. Aslında pratikte ihtiyacı hissetmek için bu kadar ağır bir kullanım yapmadım. A date- dateaslında size benzer bir şey saklıyor.


1
Bunu yapmak için başka bir yol yazdım =) tadını çıkarın.
Evan Carroll

Artıları ve eksileri yanı sıra nasıl yapılır takdir ediyorum.
phunehehe

4

Joanolo'nun yöntemine alternatif olarak =) (üzgünüm meşguldüm ama bunu yazmak istedim)

BIT JOY

Aynı şeyi yapacağız, ama bitlerle. int4PostgreSQL'de bir tanesi -2147483648 ile +2147483647 arasında değişen işaretli bir tamsayıdır

İşte yapımızın bir özeti.

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

Depolama ayı.

  • Bir ay 12 seçenek gerektirir pow(2,4)olan 4 bit .
  • Kalan yıla ayırıyoruz, 32-4 = 28 bit .

İşte ayların nerede saklandığına dair bit haritamız.

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

Aylar, 1 Ocak - 12 Ara

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

Yıllar. Kalan 28 bit, yıl bilgilerimizi saklamamızı sağlar

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

Bu noktada bunu nasıl yapmak istediğimize karar vermeliyiz. Biz sadece 5,000 AD'yi kapsayacak şekilde gerekirse bizim amacımız için, biz geri gidebiliriz, statik ofset kullanabilirsiniz 268,430,455 BChangi hemen hemen kapaklar tamamı Mezozoik ve her şey yararlı hareketli ileriye.

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

Ve şimdi, 2.700 yıl içinde sona erecek türümüzün ilkeleri var.

Öyleyse bazı işlevler üzerinde çalışmaya başlayalım.

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;

Hızlı bir test bu çalışmayı gösterir ..

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

Şimdi ikili tiplerimizde kullanabileceğimiz fonksiyonlarımız var.

İmzalı kısımdan bir parça daha kesebilir, yılı pozitif olarak depolayabilir ve daha sonra doğal olarak imzalı bir int olarak sıralayabilirdik. Eğer hız depolama alanından daha yüksek önceliğe sahip olsaydı, bu yoldan aşağı inerdik. Ancak şimdilik Mesozoyik ile çalışan bir randevumuz var.

Daha sonra bunun için güncelleyebilirim, sadece eğlence için.


Aralıklar henüz mümkün değil, daha sonra bakacağım.
Evan Carroll

Ben "düşük seviye C" de tüm fonksiyonları yapmak zaman "biraz optimize etmek" mantıklı olacağını düşünüyorum. Nihayet bit ve son nanosaniye ;-) Her neyse, neşeli! (
BCD'yi
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.