MySQL tarih saat alanları ve gün ışığından yararlanma saati - "ekstra" saate nasıl başvurabilirim?


89

Amerika / New York saat dilimini kullanıyorum. Sonbaharda bir saat "geri çekiliriz" - etkin bir şekilde saat 02: 00'de bir saat "kazanırız". Geçiş noktasında aşağıdakiler gerçekleşir:

saat 01:59:00 -04: 00
sonra 1 dakika sonra şu olur:
01:00:00 -05: 00

Bu nedenle, sadece "1:30" derseniz, 1:30 ilk kez mi yoksa ikinci kez mi atıfta bulunduğunuz belirsizdir. Planlama verilerini bir MySQL veritabanına kaydetmeye çalışıyorum ve zamanları nasıl düzgün kaydedeceğimi belirleyemiyorum.

Sorun şu:
"2009-11-01 00:30:00", 2009-11-01 00:30:00 -04: 00
"2009-11-01 01:30:00" olarak dahili olarak depolanır: 2009-11-01 01:30:00 -05: 00

Bu iyi ve oldukça bekleniyor. Ama herhangi bir şeyi 01:30:00 -04: 00'a nasıl kaydederim ? Dokümantasyon Ben usulüne uygun ihmal oldu ofset belirterek denedim zaman, buna göre, ofset ve belirtmek için herhangi bir destek göstermez.

Düşündüğüm tek çözüm, sunucuyu gün ışığından yararlanma saatini kullanmayan bir saat dilimine ayarlamak ve komut dosyalarımda gerekli dönüşümleri yapmaktır (bunun için PHP kullanıyorum). Ancak bu gerekli gibi görünmüyor.

Önerileriniz için çok teşekkürler.


Tutarlı bir cevap oluşturmak için MySQL veya PHP hakkında yeterince bilgim yok, ancak bunun UTC'ye ve UTC'den dönüşümle bir ilgisi olduğuna bahse girerim.
Mark Ransom

2
Dahili olarak hepsi UTC olarak saklanıyor, değil mi?
Eli

4
Web.ivy.net/~carton/rant/MySQL-timezones.txt dosyasını konu hakkında ilginç bir okuma buldum .
micahwittman

İyi bağlantı, micahwittman - çok yardımcı.
Aaron

harika sorular. ortak bir problem.
Vardumper

Yanıtlar:


47

MySQL'in tarih türleri açıkçası bozuktur ve sisteminiz UTC veya GMT-5 gibi sabit bir zaman dilimine ayarlanmadıkça her zaman doğru şekilde saklanamaz. (MySQL 5.0.45 kullanıyorum)

Bunun nedeni , Gün Işığından Yararlanma Saati sona ermeden önceki saat boyunca herhangi bir zamanı kaydedememenizdir . Tarihleri ​​nasıl girdiğiniz önemli değil, her tarih işlevi bu zamanları geçişten sonraki saatlerde gibi ele alır.

Sistemimin saat dilimi America/New_York. 1257051600 (Paz, 01 Kasım 2009 06:00:00 +0100) kaydetmeyi deneyelim.

Tescilli INTERVAL sözdizimini kullanıyoruz:

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()Doğru zamanı bile geri getirmeyecek.

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

İşin garibi, DATETIME , DST başladığında (örn. 2009-03-08 02:59:59) "Kayıp" saat içinde yine de (yalnızca dize biçiminde!) Saklayacak ve geri dönecektir . Ancak bu tarihleri ​​herhangi bir MySQL işlevinde kullanmak risklidir:

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

Paket servis: Yıl içinde her defasında saklamanız ve geri almanız gerekiyorsa , birkaç istenmeyen seçeneğiniz vardır:

  1. Sistem saat dilimini GMT + bazı sabit farklara ayarlayın. Ör. UTC
  2. Tarihleri ​​INT olarak saklayın (Aaron'un keşfettiği gibi, TIMESTAMP bile güvenilir değildir)

  3. DATETIME türünde sabit bir saat dilimi olduğunu varsayalım. Örneğin, eğer yerdeyseniz America/New_York, tarihinizi MySQL dışında GMT-5'e dönüştürün , ardından DATETIME olarak kaydedin (bu çok önemlidir: Aaron'un cevabına bakın). O halde MySQL'in tarih / saat işlevlerini kullanırken çok dikkatli olmalısınız, çünkü bazıları değerinizin sistem zaman dilimine ait olduğunu varsayar, diğerleri (özellikle zaman aritmetik işlevleri) "zaman diliminden bağımsızdır" (saatler UTC gibi davranabilirler).

