แยกคอลัมน์เดือนและปีหรือวันที่ที่มีวันตั้งเป็น 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 ล้านวันสำหรับตัวอย่างของเรา นี่คือประมาณ 5,000 แถวเป็นเวลา 200 ปีระหว่างปีพ. ศ. 2444 และปี 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

เป็นอีกทางเลือกหนึ่งสำหรับ Evan Carroll ซึ่งเป็นวิธีที่ฉันคิดว่าน่าจะเป็นตัวเลือกที่ดีที่สุดฉันเคยใช้ในบางโอกาส (และไม่ได้ใช้เป็นพิเศษเมื่อใช้ 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
ฉันเขียนอีกวิธีในการทำ =) สนุกกับมัน
Evan Carroll

ฉันขอขอบคุณวิธีการเช่นเดียวกับข้อดีและข้อเสีย
phunehehe

4

เพื่อเป็นทางเลือกแทนวิธีการของ joanolo =) (ขออภัยที่ฉันไม่ว่าง แต่ต้องการเขียนสิ่งนี้)

BIT JOY

เราจะทำสิ่งเดียวกัน แต่ใช้บิต หนึ่ง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)

ณ จุดนี้เราต้องตัดสินใจว่าเราต้องการทำสิ่งนี้อย่างไร เพื่อจุดประสงค์ของเราเราสามารถใช้ offset แบบคงที่หากเราต้องการครอบคลุม 5,000 AD เท่านั้นเราสามารถย้อนกลับไปได้268,430,455 BCว่าMesozoicมีเนื้อหาครอบคลุมมากเท่าไหร่และทุกอย่างมีประโยชน์ในการก้าวไปข้างหน้า

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

และตอนนี้เรามีพื้นฐานของประเภทของเราที่จะหมดอายุใน 2,700 ปี

งั้นลองทำฟังก์ชั่นกันบ้าง

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

ตอนนี้เรามีฟังก์ชั่นที่เราสามารถใช้กับประเภทไบนารีของเรา ..

เราสามารถตัดอีกเล็กน้อยจากส่วนที่ลงนามแล้วเก็บปีไว้เป็นบวกและจากนั้นก็เรียงลำดับตามธรรมชาติเป็นลายเซ็น ถ้าความเร็วมีความสำคัญสูงกว่าพื้นที่เก็บข้อมูลนั่นจะเป็นเส้นทางที่เราลงไป แต่สำหรับตอนนี้เรามีนัดที่ทำงานร่วมกับ Mesozoic

ฉันอาจอัปเดตในภายหลังด้วยเพื่อความสนุก


ช่วงเป็นไปไม่ได้ฉันจะดูทีหลัง
Evan Carroll

ฉันคิดว่า "การปรับให้เหมาะสมกับบิต" จะทำให้ทุกความรู้สึกเมื่อคุณทำฟังก์ชั่นทั้งหมดใน "ระดับต่ำ C" คุณบันทึกบิต nanosecond ลงไปล่าสุดและสุดท้ายลงไป ;-) อย่างไรก็ตามมีความสุข! (ฉันยังจำ BCD ไม่จำเป็นต้องมีความสุขเสมอไป)
joanolo
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.