- Learn
- PostgreSQL
- PostgreSQL timestamp vs timestamptz
PostgreSQL timestamp vs timestamptz
Use timestamptz. It is the same 8 bytes as timestamp and it does not throw away the offset.
Quick answer
Use timestamptz unless you have a specific reason not to.
Both types take 8 bytes. timestamptz is not more expensive, and it is the one that does not silently discard information.
The two types
| Type | Aliases | Bytes | Stores |
|---|---|---|---|
timestamp | timestamp without time zone | 8 | A date and time, with no reference to any zone |
timestamptz | timestamp with time zone | 8 | A specific moment in time |
The names are misleading, and this is the root of most of the confusion: timestamptz does not store a time zone. Neither type stores a time zone.
What actually happens:
timestamptakes the date and time you give it and stores those digits. If your input had an offset, it is thrown away.timestamptzuses the offset to convert your input to UTC, stores that, and then renders it back in your session's time zone when you read it.
So timestamptz stores a moment. timestamp stores a reading on a clock, without saying whose clock.
Watch it happen
Insert the same literal, with an offset, into both:
The timestamp column kept 12:00:00 and dropped the +05. The timestamptz column understood that 12:00 +05 is 07:00 UTC and stored that moment.
Now change the session time zone and read the same row again:
The timestamp value did not move, because it never meant anything in particular. The timestamptz value is the same instant, displayed for a reader in New York.
Nothing was rewritten on disk. Only the rendering changed.
And the storage cost:
Identical. Choosing timestamp to save space saves nothing.
Which one should you use?
Use timestamptz for anything that records when something happened. Created-at, updated-at, logged-in-at, ordered-at. These are moments. Your users are in different zones, your servers may be too, and daylight saving will move underneath you twice a year.
Use timestamp only for a wall-clock reading with no zone, which is rarer than people think. A genuine example: a recurring alarm that should fire at 09:00 local time wherever the user happens to be. That is not a moment, it is a clock reading, so timestamp is right and timestamptz would be wrong.
If you are unsure, you want timestamptz. The failure mode of timestamp is that it looks fine in development, where everything is one zone, and produces off-by-hours bugs in production that only appear for some users, or only in summer.
The session time zone
timestamptz renders in your session's zone, which is set by the timezone parameter:
Set it per session:
Or per database, permanently:
Running your database in UTC is a good default. It means what you see in psql matches what is stored, which removes a whole class of "is this already converted?" confusion when debugging.
Note that this is a display setting for timestamptz. Changing it does not alter stored data.
AT TIME ZONE
AT TIME ZONE converts between the two types, and its behaviour depends on which type you feed it. This trips people up because the same syntax does two opposite things.
timestamptz AT TIME ZONE zone gives you a timestamp. It asks "what did the clock read in that zone at that moment?"
timestamp AT TIME ZONE zone gives you a timestamptz. It asks "this clock reading was in that zone, so which moment was it?"
The rule: it always converts to the other type. If you are reaching for a second AT TIME ZONE to fix the first one, you probably want just one, in the other direction.
now(), current_timestamp, and clock_timestamp()
All three return a timestamptz, but they do not mean the same thing.
now()andcurrent_timestampare the same function. They return the time the transaction started, and that value does not move for the life of the transaction.clock_timestamp()returns the actual current time, and it changes every time you call it.
This matters more than it looks. Inside a long transaction, now() is frozen. Every row you insert gets the same timestamp, which is usually what you want for consistency, and occasionally very much not what you want if you were trying to measure how long something took. For that, use clock_timestamp().
statement_timestamp() sits between them, returning the time the current statement started.
Common operations
Truncate to a unit:
date_trunc on a timestamptz truncates according to your session zone, so "day" means a day in your zone. To truncate to a day in a specific zone regardless of session:
Extract a part:
Arithmetic with intervals:
Interval arithmetic on timestamptz is daylight-saving aware. now() + interval '1 day' is the same clock time tomorrow, which may be 23 or 25 real hours. now() + interval '24 hours' is always 24 hours. They are different, and the difference bites exactly twice a year.
Difference between two timestamps:
Indexing and the trap in it
A plain index works as expected:
But wrapping the indexed column in a function stops the index being used:
Rewrite as a range, which can:
Range conditions are also more honest about zone boundaries. If you genuinely need the function, index the expression instead:
Note that an expression index on date_trunc over a timestamptz depends on the session time zone, so PostgreSQL will not let you build one unless you pin the zone explicitly.
Migrating timestamp to timestamptz
If you already have a timestamp column holding UTC values, tell PostgreSQL that when you convert, or it will assume the values were in the session zone and shift them:
Without the USING clause you get a silent, wrong conversion on every row. Take a backup first, and check a few rows afterwards.
Common problems
My timestamps are off by a few hours. You stored a moment in a timestamp column, so the offset was discarded, and something later assumed a zone. This is the type doing exactly what it promises.
Times shift when I change the server time zone. If they shift on disk, the column is timestamp. timestamptz values do not move; only their rendering changes.
now() returns the same value repeatedly. You are in a transaction. That is correct behaviour. Use clock_timestamp() for the moving value.
My date filter misses rows near midnight. Almost always a zone boundary. date_trunc('day', ...) on a timestamptz uses the session zone, so "today" depends on who is asking.
Quick reference
| Task | Syntax |
|---|---|
| Recommended column type | timestamptz |
| Current transaction time | now() or current_timestamp |
| Actual current time | clock_timestamp() |
| Show session zone | SHOW timezone; |
| Set session zone | SET timezone = 'UTC'; |
| Set database zone | ALTER DATABASE db SET timezone = 'UTC'; |
| Moment to local clock reading | tstz AT TIME ZONE 'Europe/London' |
| Local clock reading to moment | ts AT TIME ZONE 'Europe/London' |
| Truncate | date_trunc('day', ts) |
| Add or subtract | ts + interval '7 days' |
| Seconds between | EXTRACT(epoch FROM (b - a)) |
| Convert column safely | ALTER ... TYPE timestamptz USING col AT TIME ZONE 'UTC' |