Kolom waktu MySQL dan waktu musim panas - bagaimana cara mereferensikan jam "ekstra"?

88

Saya menggunakan zona waktu Amerika / New York. Pada musim gugur kami "mundur" satu jam - secara efektif "mendapatkan" satu jam pada pukul 2 pagi. Pada titik transisi terjadi hal berikut:

01:59:00 -04: 00
kemudian 1 menit kemudian menjadi:
01:00:00 -05: 00

Jadi, jika Anda hanya mengatakan "1:30 pagi", tidak jelas apakah Anda mengacu pada saat pertama kali 1:30 berputar atau yang kedua. Saya mencoba menyimpan data penjadwalan ke database MySQL dan tidak dapat menentukan cara menyimpan waktu dengan benar.

Inilah masalahnya:
"2009-11-01 00:30:00" disimpan secara internal sebagai 2009-11-01 00:30:00 -04: 00
"2009-11-01 01:30:00" disimpan secara internal sebagai 2009-11-01 01:30:00 -05: 00

Ini bagus dan cukup diharapkan. Tapi bagaimana cara menyimpan apa pun hingga 01:30:00 -04: 00 ? The dokumentasi tidak menunjukkan dukungan untuk menentukan offset dan, sesuai, ketika saya sudah mencoba menentukan offset itu sudah sepatutnya diabaikan.

Satu-satunya solusi yang saya pikirkan melibatkan pengaturan server ke zona waktu yang tidak menggunakan waktu musim panas dan melakukan transformasi yang diperlukan dalam skrip saya (saya menggunakan PHP untuk ini). Tapi sepertinya itu tidak perlu.

Terima kasih banyak atas sarannya.

Aaron
sumber
Saya tidak cukup tahu tentang MySQL atau PHP untuk membentuk jawaban yang koheren, tetapi saya yakin itu ada hubungannya dengan konversi ke dan dari UTC.
Mark Ransom
2
Secara internal semuanya disimpan sebagai UTC, bukan?
Eli
4
Saya menemukan web.ivy.net/~carton/rant/MySQL-timezones.txt sebagai bacaan yang menarik tentang topik tersebut.
micahwittman
Tautan bagus, micahwittman - sangat membantu.
Aaron
pertanyaan bagus. masalah umum.
Vardumper

Jawaban:

47

Jenis tanggal MySQL, sejujurnya, rusak dan tidak dapat disimpan sepanjang waktu dengan benar kecuali sistem Anda disetel ke zona waktu offset konstan, seperti UTC atau GMT-5. (Saya menggunakan MySQL 5.0.45)

Ini karena Anda tidak dapat menyimpan kapan pun selama satu jam sebelum Waktu Musim Panas berakhir . Tidak peduli bagaimana Anda memasukkan tanggal, setiap fungsi tanggal akan memperlakukan waktu-waktu ini seolah-olah selama satu jam setelah sakelar.

Zona waktu sistem saya adalah America/New_York. Mari kita coba menyimpan 1257051600 (Sun, 01 Nov 2009 06:00:00 +0100).

Ini menggunakan sintaks INTERVAL berpemilik:

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

Bahkan FROM_UNIXTIME()tidak akan mengembalikan waktu yang akurat.

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

Anehnya, DATETIME masih akan menyimpan dan mengembalikan (dalam bentuk string saja!) Kali dalam jam "hilang" ketika DST dimulai (misalnya 2009-03-08 02:59:59). Tetapi menggunakan tanggal-tanggal ini dalam fungsi MySQL apa pun berisiko:

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

Kesimpulannya: Jika Anda perlu menyimpan dan mengambil setiap kali dalam setahun, Anda memiliki beberapa opsi yang tidak diinginkan:

  1. Setel zona waktu sistem ke GMT + beberapa offset konstan. Misalnya UTC
  2. Simpan tanggal sebagai INT (seperti yang ditemukan Aaron, TIMESTAMP bahkan tidak dapat diandalkan)

  3. Anggaplah tipe DATETIME memiliki zona waktu offset yang konstan. Misalnya Jika Anda masuk America/New_York, ubah tanggal Anda menjadi GMT-5 di luar MySQL , lalu simpan sebagai DATETIME (ini ternyata penting: lihat jawaban Aaron). Maka Anda harus sangat berhati-hati menggunakan fungsi tanggal / waktu MySQL, karena beberapa menganggap nilai Anda dari zona waktu sistem, yang lain (fungsi aritmatika khususnya waktu) adalah "agnostik zona waktu" (mereka mungkin berperilaku seolah-olah waktunya UTC).

