ฟิลด์วันที่และเวลาของ MySQL และเวลาออมแสง - ฉันจะอ้างอิงชั่วโมง“ พิเศษ” ได้อย่างไร


89

ฉันใช้เขตเวลาอเมริกา / นิวยอร์ก ในฤดูใบไม้ร่วงเรา "ถอยกลับ" หนึ่งชั่วโมง - "ได้รับ" อย่างมีประสิทธิภาพหนึ่งชั่วโมงตอนตีสอง เมื่อถึงจุดเปลี่ยนสิ่งต่อไปนี้จะเกิดขึ้น:

01:59:00 น. -04: 00 น.
จากนั้น 1 นาทีจะกลายเป็น:
01:00:00 -05: 00 น

ดังนั้นหากคุณเพียงแค่พูดว่า "01:30 น." ก็ไม่ชัดเจนว่าคุณกำลังอ้างถึงครั้งแรกที่ 1:30 หมุนรอบหรือครั้งที่สองหรือไม่ ฉันกำลังพยายามบันทึกข้อมูลการจัดกำหนดการลงในฐานข้อมูล MySQL และไม่สามารถกำหนดวิธีการบันทึกเวลาได้อย่างเหมาะสม

นี่คือปัญหา:
"2009-11-01 00:30:00" ถูกจัดเก็บไว้ภายในเมื่อ 2009-11-01 00:30:00 -04: 00
"2009-11-01 01:30:00" ถูกเก็บไว้ภายในเป็น 2552-11-01 01:30:00 -05: 00 น

นี่เป็นสิ่งที่ดีและเป็นธรรม แต่อย่างไรฉันบันทึกอะไรที่จะ 01:30:00 -04: 00 ? เอกสารไม่ได้แสดงการสนับสนุนใด ๆ สำหรับการระบุชดเชยและดังนั้นเมื่อฉันได้พยายามระบุชดเชยจะได้รับการละเว้นรับรองสำเนาถูกต้อง

วิธีแก้ปัญหาเดียวที่ฉันคิดว่าเกี่ยวข้องกับการตั้งค่าเซิร์ฟเวอร์เป็นเขตเวลาที่ไม่ใช้เวลาออมแสงและทำการเปลี่ยนแปลงที่จำเป็นในสคริปต์ของฉัน (ฉันใช้ PHP สำหรับสิ่งนี้) แต่ดูเหมือนว่ามันจะไม่จำเป็น

ขอบคุณมากสำหรับคำแนะนำใด ๆ


ฉันไม่รู้เกี่ยวกับ MySQL หรือ PHP มากพอที่จะสร้างคำตอบที่สอดคล้องกัน แต่ฉันจะพนันได้เลยว่ามันเกี่ยวข้องกับการแปลงเป็นและจาก UTC
Mark Ransom

2
ภายในทั้งหมดจะถูกจัดเก็บเป็น UTC ใช่หรือไม่?
Eli

4
ฉันพบว่าweb.ivy.net/~carton/rant/MySQL-timezones.txtน่าสนใจในหัวข้อนี้
micahwittman

ลิงค์ดี micahwittman - เป็นประโยชน์มาก
Aaron

คำถามที่ดี ปัญหาทั่วไป
Vardumper

คำตอบ:


47

ประเภทวันที่ของ MySQL ตรงไปตรงมาเสียและไม่สามารถจัดเก็บเวลาทั้งหมดได้อย่างถูกต้องเว้นแต่ระบบของคุณจะตั้งค่าเป็นเขตเวลาออฟเซ็ตคงที่เช่น UTC หรือ GMT-5 (ฉันใช้ MySQL 5.0.45)

นี้เป็นเพราะคุณไม่สามารถเก็บเวลาใด ๆ ในระหว่างชั่วโมงก่อนเวลาออมแสงที่ปลาย ไม่ว่าคุณจะป้อนวันที่ด้วยวิธีใดฟังก์ชันวันที่ทุกครั้งจะปฏิบัติต่อเวลาเหล่านี้ราวกับว่าอยู่ในช่วงชั่วโมงหลังสวิตช์

America/New_Yorkเขตเวลาระบบของฉันคือ มาลองเก็บ 1257051600 (อา. 01 พ.ย. 2552 06:00:00 +0100)

