Các trường ngày giờ trong MySQL và thời gian tiết kiệm ánh sáng ban ngày - làm cách nào để tham chiếu đến giờ “bổ sung”?


88

Tôi đang sử dụng múi giờ Châu Mỹ / New York. Vào mùa Thu, chúng ta "lùi lại" một giờ - hiệu quả là "tăng" một giờ vào lúc 2 giờ sáng. Tại điểm chuyển tiếp, điều sau sẽ xảy ra:

bây giờ là 01:59:00 -04: 00
rồi 1 phút sau nó trở thành:
01:00:00 -05: 00

Vì vậy, nếu bạn chỉ đơn giản nói "1:30 sáng" thì sẽ không rõ ràng về việc bạn đang đề cập đến lần đầu tiên 1:30 xoay quanh hay lần thứ hai. Tôi đang cố gắng lưu dữ liệu lập lịch vào cơ sở dữ liệu MySQL và không thể xác định cách lưu thời gian đúng cách.

Đây là sự cố: "2009-11-01 00:30:00" được lưu trữ nội bộ vì 2009-11-01 00:30:00
-04: 00
"2009-11-01 01:30:00" được lưu trữ nội bộ như 2009-11-01 01:30:00 -05: 00

Điều này là tốt và khá mong đợi. Nhưng làm cách nào để lưu mọi thứ vào 01:30:00 -04: 00 ? Các tài liệu không hiển thị bất kỳ hỗ trợ cho các quy định cụ thể bù đắp và, theo đó, khi tôi đã cố gắng xác định bù đắp nó được bỏ qua theo thẩm quyền.

Các giải pháp duy nhất tôi nghĩ đến liên quan đến việc đặt máy chủ thành múi giờ không sử dụng thời gian tiết kiệm ánh sáng ban ngày và thực hiện các chuyển đổi cần thiết trong các tập lệnh của mình (tôi đang sử dụng PHP cho việc này). Nhưng điều đó có vẻ không cần thiết.

Cảm ơn rất nhiều về sựh gợi ý.


Tôi không biết đủ về MySQL hoặc PHP để tạo ra một câu trả lời mạch lạc, nhưng tôi cá rằng nó có liên quan đến chuyển đổi đến và từ UTC.
Mark Ransom

2
Bên trong chúng đều được lưu trữ dưới dạng UTC, phải không?
Eli

4
Tôi thấy web.ivy.net/~carton/rant/MySQL-timezones.txt một bài đọc thú vị về chủ đề này.
micahwittman

Liên kết tốt, micahwittman - rất hữu ích.
Aaron

những câu hỏi hay. một vấn đề chung.
Vardumper

Câu trả lời:


47

Thành thật mà nói, các loại ngày của MySQL bị hỏng và không thể lưu trữ mọi thời điểm một cách chính xác trừ khi hệ thống của bạn được đặt thành múi giờ bù không đổi, như UTC hoặc GMT-5. (Tôi đang sử dụng MySQL 5.0.45)

Điều này là do bạn không thể lưu trữ bất kỳ thời gian nào trong giờ trước khi Giờ tiết kiệm ánh sáng ban ngày kết thúc . Bất kể bạn nhập ngày tháng như thế nào, mọi hàm ngày tháng sẽ coi những thời điểm này như thể chúng nằm trong giờ sau khi chuyển đổi.

Múi giờ hệ thống của tôi là America/New_York. Hãy thử lưu trữ 1257051600 (CN, 01/11/2009 06:00:00 +0100).

Đây là cách sử dụng cú pháp INTERVAL độc quyền:

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

Thậm chí FROM_UNIXTIME()sẽ không trả lại thời gian chính xác.

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

Thật kỳ lạ, DATETIME sẽ vẫn lưu trữ và trả lại (chỉ ở dạng chuỗi!) Lần trong vòng giờ "bị mất" khi DST bắt đầu (ví dụ 2009-03-08 02:59:59:). Nhưng việc sử dụng những ngày này trong bất kỳ hàm MySQL nào đều có rủi ro:

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

