TIMESTAMPTZ vs TIMESTAMP: PostgreSQL panoları ve API’ler
PostgreSQL’de TIMESTAMPTZ ile TIMESTAMP: seçiminiz panoları, API yanıtlarını, saat dilimi dönüşümlerini ve yaz saati uygulaması hatalarını nasıl etkiler.

Gerçek sorun: bir olay, birçok yorum
Bir olay bir kere olur, ama düzinelerce şekilde raporlanır. Veritabanı bir değer saklar, bir API bunu serileştirir, bir pano gruplayıp gösterir ve her kişi bunu kendi saat diliminde görür. Katmanlardan herhangi biri farklı bir varsayım yaparsa aynı satır iki farklı anmış gibi görünebilir.
İşte bu yüzden TIMESTAMPTZ ile TIMESTAMP arasındaki seçim sadece bir veri tipi tercihi değildir. Bu seçim, saklanan değerin belirli bir anı mı temsil ettiği yoksa yalnızca belirli bir yerde anlamlı olan bir duvar-saati zamanı mı olduğunu belirler.
Genellikle ilk bozulan şeyler şunlardır: bir satış panosu New York ve Berlin’de farklı günlük toplamlar gösterir. Saatlik grafikte yaz saati uygulaması (DST) değişikliklerinde bir saat eksik veya tekrar eden bir saat olur. Denetim kaydı sıralama dışı görünür çünkü iki sistem tarihte “anlaşmıştır” ama gerçek anda değil.
Basit bir model sizi sıkıntıdan korur:
- Depolama: PostgreSQL’de ne kaydettiğiniz ve bunun neyi temsil ettiği.
- Gösterim: bir UI’da, ihracatta veya raporda nasıl biçimlendirdiğiniz.
- Kullanıcı yerel ayarı: izleyicinin saat dilimi ve takvim kuralları, DST dahil.
Bunları karıştırırsanız sessiz raporlama hataları elde edersiniz. Bir destek ekibi bir panodan “dün oluşturulan biletleri” dışa aktarır, sonra bunu bir API raporu ile karşılaştırır. İkisi de makul görünür, ama biri görüntüleyicinin yerel gece yarısı sınırını kullanırken diğeri UTC kullandı.
Amaç basit: her zaman değeri için iki net seçim yapın. Ne saklayacağınızı ve ne göstereceğinizi belirleyin. Bu açıklık veri modelinizde, API yanıtlarında ve panolarda korunmalı, böylece herkes aynı zaman çizelgesini görür.
TIMESTAMP ve TIMESTAMPTZ gerçekte ne anlama geliyor
PostgreSQL’de isimler yanıltıcıdır. Sanki ne saklandığını tanımlıyor gibi görünürler ama aslında PostgreSQL’in girişi nasıl yorumladığını ve çıktıyı nasıl biçimlendirdiğini tanımlarlar.
TIMESTAMP (diğer adıyla timestamp without time zone) sadece bir takvim tarihi ve saatidir, örneğin 2026-01-29 09:00:00. Saat dilimi eklenmez. PostgreSQL bunu sizin için dönüştürmez. Farklı saat dilimlerindeki iki kişi aynı TIMESTAMP’i okuyup farklı gerçek dünya anları varsayabilir.
TIMESTAMPTZ (diğer adıyla timestamp with time zone) gerçek bir zamanı, yani bir anı temsil eder. Bunu bir an olarak düşünün. PostgreSQL bunu dahili olarak normalize eder (etkili olarak UTC’ye) ve sonra oturumunuzun kullandığı saat diliminde görüntüler.
Çoğu sürprizin arkasındaki davranış:
- Girişte: PostgreSQL
TIMESTAMPTZdeğerlerini tek bir karşılaştırılabilir ana dönüştürür. - Çıktıda: PostgreSQL o anı mevcut oturum saat dilimini kullanarak biçimlendirir.
TIMESTAMPiçin: girişte veya çıktıda otomatik dönüşüm yapılmaz.
Küçük bir örnek farkı gösterir. Diyelim uygulamanız bir kullanıcıdan 2026-03-08 02:30 alıyor. Bunu bir TIMESTAMP sütununa eklerseniz, PostgreSQL tam olarak o duvar-saati değerini saklar. Eğer o yerel saat DST atlaması nedeniyle mevcut değilse, raporlama bozulana kadar fark etmeyebilirsiniz.
Bunu TIMESTAMPTZ’e eklerseniz, PostgreSQL değeri yorumlamak için bir saat dilimine ihtiyaç duyar. Eğer 2026-03-08 02:30 America/New_York verirseniz, PostgreSQL bunu bir ana dönüştürür (veya kurallara ve değerin tam haline bağlı olarak hata fırlatır). Sonrasında bir pano Londra’da farklı bir yerel saat gösterir, ama hepsi aynı anı temsil eder.
Yaygın bir yanlış anlama: insanlar “with time zone” görünce PostgreSQL’in orijinal saat dilimi etiketini saklayacağını bekler. Saklamaz. PostgreSQL anı saklar, etiketini değil. Kullanıcının orijinal saat dilimini gösterme ihtiyacınız varsa (örneğin “müşterinin yerel saatinde göster”), o bölgeyi ayrı bir metin alanı olarak saklayın.
Oturum saat dilimi: birçok sürprizin arkasındaki gizli ayar
PostgreSQL, ne gördüğünüzü sessizce değiştiren bir ayara sahiptir: oturum saat dilimi. Aynı sorguyu aynı veride iki kişi çalıştırabilir ve farklı saatleri alabilir çünkü oturumları farklı saat dilimleri kullanıyor.
Bu çoğunlukla TIMESTAMPTZ’i etkiler. PostgreSQL mutlak bir an saklar, sonra onu oturum saat diliminde görüntüler. TIMESTAMP (saat dilimsiz) için PostgreSQL değeri düz takvim zamanı gibi davranır. Görüntüleme için kaydırmaz, ama TIMESTAMPTZ ile dönüştürürken veya saat dilimli değerlerle karşılaştırırken oturum saat dilimi sizi zorlayabilir.
Oturum saat dilimleri genellikle farkında olmadan ayarlanır: uygulama başlangıç yapılandırması, sürücü parametreleri, bağlantı havuzlarının eski oturumları yeniden kullanması, BI araçlarının kendi varsayılanları, ETL işlerinin sunucu yerel ayarlarını devralması veya elle kullanılan SQL konsollarının laptop tercihleri.
Ekiplerin nasıl tartışmaya girdiği şu örnekle görülür. Diyelim bir olay TIMESTAMPTZ sütununda 2026-03-08 01:30:00+00 olarak saklandı. America/Los_Angeles oturumundaki bir pano bunu yerel saate göre önceki akşam olarak gösterirken, bir API oturumu UTC olarak farklı bir saat gösterir. Bir grafik günü oturum yerel gün kullanarak gruplayınca farklı günlük toplamlar elde edebilirsiniz.
-- Make your output consistent for a reporting job
SET TIME ZONE 'UTC';
SELECT created_at, date_trunc('day', created_at) AS day_bucket
FROM events;
Rapor veya API çıktısı üreten her şey için saat dilimini açık yapın. Bağlanırken ayarlayın (veya önce SET TIME ZONE çalıştırın), makine çıktıları için bir standart seçin (çoğunlukla UTC) ve “yerel iş zamanı” raporları için işi içinde iş zonunu belirleyin, birinin laptop ayarıyla değil. Eğer havuzlanmış bağlantı kullanıyorsanız, bir bağlantı alındığında oturum ayarlarını sıfırlayın.
Panoların bozulduğu yerler: gruplayma, kova doldurma ve DST boşlukları
Panolar basit görünür: günlük siparişleri say, saatlik kayıtları göster, hafta karşılaştırması yap. Sorunlar veritabanı bir “anı” sakladığında ama grafik bunu kimin baktığına göre birçok farklı “gün”e dönüştürdüğünde başlar.
Eğer kullanıcı yerel saat dilimine göre gruplayorsanız, iki kişi aynı olay için farklı tarihler görebilir. Los Angeles’ta 23:30’ta verilen bir sipariş Berlin’de zaten “ertesi gün”dür. Ve eğer SQL’iniz düz bir TIMESTAMP üzerinde DATE(created_at) ile gruplayorsa, gerçek bir ana göre gruplayamazsınız. Saat dilimi eklenmemiş bir duvar-saati okumasına göre gruplayorsunuz demektir.
Saatlik grafikler DST çevresinde daha karmaşık olur. İlkbaharda bir yerel saat hiç oluşmaz, bu yüzden grafikte boşluk görünür. Sonbaharda bir yerel saat iki kez olur, bu yüzden hangi 01:30’u kastettiğinize dair sorgu ve pano anlaşmazsa zirve veya çift kova alabilirsiniz.
Pratik bir soru yardımcı olur: gerçek anları mı grafikliyorsunuz (güvenle dönüştürülebilir), yoksa yerel program saatini mi (dönüştürülmemeli)? Panolar neredeyse her zaman gerçek anları ister.
UTC ile iş saat dilimi arasında ne zaman gruplayacaksınız
Bir kural seçin ve her yerde uygulayın (SQL, API, BI aracı), aksi halde toplamlar kayar.
Küresel, tutarlı bir seri istiyorsanız UTC ile gruplayın (sistem sağlığı, API trafiği, küresel kayıtlar). “Gün” hukuki veya operasyonel bir anlam taşıyorsa iş saat dilimiyle gruplayın (mağaza günü, destek SLA’ları, finans kapanışı). Sadece kişiselleştirme karşılaştırılabilirlikten daha önemliyse görüntüleyicinin saat dilimiyle gruplayın (kişisel etkinlik akışları).
Tutarlı “iş günü” gruplayıcısı için desen şöyledir:
SELECT date_trunc('day', created_at AT TIME ZONE 'America/New_York') AS business_day,
count(*)
FROM orders
GROUP BY 1
ORDER BY 1;
Güven kırmayı önleyen etiketler
Sayılarda sıçrama olunca insanlar grafiklere güvenmeyi bırakır. Kuralı UI'de açıkça etiketleyin: “Daily orders (America/New_York)” veya “Hourly events (UTC)”. Aynı kuralı dışa aktarmalarda ve API’lerde de kullanın.
Raporlama ve API’ler için basit kural seti
Sakladığınız değerin bir an mı yoksa yerel bir saat okuması mı olduğunu belirleyin. Bu ikisini karıştırmak panoların ve API’lerin birbirleriyle anlaşmazlığına yol açar.
Raporlamayı öngörülebilir tutacak bir kural seti:
- Gerçek dünya olaylarını anlar olarak
TIMESTAMPTZile saklayın ve UTC’yi doğruluk kaynağı olarak kabul edin. - “Fatura günü” gibi iş kavramlarını ayrı olarak
DATE(veya gerçekten duvar-saati gerekiyorsa yerel zaman alanı) olarak saklayın. - API’lerde zaman damgalarını ISO 8601 formatında döndürün ve tutarlı olun: her zaman bir ofset (
+02:00gibi) ya da UTC içinZkullanın. - Dönüştürmeyi kenarlarda yapın (UI ve raporlama katmanı). Veritabanı mantığı ve arka plan işler içinde sürekli geri dönüşüm yapmaktan kaçının.
Bunun neden işe yaradığı: panolar aralıkları kovalar ve karşılaştırır. Eğer anları (TIMESTAMPTZ) saklarsanız, PostgreSQL DST değişimlerinde bile olayları güvenilir şekilde sıralayıp filtreleyebilir. Sonra nasıl görüntüleyeceğinize veya gruplayacağınıza karar verirsiniz. Eğer yerel bir saat (TIMESTAMP) saklarsanız PostgreSQL bunun ne anlama geldiğini bilemez, bu yüzden oturum saat dilimi değiştiğinde gruplayma değişebilir.
“Yerel iş tarihlerini” ayrı tutun çünkü onlar an değildir. “2026-03-08 tarihinde teslim et” bir tarih kararıdır, bir an değil. Bunu zorla bir zaman damgasına sokarsanız DST günleri eksik veya tekrar eden saatler yaratabilir ve bu da sonra boşluklar veya sıçramalar olarak ortaya çıkar.
Adım adım: her zaman değeri için doğru tipi seçme
TIMESTAMPTZ ile TIMESTAMP arasında seçim bir soruyla başlar: bu değer gerçek bir anda mı oldu, yoksa tam olarak yazıldığı gibi kalması gereken yerel bir zaman mı?
1) Gerçek olayları ve planlanmış yerel zamanları ayırın
Sütunlarınızın hızlı bir envanterini çıkarın.
Gerçek olaylar (tıklamalar, ödemeler, girişler, gönderimler, sensör okumaları, destek mesajları) genellikle TIMESTAMPTZ olarak saklanmalıdır. Farklı saat dilimlerinden bakıldığında bile tek anlamlı anı istersiniz.
Planlanmış yerel zamanlar farklıdır: “Mağaza sabah 09:00’de açılır”, “Teslim alma aralığı 16:00–18:00”, “Faturalama yerel saate göre 1’inde 10:00’da çalışır”. Bunlar genellikle TIMESTAMP + ayrı bir saat dilimi alanı olarak daha uygundur, çünkü niyet bir yerin duvar saatine bağlıdır.
2) Bir standart seçin ve yazılı hale getirin
Çoğu ürün için iyi bir varsayılan: olay zamanlarını UTC’de saklayın, gösterimleri kullanıcının saat diliminde yapın. Bunu şemada, API dokümantasyonunda ve pano açıklamalarında yazın. Ayrıca “iş günü”nün ne anlama geldiğini (UTC günü, iş zonu günü veya görüntüleyici-yerel gün) tanımlayın; çünkü bu seçim günlük raporlamayı belirler.
Pratikte işe yarayan kısa bir kontrol listesi:
- Her zaman sütununu “olay anı” veya “yerel program” olarak etiketleyin.
- Olay anları için varsayılan olarak UTC’de
TIMESTAMPTZkullanın. - Şemayı değiştirirken dikkatle backfill yapın ve örnek satırları elle doğrulayın.
- API formatlarını standartlaştırın (anlar için her zaman
Zveya bir ofset dahil olsun). - ETL işlerinizde, BI bağlayıcılarında ve arka plan işlerinde oturum saat dilimini açıkça ayarlayın.
“Dönüştür ve geri doldur” işlerindeki dikkat: bir sütun tipini değiştirmek eski değerler farklı bir oturum saat dilimi altında yorumlandıysa anlamı sessizce değiştirebilir.
Bir gün eksi veya fazlası yapan ve DST hatalarına neden olan yaygın hatalar
Çoğu zaman hatası “PostgreSQL garip davranıyor”dan kaynaklanmaz. Doğru görünen değeri yanlış anlama ile saklamak ve farklı katmanların eksik bağlamı tahmin etmesine izin vermekten kaynaklanır.
Hata 1: Duvar-saati zamanını mutlakmış gibi kaydetmek
Yaygın tuzaklardan biri Berlin’deki “2026-03-29 09:00” gibi yerel duvar-saati zamanlarını TIMESTAMPTZ içine kaydetmektir. PostgreSQL bunu bir an olarak ele alır ve mevcut oturum saat dilimine göre dönüştürür. Eğer niyet her zaman yerel saat 9 ise, bunu kaybedersiniz. Aynı satırı farklı bir oturum saat diliminde görüntülemek saati kaydırır.
Randevular için yerel zamanı TIMESTAMP + ayrı saat dilimi (veya konum) alanı olarak saklayın. Gerçek anı ifade eden olaylar (ödeme, giriş) için TIMESTAMPTZ saklayın.
Hata 2: Farklı ortamlar, farklı varsayımlar
Laptopunuz, staging ve prod aynı saat dilimini paylaşmayabilir. Bir ortam UTC çalıştırırken diğeri yerel zamanı kullanır ve “gün ile grupla” raporları uyuşmaz. Veri değişmedi; oturum ayarı değişti.
Hata 3: Saat fonksiyonlarının ne vaat ettiğini bilmeden kullanmak
now() ve current_timestamp bir işlem içinde kararlı kalır. clock_timestamp() her çağrıda değişir. Bir işlem içinde birden çok noktada zaman damgası üretir ve bu fonksiyonları karıştırırsanız sıralama ve süreler garip görünebilir.
Hata 4: İki kez (veya hiç) dönüştürme
Sık görülen bir API hatası: uygulama yerel zamanı UTC’ye çevirir, sonra bunu naif bir string olarak gönderir, veritabanı oturumu girişi yerel olarak yorumladığı için tekrar dönüştürür. Tersi de olur: uygulama yerel zamanı gönderir ama bunu Z ile etiketler (UTC), render ederken kayma olur.
Hata 5: Gruplarken hangi saat diliminin kullanılacağını söylememek
“Günlük toplamlar” hangi gün sınırını kastettiğinize bağlıdır. date(created_at) ile gruplayınca sonuç oturum saat dilimine göre olur (eğer TIMESTAMPTZ ise). Gece geç saat olayları önceki veya sonraki güne kayabilir.
Bir pano veya API göndermeden önce temel kontrolleri yapın: her grafik için bir raporlama saat dilimi seçin ve bunu tutarlı uygulayın, API’lerde ofset (veya Z) ekleyin, staging ve prod’u saat dilimi politikasında hizalayın ve gruplayınca hangi saat dilimini kastettiğinizi açıkça belirtin.
Göndermeden önce hızlı kontroller
Zaman hataları nadiren tek kötü sorgudan kaynaklanır. Depolama, raporlama ve API her biri biraz farklı varsayım yaptığında ortaya çıkar.
Kısa bir gönderim öncesi kontrol listesi:
- Gerçek dünya olayları (kayıtlar, ödemeler, sensör pingleri) için anı
TIMESTAMPTZolarak saklayın. - İşe özgü kavramlar (fatura günü, raporlama tarihi) için
DATEveyaTIMEkullanın, sonra dönüştürmeyi planlamayın. - Zamanlanan işler ve rapor çalıştırıcılarında oturum saat dilimini kasıtlı olarak ayarlayın.
- API yanıtlarında bir ofset veya
Zekleyin ve istemcinin bunu saat dilimi farkındalığıyla parse ettiğinden emin olun. - Hedef saat dilimi için DST geçiş haftasını test edin.
Hızlı bir uçtan uca doğrulama: bilinen bir kenar durum olayını seçin (örneğin 2026-03-08 01:30 DST gözlemleyen bir bölgede) ve bunu saklama, sorgu çıktısı, API JSON’u ve son grafik etiketi boyunca takip edin. Eğer grafik doğru günü ama tooltip yanlış saati gösteriyorsa (veya tam tersi), dönüşüm uyuşmazlığınız var demektir.
Örnek: aynı günün sayıları üzerinde neden iki ekip anlaşmaz
New York’taki bir destek ekibi ile Berlin’deki finans ekibi aynı panoya bakıyor. Veritabanı sunucusu UTC’de çalışıyor. Herkes kendi sayısının doğru olduğunu iddia ediyor, ama “dün” kime sorulduğuna göre farklı.
Olay: bir müşteri bileti New York’ta 23:30’da 10 Mart’ta oluşturuluyor. Bu UTC’de 11 Mart 04:30 ve Berlin’de 05:30. Bir gerçek an, üç farklı takvim tarihi.
Eğer biletin oluşturma zamanı TIMESTAMP (saat dilimsiz) olarak saklandıysa ve uygulamanız bunu “yerel” varsayarsa, sessizce tarihi yeniden yazabilirsiniz. New York 2026-03-10 23:30’u New York saati olarak ele alırken, Berlin aynı saklanan değeri Berlin saati olarak yorumlayabilir. Aynı satır farklı izleyiciler için farklı günlere düşer.
Eğer TIMESTAMPTZ olarak saklanmışsa, PostgreSQL anı tutarlı biçimde saklar ve sadece görüntülendiğinde veya biçimlendirildiğinde dönüştürür. İşte bu yüzden TIMESTAMPTZ ile TIMESTAMP arasındaki seçim raporlarda “bir gün”ün ne anlama geldiğini değiştirir.
Çözüm iki fikri ayırmaktır: olayın gerçekleştiği an ve kullanmak istediğiniz raporlama tarihi.
Pratik bir desen:
- Olay zamanını
TIMESTAMPTZolarak saklayın. - Raporlama kuralını karar verin: görüntüleyici-yerel (kişisel panolar) veya tek bir iş saat dilimi (kurum çapı finans).
- Raporlama tarihini sorgu zamanında bu kurala göre hesaplayın: anı seçilen bölgeye çevirin, sonra tarihe bakın.
Sonraki adımlar: yığınınızda zaman yönetimini standartlaştırın
Zaman yönetimi yazılı değilse, her yeni rapor bir tahmin oyunu olur. Veritabanı, API’ler ve panolar arasında sıkıcı ve öngörülebilir bir zaman davranışı hedefleyin.
Kısa bir “zaman sözleşmesi” yazın ve üç soruyu yanıtlayın:
- Olay zamanı standardı: olay anlarını
TIMESTAMPTZolarak saklayın (genelde UTC) yoksa güçlü bir neden olmadığı sürece. - İş saat dilimi: raporlama için bir bölge seçin ve “gün”, “hafta” ve “ay” tanımlarken tutarlı kullanın.
- API formatı: zaman damgalarını her zaman ofset ile gönderin (ISO 8601
Zveya+/-HH:MM) ve alanların “an” mı yoksa “yerel duvar-saati” mi olduğunu dokümante edin.
DST başlangıcı ve bitişi etrafında küçük testler ekleyin. Bunlar pahalı hataları erken yakalar. Örneğin, sabit bir iş zonu için “günlük toplam” sorgusunun DST değişimi boyunca kararlı olduğunu doğrulayın ve API girişlerinin 2026-11-01T01:30:00-04:00 ile 2026-11-01T01:30:00-05:00’in iki farklı an olarak ele alındığını kontrol edin.
Geçiş planlarını dikkatle yapın. Türleri ve varsayımları yerinde değiştirmek grafiklerde geçmişi sessizce yeniden yazabilir. Daha güvenli bir yol yeni bir sütun eklemektir (örneğin created_at_utc TIMESTAMPTZ), bilinen bir dönüşümle backfill yapın, okumaları yeni sütunu kullanacak şekilde güncelleyin, sonra yazmaları güncelleyin. Eski ve yeni raporları kısa süre birlikte tutun ki günlük sayılardaki kaymalar görünür olsun.
Eğer bu “zaman sözleşmesini” veri modelleri, API’ler ve ekranlar arasında uygulamak için tek bir yer istiyorsanız, birleşik bir oluşturma altyapısı yardımcı olur. AppMaster (appmaster.io) tek bir projeden backend, web uygulaması ve API üreterek zaman damgası saklama ve gösterme kurallarını uygulamayı kolaylaştırır.
SSS
Use TIMESTAMPTZ for anything that happened at a real moment (signups, payments, logins, messages, sensor pings). It stores one unambiguous instant and can be safely sorted, filtered, and compared across systems. Use plain TIMESTAMP only when the value is meant to be a wall-clock time that should stay exactly as written, usually paired with a separate time zone or location field.
TIMESTAMPTZ represents a real instant in time; PostgreSQL normalizes it internally and then displays it in your session time zone. TIMESTAMP is just a date and clock time with no zone attached, so PostgreSQL won’t shift it automatically. The key difference is meaning: instant versus local wall time.
Because the session time zone controls how TIMESTAMPTZ is formatted on output and how some inputs are interpreted. Two tools can query the same row and show different clock times if one session is set to UTC and another to America/Los_Angeles. For reports and APIs, set the session time zone explicitly so results don’t depend on hidden defaults.
Because “a day” depends on a time zone boundary. If one dashboard groups by viewer-local time while another groups by UTC (or a business zone), late-night events can fall on different dates and change daily totals. Fix it by picking one grouping rule per chart (UTC or a specific business zone) and using it consistently in SQL, BI, and exports.
DST creates missing or duplicated local hours, which can produce gaps or double-counted buckets when grouping by local time. If your data represents real moments, store it as TIMESTAMPTZ and choose a clear chart time zone for bucketing. Also test the DST transition week for your target zones to catch surprises early.
No, PostgreSQL does not preserve the original time zone label with TIMESTAMPTZ; it stores the instant. When you query it, PostgreSQL displays it in the session time zone, which may differ from the user’s original zone. If you need “show it in the customer’s time zone,” store that zone separately in another column.
Return ISO 8601 timestamps that include an offset, and be consistent. A simple default is to always return UTC with Z for event instants, then let clients convert for display. Avoid sending “naive” strings like 2026-03-10 23:30:00 because clients will guess the zone differently.
Convert at the edges: store event instants as TIMESTAMPTZ, then convert to the desired zone when you display or bucket for reporting. Avoid converting back and forth inside triggers, background jobs, and ETL unless you have a clear contract. Most reporting problems come from double conversion or from mixing naive and time-zone-aware values.
Use DATE for business concepts that are truly dates, like “billing day,” “reporting date,” or “delivery date.” Use TIME (or TIMESTAMP plus a separate time zone) for schedules like “opens at 09:00 local time.” Don’t force these into TIMESTAMPTZ unless you really mean a single instant, because DST and zone changes can shift the intended meaning.
First, decide whether it’s an instant (TIMESTAMPTZ) or a local wall time (TIMESTAMP plus zone), then add a new column instead of rewriting in place. Backfill with a reviewed conversion under a known session time zone, and validate sample rows around midnight and DST boundaries. Run old and new reports side by side briefly so any shifts in totals are obvious before you remove the old column.