Aaron dan saya menduga bahwa kolom TIMESTAMP yang dibuat secara otomatis juga rusak. Keduanya 2009-11-01 01:30 -0400dan 2009-11-01 01:30 -0500akan disimpan sebagai ambigu 2009-11-01 01:30.

Steve Clay
sumber
Terima kasih atas semua bantuan Anda di mrclay ini. Anda telah menguraikan situasinya di sini dengan sangat akurat.
Aaron
Tampaknya opsi 3 sebenarnya lebih aman untuk aritmatika waktu karena (tampaknya) fungsi tersebut diterapkan sebelum fungsionalitas DST ditambahkan. Misalnya TIMEDIFF ('2009-11-01 02:30:00', '2009-11-01 00:30:00') mengembalikan 2:00, yang benar untuk UTC, tetapi di Amerika / New_York waktunya 3 jam selain.
Steve Clay
1
-1: Anda telah membuat kesalahan bahwa fungsi tanggal / waktu MySQL beroperasi pada tipe DATETIME, yang merupakan agnostik zona waktu. Oleh karena itu, argumen yang Anda berikan ke UNIX_TIMSTAMP adalah select '2009-11-01 00:00:00' + INTERVAL 3600 SECOND;yang mana '2009-11-01 01:00:00'. UNIX_TIMESTAMP kemudian hanya mencoba untuk menyembunyikan ini ke UTC dalam konteks zona waktu sesi - itu tidak mencoba untuk melakukan penambahan dalam konteks aturan DST zona waktu itu.
kbro
@ kbro OK, tapi masalahnya tetap ada. Jika sesi tz adalah America/New_York, saya tidak melihat cara untuk menyimpan 1257051600. Apakah Anda?
Steve Clay
77

Saya sudah memikirkannya untuk tujuan saya. Saya akan meringkas apa yang saya pelajari (maaf, catatan ini bertele-tele; itu sama pentingnya dengan referensi saya di masa mendatang).

Bertentangan dengan apa yang saya katakan di salah satu komentar saya sebelumnya, bidang DATETIME dan TIMESTAMP jangan berperilaku berbeda. Kolom TIMESTAMP (seperti yang ditunjukkan oleh dokumen) mengambil apa pun yang Anda kirimkan dalam format "TTTT-BB-HH jj: mm: dd" dan mengubahnya dari zona waktu Anda saat ini ke waktu UTC. Kebalikannya terjadi secara transparan setiap kali Anda mengambil data. Bidang DATETIME tidak melakukan konversi ini. Mereka mengambil apa pun yang Anda kirimkan dan langsung menyimpannya.

Baik jenis bidang DATETIME maupun TIMESTAMP tidak dapat menyimpan data secara akurat dalam zona waktu yang mengamati DST . Jika Anda menyimpan "2009-11-01 01:30:00", kolom tidak memiliki cara untuk membedakan versi jam 1:30 mana yang Anda inginkan - versi -04: 00 atau -05: 00.

Ok, jadi kita harus menyimpan data kita di zona waktu non DST (seperti UTC). Kolom TIMESTAMP tidak dapat menangani data ini secara akurat karena alasan yang akan saya jelaskan: jika sistem Anda disetel ke zona waktu DST, apa yang Anda masukkan ke TIMESTAMP mungkin bukan yang Anda dapatkan kembali. Meskipun Anda mengirimkan data yang telah Anda ubah ke UTC, data tersebut akan tetap diasumsikan dalam zona waktu lokal Anda dan melakukan konversi lagi ke UTC. Perjalanan pulang pergi lokal-ke-UTC-kembali-ke-lokal yang diberlakukan TIMESTAMP ini merugikan saat zona waktu lokal Anda mengamati DST (karena "2009-11-01 01:30:00" memetakan ke 2 kemungkinan waktu yang berbeda).

