ละเว้นเขตเวลาทั้งหมดใน Rails และ PostgreSQL


164

ฉันกำลังจัดการกับวันที่และเวลาใน Rails และ Postgres และพบกับปัญหานี้:

ฐานข้อมูลอยู่ใน UTC

ผู้ใช้ตั้งค่าเขตเวลาที่เลือกในแอพ Rails แต่จะใช้เมื่อรับเวลาท้องถิ่นของผู้ใช้เพื่อเปรียบเทียบเวลาเท่านั้น

ผู้ใช้เก็บเวลาหนึ่งพูด 17 มีนาคม 2012 เวลา 19.00 น. ฉันไม่ต้องการให้มีการแปลงเขตเวลาหรือเขตเวลาที่จะจัดเก็บ ฉันแค่ต้องการบันทึกวันที่และเวลา ด้วยวิธีนี้หากผู้ใช้เปลี่ยนเขตเวลามันจะยังคงแสดง 17 มีนาคม 2012, 19:00

ฉันใช้เขตเวลาที่ผู้ใช้ระบุเพื่อรับบันทึก 'ก่อนหน้า' หรือ 'หลัง' เวลาปัจจุบันในเขตเวลาท้องถิ่นของผู้ใช้

ขณะนี้ฉันใช้ 'การประทับเวลาที่ไม่มีเขตเวลา' แต่เมื่อฉันดึงข้อมูลระเบียนทางรถไฟ (?) จะแปลงให้เป็นเขตเวลาในแอปที่ฉันไม่ต้องการ

Appointment.first.time
 => Fri, 02 Mar 2012 19:00:00 UTC +00:00 

เนื่องจากระเบียนในฐานข้อมูลดูเหมือนว่าจะออกมาในรูปแบบ UTC แฮ็คของฉันคือใช้เวลาปัจจุบันลบเขตเวลาด้วย 'Date.strptime (str, "% m /% d /% Y")' จากนั้นทำตาม ค้นหาด้วย:

.where("time >= ?", date_start)

ดูเหมือนว่าจะต้องมีวิธีที่ง่ายกว่าในการเพิกเฉยเขตเวลาทั่ว ความคิดใด ๆ

คำตอบ:


347

ชนิดข้อมูลเป็นชื่อสั้นtimestamp ๆ ตัวเลือกอื่น ๆสั้นสำหรับtimestamp without time zone
timestamptztimestamp with time zone

timestamptzเป็นประเภทที่ต้องการในตระกูลวันที่ / เวลาแท้จริง ได้typispreferredตั้งค่าไว้ในpg_typeซึ่งสามารถเกี่ยวข้อง:

ที่เก็บข้อมูลภายในและยุค

ภายในเวลาประทับมีพื้นที่เก็บข้อมูล8 ไบต์บนดิสก์และใน RAM เป็นค่าจำนวนเต็มที่แทนจำนวน microseconds จาก Postgres epoch, 2000-01-01 00:00:00 UTC

Postgres ยังมีในตัวของความรู้ที่ใช้กันทั่วไปUNIX เวลาวินาทีนับจากยุคยูนิกซ์ 1970/01/01 00:00:00 UTC และการใช้ฟังก์ชั่นว่าในหรือto_timestamp(double precision)EXTRACT(EPOCH FROM timestamptz)

รหัสแหล่งที่มา:

* Timestamps รวมถึงช่วงเวลา h / m / s ถูกเก็บไว้เป็น
* ค่า int64 พร้อมหน่วยไมโครวินาที กาลครั้งหนึ่งนานมาแล้ว  
* ค่าสองเท่าของหน่วยวินาที)

และ:

/ * Julian-date เทียบเท่ากับวันที่ 0 ใน Unix และ Postgres การคำนวณ * /  
#define UNIX_EPOCH_JDATE 2440588 / * == date2j (1970, 1, 1) * /  
#define POSTGRES_EPOCH_JDATE 2451545 / * == date2j (2000, 1, 1) * /  

ความละเอียดไมโครวินาทีแปลได้สูงสุด 6 หลักสำหรับเศษเสี้ยววินาที

timestamp

ค่าที่พิมพ์ตามที่บอก Postgres ว่าไม่มีการระบุเขตเวลาอย่างชัดเจน เขตเวลาปัจจุบันจะถือว่า Postgres ละเว้นตัวปรับเปลี่ยนเขตเวลาที่เพิ่มโดยไม่ได้ตั้งใจtimestamp [without time zone]

