A Simple Explanation of Postgres' Timestamp with Time Zone
October 10, 2024 📬 Get My Weekly Newsletter ☞
Postgres provides two ways to store a timestamp: TIMESTAMP
and TIMESTAMP WITH TIME ZONE
(or timestamptz
).
I’ve always recommended using the later, as it alleviates all confusion about time zones. Let’s see why.
What is a “time stamp”?
The terms “date”, “time”, “datetime”, “calendar”, and “timestamp” can feel interchangeable but the are not. A “timestamp” is a specific point in time, as measured from a reference time. Right now it is Oct 10, 2024 18:00 in the UK, which is the same timestamp as Oct 10 2024 14:00 in Washington, DC.
To be able to compare two timestamps, you have to include some sort of reference time. Thus, “Oct 10, 2025 18:00” is not a timestamp, since you don’t know what the reference time is.
Time zones are a way to manage these references. They can be confusing, especially when presenting timestamps or storing them in a database.
Storing time stamps without time zones
Consider this series of SQL statements:
db=# create temp table no_tz(recorded_at timestamp);
CREATE TABLE
db=# insert into no_tz(recorded_at) values (now());
INSERT 0 1
adrpg_development=# select * from no_tz;
recorded_at
----------------------------
2024-10-10 18:03:11.771989
(1 row)
The value for recorded_at
is a SQL timestamp
which does not encode timezone information (and thus, I would argue, is not an actual time stamp). Thus, to interpret this value, there must be some reference. In this case, Postgres uses whatever its currently configured timezone is. While this is often UTC, it is not guaranteed to be UTC.
db=# show timezone;
TimeZone
----------
UTC
(1 row)
This value can be changed in many ways. We can change it per session with set session timezone
:
db=# set session timezone to 'America/New_York';
SET
Once we’ve done that, the value in no_tz
is, technically, different:
db=# select * from no_tz;
recorded_at
----------------------------
2024-10-10 18:03:11.771989
(1 row)
Because the SQL timestamp
is implicitly in reference to the session or server’s time zone, this value is now
technically four hours off, since it’s now being referenced to eastern time, not UTC.
This can be solved by storing the referenced time zone.
Storing timestamps with time zones
Let’s create a new table that stores the time both with and without a timezone:
CREATE TEMP TABLE
tz_test(
with_tz TIMESTAMP WITH TIME ZONE,
without_tz TIMESTAMP WITHOUT TIME ZONE
);
We can see that, by default, the Postgres server I’m running is set to UTC:
db=# show timezone;
TimeZone
----------
Etc/UTC
Now, let’s insert the same timestamp into both fields:
INSERT INTO
tz_test(
with_tz,
without_tz
)
VALUES (
now(),
now()
)
;
The same timestamp should be stored:
db=# select * from tz_test;
with_tz | without_tz
------------------------------+---------------------------
2024-10-10 18:09:35.11292+00 | 2024-10-10 18:09:35.11292
(1 row)
Note the difference in how these values are presented. with_tz
is showing us the time zone offset—+00
. Let’s change to eastern time and run the query again:
db=# set session timezone to 'America/New_York';
SET
db=# select * from tz_test;
with_tz | without_tz
------------------------------+---------------------------
2024-10-10 14:09:35.11292-04 | 2024-10-10 18:09:35.11292
(1 row)
The value for with_tz
is still correct. There’s no way to misinterpret that value. It’s the same timestamp we
inserted. without_tz
, however, is now wrong or, at best, unclear.
Why not Just Always stay in UTC?
It’s true that if you are always careful to stay in UTC (or any time zone, really), the values for a TIMESTAMP
WITHOUT TIME ZONE
will be correct. But, it’s not always easy to do this. You saw already that I changed the
session’s timezone. That a basic configuration option can invalidate all your timestamps should give you pause.
Imagine an ops person wanting to simplify reporting by changing the server’s time zone to pacific time. If your
timestamps are stored without time zones, they are now all wrong. If you had used TIMESTAMP WITH TIME ZONE
it
wouldn’t matter.
Always use TIMESTAMP WITH TIME ZONE
There’s really no reason not to do this. If you are a Rails developer, you can make Active Record default to this like so:
# config/initializers/postgres.rb
require "active_record/connection_adapters/postgresql_adapter"
ActiveRecord::ConnectionAdapters::PostgreSQLAdapter.datetime_type = :timestamptz
This can be extremely helpful if you are setting time zones in your code. It’s not uncommon to temporarily
change the time zone to display values to a user in their time zone. If you write a timestamp to the database
while doing this, TIMESTAMP WITH TIME ZONE
will always store the correct value.
Note that Postgres also recommends you use TIMESTAMP WITH TIME ZONE
.