Dengan DATETIME Anda dapat menyimpan data Anda di zona waktu mana pun yang Anda inginkan dan yakin bahwa Anda akan mendapatkan kembali apa pun yang Anda kirimkan (Anda tidak akan dipaksa melakukan konversi bolak-balik yang merugikan yang ditimbulkan oleh bidang TIMESTAMP pada Anda). Jadi solusinya adalah dengan menggunakan bidang DATETIME dan sebelum menyimpan ke bidang, ubah dari zona waktu sistem Anda menjadi zona non-DST apa pun yang Anda inginkan untuk menyimpannya (menurut saya UTC mungkin adalah opsi terbaik). Ini memungkinkan Anda untuk membuat logika konversi ke dalam bahasa skrip Anda sehingga Anda dapat secara eksplisit menyimpan UTC yang setara dengan "2009-11-01 01:30:00 -04: 00" atau "" 2009-11-01 01:30: 00 -05: 00 ".

Hal penting lainnya yang perlu diperhatikan adalah bahwa fungsi matematika tanggal / waktu MySQL tidak berfungsi dengan baik di sekitar batas DST jika Anda menyimpan tanggal Anda di DST TZ. Jadi semakin banyak alasan untuk menabung di UTC.

Singkatnya, sekarang saya melakukan ini:

Saat mengambil data dari database:

Tafsirkan secara eksplisit data dari database sebagai UTC di luar MySQL untuk mendapatkan stempel waktu Unix yang akurat. Saya menggunakan fungsi strtotime () PHP atau kelas DateTime untuk ini. Ini tidak dapat dilakukan dengan andal di dalam MySQL menggunakan fungsi MySQL CONVERT_TZ () atau UNIX_TIMESTAMP () karena CONVERT_TZ hanya akan mengeluarkan nilai 'YYYY-MM-DD hh: mm: ss' yang mengalami masalah ambiguitas, dan UNIX_TIMESTAMP () mengasumsikannya masukan dalam zona waktu sistem, bukan zona waktu tempat data SEBENARNYA disimpan dalam (UTC).

Saat menyimpan data ke database:

Ubah tanggal Anda ke waktu UTC tepat yang Anda inginkan di luar MySQL. Misalnya: dengan kelas DateTime PHP Anda dapat menentukan "2009-11-01 1:30:00 EST" dengan jelas dari "2009-11-01 1:30:00 EDT", lalu mengubahnya menjadi UTC dan menyimpan waktu UTC yang benar ke bidang DATETIME Anda.

Fiuh. Terima kasih banyak atas masukan dan bantuan semua orang. Mudah-mudahan ini bisa membuat orang lain sakit kepala di kemudian hari.

BTW, saya melihat ini di MySQL 5.0.22 dan 5.0.27

Aaron
sumber
13

Saya pikir tautan micahwittman memiliki solusi praktis terbaik untuk batasan MySQL ini: Setel zona waktu sesi ke UTC saat Anda terhubung:

SET SESSION time_zone = '+0:00'

Kemudian Anda kirimkan saja cap waktu Unix dan semuanya akan baik-baik saja.

Steve Clay
sumber
Saran ini bekerja dengan baik. Masalah diselesaikan setelah saya memasukkan semua koneksi di kolam saya dengan pernyataan yang diberikan.
snowindy
4

Utas ini membuat saya aneh karena kami menggunakan TIMESTAMPkolom dengan On UPDATE CURRENT_TIMESTAMP(yaitu:) recordTimestamp timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMPuntuk melacak catatan yang diubah dan ETL ke gudang data.

Jika seseorang bertanya-tanya, dalam hal ini, TIMESTAMPberperilaku dengan benar dan Anda dapat membedakan antara dua tanggal serupa dengan mengonversi TIMESTAMPstempel waktu menjadi 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
Manuel Darveau
sumber
3

Tapi bagaimana cara menyimpan apa pun hingga 01:30:00 -04: 00?

Anda dapat mengonversi ke UTC seperti:

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


Lebih baik lagi, simpan tanggal sebagai bidang TIMESTAMP . Itu selalu disimpan dalam UTC, dan UTC tidak tahu tentang waktu musim panas / musim dingin.