ไม่มีการเลื่อนชั่วโมงสำหรับการแสดงผล ด้วยการตั้งค่าเขตเวลาเดียวกันทั้งหมดจึงเป็นเรื่องปกติ สำหรับเขตเวลาที่แตกต่างกันการตั้งค่าความหมายเปลี่ยนแปลง แต่ค่าและการแสดงผลยังคงเหมือนเดิม

timestamptz

การจัดการที่timestamp with time zoneแตกต่างกันอย่างละเอียด ฉันพูดคู่มือที่นี่ :

สำหรับtimestamp with time zoneค่าที่เก็บไว้ภายในจะเป็นUTC เสมอ ( เวลาสากลเชิงพิกัด ... )

เหมืองเน้นหนัก โซนเวลาตัวเองจะไม่ถูกเก็บไว้ มันเป็นตัวดัดแปลงอินพุตที่ใช้ในการคำนวณเวลา UTC ตามที่จัดเก็บ - หรือและตัวดัดแปลงเอาท์พุทที่ใช้ในการคำนวณเวลาท้องถิ่นที่จะแสดง - ด้วยออฟเซ็ตเขตเวลาต่อท้าย หากคุณไม่ผนวกออฟเซ็ตสำหรับtimestamptzon อินพุตการตั้งค่าโซนเวลาปัจจุบันของเซสชันจะถือว่า การคำนวณทั้งหมดเสร็จสิ้นด้วยค่าเวลาประทับ UTC หากคุณมี (หรืออาจจะต้อง) timestamptzการจัดการกับโซนเวลามากกว่าหนึ่งการใช้งาน

ไคลเอนต์เช่น psql หรือ pgAdmin หรือแอปพลิเคชันใด ๆ ที่สื่อสารผ่านlibpq (เช่น Ruby กับ pg gem) จะแสดงด้วยการประทับเวลาบวกกับออฟเซ็ตสำหรับเขตเวลาปัจจุบันหรือตามโซนเวลาที่ร้องขอ (ดูด้านล่าง) มันเป็นจุดเดิมเสมอเวลารูปแบบการแสดงแตกต่างกันไปเท่านั้น หรือตามที่คู่มือวางไว้ :

วันที่และเวลาที่ทราบเขตเวลาทั้งหมดจะถูกเก็บไว้ภายใน UTC พวกเขาจะถูกแปลงเป็นเวลาท้องถิ่นในโซนที่ระบุโดย พารามิเตอร์การกำหนดค่าTimeZoneก่อนที่จะแสดงให้ลูกค้า

ลองพิจารณาตัวอย่างง่าย ๆ นี้ (ใน psql):

db = # SELECT timestamptz '2012-03-05 20:00 +03 ';
      timestamptz
------------------------
 2012-03-05 18:00:00 +01

เหมืองเน้นหนัก เกิดอะไรขึ้นที่นี่?
ฉันเลือกเขตเวลาโดยพลการ+3สำหรับตัวอักษรอินพุต เพื่อ Postgres 2012-03-05 17:00:00นี้เป็นเพียงหนึ่งในหลายวิธีที่จะป้อนข้อมูลการประทับเวลา ผลลัพธ์ของแบบสอบถามจะปรากฏขึ้นสำหรับการตั้งค่าโซนเวลาปัจจุบันเวียนนา / ออสเตรียในการทดสอบของฉันซึ่งมีการชดเชย+1ในช่วงฤดูหนาวและ+2ในช่วงฤดูร้อน: 2012-03-05 18:00:00+01เพราะมันอยู่ในช่วงฤดูหนาว

Postgres ลืมวิธีการป้อนค่านี้ไปแล้ว ทั้งหมดที่จำได้คือค่าและชนิดข้อมูล เช่นเดียวกับตัวเลขทศนิยม numeric '003.4', numeric '3.40'หรือnumeric '+3.4'- ผลทั้งหมดในมูลค่าภายในเดียวกันแน่นอน

AT TIME ZONE

ทันทีที่คุณเข้าใจตรรกะนี้คุณสามารถทำอะไรก็ได้ที่คุณต้องการ สิ่งที่ขาดหายไปในตอนนี้เป็นเครื่องมือในการแปลหรือแสดงตัวอักษรเวลาประทับตามโซนเวลาที่ระบุ นั่นคือสิ่งที่AT TIME ZONEโครงสร้างเข้ามามีสองกรณีใช้ที่แตกต่างกัน timestamptzถูกแปลงเป็นtimestampและในทางกลับกัน

ในการเข้าสู่ UTC timestamptz 2012-03-05 17:00:00+0:

SELECT timestamp '2012-03-05 17:00:00' AT TIME ZONE 'UTC'