Aaron ve ben otomatik oluşturulan TIMESTAMP sütunlarının da bozuk olduğundan şüpheleniyoruz. Hem 2009-11-01 01:30 -0400ve 2009-11-01 01:30 -0500belirsiz olarak saklanır 2009-11-01 01:30.


Bu oyunla ilgili tüm yardımlarınız için teşekkürler. Buradaki durumu çok doğru bir şekilde özetlediniz.
Aaron

Görünüşe göre seçenek 3, zaman aritmetiği için aslında daha güvenli çünkü (öyle görünüyor ki) işlevler DST işlevselliği eklenmeden önce uygulanıyordu. Örneğin, TIMEDIFF ('2009-11-01 02:30:00', '2009-11-01 00:30:00') UTC için doğru olan 2:00 döndürür, ancak America / New_York'ta saatler 3 saattir ayrı.
Steve Clay

1
-1: MySQL tarih / saat işlevlerinin, saat diliminden bağımsız bir DATETIME türünde çalıştığı hatası yaptınız. Eğer UNIX_TIMSTAMP için geçiyoruz argüman nedenle select '2009-11-01 00:00:00' + INTERVAL 3600 SECOND;hangi '2009-11-01 01:00:00'. UNIX_TIMESTAMP daha sonra bunu oturum saat dilimi bağlamında UTC'ye dönüştürmeye çalışır - eklemeyi o saat diliminin DST kuralları bağlamında gerçekleştirmeye çalışmaz.
kbro

@kbro Tamam, ancak sorun devam ediyor. Tz oturumu ise America/New_York, 1257051600'ü saklamanın bir yolunu göremiyorum. Siz misiniz?
Steve Clay

78

Bunu amaçlarım için çözdüm. Öğrendiklerimi özetleyeceğim (üzgünüm, bu notlar ayrıntılı; bunlar benim gelecekteki tavsiyem için başka herhangi bir şey kadar çok).

Benim daha önceki yorumların birinde söylediklerini aksine, DATETIME ve TIMESTAMP alanları yok farklı davranır. TIMESTAMP alanları (belgelerde belirtildiği gibi) "YYYY-AA-GG ss: dd: ss" biçiminde gönderdiğiniz her şeyi alır ve geçerli saat diliminizden UTC saatine dönüştürür. Verileri her aldığınızda bunun tersi şeffaf bir şekilde gerçekleşir. DATETIME alanları bu dönüşümü yapmaz. Gönderdiğiniz her şeyi alırlar ve doğrudan saklarlar.

Ne DATETIME ne de TIMESTAMP alan türleri, DST'yi gözlemleyen bir saat diliminde verileri doğru şekilde depolayamaz . "2009-11-01 01:30:00" depolarsanız, alanların hangi 1:30 am sürümünü istediğinizi ayırt etme yolu yoktur - -04: 00 veya -05: 00 sürümü.

Tamam, bu nedenle verilerimizi DST olmayan bir saat diliminde (UTC gibi) saklamalıyız. TIMESTAMP alanları, açıklayacağım nedenlerden dolayı bu verileri doğru şekilde işleyemiyor: Eğer sisteminiz bir DST saat dilimine ayarlanmışsa, o zaman TIMESTAMP'a koyduğunuz şey, geri aldığınız şey olmayabilir. Daha önce UTC'ye dönüştürdüğünüz verileri gönderseniz bile, verilerin yerel saat diliminizde olduğunu varsayar ve yine UTC'ye başka bir dönüştürme yapar. Yerel saat diliminiz DST'yi gözlemlediğinde bu TIMESTAMP tarafından zorunlu kılınan yerel-UTC-dönüş-yerel gidiş-dönüş kayıplı olur ("2009-11-01 01:30:00", 2 farklı olası zamana eşleşir).