Bài học rút ra: Nếu bạn cần lưu trữ và truy xuất mọi lúc trong năm, bạn có một số lựa chọn không mong muốn:

  1. Đặt múi giờ hệ thống thành GMT + một số bù không đổi. Ví dụ: UTC
  2. Lưu trữ ngày tháng dưới dạng INT (như Aaron đã phát hiện ra, TIMESTAMP thậm chí không đáng tin cậy)

  3. Giả sử kiểu DATETIME có một số múi giờ bù không đổi. Ví dụ: Nếu bạn đang ở trong đó America/New_York, hãy chuyển đổi ngày của bạn thành GMT-5 bên ngoài MySQL , sau đó lưu trữ dưới dạng DATETIME (điều này hóa ra rất cần thiết: hãy xem câu trả lời của Aaron). Sau đó, bạn phải hết sức cẩn thận khi sử dụng các hàm ngày / giờ của MySQL, bởi vì một số giả định giá trị của bạn thuộc múi giờ hệ thống, những người khác (đặc biệt là các hàm số học thời gian) là "bất khả tri về múi giờ" (chúng có thể hoạt động như thể thời gian là UTC).

Aaron và tôi nghi ngờ rằng các cột TIMESTAMP tự động tạo cũng bị hỏng. Cả hai 2009-11-01 01:30 -04002009-11-01 01:30 -0500sẽ được lưu trữ dưới dạng mơ hồ 2009-11-01 01:30.


Cảm ơn tất cả sự giúp đỡ của bạn trên mrclay này. Bạn đã vạch ra tình huống ở đây rất chính xác.
Aaron

Có vẻ như tùy chọn 3 thực sự an toàn hơn cho số học thời gian vì (có vẻ như) các chức năng đã được triển khai trước khi chức năng DST được thêm vào. Ví dụ: TIMEDIFF ('2009-11-01 02:30:00', '2009-11-01 00:30:00') trả về 2:00, đúng với UTC, nhưng ở Mỹ / New_York thời gian là 3 giờ riêng biệt.
Steve Clay

1
-1: Bạn đã mắc lỗi khi các hàm ngày / giờ của MySQL hoạt động trên kiểu DATETIME, kiểu múi giờ bất khả tri. Do đó, đối số bạn đang đi qua để UNIX_TIMSTAMP là select '2009-11-01 00:00:00' + INTERVAL 3600 SECOND;đó là '2009-11-01 01:00:00'. UNIX_TIMESTAMP sau đó chỉ cần cố gắng giấu điều này đến UTC trong ngữ cảnh của múi giờ phiên - nó không cố gắng thực hiện việc bổ sung trong ngữ cảnh của quy tắc DST của múi giờ đó.
kbro

@kbro OK, nhưng vấn đề vẫn còn. Nếu phiên tz America/New_Yorkthì mình thấy không có cách nào để lưu trữ được 1257051600. Bạn ơi?
Steve Clay

75

Tôi đã tìm ra nó cho mục đích của tôi. Tôi sẽ tóm tắt những gì tôi đã học được (xin lỗi, những ghi chú này dài dòng; chúng cũng dành cho người giới thiệu trong tương lai của tôi như bất cứ điều gì khác).

Trái với những gì tôi đã nói trong một trong những bình luận trước đây của tôi, DATETIME và dấu thời gian lĩnh vực làm hành xử khác nhau. Các trường TIMESTAMP (như tài liệu chỉ ra) lấy bất cứ thứ gì bạn gửi cho chúng ở định dạng "YYYY-MM-DD hh: mm: ss" và chuyển đổi nó từ múi giờ hiện tại của bạn sang giờ UTC. Điều ngược lại xảy ra một cách minh bạch bất cứ khi nào bạn lấy dữ liệu. Các trường DATETIME không thực hiện chuyển đổi này. Họ lấy bất cứ thứ gì bạn gửi cho họ và chỉ cần lưu trữ trực tiếp.

Cả hai loại trường DATETIME và TIMESTAMP đều không thể lưu trữ chính xác dữ liệu trong múi giờ quan sát DST . Nếu bạn lưu trữ "2009-11-01 01:30:00", các trường không có cách nào để phân biệt phiên bản 1:30 sáng mà bạn muốn - phiên bản -04: 00 hoặc -05: 00.

Được, vì vậy chúng tôi phải lưu trữ dữ liệu của mình ở múi giờ không phải DST (chẳng hạn như UTC). Các trường TIMESTAMP không thể xử lý chính xác dữ liệu này vì những lý do tôi sẽ giải thích: nếu hệ thống của bạn được đặt thành múi giờ DST thì những gì bạn đưa vào TIMESTAMP có thể không phải là thứ bạn lấy lại được. Ngay cả khi bạn gửi cho nó dữ liệu mà bạn đã chuyển đổi sang UTC, nó vẫn sẽ giả định là dữ liệu trong múi giờ địa phương của bạn và thực hiện một chuyển đổi khác sang UTC. Vòng quay từ địa phương đến UTC-trở lại địa phương do TIMESTAMP thực thi này sẽ bị mất khi múi giờ địa phương của bạn quan sát DST (kể từ "2009-11-01 01:30:00" ánh xạ đến 2 thời điểm khác nhau có thể xảy ra).

