PostgreSQL timestamp vs timestamptz: co naprawdę jest zapisane pod spodem?
PostgreSQL przechowuje zarówno timestamp, jak i timestamptz jako pojedynczą 64-bitową liczbę całkowitą: liczbę mikrosekund od 1970-01-01 00:00:00 UTC. Różnica ujawnia się dopiero przy formatowaniu danych dla człowieka.
Dlaczego to wszystkich myli?
- Dwie kolumny, jedna data… i dwa różne wyniki zapytań
- Aplikacja zapisuje
2025-07-29 10:00, a inny zespół widzi02:00 - Frontend wyświetla łańcuch ISO, który nie zgadza się z logiem backendu
Dwie puszki brzoskwiń: jedna bez etykiety, druga z napisem
| Typ danych | Pełna nazwa | Zapisana wartość | Co dzieje się przy SELECT |
|---|---|---|---|
timestamp | timestamp without time zone | surowa liczba mikrosekund | Zwracana bez zmian — PostgreSQL nigdy nie zgaduje strefy czasowej |
timestamptz | timestamp with time zone | ta sama liczba mikrosekund | PostgreSQL stosuje ustawienie sesji TimeZone tuż przed wysłaniem tekstu |
Analogia
timestamp= słoik brzoskwiń bez etykiety pochodzenia. Wiadomo, że to owoce, ale nie wiadomo, gdzie zapakowano.timestamptz= słoik z dumnie wybitym napisem „Wyprodukowano w UTC+8”. Każdy, kto go otworzy, sam decyduje, czy przeliczyć tabelę wartości odżywczych.
Pod maską: to po prostu wielka liczba
2000-01-01 00:00:00 UTC → 0
2000-01-01 00:00:01 UTC → 1 000 000
- Jednostka: mikrosekundy (jedna milionowa sekundy)
- Zakres czasu: 4713 p.n.e. – 294276 n.e. — z aprobatą Indiany Jonesa
- Sposób przechowywania
timestampitimestamptzjest identyczny; różni się jedynie interpretacja
Demo w 15 sekund
-- Klient myśli czasem szanghajskim
SET TimeZone = 'Asia/Shanghai';
CREATE TABLE demo (
created_ts timestamp,
created_tz timestamptz
);
INSERT INTO demo VALUES ('2025-07-29 10:00', '2025-07-29 10:00');
| Zapytanie | Wynik | Dlaczego |
|---|---|---|
SELECT created_ts FROM demo; | 2025-07-29 10:00:00 | Surowa wartość, bez przeliczeń strefy |
SELECT created_tz FROM demo; | 2025-07-29 10:00:00+08 | Etykieta dodawana przy wyjściu |
SET TimeZone = 'UTC'; i ponowny SELECT | 2025-07-29 02:00:00+00 | Ten sam moment, inna soczewka |
Arytmetyka timestampów i interwały
Jednym z najbardziej praktycznych aspektów timestampów w PostgreSQL jest arytmetyka interwałów. Ponieważ oba typy przechowują liczbę mikrosekund, można bezpośrednio dodawać i odejmować interwały:
-- Dodaj 3 godziny i 30 minut
SELECT '2025-07-29 10:00'::timestamptz + INTERVAL '3 hours 30 minutes';
-- → 2025-07-29 13:30:00+08
-- Znajdź różnicę między dwoma timestampami
SELECT '2025-07-30 09:00'::timestamptz - '2025-07-29 10:00'::timestamptz;
-- → 23:00:00 (interwał)
-- Wyodrębnij konkretne pola
SELECT EXTRACT(EPOCH FROM '2025-07-29 10:00:00+08'::timestamptz);
-- → 1753768800 (Unix timestamp w sekundach)
-- Zaokrąglij do granicy doby (przydatne przy agregacjach dziennych)
SELECT date_trunc('day', '2025-07-29 15:42:19+08'::timestamptz);
-- → 2025-07-29 00:00:00+08
Funkcja EXTRACT(EPOCH FROM ...) jest szczególnie użyteczna, gdy trzeba przekazać timestamp do zewnętrznych systemów oczekujących sekund Unix epoch. Odwrotnie, można przekonwertować epoch z powrotem na timestamp:
SELECT to_timestamp(1753768800);
-- → 2025-07-29 10:00:00+08 (w sesji Asia/Shanghai)
Subtelny, ale ważny szczegół: arytmetyka interwałów na typie timestamp (bez strefy czasowej) całkowicie ignoruje przejścia DST, podczas gdy timestamptz je uwzględnia. Oznacza to, że dodanie INTERVAL '1 day' do wartości timestamptz przekraczającej granicę DST poprawnie zwróci ten sam czas zegarowy — niekoniecznie dokładnie 24 godziny później.
Indeksowanie i wydajność
Zarówno timestamp, jak i timestamptz są przechowywane jako 8-bajtowe liczby całkowite, więc nie ma między nimi różnicy wydajnościowej w zakresie przechowywania ani indeksowania. Indeksy B-tree działają identycznie dla obu typów, ponieważ porównanie sprowadza się do porównania liczb całkowitych.
Warto jednak pamiętać o kilku praktycznych kwestiach:
- Zapytania zakresowe:
WHERE created_at > '2025-07-01'działa wydajnie z indeksem na obu typach. W przypadkutimestamptzPostgreSQL konwertuje literał na UTC przed porównaniem, więc indeks nadal jest wykorzystywany. - Klucze partycjonowania: przy partycjonowaniu zakresowym po kolumnach timestampowych
timestamptzjest zazwyczaj bezpieczniejszy, ponieważ granice partycji są jednoznaczne (zawsze UTC). Przytimestampgranica taka jak'2025-07-01 00:00'może oznaczać różne rzeczy dla różnych sesji. - Indeksy funkcyjne: jeśli często zadajesz zapytania tylko po dacie (bez czasu), warto rozważyć indeks na
date_trunc('day', created_at), by przyspieszyć dzienne agregacje.
Częste pułapki i szybkie rozwiązania
1. Różni użytkownicy, różne zegary
- Przyczyna: klienci używają różnych ustawień
TimeZoneztimestamptz - Rozwiązanie: albo trzymaj wszystko w
timestampi ustal jedną strefę, albo wymuszajSET TimeZone = 'UTC'przy inicjalizacji połączenia
Częsty wzorzec w kodzie aplikacji to ustawianie strefy czasowej raz, przy inicjalizacji puli połączeń:
-- W konfiguracji połączenia (np. konfiguracja puli pg)
SET timezone = 'UTC';
Dzięki temu wszystkie sesje widzą tę samą reprezentację UTC, a warstwa aplikacji odpowiada za konwersję na czas lokalny przy wyświetlaniu.
2. Przechowywanie „czasu zegarowego” w niewłaściwym typie
- Kalendarze biznesowe (godziny otwarcia, terminy) powinny używać
timestamp - Procesy międzynarodowe (zamówienia, logi) powinny przechowywać UTC w
timestamptz
Test jest prosty: jeśli pytanie brzmi „w jakim momencie to się wydarzyło?”, użyj timestamptz. Jeśli pytanie brzmi „co pokazuje zegar na ścianie?”, użyj timestamp.
3. API, które dryfują
- Zawsze przesyłaj
timestamptzjako łańcuchy ISO-8601 z offsetem (Zlub+08:00) - Pozwól, by interfejs użytkownika sformatował je lokalnie
4. Porównywanie timestampów różnych typów
Mieszanie timestamp i timestamptz w porównaniach lub złączeniach to częste źródło subtelnych błędów:
-- Niebezpieczne: niejawna konwersja stosuje strefę czasową sesji
SELECT * FROM orders o
JOIN schedules s ON o.created_tz = s.start_ts;
-- PostgreSQL rzutuje s.start_ts na timestamptz, używając strefy czasowej sesji
-- Różne sesje mogą zwracać różne wyniki złączenia!
Rozwiązanie: zawsze rzutuj jawnie przy porównywaniu typów lub ustandaryzuj jeden typ na domenę.
5. Pułapki domyślnych ustawień ORM
Wiele ORM-ów (Django, SQLAlchemy, ActiveRecord) domyślnie używa timestamp bez strefy czasowej. Sprawdź pliki migracji — jeśli aplikacja obsługuje użytkowników w różnych strefach czasowych, nadpisz domyślny typ na timestamptz. W Django ustaw USE_TZ = True w ustawieniach. W SQLAlchemy użyj DateTime(timezone=True).
Ściąga: którego typu użyć?
Tylko kalendarz lokalny → timestamp
Cokolwiek globalnego → timestamptz (przechowuj UTC)
- Raporty finansowe, plany zajęć →
timestamp - Logi audytowe, zamówienia e-commerce →
timestamptz
Sprawdź to w kilka sekund z Go Tools
| Potrzeba | Narzędzie | Jak |
|---|---|---|
| Sprawdzić wartość epoch z SQL | Konwerter Unix timestamp | Wklej 1690622400 i kliknij Konwertuj |
| Uporządkować masowy JSON z polami czasowymi | Formatowanie JSON | Wklej payload, sformatuj i przejrzyj |
Wszystkie narzędzia działają w całości w przeglądarce — żadne dane nie opuszczają komputera.
Najczęściej zadawane pytania
Jaka jest różnica między timestamp a timestamptz w PostgreSQL?
timestamp (bez strefy czasowej) przechowuje wartość daty i czasu w niezmienionej postaci, bez kontekstu strefy czasowej. timestamptz (ze strefą czasową) konwertuje dane wejściowe na UTC, by je zapisać, a przy odczycie konwertuje z powrotem na strefę czasową sesji. W niemal wszystkich przypadkach lepiej użyć timestamptz — eliminuje błędy związane ze strefami czasowymi w systemach rozproszonych.
Czy PostgreSQL faktycznie przechowuje strefę czasową w timestamptz?
Nie — wbrew nazwie PostgreSQL nie przechowuje samej strefy czasowej. Konwertuje wartość wejściową na UTC i zapisuje wyłącznie wartość UTC (liczbę mikrosekund od 2000-01-01). Przy odczycie konwertuje z UTC na strefę czasową wskazaną przez ustawienie timezone sesji. Pierwotna informacja o strefie czasowej zostaje odrzucona.
Jak zmienić strefę czasową dla sesji PostgreSQL?
Wykonaj SET timezone = 'America/New_York';, by zmienić strefę czasową sesji. Wpływa to na sposób wyświetlania i interpretowania wartości timestamptz. Aby ustawić wartości domyślne dla całego serwera, ustaw timezone w postgresql.conf. Zawsze używaj nazw stref IANA (np. Asia/Shanghai), a nie skrótów (np. CST), by uniknąć niejednoznaczności.
Czy do zapisywania czasu zdarzeń lepiej użyć timestamp czy timestamptz?
Niemal we wszystkich przypadkach używaj timestamptz — działania użytkowników, wywołania API, logi audytowe, zaplanowane zdarzenia. Typu timestamp (bez strefy czasowej) używaj wyłącznie do abstrakcyjnych godzin niezwiązanych z konkretnym momentem, np. „sklep otwiera się o 09:00” oznacza godzinę 9 rano w lokalnej strefie czasowej, a nie konkretny moment UTC.
Jak PostgreSQL obsługuje czas letni (DST) w timestamptz?
PostgreSQL poprawnie obsługuje DST, gdy używany jest timestamptz, ponieważ wewnętrznie wszystko przechowuje w UTC. Przy odczycie wartości PostgreSQL konwertuje ją z UTC, korzystając z aktualnych reguł DST dla strefy czasowej sesji. Oznacza to, że ten sam zapisany moment UTC poprawnie wyświetla różne czasy lokalne przed zmianą czasu i po niej.
Pełny przewodnik po Unix timestamp — w tym obsługa precyzji, dobre praktyki dla stref czasowych oraz przykłady kodu w JavaScripcie, Pythonie i Go — znajdziesz w naszym przewodniku po Unix timestamp.
Podsumowanie
- Oba typy czasu w PostgreSQL to liczniki mikrosekund; cała różnica tkwi w etykiecie
- Wybranie złego typu prowadzi do mylących timestampów i błędnej arytmetyki
- Testuj, konwertuj i weryfikuj poprawność za pomocą właściwych narzędzi, by zaoszczędzić godziny debugowania