DATETIME ile verilerinizi istediğiniz herhangi bir saat diliminde saklayabilir ve ne gönderirseniz gönderin geri alacağınızdan emin olabilirsiniz (TIMESTAMP alanlarının size dayattığı kayıplı gidiş-dönüş dönüşümlerine zorlanamazsınız). Bu nedenle çözüm, bir DATETIME alanı kullanmak ve alana kaydetmeden önce , sistem saat diliminizden onu kaydetmek istediğiniz DST olmayan bölgeye dönüştürün (UTC'nin muhtemelen en iyi seçenek olduğunu düşünüyorum). Bu, "2009-11-01 01:30:00 -04: 00" veya "" 2009-11-01 01:30 UTC eşdeğerini açıkça kaydedebilmeniz için dönüştürme mantığını komut dosyası dilinize oluşturmanıza olanak tanır: 00-05: 00 ".

Unutulmaması gereken bir diğer önemli nokta da, tarihlerinizi bir DST TZ'de saklarsanız, MySQL'in tarih / saat matematik işlevlerinin DST sınırları etrafında düzgün çalışmamasıdır. Bu yüzden UTC'de kaydetmek için daha fazla neden var.

Özetle, şimdi şunu yapıyorum:

Veritabanından veri alırken:

Doğru bir Unix zaman damgası elde etmek için veritabanındaki verileri MySQL dışında UTC olarak açıkça yorumlayın. Bunun için PHP'nin strtotime () işlevini veya DateTime sınıfını kullanıyorum. MySQL'in CONVERT_TZ () veya UNIX_TIMESTAMP () fonksiyonları kullanılarak MySQL içinde güvenilir bir şekilde yapılamaz çünkü CONVERT_TZ yalnızca belirsizlik problemlerinden muzdarip bir 'YYYY-AA-GG ss: dd: ss' değeri verir ve UNIX_TIMESTAMP (), girdi sistem saat dilimindedir, verilerin GERÇEKTEN (UTC) olarak depolandığı saat diliminde değildir.

Verileri veritabanına saklarken:

Tarihinizi MySQL dışında istediğiniz kesin UTC saatine dönüştürün. Örneğin: PHP'nin DateTime sınıfı ile "2009-11-01 1:30:00 EST" yi "2009-11-01 1:30:00 EDT" den farklı olarak belirleyebilir, ardından UTC'ye çevirebilir ve doğru UTC saatini kaydedebilirsiniz. DATETIME alanınıza.

Phew. Herkesin katkısı ve yardımı için çok teşekkürler. Umarım bu, bir başkasını yolda baş ağrısından kurtarır.

BTW, bunu MySQL 5.0.22 ve 5.0.27'de görüyorum


13

Micahwittman'ın bağlantısının bu MySQL sınırlamaları için en iyi pratik çözüme sahip olduğunu düşünüyorum: Bağlandığınızda oturum saat dilimini UTC'ye ayarlayın:

SET SESSION time_zone = '+0:00'

Sonra ona Unix zaman damgaları gönderirsiniz ve her şey yoluna girecek.


Bu tavsiye iyi çalışıyor. Havuzumdaki tüm bağlantıları verilen ifadeyle başlattıktan sonra sorun çözüldü.
snowindy

4

Bu iş parçacığı, değişen kayıtları ve bir veri ambarına giden ETL'yi izlemek için (yani TIMESTAMP:) ile sütunları kullandığımız için beni çılgına çevirdi.On UPDATE CURRENT_TIMESTAMPrecordTimestamp timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP

Birisi merak ederse, bu durumda TIMESTAMPdoğru davranır ve iki benzer tarihi TIMESTAMPunix zaman damgasına dönüştürerek ayırt edebilirsiniz :

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

Ama herhangi bir şeyi 01:30:00 -04: 00'a nasıl kaydederim?

UTC'ye şu şekilde dönüştürebilirsiniz:

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


Daha da iyisi, tarihleri TIMESTAMP alanı olarak kaydedin . Bu her zaman UTC'de saklanır ve UTC yaz / kış saatini bilmiyor.

CONVERT_TZ kullanarak UTC'den yerel zamana dönüştürebilirsiniz :

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

Burada '+00: 00' UTC, başlangıç ​​saat dilimi ve 'SİSTEM' MySQL'in çalıştığı işletim sisteminin yerel saat dilimidir.


Cevabınız için teşekkürler. Söyleyebileceğim en iyi gibi, belgelerin söylediğine rağmen, TIMESTAMP ve Datetime alanları aynı şekilde davranıyor: verilerini UTC'de depoluyorlar, ancak girdilerinin yerel saatte olmasını bekliyorlar ve otomatik olarak UTC'ye dönüştürüyorlar - eğer Önce UTC, veritabanı bunu yaptığımı bilmiyor ve saate 4 (veya DST olup olmamasına bağlı olarak 5) saat daha ekliyor. Öyleyse sorun devam ediyor: 2009-11-01 01:30:00 -04: 00'ı girdi olarak nasıl belirtebilirim?
Aaron

Pekala, kafa karışıklığımın çoğunun kaynağının, verileri bir TIMESTAMP veya DATETIME alanından alıp almasanız da, UNIX_TIMESTAMP () işlevinin her zaman tarih parametresini geçerli saat dilimine göre yorumlaması olduğunu keşfettim. . Şimdi düşünüyorum da bu mantıklı. Daha sonra daha fazla güncelleyeceğim.
Aaron

2

Mysql doğası gereği mysql db'deki time_zone_name tablosunu kullanarak bu sorunu çözer. Yaz saati uygulaması konusunda endişelenmeden tarih saatini güncellemek için CRUD sırasında CONVERT_TZ kullanın.

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

Sayfaların ziyaret sayılarını günlüğe kaydetmeye ve sayıları grafikte görüntülemeye çalışıyordum (Flot jQuery eklentisini kullanarak). Tabloyu test verileriyle doldurdum ve her şey yolunda görünüyordu, ancak grafiğin sonunda noktaların x eksenindeki etiketlere göre bir gün kapalı olduğunu fark ettim. İncelemeden sonra 2015-10-25 gününün görüntüleme sayısının veritabanından iki kez alındığını ve Flot'a geçtiğini, bu nedenle bu tarihten sonraki her gün bir gün sağa kaydırıldığını fark ettim.
Bir süre kodumda bir hata aradıktan sonra, bu tarihin DST'nin gerçekleştiği tarih olduğunu anladım. Sonra bu SO sayfasına geldim ...
... ama önerilen çözümler, ihtiyacım olan şey için aşırıya kaçtı veya başka dezavantajları vardı. Belirsiz zaman damgaları arasında ayrım yapamama konusunda çok endişelenmiyorum. Sadece günlük kayıtları saymam ve görüntülemem gerekiyor.

İlk olarak, tarih aralığını alıyorum:

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

Sonra, bir gün ( ) min_dateile başlayıp biten bir for döngüsünde, sayıları alıyorum:max_date60*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
}

Benim nihai ve hızlı çözüm için benim problem şuydu:

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

Bu yüzden , günün başlangıcında değil, iki saat sonra döngüye bakıyorum . Gün hala aynı ve zaman damgasının gerçek saatine bakılmaksızın veritabanından günün 00:00:00 ile 23:59:59 arasındaki kayıtları açık bir şekilde istediğim için hala doğru sayıları alıyorum. Ve zaman bir saat atladığında, hala doğru gündeyim.

Not: Bunun 5 yıllık bir iş parçacığı olduğunu biliyorum ve bunun OPs sorusuna bir cevap olmadığını biliyorum, ancak bu sayfayla karşılaşan benim gibi insanlara anlattığım soruna çözüm bulmak için yardımcı olabilir.


Muhtemelen asıl soruyla ilgili değildir, ancak bu korkunç derecede verimsizdir ve kimse bunu kopyalamamalıdır! Bunun yerine, aşağıdaki gibi tek bir sorgu çalıştırın:
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
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.