Với DATETIME, bạn có thể lưu trữ dữ liệu của mình ở bất kỳ múi giờ nào bạn muốn và tự tin rằng bạn sẽ nhận lại được bất kỳ thứ gì bạn gửi (bạn không bị buộc phải chuyển đổi khứ hồi mất mát mà các trường TIMESTAMP cung cấp cho bạn). Vì vậy, giải pháp là sử dụng trường DATETIME và trước khi lưu vào trường, hãy chuyển đổi từ múi giờ hệ thống của bạn thành bất kỳ múi giờ không phải DST nào mà bạn muốn lưu (tôi nghĩ UTC có lẽ là lựa chọn tốt nhất). Điều này cho phép bạn xây dựng logic chuyển đổi thành ngôn ngữ kịch bản của mình để bạn có thể lưu rõ ràng UTC tương đương với "2009-11-01 01:30:00 -04: 00" hoặc "" 2009-11-01 01:30: 00-05: 00 ”.

Một điều quan trọng khác cần lưu ý là các hàm toán học ngày / giờ của MySQL không hoạt động đúng xung quanh ranh giới DST nếu bạn lưu trữ ngày tháng của mình trong DST TZ. Vì vậy, tất cả các lý do hơn để tiết kiệm trong UTC.

Tóm lại, bây giờ tôi làm điều này:

Khi truy xuất dữ liệu từ cơ sở dữ liệu:

Diễn giải rõ ràng dữ liệu từ cơ sở dữ liệu dưới dạng UTC bên ngoài MySQL để có được dấu thời gian Unix chính xác. Tôi sử dụng hàm strtotime () của PHP hoặc lớp DateTime của nó cho việc này. Nó không thể được thực hiện một cách đáng tin cậy bên trong MySQL bằng cách sử dụng các hàm CONVERT_TZ () hoặc UNIX_TIMESTAMP () của MySQL vì CONVERT_TZ sẽ chỉ xuất ra giá trị 'YYYY-MM-DD hh: mm: ss' gặp phải vấn đề không rõ ràng và UNIX_TIMESTAMP () giả định giá trị của nó đầu vào nằm trong múi giờ hệ thống, không phải múi giờ mà dữ liệu THỰC SỰ được lưu trữ trong (UTC).

Khi lưu trữ dữ liệu vào cơ sở dữ liệu:

Chuyển đổi ngày của bạn thành thời gian UTC chính xác mà bạn mong muốn bên ngoài MySQL. Ví dụ: với lớp DateTime của PHP, bạn có thể chỉ định "2009-11-01 1:30:00 EST" khác biệt với "2009-11-01 1:30:00 EDT", sau đó chuyển nó thành UTC và lưu thời gian UTC chính xác vào trường DATETIME của bạn.

Phù. Cảm ơn rất nhiều vì sự đóng góp và giúp đỡ của mọi người. Hy vọng rằng điều này sẽ giúp ai đó bớt đau đầu trên đường.

BTW, tôi thấy điều này trên MySQL 5.0.22 và 5.0.27


13

Tôi nghĩ rằng liên kết của micahwittman có giải pháp thực tế tốt nhất cho những hạn chế này của MySQL: Đặt múi giờ phiên thành UTC khi bạn kết nối:

SET SESSION time_zone = '+0:00'

Sau đó, bạn chỉ cần gửi cho nó dấu thời gian Unix và mọi thứ sẽ ổn.


Lời khuyên này hoạt động tốt. Sự cố được giải quyết sau khi tôi kết nối tất cả các kết nối trong nhóm của mình bằng câu lệnh đã cho.
snowindy

4

Chủ đề này khiến tôi thấy khó chịu vì chúng tôi sử dụng TIMESTAMPcác cột có On UPDATE CURRENT_TIMESTAMP(ví dụ recordTimestamp timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP:) để theo dõi các bản ghi đã thay đổi và ETL vào một datawarehouse.

Trong trường hợp ai đó thắc mắc, trong trường hợp này, hãy TIMESTAMPcư xử chính xác và bạn có thể phân biệt giữa hai ngày tương tự bằng cách chuyển đổi TIMESTAMPdấu thời gian thành 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

Nhưng làm cách nào để lưu mọi thứ vào 01:30:00 -04: 00?

Bạn có thể chuyển đổi sang UTC như:

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


Tốt hơn nữa, hãy lưu ngày tháng dưới dạng trường TIMESTAMP . Thông tin đó luôn được lưu trữ trong UTC và UTC không biết về thời gian mùa hè / mùa đông.