นี่คือการใช้ไวยากรณ์ INTERVAL ที่เป็นกรรมสิทธิ์:

SELECT UNIX_TIMESTAMP('2009-11-01 00:00:00' + INTERVAL 3599 SECOND); # 1257051599
SELECT UNIX_TIMESTAMP('2009-11-01 00:00:00' + INTERVAL 3600 SECOND); # 1257055200

SELECT UNIX_TIMESTAMP('2009-11-01 01:00:00' - INTERVAL 1 SECOND); # 1257051599
SELECT UNIX_TIMESTAMP('2009-11-01 01:00:00' - INTERVAL 0 SECOND); # 1257055200

แม้FROM_UNIXTIME()จะไม่คืนเวลาที่ถูกต้อง

SELECT UNIX_TIMESTAMP(FROM_UNIXTIME(1257051599)); # 1257051599
SELECT UNIX_TIMESTAMP(FROM_UNIXTIME(1257051600)); # 1257055200

ผิดปกติ DATETIME จะยังคงจัดเก็บและส่งคืน (ในรูปแบบสตริงเท่านั้น!) ครั้งภายในชั่วโมงที่ "สูญหาย" เมื่อ DST เริ่มทำงาน (เช่น2009-03-08 02:59:59) แต่การใช้วันที่เหล่านี้ในฟังก์ชัน MySQL มีความเสี่ยง:

SELECT UNIX_TIMESTAMP('2009-03-08 01:59:59'); # 1236495599
SELECT UNIX_TIMESTAMP('2009-03-08 02:00:00'); # 1236495600
# ...
SELECT UNIX_TIMESTAMP('2009-03-08 02:59:59'); # 1236495600
SELECT UNIX_TIMESTAMP('2009-03-08 03:00:00'); # 1236495600

สิ่งที่ต้องซื้อกลับบ้าน: หากคุณต้องการจัดเก็บและเรียกคืนทุกครั้งในปีคุณมีตัวเลือกที่ไม่พึงปรารถนาบางประการ:

  1. ตั้งค่าเขตเวลาของระบบเป็น GMT + ออฟเซ็ตคงที่ เช่น UTC
  2. จัดเก็บวันที่เป็น INT (ตามที่ Aaron ค้นพบ TIMESTAMP ไม่น่าเชื่อถือแม้แต่น้อย)

  3. แกล้งทำเป็นประเภท DATETIME มีเขตเวลาออฟเซ็ตคงที่ เช่นหากคุณเข้ามาAmerica/New_Yorkให้แปลงวันที่ของคุณเป็น GMT-5 นอก MySQLจากนั้นจัดเก็บเป็น DATETIME (สิ่งนี้มีความสำคัญ: ดูคำตอบของ Aaron) จากนั้นคุณต้องใช้ความระมัดระวังเป็นอย่างยิ่งโดยใช้ฟังก์ชันวันที่ / เวลาของ MySQL เนื่องจากบางคนคิดว่าค่าของคุณเป็นเขตเวลาของระบบส่วนอื่น ๆ (โดยเฉพาะฟังก์ชันเลขคณิตเวลา) เป็น "เขตเวลาไม่เชื่อเรื่องพระเจ้า" (อาจทำงานราวกับว่าเวลาเป็น UTC)

แอรอนและฉันสงสัยว่าคอลัมน์ TIMESTAMP ที่สร้างขึ้นโดยอัตโนมัติก็เสียเช่นกัน ทั้งสอง2009-11-01 01:30 -0400และจะถูกเก็บไว้เป็นความคลุมเครือ2009-11-01 01:30 -05002009-11-01 01:30


ขอบคุณสำหรับความช่วยเหลือของคุณเกี่ยวกับ mrclay นี้ คุณสรุปสถานการณ์ได้ถูกต้องมาก
Aaron

ดูเหมือนว่าตัวเลือกที่ 3 จะปลอดภัยกว่าสำหรับการคำนวณเวลาเนื่องจาก (ดูเหมือนว่า) มีการใช้งานฟังก์ชันก่อนที่จะเพิ่มฟังก์ชัน DST เช่น TIMEDIFF ('2009-11-01 02:30:00', '2009-11-01 00:30:00') ส่งคืน 2:00 ซึ่งถูกต้องสำหรับ UTC แต่ในอเมริกา / New_York เวลาคือ 3 ชั่วโมง ห่างกัน
Steve Clay

