TIMESTAMPTZ vs TIMESTAMP: dashboardy PostgreSQL i API
TIMESTAMPTZ vs TIMESTAMP w PostgreSQL: jak wybór typu wpływa na dashboardy, odpowiedzi API, konwersje stref czasowych i błędy związane ze zmianą czasu.

Prawdziwy problem: jedno zdarzenie, wiele interpretacji
Zdarzenie dzieje się raz, ale jest raportowane na tuzin sposobów. Baza danych przechowuje wartość, API ją serializuje, dashboard grupuje, a każdy użytkownik ogląda ją w swojej strefie czasowej. Jeśli któraś warstwa przyjmie inną domyślną interpretację, ten sam wiersz może wyglądać jak dwa różne momenty.
Dlatego wybór między TIMESTAMPTZ a TIMESTAMP to nie tylko preferencja typu danych. Decyduje on, czy przechowywana wartość reprezentuje konkretny punkt w czasie, czy czas „na zegarze ściennym”, który ma sens tylko w określonym miejscu.
Zwykle to właśnie to najpierw się psuje: panel sprzedaży pokazuje różne dzienne sumy w Nowym Jorku i w Berlinie. Wykres godzinowy ma brakującą godzinę lub zdublowaną godzinę podczas zmiany czasu (DST). Dziennik audytu wygląda nieuporządkowany, bo dwa systemy „zgadzają się” co do daty, ale nie co do rzeczywistego momentu.
Prosty model chroni przed problemami:
- Przechowywanie: co zapisujesz w PostgreSQL i co to reprezentuje.
- Wyświetlanie: jak formatujesz je w UI, eksporcie lub raporcie.
- Lokalizacja użytkownika: strefa czasowa i reguły kalendarza widza, w tym DST.
Jeśli je pomieszasz, powstaną ciche błędy raportowe. Zespół wsparcia eksportuje „zgłoszenia utworzone wczoraj” z dashboardu, a potem porównuje to z raportem API. Oba wyglądają rozsądnie, ale jedno użyło lokalnego północy widza, a drugie użyło UTC.
Cel jest prosty: dla każdej wartości czasu podejmij dwie jasne decyzje. Zdecyduj, co przechowujesz, i zdecyduj, co pokazujesz. Ta sama jasność musi przejść przez model danych, odpowiedzi API i dashboardy, żeby wszyscy widzieli tę samą linię czasu.
Co naprawdę znaczą TIMESTAMP i TIMESTAMPTZ
W PostgreSQL nazwy mogą wprowadzać w błąd. Wyglądają, jakby opisywały to, co jest przechowywane, ale w rzeczywistości głównie opisują, jak PostgreSQL interpretuje wejście i formatuje wyjście.
TIMESTAMP (czyli timestamp without time zone) to po prostu data i godzina kalendarzowa, np. 2026-01-29 09:00:00. Nie jest dołączona żadna strefa czasowa. PostgreSQL nie będzie jej za Ciebie konwertował. Dwie osoby w różnych strefach mogą odczytać ten sam TIMESTAMP i założyć różne rzeczywiste momenty.
TIMESTAMPTZ (czyli timestamp with time zone) reprezentuje prawdziwy punkt w czasie. Myśl o nim jak o instancie. PostgreSQL normalizuje go wewnętrznie (efektywnie do UTC), a potem wyświetla w strefie czasowej używanej przez sesję.
Zachowanie stojące za większością niespodzianek to:
- Przy wprowadzaniu: PostgreSQL konwertuje wartości
TIMESTAMPTZdo jednego porównywalnego instantu. - Przy wyjściu: PostgreSQL formatuje ten instant używając bieżącej strefy sesji.
- Dla
TIMESTAMP: nie zachodzi automatyczna konwersja przy wejściu ani wyjściu.
Mały przykład pokazuje różnicę. Załóżmy, że twoja aplikacja otrzymuje 2026-03-08 02:30 od użytkownika. Jeśli wstawisz to do kolumny TIMESTAMP, PostgreSQL zapisze dokładnie ten odczyt ściany zegarowej. Jeśli ta lokalna godzina nie istnieje z powodu skoku DST, możesz tego nie zauważyć, dopóki raportowanie się nie zepsuje.
Jeśli wstawisz do TIMESTAMPTZ, PostgreSQL potrzebuje strefy czasowej, żeby zinterpretować wartość. Jeśli podasz 2026-03-08 02:30 America/New_York, PostgreSQL przekonwertuje to na instant (albo wyrzuci błąd, w zależności od reguł i dokładnej wartości). Później dashboard w Londynie pokaże inną lokalną godzinę na zegarze, ale to ten sam instant.
Jedno powszechne nieporozumienie: ludzie widzą „with time zone” i oczekują, że PostgreSQL przechowa oryginalną etykietę strefy. Nie robi tego. PostgreSQL przechowuje moment, nie etykietę. Jeśli potrzebujesz oryginalnej strefy użytkownika do wyświetlania (np. „pokaż w lokalnym czasie klienta”), przechowaj strefę oddzielnie jako pole tekstowe.
Strefa sesji: ukryte ustawienie stojące za wieloma niespodziankami
PostgreSQL ma ustawienie, które cicho zmienia to, co widzisz: strefę czasową sesji. Dwie osoby mogą uruchomić to samo zapytanie na tych samych danych i otrzymać różne czasy zegarowe, ponieważ ich sesje używają różnych stref.
To głównie wpływa na TIMESTAMPTZ. PostgreSQL przechowuje absolutny moment, a potem wyświetla go w strefie sesji. W przypadku TIMESTAMP (bez strefy) PostgreSQL traktuje wartość jako zwykły czas kalendarzowy. Nie przesuwa jej dla wyświetlenia, ale strefa sesji nadal może zaszkodzić, gdy konwertujesz go do TIMESTAMPTZ lub porównujesz z wartościami świadomymi strefy.
Strefy sesji często są ustawiane bez twojej wiedzy: konfiguracja startowa aplikacji, parametry sterownika, pule połączeń używające starych sesji, narzędzia BI z własnymi domyślnymi ustawieniami, zadania ETL dziedziczące ustawienia lokalne serwera, albo ręczne konsole SQL używające preferencji twojego laptopa.
Tak zespoły kończą kłótnie. Załóżmy, że zdarzenie jest zapisane jako 2026-03-08 01:30:00+00 w kolumnie TIMESTAMPTZ. Sesja dashboardu ustawiona na America/Los_Angeles wyświetli to jako lokalny czas poprzedniego wieczoru, podczas gdy sesja API w UTC pokaże inną godzinę. Jeśli wykres grupuje wg dnia używając lokalnego dnia sesji, możesz dostać różne dzienne sumy.
-- 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;
Dla wszystkiego, co generuje raporty lub odpowiedzi API, ustaw strefę czasową jawnie. Ustaw ją przy połączeniu (lub uruchom SET TIME ZONE najpierw), wybierz jeden standard dla wyjść maszynowych (często UTC), a dla raportów "lokalnego czasu biznesowego" ustaw strefę biznesową wewnątrz zadania, nie na laptopie kogoś. Jeśli używasz połączeń w puli, resetuj ustawienia sesji, gdy połączenie jest pobierane z puli.
Jak dashboardy się psują: grupowanie, kubełki i luki DST
Dashboardy wydają się proste: policz zamówienia na dzień, pokaż rejestracje na godzinę, porównaj tydzień do tygodnia. Problemy zaczynają się, gdy baza przechowuje jeden „moment”, ale wykres zamienia go w wiele różnych „dni”, w zależności od tego, kto patrzy.
Jeśli grupujesz wg dnia używając lokalnej strefy użytkownika, dwie osoby mogą widzieć różne daty tego samego zdarzenia. Zamówienie złożone o 23:30 w Los Angeles jest już „jutro” w Berlinie. A jeśli w SQL grupujesz przez DATE(created_at) na zwykłym TIMESTAMP, to nie grupujesz po prawdziwym momencie — grupujesz po odczycie ściany zegarowej bez dołączonej strefy.
Wykresy godzinowe robią się bardziej skomplikowane wokół DST. Na wiosnę jedna lokalna godzina nigdy nie występuje, więc wykresy mogą pokazywać lukę. Jesienią jedna lokalna godzina występuje dwa razy, więc możesz dostać skok lub podwójne kubełki, jeśli zapytanie i dashboard nie zgadzają się, o którą godzinę 01:30 chodzi.
Pytanie praktyczne pomaga: czy wykresujesz prawdziwe momenty (bezpiecznie konwertowalne), czy lokalne godziny harmonogramu (nie wolno konwertować)? Dashboardy prawie zawsze chcą prawdziwych momentów.
Kiedy grupować wg UTC, a kiedy wg strefy biznesowej
Wybierz jedną regułę grupowania i stosuj ją wszędzie (SQL, API, narzędzie BI), inaczej sumy będą dryfować.
Grupuj wg UTC, gdy chcesz globalnej, spójnej serii (zdrowie systemu, ruch API, globalne rejestracje). Grupuj wg strefy biznesowej, gdy „dzień” ma znaczenie prawnicze lub operacyjne (dzień sklepu, SLA wsparcia, zamknięcie finansowe). Grupuj wg strefy widza tylko, gdy personalizacja jest ważniejsza niż porównywalność (osobiste kanały aktywności).
Oto wzorzec dla spójnego grupowania „dnia biznesowego":
SELECT date_trunc('day', created_at AT TIME ZONE 'America/New_York') AS business_day,
count(*)
FROM orders
GROUP BY 1
ORDER BY 1;
Etykiety, które zapobiegają utracie zaufania
Ludzie przestają ufać wykresom, gdy liczby skaczą, a nikt nie potrafi wyjaśnić dlaczego. Etykietuj regułę bezpośrednio w UI: „Dzienne zamówienia (America/New_York)” lub „Wydarzenia godzinowe (UTC)”. Używaj tej samej reguły w eksportach i API.
Prosty zbiór zasad dla raportowania i API
Zdecyduj, czy przechowujesz instant, czy lokalny odczyt zegara. Mieszanie tych dwóch powoduje, że dashboardy i API zaczynają się nie zgadzać.
Zestaw zasad, który utrzymuje przewidywalność raportowania:
- Przechowuj rzeczywiste zdarzenia jako instants używając
TIMESTAMPTZ, i traktuj UTC jako źródło prawdy. - Przechowuj pojęcia biznesowe typu „dzień rozliczeniowy” oddzielnie jako
DATE(lub lokalny czas jeśli naprawdę potrzebujesz odczytu ściany zegarowej). - W API zwracaj znaczniki czasu w ISO 8601 i bądź konsekwentny: zawsze dołączaj przesunięcie (np.
+02:00) albo zawsze używajZdla UTC. - Konwertuj na brzegach (UI i warstwa raportowania). Unikaj konwersji tam i z powrotem wewnątrz logiki bazy i zadań tła.
Dlaczego to działa: dashboardy kubełkują i porównują zakresy. Jeśli przechowujesz instants (TIMESTAMPTZ), PostgreSQL może niezawodnie sortować i filtrować zdarzenia nawet podczas przesunięć DST. Potem wybierasz, jak je wyświetlić lub pogrupować. Jeśli przechowujesz lokalny odczyt (TIMESTAMP) bez strefy, PostgreSQL nie może wiedzieć, co to znaczy, więc grupowanie może się zmieniać, gdy zmienia się strefa sesji.
Trzymaj „lokalne daty biznesowe” oddzielnie, bo to nie są instants. „Dostawa w dniu 2026-03-08” to decyzja daty, nie momentu. Jeśli wymusisz to w znaczniku czasu, dni DST mogą tworzyć brakujące lub zdublowane lokalne godziny, które później pojawią się jako luki lub skoki.
Krok po kroku: wybieranie typu dla każdej wartości czasu
Wybór między TIMESTAMPTZ a TIMESTAMP zaczyna się od jednego pytania: czy ta wartość opisuje prawdziwy moment, czy lokalny czas, który chcesz zachować dokładnie tak, jak został zapisany?
1) Rozdziel prawdziwe zdarzenia od zaplanowanych lokalnych czasów
Zrób szybki inwentaryzację kolumn.
Prawdziwe zdarzenia (kliknięcia, płatności, logowania, wysyłki, odczyty czujników, wiadomości wsparcia) zwykle powinny być przechowywane jako TIMESTAMPTZ. Chcesz jeden jednoznaczny instant, nawet jeśli ludzie będą go oglądać z różnych stref.
Zaplanowane czasy lokalne są inne: „Sklep otwiera się o 09:00”, „Okno odbioru 16:00–18:00”, „Rozliczenie działa 1. dnia o 10:00 lokalnego czasu”. Często lepiej przechowywać je jako TIMESTAMP plus oddzielne pole strefy, bo intencja wiąże się z zegarem lokalnym.
2) Wybierz standard i go spisz
Dla większości produktów dobrym domyślnym wyborem jest: przechowuj czasy zdarzeń w UTC, prezentuj je w strefie użytkownika. Udokumentuj to w miejscach, które ludzie naprawdę czytają: notatki do schematu, dokumentacja API i opisy dashboardów. Zdefiniuj też, co znaczy „dzień biznesowy” (dzień UTC, dzień w strefie biznesowej czy dzień lokalny widza), bo ten wybór determinuje raportowanie dzienne.
Krótka lista kontrolna, która działa w praktyce:
- Oznacz każdą kolumnę czasu jako „instant zdarzenia” lub „lokalny harmonogram”.
- Domyślnie instants zapisuj jako
TIMESTAMPTZw UTC. - Przy zmianach schematu wykonaj backfill ostrożnie i ręcznie zweryfikuj próbki wierszy.
- Standaryzuj formaty API (zawsze dołączaj
Zlub przesunięcie dla instants). - Ustawaj strefę sesji jawnie w zadaniach ETL, konektorach BI i workerach.
Bądź ostrożny przy „konwertuj i wypełnij” pracy. Zmiana typu kolumny może cicho zmienić znaczenie, jeśli stare wartości były interpretowane w innej strefie sesji.
Częste błędy powodujące przesunięcia o jeden dzień i błędy DST
Większość błędów czasowych to nie „PostgreSQL zachowuje się dziwnie”. Wynikają z przechowywania wartości, która wygląda poprawnie, ale ma niewłaściwe znaczenie, a potem pozwalamy warstwom zgadywać brakujący kontekst.
Błąd 1: Zapisanie czasu ściany zegarowej tak, jakby był absolutny
Częstą pułapką jest zapisywanie lokalnych czasów (np. „2026-03-29 09:00” w Berlinie) do TIMESTAMPTZ. PostgreSQL traktuje to jako instant i konwertuje je bazując na bieżącej strefie sesji. Jeśli intencją było „zawsze 9:00 czasu lokalnego”, to właśnie to straciłeś. Oglądanie tego samego wiersza w innej strefie sesji przesunie wyświetlaną godzinę.
Dla spotkań zapisuj lokalny czas jako TIMESTAMP plus oddzielna strefa (lub lokalizacja). Dla zdarzeń, które faktycznie zaszły w danym momencie (płatności, logowania), zapisuj instant jako TIMESTAMPTZ.
Błąd 2: Różne środowiska, różne założenia
Twój laptop, staging i produkcja mogą nie dzielić tej samej strefy czasowej. Jedno środowisko działa w UTC, inne w lokalnym czasie, i raporty „group by day” zaczynają się różnić. Dane się nie zmieniły — zmieniło się ustawienie sesji.
Błąd 3: Używanie funkcji czasu bez wiedzy, co obiecują
now() i current_timestamp są stabilne w ramach transakcji. clock_timestamp() zmienia się przy każdym wywołaniu. Jeśli generujesz znaczniki czasu w wielu miejscach jednej transakcji i mieszaj te funkcje, kolejność i obliczenia czasów mogą wyglądać dziwnie.
Błąd 4: Podwójna (albo zerowa) konwersja
Częsty błąd w API: aplikacja konwertuje lokalny czas na UTC, wysyła go jako „naiwny” ciąg, a baza znowu go konwertuje, bo zakładała, że wejście było w lokalnej strefie. Działa to też odwrotnie: aplikacja wysyła lokalny czas, ale oznacza go Z (UTC), co powoduje przesunięcie przy renderowaniu.
Błąd 5: Grupowanie po dacie bez deklaracji strefy
„Dzienne sumy” zależą od tego, którą granicę dnia masz na myśli. Jeśli grupujesz przez date(created_at) na TIMESTAMPTZ, wynik podąża za strefą sesji. Późnonocne zdarzenia mogą przesunąć się do poprzedniego lub następnego dnia.
Zanim wypuścisz dashboard lub API, sprawdź podstawy: wybierz jedną strefę raportowania na wykres i stosuj ją konsekwentnie, dołącz przesunięcia (lub Z) w payloadach API, trzymaj staging i produkcję zgodne co do polityki strefy czasowej i bądź jawny, którą strefę masz na myśli przy grupowaniu.
Szybkie kontrole przed wypuszczeniem dashboardu lub API
Błędy czasowe rzadko wynikają z jednego złego zapytania. Powstają, ponieważ przechowywanie, raportowanie i API każde przyjmują nieco inne założenia.
Użyj krótkiej listy kontrolnej przed wdrożeniem:
- Dla realnych zdarzeń (rejestracje, płatności, pingi czujników) zapisuj instant jako
TIMESTAMPTZ. - Dla pojęć lokalnych biznesowo (dzień rozliczeniowy, data raportu) zapisuj
DATElubTIME, a nie timestamp, który planujesz „konwertować później”. - W zadaniach harmonogramowych i runnerach raportów ustawiaj strefę sesji celowo.
- W odpowiedziach API dołącz przesunięcie lub
Zi potwierdź, że klient parsuje je jako czas ze strefą. - Przetestuj tydzień przejścia DST dla przynajmniej jednej docelowej strefy.
Szybka walidacja end-to-end: wybierz jedno znane zdarzenie krawędziowe (np. 2026-03-08 01:30 w strefie obserwującej DST) i śledź je przez przechowywanie, wynik zapytania, JSON API i etykietę wykresu. Jeśli wykres pokazuje właściwy dzień, ale dymek pokazuje złą godzinę (albo odwrotnie), masz niezgodność konwersji.
Przykład: dlaczego dwa zespoły kłócą się o liczby tego samego dnia
Zespół wsparcia w Nowym Jorku i zespół finansów w Berlinie patrzą na ten sam dashboard. Serwer bazy działa w UTC. Wszyscy są przekonani, że mają rację, ale „wczoraj” jest różne, w zależności od tego, kogo zapytasz.
Oto zdarzenie: ticket klienta utworzono o 23:30 w Nowym Jorku 10 marca. To jest 04:30 UTC 11 marca i 05:30 w Berlinie. Jeden moment, trzy różne daty kalendarzowe.
Jeśli czas utworzenia ticketu jest zapisany jako TIMESTAMP (bez strefy), a aplikacja zakłada, że jest to „czas lokalny”, możesz cicho przepisać historię. Nowy Jork może traktować 2026-03-10 23:30 jako czas nowojorski, podczas gdy Berlin interpretuje ten sam zapis jako czas berliński. Ten sam wiersz ląduje na różnych dniach dla różnych widzów.
Jeśli jest zapisany jako TIMESTAMPTZ, PostgreSQL przechowa instant konsekwentnie i tylko konwertuje go, gdy ktoś ogląda lub formatuje. Dlatego wybór między TIMESTAMPTZ a TIMESTAMP zmienia, co znaczy „dzień” w raportach.
Naprawa to rozdzielenie dwóch idei: momentu, kiedy zdarzenie miało miejsce, i daty raportowania, której chcesz użyć.
Praktyczny wzorzec:
- Przechowuj czas zdarzenia jako
TIMESTAMPTZ. - Zdecyduj regułę raportowania: lokalna widza (personalne dashboardy) lub jedna strefa biznesowa (firmowe finanse).
- Oblicz datę raportowania w zapytaniu używając tej reguły: skonwertuj instant do wybranej strefy, a potem weź
date.
Następne kroki: ujednolić obsługę czasu w całym stacku
Jeśli obsługa czasu nie jest spisana, każdy nowy raport staje się grą domysłów. Dąż do zachowań dotyczących czasu, które są nudne i przewidywalne w bazie, API i dashboardach.
Napisz krótki „kontrakt czasowy”, który odpowiada na trzy pytania:
- Standard czasu zdarzenia: przechowuj instants zdarzeń jako
TIMESTAMPTZ(zwykle w UTC), chyba że masz mocny powód, żeby nie. - Strefa biznesowa: wybierz jedną strefę dla raportów i stosuj ją konsekwentnie przy definiowaniu „dnia”, „tygodnia” i „miesiąca”.
- Format API: zawsze wysyłaj znaczniki czasu z przesunięciem (ISO 8601 z
Zlub+/-HH:MM) i dokumentuj, czy pola oznaczają "instant", czy "lokalny czas ściany".
Dodaj małe testy wokół startu i końca DST. Wykrywają kosztowne błędy wcześnie. Na przykład sprawdź, czy zapytanie „dzienna suma” jest stabilne dla wybranej strefy biznesowej podczas zmiany DST i czy wejścia API takie jak 2026-11-01T01:30:00-04:00 i 2026-11-01T01:30:00-05:00 są traktowane jako dwa różne instants.
Plan migracje ostrożnie. Zmiana typów i założeń in-place może cicho przepisać historię w wykresach. Bezpieczniejszym podejściem jest dodanie nowej kolumny (np. created_at_utc TIMESTAMPTZ), wypełnienie jej zweryfikowaną konwersją, zaktualizowanie odczytów do używania nowej kolumny, a potem aktualizacja zapisów. Przez krótki czas trzymaj stare i nowe raporty obok siebie, żeby przesunięcia w dziennych liczbach były oczywiste.
Jeśli chcesz jedno miejsce do egzekwowania tego „kontraktu czasowego” w modelach danych, API i ekranach, zunifikowany proces budowania pomaga. AppMaster (appmaster.io) generuje backend, aplikację webową i API z jednego projektu, co ułatwia zachowanie spójnych zasad przechowywania i wyświetlania znaczników czasu w miarę rozwoju aplikacji.
FAQ
Używaj TIMESTAMPTZ dla wszystkiego, co wydarzyło się w konkretnym momencie (rejestracje, płatności, logowania, wiadomości, odczyty czujników). Przechowuje jedno jednoznaczne „instant” i można bezpiecznie sortować, filtrować i porównywać między systemami. Zwykłego TIMESTAMP używaj tylko wtedy, gdy wartość ma być odczytem ściany zegarowej, który ma pozostać dokładnie taki, jaki został zapisany — zwykle w parze z oddzielnym polem strefy czasowej lub lokalizacji.
TIMESTAMPTZ reprezentuje prawdziwy moment w czasie; PostgreSQL normalizuje go wewnętrznie, a następnie wyświetla w strefie czasowej sesji. TIMESTAMP to tylko data i godzina bez przypisanej strefy, więc PostgreSQL nie będzie jej automatycznie przesuwał. Kluczowa różnica to znaczenie: instant kontra lokalny odczyt ściany zegarowej.
Ponieważ strefa czasowa sesji kontroluje formatowanie TIMESTAMPTZ przy wyjściu i sposób interpretacji niektórych wejść. Dwa narzędzia mogą zapytać ten sam wiersz i pokazać różne czasy zegarowe, jeśli jedno używa UTC, a drugie America/Los_Angeles. Dla raportów i API ustawaj strefę czasową sesji jawnie, żeby wyniki nie zależały od ukrytych domyślnych ustawień.
Bo „dzień” zależy od granicy strefy czasowej. Jeśli jeden dashboard grupuje wg czasu widza, a inny wg UTC (lub określonej strefy biznesowej), późnonocne zdarzenia mogą przypaść na różne daty i zmienić dzienne sumy. Napraw to, wybierając jedną regułę grupowania dla wykresu (UTC lub konkretna strefa biznesowa) i stosując ją konsekwentnie w SQL, BI i eksportach.
DST powoduje brakujące lub zdublowane lokalne godziny, co może skutkować lukami lub podwójnymi kubełkami przy grupowaniu wg czasu lokalnego. Jeśli twoje dane reprezentują rzeczywiste momenty, przechowuj je jako TIMESTAMPTZ i wybierz jasną strefę wykresu do bucketowania. Testuj tydzień przejścia DST dla docelowych stref, żeby wychwycić niespodzianki wcześnie.
Nie, PostgreSQL nie przechowuje oryginalnej etykiety strefy czasowej przy TIMESTAMPTZ; przechowuje instant. Gdy zapytasz wartość, PostgreSQL wyświetli ją w strefie sesji, która może różnić się od oryginalnej strefy użytkownika. Jeśli chcesz „pokaż w strefie klienta”, zapisz tę strefę oddzielnie w innym polu.
Zwracaj znaczniki czasu w formacie ISO 8601 z przesunięciem i bądź konsekwentny. Prosty domyślny wybór to zawsze zwracać UTC z Z dla instantów zdarzeń, a klientom zostawić konwersję do wyświetlenia. Unikaj wysyłania "naiwnych" ciągów jak 2026-03-10 23:30:00, bo klienci będą zgadywać strefę różnie.
Konwersje wykonuj na krawędziach: przechowuj instants jako TIMESTAMPTZ, a potem konwertuj do pożądanej strefy przy wyświetlaniu lub bucketowaniu do raportów. Unikaj wielokrotnych konwersji wewnątrz triggerów, zadań tła i ETL, chyba że masz jasny kontrakt. Większość problemów raportowych wynika z podwójnej konwersji lub mieszania wartości „naiwnych” i świadomych strefy czasowej.
Używaj DATE dla pojęć biznesowych typu „dzień rozliczeniowy”, „data raportu” lub „data dostawy”. Użyj TIME (lub TIMESTAMP plus oddzielna strefa) dla harmonogramów jak „otwarte o 09:00 lokalnego czasu”. Nie wymuszaj tych pojęć do TIMESTAMPTZ, jeśli naprawdę nie chodzi o jeden instant — DST i zmiany strefy mogą przesunąć zamierzony sens.
Najpierw ustal, czy to instant (TIMESTAMPTZ), czy lokalny czas ściany (TIMESTAMP + strefa). Dodaj nową kolumnę zamiast przepisywania w miejscu. Wypełnij ją konwersją przeprowadzoną pod znaną strefą sesji, zweryfikuj przykładowe wiersze wokół północy i granic DST. Przez krótki czas porównuj stare i nowe raporty obok siebie, aby przesunięcia w sumach były oczywiste, zanim usuniesz stare pole.