Anda dapat mengonversi dari UTC ke waktu lokal menggunakan CONVERT_TZ :

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

Di mana '+00: 00' adalah UTC, zona waktu from, dan 'SYSTEM' adalah zona waktu lokal OS tempat MySQL dijalankan.

Andomar
sumber
Terima kasih atas tanggapannya. Yang terbaik yang bisa saya katakan, terlepas dari apa yang dikatakan dokumen, bidang TIMESTAMP dan Datetime berperilaku sama: mereka menyimpan datanya di UTC, tetapi mereka mengharapkan masukan mereka dalam waktu lokal dan mereka secara otomatis mengubahnya menjadi UTC - jika saya mengonversi ke UTC pertama database tidak tahu saya melakukan itu dan itu menambahkan 4 (atau 5, tergantung pada apakah kita DST atau tidak) lebih banyak jam ke waktu. Jadi masalahnya tetap: bagaimana saya menetapkan 2009-11-01 01:30:00 -04: 00 sebagai input?
Aaron
Nah, saya telah menemukan bahwa sumber dari sebagian besar kebingungan saya adalah fakta bahwa fungsi UNIX_TIMESTAMP () selalu menafsirkan parameter tanggalnya relatif terhadap zona waktu saat ini apakah Anda menarik data dari TIMESTAMP atau bidang DATETIME . Ini masuk akal sekarang setelah saya memikirkannya. Saya akan memperbarui lebih banyak nanti.
Aaron
2

Mysql secara inheren memecahkan masalah ini menggunakan tabel time_zone_name dari mysql db. Gunakan CONVERT_TZ saat CRUD untuk memperbarui tanggal waktu tanpa mengkhawatirkan waktu musim panas.

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;
Arpan Jain
sumber
1

Saya sedang mengerjakan pencatatan jumlah kunjungan halaman dan menampilkan jumlah tersebut dalam grafik (menggunakan plugin Flot jQuery). Saya mengisi tabel dengan data uji dan semuanya tampak baik-baik saja, tetapi saya perhatikan bahwa pada akhir grafik, titik-titik itu satu hari libur menurut label pada sumbu x. Setelah pemeriksaan, saya melihat bahwa jumlah tampilan untuk hari 2015-10-25 diambil dua kali dari database dan diteruskan ke Flot, jadi setiap hari setelah tanggal ini dipindahkan satu hari ke kanan.
Setelah mencari bug di kode saya untuk beberapa saat, saya menyadari bahwa tanggal ini adalah saat DST berlangsung. Kemudian saya datang ke halaman SO ini ...
... tetapi solusi yang disarankan berlebihan untuk apa yang saya butuhkan atau mereka memiliki kelemahan lain. Saya tidak terlalu khawatir karena tidak dapat membedakan antara cap waktu yang ambigu. Saya hanya perlu menghitung dan menampilkan catatan per hari.

Pertama, saya mengambil rentang tanggal:

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

Kemudian, dalam perulangan for, dimulai dengan min_date, diakhiri dengan max_date, demi langkah satu hari ( 60*60*24), saya mengambil hitungan:

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
}

Saya solusi akhir dan cepat untuk saya masalah adalah ini:

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

Jadi saya tidak menatap lingkaran di awal hari, tetapi dua jam kemudian . Hari ini masih sama, dan saya masih mendapatkan hitungan yang benar, karena saya secara eksplisit meminta catatan database antara 00:00:00 dan 23:59:59 pada hari itu, terlepas dari waktu aktual stempel waktu. Dan ketika waktu melonjak satu jam, saya masih di hari yang benar.

Catatan: Saya tahu ini adalah utas berusia 5 tahun, dan saya tahu ini bukan jawaban untuk pertanyaan OP, tetapi mungkin membantu orang-orang seperti saya yang menemukan halaman ini mencari solusi untuk masalah yang saya jelaskan.

Lukas
sumber
Mungkin tidak relevan dengan pertanyaan yang sebenarnya, tetapi ini sangat tidak efisien, dan tidak ada yang boleh menyalinnya! Sebagai gantinya, keluarkan satu kueri seperti:
Lakukan
"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";
Lakukan