1
-1: คุณทำผิดพลาดที่ฟังก์ชันวันที่ / เวลาของ MySQL ทำงานในประเภท DATETIME ซึ่งไม่เชื่อเรื่องพระเจ้า อาร์กิวเมนต์ที่คุณกำลังผ่านไป UNIX_TIMSTAMP ดังนั้นจึงเป็นเรื่องซึ่งเป็นselect '2009-11-01 00:00:00' + INTERVAL 3600 SECOND; '2009-11-01 01:00:00'UNIX_TIMESTAMP จากนั้นก็พยายามปกปิดสิ่งนี้เป็น UTC ในบริบทของเขตเวลาเซสชัน - ไม่พยายามดำเนินการเพิ่มเติมในบริบทของกฎ DST ของเขตเวลานั้น
kbro

@kbro ตกลง แต่ปัญหายังคงอยู่ ถ้าเซสชัน tz เป็นAmerica/New_Yorkฉันไม่เห็นวิธีที่จะจัดเก็บ 1257051600 คุณ?
Steve Clay

78

ฉันได้คิดออกเพื่อจุดประสงค์ของฉัน ฉันจะสรุปสิ่งที่ฉันได้เรียนรู้ (ขออภัยบันทึกเหล่านี้เป็นรายละเอียดพวกมันมากพอ ๆ กับการอ้างอิงในอนาคตของฉันเหมือนอย่างอื่น)

ตรงกันข้ามกับสิ่งที่ฉันพูดในความคิดเห็นก่อนหน้านี้ช่อง DATETIME และ TIMESTAMP จะทำงานแตกต่างกัน ช่อง TIMESTAMP (ตามที่เอกสารระบุ) รับสิ่งที่คุณส่งในรูปแบบ "YYYY-MM-DD hh: mm: ss" และแปลงจากเขตเวลาปัจจุบันของคุณเป็นเวลา UTC การย้อนกลับเกิดขึ้นอย่างโปร่งใสทุกครั้งที่คุณดึงข้อมูล ช่อง DATETIME ไม่ทำให้เกิด Conversion นี้ พวกเขานำสิ่งที่คุณส่งไปและจัดเก็บไว้โดยตรง

ทั้งชนิดเขตข้อมูลการประทับเวลา DATETIME มิได้ถูกต้องสามารถเก็บข้อมูลในเขตที่ตั้งข้อสังเกต DST หากคุณจัดเก็บ "2009-11-01 01:30:00" ช่องจะไม่มีทางแยกได้ว่าคุณต้องการเวอร์ชันใดของ 01:30 น. - เวอร์ชัน -04: 00 หรือ -05: 00 น.

ตกลงดังนั้นเราต้องจัดเก็บข้อมูลของเราในเขตเวลาที่ไม่ใช่ DST (เช่น UTC) ช่อง TIMESTAMP ไม่สามารถจัดการข้อมูลนี้ได้อย่างถูกต้องด้วยเหตุผลที่ฉันจะอธิบาย: หากระบบของคุณตั้งค่าเป็นเขตเวลา DST สิ่งที่คุณใส่ไว้ใน TIMESTAMP อาจไม่ใช่สิ่งที่คุณได้รับคืน แม้ว่าคุณจะส่งข้อมูลที่แปลงเป็น UTC ไปแล้ว แต่ก็ยังถือว่าข้อมูลอยู่ในเขตเวลาท้องถิ่นของคุณและทำการแปลงเป็น UTC อีกครั้ง การเดินทางไปกลับในท้องถิ่นถึง UTC-back-to-local ที่บังคับใช้ TIMESTAMP นี้จะสูญเสียไปเมื่อเขตเวลาท้องถิ่นของคุณสังเกต DST (ตั้งแต่ "2009-11-01 01:30:00" จะจับคู่เวลาที่เป็นไปได้ 2 แบบ)

