Rapor bir saat kayık: tarih, saat dilimi ve yaz saati nasıl doğru saklanır?
Aynı siparişi panelde 14:30, e-postada 11:30, raporda bir gün önce gösteren sistemlerin ortak bir tasarım hatası vardır: zamanın tek çeşit olduğu varsayılmıştır. Pratik kural üç maddeye sığar. Olmuş bitmiş bir olayı UTC olarak saklayın. Gelecekteki bir randevuyu yerel saatiyle ve IANA saat dilimi kimliğiyle (Europe/Istanbul gibi) birlikte saklayın. Doğum tarihi, fatura vadesi gibi takvim tarihlerini hiç saat bilgisine çevirmeyin. Geri kalan hataların büyük kısmı bu üç ayrımın karıştırılmasından çıkar.
Üç farklı zaman türü aynı sütuna yazılmaz
Birincisi anlık: siparişin oluştuğu, ödemenin geçtiği, kaydın değiştiği o tek an. Dünyanın her yerinde aynı andır, saat dilimi yalnızca onu nasıl gösterdiğinizle ilgilidir. UTC olarak saklanır.
İkincisi takvim tarihi ya da duvar saati: doğum tarihi, sözleşme başlangıcı, fatura vadesi, otel giriş tarihi. Bunların saat dilimi yoktur. Doğum tarihini timestamp'e çevirip UTC gece yarısına yazan bir sistem, UTC-5'teki kullanıcıya tarihi bir gün geriden gösterir. Klasik ve sinsi bir hatadır, çünkü Türkiye'deki test ortamında hiç görünmez.
Üçüncüsü gelecekteki yerel zaman: "3 Kasım saat 10:00'da Ankara'da toplantı". Bu bir anlık değildir. Hangi mutlak ana denk geleceği, o tarihe kadar kuralların değişmemesine bağlıdır. Kulağa teorik geliyor olabilir; değil.
UTC her sorunu çözmez, gelecekteki randevuyu bozar
Türkiye Eylül 2016'da yaz saati uygulamasını bıraktı ve kalıcı olarak UTC+3'e geçti. O karardan önce kasım ayına randevu alan bir sistem düşünün. Randevuyu "07:00 UTC" olarak kaydetmişse, kayıt teknik olarak doğruydu: o tarihte Türkiye UTC+2 olacaktı ve yerel karşılığı 09:00'du. Kural değişince kayıt aynı kaldı, yerel karşılığı 10:00'a kaydı. Aynı sorunun tersi de olur, saatler kaydığında kimse veritabanına gidip düzeltmez.
Doğrusu şu: gelecekteki olay için yerel saati ve saat dilimi kimliğini asıl kayıt olarak tutun, UTC karşılığını türetilmiş bir sütun olarak yazın. Sıralama ve indeks için UTC sütunu işinizi görür, kurallar değiştiğinde de o sütunu yeniden hesaplarsınız. Geçmişteki olaylarda tersi geçerlidir: onlar zaten yaşandı, UTC anı değişmez gerçektir.
Saat dilimi kimliği ile saat farkı aynı şey değil
+03:00 bir farktır, bir kural değil. Yalnızca tek bir andaki durumu anlatır, yarın aynı yerde ne olacağını söylemez. Saat dilimi kimliği (Europe/Istanbul, America/Sao_Paulo) ise o bölgenin bütün geçmiş ve gelecek kurallarını taşır. Kullanıcı profiline saat farkını değil kimliği yazın.
Kimlikler de sabit değildir. tz veritabanının 2022b sürümünde Europe/Kiev, Europe/Kyiv olarak yeniden adlandırıldı; eski ad geriye dönük uyumluluk için takma ad olarak korundu. Bu yüzden kimlikleri kendi tablonuza kopyalayıp orada dondurmak yerine, sistemin sağladığı listeyi kullanın ve karşılaştırmaları takma adları da çözecek şekilde yapın.
tz veritabanı bir bağımlılıktır ve her katmanda ayrı kopyası vardır
Ülkeler kural değiştirdikçe IANA tz veritabanı güncellenir. Yılda birkaç sürüm çıkar; 2026'nın ilk yarısında 2026a (mart), 2026b (nisan) ve 2026c (temmuz) yayımlandı. Son yıllarda kural değiştiren ülke az değil: Brezilya 2019'da, İran ve Meksika'nın büyük bölümü 2022'de yaz saatini kaldırdı. Avrupa Birliği ise 2018'deki öneriye rağmen hâlâ mart ve ekim sonunda saatleri değiştiriyor.
Sorun şurada: bu veritabanının işletim sisteminde, çalışma zamanında, veritabanı sunucusunda ve tarayıcıda ayrı kopyaları vardır ve sürümleri farklı olabilir. Uzun süredir yeniden derlenmemiş bir konteyner imajı eski kurallarla çalışır. Hata vermez, sessizce yanlış hesaplar. MySQL kullanıyorsanız ek bir tuzak var: saat dilimi tabloları yüklenmemişse isimle saat dilimi çeviren fonksiyonlar beklediğiniz gibi çalışmaz.
Veritabanında hangi tipi seçmeli
PostgreSQL'de timestamptz değeri UTC olarak saklar ve adının çağrıştırdığının aksine kaynağın saat dilimini saklamaz; girişte UTC'ye çevirir, çıkışta oturumun TimeZone ayarına göre gösterir. Anlıklar için doğru tip budur. timestamp (saat dilimsiz) hiçbir dönüşüm yapmaz, duvar saati için uygundur.
MySQL'de TIMESTAMP oturumun saat dilimine göre UTC'ye çevrilir, DATETIME çevrilmez. Buradaki asıl sınır ise tarihte: TIMESTAMP en fazla 2038-01-19 03:14:07 UTC değerini tutar. 15 yıllık bir kira sözleşmesinin bitiş tarihi ya da uzun vadeli bir kredi taksiti bugün bile o sınırın ötesine düşüyor. DATETIME 9999 yılına kadar gider ama dönüşüm yapmadığı için saat dilimini siz yönetmek zorundasınız.
Gelecekteki olayları saklarken üç sütunlu desen işi çözer: yerel duvar saati, saat dilimi kimliği, hesaplanmış UTC anı. Üçünü birlikte tutmak, hem sorgulamayı hem kural değişikliğinden sonra düzeltmeyi mümkün kılar.
Var olmayan saatler ve iki kez yaşanan saatler
Saatlerin ileri alındığı gecede yerel saat 03:00'a atlar, yani 02:30 diye bir an hiç yaşanmaz. Geri alındığı gecede ise 02:30 iki kez yaşanır. Kütüphaneler bu iki durumu farklı ele alır: kimi hata fırlatır, kimi ileri kaydırır, kimi ilk oluşumu seçer. Uygulamanızda hangi politikanın geçerli olduğunu bilerek yazmadıysanız, kütüphanenin varsayılanını miras alırsınız.
Zamanlanmış işlerde bu doğrudan üretim sorununa dönüşür. Yerel saatle 02:30'da çalışan bir gece işi, yılda bir kez hiç çalışmaz, bir kez de iki defa çalışır. Sunucuların ve zamanlayıcıların saatini UTC'de tutmak birinci savunmadır; ikincisi işleri aynı girdiyle iki kez çalıştığında zarar vermeyecek şekilde yazmaktır. Arka plan işleri ve kuyruk yazımızda bu tekrar güvenliğini ayrıntılandırmıştık.
Raporun gün sınırı sunucunun değil işin saat dilimidir
"Dün kaç sipariş aldık" sorusunun cevabı, dünün hangi gece yarısında başladığına bağlıdır. Sunucu UTC'de çalışıyorsa ve iş İstanbul'daysa, her gün 00:00 ile 03:00 arasında gelen siparişler raporda bir önceki güne düşer. Ay sonlarında bu fark ciro tablosunu tutmaz hale getirir.
Çözüm teknik değil tanımsal: rapor tanımının içine gün sınırının hangi saat diliminde hesaplandığını yazın, sorguyu da o tanıma göre kurun. Metrik tanımlarının nerede yaşaması gerektiğini raporlarda tutarlılık yazımızda anlatmıştık.
Uygulama katmanında doğru tipleri kullanın
Java'da java.time paketi ayrımı zaten yapmış durumda: anlık için Instant, takvim tarihi için LocalDate, saat dilimli yerel zaman için ZonedDateTime. Python'da 3.9 ile gelen zoneinfo modülü standart kütüphanenin parçası; saat dilimi bilgisi taşımayan "naive" datetime nesnelerini uygulama sınırlarında dolaştırmayın.
JavaScript uzun süre bu konunun zayıf halkasıydı, çünkü Date yalnızca UTC ve tarayıcının yerel saatini bilir. Temporal API bunu değiştiriyor: TC39'un mart 2026 toplantısında Stage 4'e geçti, Chrome 144 (ocak 2026) ve Firefox 139 (mayıs 2025) destekliyor, Safari kararlı sürümde henüz yok. Yani bugün üretimde kullanacaksanız polyfill gerekiyor. Yalnızca gösterim yapacaksanız Intl.DateTimeFormat fonksiyonuna timeZone seçeneğini vermek çoğu ihtiyacı karşılar.
Sınırdan geçen her zaman damgası saat dilimini taşısın
API'lerde ve dosya aktarımlarında zaman damgasını ISO 8601 / RFC 3339 biçiminde, farkıyla birlikte gönderin: 2026-09-09T14:30:00+03:00. 09.09.2026 14:30 gibi bir metin, alıcının yerel ayarına göre yorumlanır ve entegrasyonlarda gördüğünüz "bir gün kayma" hatalarının önemli bir kısmı buradan gelir.
Saat dilimi kimliğini de taşımak isterseniz RFC 9557 (nisan 2024) tam olarak bunu standartlaştırdı: 2026-11-03T10:00:00+03:00[Europe/Istanbul]. Temporal de bu biçimi kullanıyor. Böyle bir alan eklemek API sözleşmenizde kırıcı bir değişikliktir; sürümleme ve geriye dönük uyumluluk yazımızdaki yaklaşımı uygulayın.
Test etmeden emin olamazsınız
Bu hataların hepsi Türkiye'de, UTC+3 sabit farkla çalışan bir makinede sessiz kalır. Ortaya çıkmaları için ortamı zorlamak gerekir. CI'da test paketini UTC dışı bir saat diliminde çalıştırın; Pacific/Chatham gibi 45 dakikalık farkı olan bir bölge, tam saat varsayan kodu hemen yakalar. Yaz saati geçiş tarihlerini sabit test verisi haline getirin, 29 Şubat'ı da unutmayın.
En önemlisi, kodun her yerinden doğrudan "şu an" çağırmayı bırakın. Saati enjekte edilebilir bir bağımlılık haline getirirseniz, testte zamanı istediğiniz ana sabitleyip geçiş anlarını tekrarlanabilir biçimde deneyebilirsiniz. Test otomasyonu stratejisi yazımız bu tür kenar durumlarının hangi test katmanına ait olduğunu tartışıyor.
Bugün yapılabilecek beş kontrol
- Veritabanı şemanızda tarih tutan sütunları listeleyin ve her birinin anlık mı, takvim tarihi mi, gelecekteki yerel zaman mı olduğunu işaretleyin. Yanlış tipte olanlar hemen görünür.
TIMESTAMPkullanan MySQL sütunlarında 2038 sonrası değer gerekip gerekmediğini kontrol edin.- Üretim imajlarınızdaki tz veritabanı sürümüne bakın ve güncelleme akışına bağlayın.
- Zamanlanmış işlerinizin yerel saate göre mi UTC'ye göre mi çalıştığını doğrulayın, geçiş gecelerinde ne olacağını yazılı hale getirin.
- Raporlarınızın gün sınırını hangi saat diliminde hesapladığını tanımlarına ekleyin.
Bu maddelerden birinde takıldıysanız ya da mevcut sisteminizde saat kaymalarının nereden geldiğini bulamıyorsanız, şemayı ve akışı birlikte gözden geçirip somut bir düzeltme planı çıkarabiliriz.
Bu konuda yardıma mı ihtiyacınız var?