Postgres has these 2 timestamp types: timestamp and timestamptz.
timestamp doesn't contain the timezone information. Technically, it doesn't represent a time in the real world. Thinking about it deeply, when we say "5pm" in the real world, it actually means "5pm in our timezone". Even Postgres Wiki says: Don't use timestamp without time zone.
I already know this for quite a while. What I didn't realize is that I might not be able to completely avoid timestamp without time zone.
In many time-series products, you may be building a chart that shows the deltas from month to month and have a SQL like below:
SELECT
a.month_start,
(a.value - b.value) AS delta
FROM data a
JOIN data b
ON a.month_start = b.month_start + INTERVAL '1 months'
Now the first issue is: adding months is actually timezone-dependent.
For example, if your timezone is PT, and your product works in UTC, 2026-03-01 00:00:00+00 is equal to 2026-02-28 16:00:00-08. Your machine's default setting is PT. This means Postgres will use 2026-02-28 16:00:00-08, and '2026-02-28 16:00:00-08'::timestamptz + INTERVAL '1 months' will yield 2026-03-28 16:00:00-08. That's not what we want.
Since we operate in UTC, we should convert the timestamp to UTC before adding a month. We modify the SQL to be:
SELECT
a.month_start,
(a.value - b.value) AS delta
FROM data a
JOIN data b
ON a.month_start = (b.month_start AT TIME ZONE 'UTC') + INTERVAL '1 months'
It turns out the SQL still doesn't produce the correct result because:
'2026-02-28 16:00:00-08'::timestamptz AT TIME ZONE 'UTC' will convert the data type from timestamptz (with time zone) to timestamp (without time zone) and produces 2026-03-01 00:00:00 (Notice there's no +00 at the end). Example- Comparing a
timestamp and timestamptz will always result in false.- It makes sense because
timestamp is not a real-world point in time. The comparison is technically absurd. - It gets even more confusing because, if you perform
EXTRACT(EPOCH FROM <timestamp>), they will both produce the same value.
In order to fix it, we have to convert timestamp back to timestamptz with another AT TIME ZONE 'UTC' like below:
ON a.month_start = ((b.month_start AT TIME ZONE 'UTC') + INTERVAL '1 months') AT TIME ZONE 'UTC'
Now the equality will work as you expect it to work.