Bạn có thể chuyển đổi từ UTC sang localtime bằng CONVERT_TZ :

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

Trong đó '+00: 00' là UTC, múi giờ từ và 'SYSTEM' là múi giờ cục bộ của hệ điều hành nơi MySQL chạy.


Cảm ơn vì sự trả lời. Tốt nhất tôi có thể nói, bất chấp những gì tài liệu nói, các trường TIMESTAMP và Datetime đang hoạt động giống hệt nhau: chúng lưu trữ dữ liệu của mình bằng UTC, nhưng chúng mong đợi dữ liệu đầu vào của chúng theo giờ địa phương và chúng tự động chuyển đổi nó thành UTC - nếu tôi chuyển đổi sang UTC trước tiên, cơ sở dữ liệu không biết tôi đã làm điều đó và nó thêm 4 (hoặc 5, tùy thuộc vào việc chúng tôi có DST hay không) vào thời gian đó. Vì vậy, vấn đề vẫn còn: làm thế nào để chỉ định 2009-11-01 01:30:00 -04: 00 làm đầu vào?
Aaron

Chà, tôi đã phát hiện ra rằng nguồn gốc của hầu hết sự nhầm lẫn của tôi là thực tế là hàm UNIX_TIMESTAMP () luôn diễn giải tham số ngày của nó so với múi giờ hiện tại cho dù bạn đang lấy dữ liệu từ trường TIMESTAMP hay DATETIME . Điều này có ý nghĩa bây giờ khi tôi nghĩ về nó. Tôi sẽ cập nhật thêm sau.
Aaron

2

Mysql vốn đã giải quyết vấn đề này bằng cách sử dụng bảng time_zone_name từ mysql db. Sử dụng CONVERT_TZ trong khi CRUD để cập nhật ngày giờ mà không cần lo lắng về giờ tiết kiệm ánh sáng ban ngày.

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

Tôi đang làm việc trên ghi nhật ký số lượt truy cập các trang và hiển thị số lượng trong biểu đồ (sử dụng plugin Flot jQuery). Tôi điền vào bảng với dữ liệu thử nghiệm và mọi thứ trông ổn, nhưng tôi nhận thấy rằng ở cuối biểu đồ, các điểm đã nghỉ một ngày theo nhãn trên trục x. Sau khi kiểm tra, tôi nhận thấy rằng số lượt xem cho ngày 2015-10-25 được truy xuất hai lần từ cơ sở dữ liệu và được chuyển đến Flot, vì vậy mỗi ngày sau ngày này được chuyển sang phải một ngày.
Sau khi tìm kiếm lỗi trong mã của mình một lúc, tôi nhận ra rằng ngày này là khi DST diễn ra. Sau đó, tôi đến trang SO này ...
... nhưng các giải pháp được đề xuất là quá mức cần thiết cho những gì tôi cần hoặc chúng có những nhược điểm khác. Tôi không lo lắng lắm về việc không thể phân biệt giữa các dấu thời gian mơ hồ. Tôi chỉ cần đếm và hiển thị các bản ghi mỗi ngày.

Đầu tiên, tôi truy xuất phạm vi ngày:

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

Sau đó, trong vòng lặp for, bắt đầu bằng min_date, kết thúc bằng max_date, theo từng bước của một ngày ( 60*60*24), tôi đang truy xuất số lượng:

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
}

My giải pháp cuối cùng và nhanh chóng để tôi vấn đề là thế này:

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

Vì vậy, tôi không nhìn chằm chằm vào vòng lặp vào đầu ngày, mà là hai giờ sau . Ngày vẫn như cũ và tôi vẫn đang truy xuất số lượng chính xác, vì tôi yêu cầu cơ sở dữ liệu rõ ràng về các bản ghi từ 00:00:00 đến 23:59:59 trong ngày, bất kể thời gian thực của dấu thời gian. Và khi thời gian nhảy thêm một giờ, tôi vẫn đang ở đúng ngày.

Lưu ý: Tôi biết đây là chủ đề 5 năm tuổi và tôi biết đây không phải là câu trả lời cho câu hỏi OP, nhưng nó có thể giúp những người như tôi gặp phải trang này tìm kiếm giải pháp cho vấn đề tôi đã mô tả.


Có lẽ không liên quan đến câu hỏi thực tế, nhưng điều này không hiệu quả kinh khủng, và không ai nên sao chép nó! Thay vào đó, hãy đưa ra một truy vấn duy nhất như:
Doin vào

"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
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.