ด้วย DATETIME คุณสามารถจัดเก็บข้อมูลของคุณในเขตเวลาใดก็ได้ที่คุณต้องการและมั่นใจได้ว่าคุณจะได้รับสิ่งที่คุณส่งกลับมา (คุณไม่ได้ถูกบังคับให้ต้องรับการแปลงแบบไปกลับที่สูญเสียซึ่ง TIMESTAMP จะส่งผลให้คุณ) ดังนั้นวิธีแก้ปัญหาคือใช้ฟิลด์ DATETIME และก่อนที่จะบันทึกลงในฟิลด์ให้แปลงจากโซนเวลาระบบของคุณเป็นโซนที่ไม่ใช่ DST ที่คุณต้องการบันทึกไว้ (ฉันคิดว่า UTC น่าจะเป็นตัวเลือกที่ดีที่สุด) สิ่งนี้ช่วยให้คุณสร้างตรรกะการแปลงเป็นภาษาสคริปต์ของคุณเพื่อให้คุณสามารถบันทึกค่า UTC ที่เทียบเท่ากับ "2009-11-01 01:30:00 -04: 00" หรือ "" 2009-11-01 01:30 ได้อย่างชัดเจน: 00 -05: 00 น.”.

สิ่งสำคัญอีกอย่างที่ควรทราบคือฟังก์ชันคณิตศาสตร์วันที่ / เวลาของ MySQL ทำงานไม่ถูกต้องตามขอบเขต DST หากคุณจัดเก็บวันที่ของคุณใน DST TZ ดังนั้นเหตุผลอื่น ๆ ทั้งหมดที่จะบันทึกใน UTC

สรุปตอนนี้ฉันทำสิ่งนี้:

เมื่อดึงข้อมูลจากฐานข้อมูล:

ตีความข้อมูลจากฐานข้อมูลอย่างชัดเจนว่าเป็น UTC นอก MySQL เพื่อให้ได้เวลาประทับของ Unix ที่ถูกต้อง ฉันใช้ฟังก์ชัน strtotime () ของ PHP หรือคลาส DateTime สำหรับสิ่งนี้ ไม่สามารถทำได้อย่างน่าเชื่อถือภายใน MySQL โดยใช้ฟังก์ชัน CONVERT_TZ () หรือ UNIX_TIMESTAMP () ของ MySQL เนื่องจาก CONVERT_TZ จะแสดงเฉพาะค่า 'YYYY-MM-DD hh: mm: ss' ซึ่งมีปัญหาด้านความคลุมเครือและ UNIX_TIMESTAMP () จะถือว่า อินพุตอยู่ในเขตเวลาของระบบไม่ใช่เขตเวลาที่ข้อมูลถูกเก็บไว้ใน (UTC) จริง

เมื่อจัดเก็บข้อมูลลงในฐานข้อมูล:

แปลงวันที่ของคุณเป็นเวลา UTC ที่แม่นยำที่คุณต้องการนอก MySQL ตัวอย่างเช่นด้วยคลาส DateTime ของ PHP คุณสามารถระบุ "2009-11-01 1:30:00 EST" ให้แตกต่างจาก "2009-11-01 1:30:00 EDT" จากนั้นแปลงเป็น UTC และบันทึกเวลา UTC ที่ถูกต้อง ไปยังช่อง DATETIME ของคุณ

วุ้ย. ขอบคุณมากสำหรับข้อมูลและความช่วยเหลือของทุกคน หวังว่าสิ่งนี้จะช่วยให้คนอื่นปวดหัวได้บ้าง

BTW ฉันเห็นสิ่งนี้ใน MySQL 5.0.22 และ 5.0.27


13

ฉันคิดว่าลิงก์ของ micahwittman เป็นทางออกที่ดีที่สุดสำหรับข้อ จำกัด MySQL เหล่านี้: ตั้งค่าเขตเวลาเซสชันเป็น UTC เมื่อคุณเชื่อมต่อ:

SET SESSION time_zone = '+0:00'

จากนั้นคุณก็ส่งการประทับเวลา Unix ไปและทุกอย่างจะเรียบร้อย


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

4