... ซึ่งเทียบเท่ากับ:

SELECT timestamptz '2012-03-05 17:00:00 UTC'

หากต้องการแสดงจุดในเวลาเดียวกันเป็น EST timestamp(เวลามาตรฐานตะวันออก):

SELECT timestamp '2012-03-05 17:00:00' AT TIME ZONE 'UTC' AT TIME ZONE 'EST'

ที่เหมาะสมเป็นครั้งที่สองAT TIME ZONE 'UTC' คนแรกที่ตีความtimestampคุ้มค่าเป็น (รับ) UTC timestamptzประทับเวลากลับชนิด อันที่สองแปลงtimestamptzไปเป็นtimestampในเขตเวลาที่กำหนด 'EST' - นาฬิกาในเขตเวลา EST แสดงเวลาที่ไม่ซ้ำกันในเวลานี้

ตัวอย่าง

SELECT ts AT TIME ZONE 'UTC'
FROM  (
   VALUES
      (1, timestamptz '2012-03-05 17:00:00+0')
    , (2, timestamptz '2012-03-05 18:00:00+1')
    , (3, timestamptz '2012-03-05 17:00:00 UTC')
    , (4, timestamp   '2012-03-05 11:00:00'  AT TIME ZONE '+6') 
    , (5, timestamp   '2012-03-05 17:00:00'  AT TIME ZONE 'UTC') 
    , (6, timestamp   '2012-03-05 07:00:00'  AT TIME ZONE 'US/Hawaii')  -- 
    , (7, timestamptz '2012-03-05 07:00:00 US/Hawaii')                  -- 
    , (8, timestamp   '2012-03-05 07:00:00'  AT TIME ZONE 'HST')        -- 
    , (9, timestamp   '2012-03-05 18:00:00+1')  --  loaded footgun!
      ) t(id, ts);

ผลตอบแทนที่ 8 (หรือ 9) เหมือนแถวกับคอลัมน์ timestamptz 2012-03-05 17:00:00ถือประทับเวลา แถวที่ 9 เกิดขึ้นเพื่อทำงานในเขตเวลาของฉัน แต่เป็นกับดักที่ชั่วร้าย ดูด้านล่าง

ows แถวที่ 6 - 8 ที่มีชื่อเขตเวลาและตัวย่อเขตเวลาสำหรับเวลาฮาวายนั้นขึ้นอยู่กับ DST (ปรับเวลาตามฤดูกาล) และอาจแตกต่างกันไป ชื่อเขตเวลาเช่น'US/Hawaii'นั้นทราบถึงกฎ DST และการเปลี่ยนแปลงทางประวัติศาสตร์ทั้งหมดโดยอัตโนมัติในขณะที่ตัวย่อเช่นHSTนั้นเป็นเพียงรหัสใบ้สำหรับออฟเซ็ตคงที่ คุณอาจจำเป็นต้องใช้ตัวย่ออื่นสำหรับฤดูร้อน / เวลามาตรฐาน ชื่ออย่างถูกต้องตีความใด ๆประทับเวลาที่เขตเวลาที่กำหนด ย่อมีราคาถูก แต่จะต้องมีการหนึ่งที่เหมาะสมสำหรับการประทับเวลาที่กำหนด:

เวลาออมแสงไม่ได้เป็นความคิดที่เจิดจ้าที่สุดเท่าที่มนุษย์เคยมีมา

②แถว 9 ที่ทำเครื่องหมายว่าเป็นปืนพกเท้าโหลดนั้นเหมาะกับฉันแต่เป็นเรื่องบังเอิญเท่านั้น ถ้าคุณโยนอย่างชัดเจนตัวอักษรเพื่อtimestamp [without time zone], เขตเวลาใดก็ได้ชดเชยจะถูกละเว้น ! ใช้การประทับเวลาเปลือยเท่านั้น จากนั้นค่าจะถูกรวมเข้าtimestamptzกับตัวอย่างเพื่อให้ตรงกับประเภทคอลัมน์ สำหรับขั้นตอนนี้การtimezoneตั้งค่าของเซสชันปัจจุบันจะถือว่าเป็นเขตเวลาเดียวกัน+1ในกรณีของฉัน (ยุโรป / เวียนนา) แต่อาจไม่ใช่ในกรณีของคุณ - ซึ่งจะส่งผลให้ค่าที่แตกต่าง กล่าวโดยย่อ: อย่าโยนtimestamptzตัวอักษรลงไปtimestampหรือคุณจะสูญเสียเขตเวลาชดเชย

คำถามของคุณ