เธรดนี้ทำให้ฉันประหลาดเนื่องจากเราใช้TIMESTAMPคอลัมน์ที่มีOn UPDATE CURRENT_TIMESTAMP(เช่น:) recordTimestamp timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMPเพื่อติดตามบันทึกที่เปลี่ยนแปลงและ ETL ไปยัง datawarehouse

ในกรณีที่มีคนสงสัยในกรณีนี้ให้TIMESTAMPปฏิบัติอย่างถูกต้องและคุณสามารถแยกความแตกต่างระหว่างวันที่สองวันที่คล้ายกันได้โดยการแปลงการTIMESTAMPประทับเวลาเป็น unix:

select TestFact.*, UNIX_TIMESTAMP(recordTimestamp) from TestFact;

id  recordTimestamp         UNIX_TIMESTAMP(recordTimestamp)
1   2012-11-04 01:00:10.0   1352005210
2   2012-11-04 01:00:10.0   1352008810

3

แต่ฉันจะบันทึกอะไรก็ได้ถึง 01:30:00 -04: 00 น.

คุณสามารถแปลงเป็น UTC เช่น:

SELECT CONVERT_TZ('2009-11-29 01:30:00','-04:00','+00:00');


ยิ่งไปกว่านั้นวันที่บันทึกเป็นTIMESTAMPฟิลด์ ซึ่งจะเก็บไว้ใน UTC เสมอและ UTC ไม่ทราบเกี่ยวกับเวลาฤดูร้อน / ฤดูหนาว

คุณสามารถแปลงจาก UTC เป็นเวลาท้องถิ่นโดยใช้CONVERT_TZ :

SELECT CONVERT_TZ(UTC_TIMESTAMP(),'+00:00','SYSTEM');

โดยที่ '+00: 00' คือ UTC เขตเวลาจากและ 'SYSTEM' คือเขตเวลาท้องถิ่นของระบบปฏิบัติการที่ MySQL ทำงาน


ขอบคุณสำหรับการตอบกลับ อย่างที่ดีที่สุดที่ฉันสามารถบอกได้แม้ว่าเอกสารจะพูดอะไรฟิลด์ TIMESTAMP และ Datetime ก็ทำงานเหมือนกัน: พวกเขาจัดเก็บข้อมูลใน UTC แต่คาดว่าข้อมูลที่ป้อนจะเป็นเวลาท้องถิ่นและจะแปลงเป็น UTC โดยอัตโนมัติหากฉันแปลงเป็น UTC ก่อนอื่นฐานข้อมูลไม่ทราบว่าฉันทำอย่างนั้นและเพิ่ม 4 (หรือ 5 ขึ้นอยู่กับว่าเราเป็น DST) เป็นชั่วโมงมากขึ้น ปัญหายังคงอยู่: ฉันจะระบุ 2009-11-01 01:30:00 -04: 00 เป็นอินพุตได้อย่างไร
Aaron

ฉันได้ค้นพบว่าที่มาของความสับสนส่วนใหญ่ของฉันคือความจริงที่ว่าฟังก์ชัน UNIX_TIMESTAMP () จะตีความพารามิเตอร์วันที่โดยสัมพันธ์กับเขตเวลาปัจจุบันเสมอไม่ว่าคุณจะดึงข้อมูลจากฟิลด์ TIMESTAMP หรือ DATETIME . สิ่งนี้ทำให้รู้สึกว่าตอนนี้ฉันคิดเกี่ยวกับเรื่องนี้ ฉันจะอัปเดตเพิ่มเติมในภายหลัง
Aaron

2

Mysql แก้ปัญหานี้โดยเนื้อแท้โดยใช้ตาราง time_zone_name จาก mysql db ใช้ CONVERT_TZ ในขณะที่ CRUD เพื่ออัปเดตวันที่และเวลาโดยไม่ต้องกังวลเกี่ยวกับเวลาออมแสง

SELECT
  CONVERT_TZ('2019-04-01 00:00:00','Europe/London','UTC') AS time1,
  CONVERT_TZ('2019-03-01 00:00:00','Europe/London','UTC') AS time2;

1