ผู้ใช้เก็บเวลาหนึ่งพูด 17 มีนาคม 2012 เวลา 19.00 น. ฉันไม่ต้องการให้มีการแปลงเขตเวลาหรือเขตเวลาที่จะจัดเก็บ

เขตเวลาจะไม่ถูกจัดเก็บ ใช้วิธีใดวิธีหนึ่งข้างต้นเพื่อป้อนเวลาประทับ UTC

ฉันใช้เขตเวลาที่ผู้ใช้ระบุเพื่อรับบันทึก 'ก่อนหน้า' หรือ 'หลัง' เวลาปัจจุบันในเขตเวลาท้องถิ่นของผู้ใช้

คุณสามารถใช้หนึ่งแบบสอบถามสำหรับลูกค้าทั้งหมดในเขตเวลาที่แตกต่างกัน
สำหรับเวลาสากลทั้งหมด:

SELECT * FROM tbl WHERE time_col > (now() AT TIME ZONE 'UTC')::time

สำหรับเวลาตามเวลาท้องถิ่น:

SELECT * FROM tbl WHERE time_col > now()::time

ไม่เบื่อกับข้อมูลพื้นหลังใช่ไหม? มีมากในคู่มือ


2
รายละเอียดเล็ก ๆ น้อย ๆ แต่ฉันคิดว่าการประทับเวลาจะถูกเก็บไว้ภายในเป็นจำนวนไมโครวินาทีตั้งแต่ 2000-01-01 - ดูส่วนประเภทข้อมูลวันที่ / เวลาของคู่มือ การตรวจสอบแหล่งที่มาของฉันเองดูเหมือนจะยืนยัน แปลกที่จะใช้ต้นกำเนิดที่แตกต่างกันสำหรับยุค!
อันตรายเมื่อ

2
@harmic สำหรับยุคที่แตกต่าง ... จริงๆแล้วไม่แปลกเลย นี้วิกิพีเดียหน้ารายการสองโหล epochs ใช้โดยระบบคอมพิวเตอร์ต่างๆ ในขณะที่ยุค Unixเป็นเรื่องปกติมันไม่ได้เป็นเพียงคนเดียว
Basil Bourque

4
@ErwinBrandstetter นี่คือคำตอบที่ดียกเว้นข้อบกพร่องร้ายแรงหนึ่งข้อ ตามความเห็นที่เป็นอันตราย Postgres ไม่ได้ใช้เวลา Unix ตามเอกสาร : (a) ยุคคือ 2001-01-01 มากกว่าของ Unix '1970-01-01 และ (b) ในขณะที่เวลา Unix มีความละเอียดทั้งวินาที, Postgres เก็บเศษเสี้ยววินาที จำนวนของตัวเลขเศษส่วนขึ้นอยู่กับตัวเลือกเวลาในการรวบรวม: 0 ถึง 6 เมื่อใช้ที่เก็บข้อมูลจำนวนเต็มแปดไบต์ (ค่าเริ่มต้น) หรือจาก 0 ถึง 10 เมื่อใช้หน่วยความจำแบบลอยตัว (คัดค้าน)
Basil Bourque

2
@BasilBourque: ฉันรู้ถึงความผิดพลาดที่โชคร้ายนี้ หากคุณไม่เป็นไรคุณก็ยินดีที่จะแก้ไข ฉันได้เห็นคำตอบของคุณในอดีตและคุณทำได้ดี อีกหนึ่งการแก้ไขจากฉันจะบังคับให้เรื่องนี้เป็นวิกิของชุมชน - เมื่อเวลาผ่านไปฉันได้ใช้ความพยายามอย่างมาก (และการแก้ไข) เพื่อทำให้ชัดเจนและครอบคลุม
Erwin Brandstetter

2
แก้ไข:ในความคิดเห็นก่อนหน้านี้ของฉันฉันไม่ถูกต้องอ้าง Postgres ยุค 2001 เป็นจริงๆแล้วมันเป็น2000
Basil Bourque

1

หากคุณต้องการจัดการใน UTC โดยค่าเริ่มต้น:

ในconfig/application.rbเพิ่ม:

config.time_zone = 'UTC'

จากนั้นถ้าคุณเก็บชื่อเขตเวลาของผู้ใช้ปัจจุบันคือcurrent_user.timezoneคุณสามารถพูดได้

post.created_at.in_time_zone(current_user.timezone)

current_user.timezoneควรเป็นชื่อเขตเวลาที่ถูกต้องมิฉะนั้นคุณจะได้รับการArgumentError: Invalid Timezoneดูรายการเต็มรูปแบบ

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.