ฉันกำลังดำเนินการบันทึกจำนวนการเข้าชมของเพจและแสดงจำนวนในกราฟ (โดยใช้ปลั๊กอิน Flot jQuery) ฉันกรอกข้อมูลการทดสอบในตารางและทุกอย่างดูดี แต่ฉันสังเกตว่าในตอนท้ายของกราฟคะแนนจะหยุดลงหนึ่งวันตามป้ายกำกับบนแกน x หลังจากการตรวจสอบฉันสังเกตเห็นว่าจำนวนการดูสำหรับวันที่ 2015-10-25 ถูกดึงข้อมูลสองครั้งจากฐานข้อมูลและส่งต่อไปยัง Flot ดังนั้นทุกวันหลังจากวันนี้จะถูกย้ายไปทางขวาหนึ่งวัน
หลังจากค้นหาจุดบกพร่องในโค้ดของฉันมาระยะหนึ่งฉันก็รู้ว่าวันนี้เป็นวันที่ DST เกิดขึ้น จากนั้นฉันก็มาที่หน้า SO นี้ ...
... แต่วิธีแก้ปัญหาที่แนะนำนั้นเกินความจำเป็นสำหรับสิ่งที่ฉันต้องการหรือมีข้อเสียอื่น ๆ ฉันไม่กังวลมากนักว่าจะไม่สามารถแยกแยะระหว่างการประทับเวลาที่ไม่ชัดเจน ฉันแค่ต้องนับและแสดงบันทึกต่อวัน

ก่อนอื่นฉันเรียกดูช่วงวันที่:

SELECT 
    DATE(MIN(created_timestamp)) AS min_date, 
    DATE(MAX(created_timestamp)) AS max_date 
FROM page_display_log
WHERE item_id = :item_id

จากนั้นในการวนซ้ำเริ่มต้นด้วยmin_dateลงท้ายด้วยmax_dateทีละขั้นตอนของวันเดียว ( 60*60*24) ฉันกำลังเรียกดูจำนวน:

for( $day = $min_date_timestamp; $day <= $max_date_timestamp; $day += 60 * 60 * 24 ) {
    $query = "
        SELECT COUNT(*) AS count_per_day
        FROM page_display_log
        WHERE 
            item_id = :item_id AND
            ( 
                created_timestamp BETWEEN 
                '" . date( "Y-m-d 00:00:00", $day ) . "' AND
                '" . date( "Y-m-d 23:59:59", $day ) . "'
            )
    ";
    //execute query and do stuff with the result
}

ฉันทางออกสุดท้ายและรวดเร็วเพื่อฉันปัญหาเป็นแบบนี้:

$min_date_timestamp += 60 * 60 * 2; // To avoid DST problems
for( $day = $min_date_timestamp; $day <= $max_da.....

ดังนั้นฉันไม่ได้จ้องมองห่วงในการเริ่มต้นของวัน แต่สองชั่วโมงต่อมา วันนี้ยังคงเหมือนเดิมและฉันยังคงเรียกดูจำนวนที่ถูกต้องเนื่องจากฉันขอบันทึกจากฐานข้อมูลอย่างชัดเจนระหว่าง 00:00:00 ถึง 23:59:59 น. ของวันโดยไม่คำนึงถึงเวลาจริงของการประทับเวลา และเมื่อเวลาผ่านไปหนึ่งชั่วโมงฉันก็ยังอยู่ในวันที่ถูกต้อง

หมายเหตุ: ฉันรู้ว่านี่เป็นเธรดเก่า 5 ปีและฉันรู้ว่านี่ไม่ใช่คำตอบสำหรับคำถาม OPs แต่อาจช่วยให้คนอย่างฉันที่พบหน้านี้กำลังมองหาวิธีแก้ปัญหาที่ฉันอธิบายไว้


อาจไม่เกี่ยวข้องกับคำถามจริง แต่นี่ไม่มีประสิทธิภาพอย่างมากและไม่มีใครควรลอกเลียนแบบ! ให้ออกแบบสอบถามเดียวแทนเช่น
Doin

"SELECT CAST(created_timestamp AS date) day,COUNT(*) WHERE item_id=:item_id AND (created_timestamp BETWEEN '".date("Y-m-d 00:00:00", $min_date_timestamp)."' AND '".date("Y-m-d 23:59:59", $max_date_timestamp)."') GROUP BY day ORDER BY day";
Doin
โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.