166 points•birdculture•4 days ago•111 comments•

111 comments

heurekamala3 days ago
The text says: "Comparing a timestamp and timestamptz will always result in false."

That is wrong. Postgres changes the timestamp to timestamptz very quietly in the background. Wether it uses the time zone of the session for this. If it is true or false depends on the TimeZone setting. This is more bad than "always false". In production with UTC it works. On a laptop of a California developer it does not work.

Just tested:

  SET TIME ZONE 'America/Los_Angeles';
  SELECT '2026-03-01 00:00:00'::timestamp = '2026-03-01 00:00:00+00'::timestamptz; /* f   */

  SET TIME ZONE 'UTC';
  SELECT '2026-03-01 00:00:00'::timestamp = '2026-03-01 00:00:00+00'::timestamptz; /* t  */
If you can, use Postgres 16. The double AT TIME ZONE 'UTC' thing from the text is not needed there. There you have date_add with a time zone as the third argument:

date_add(b.month_start, interval '1 month', 'UTC')

This adds the month in UTC and it stays a timestamptz.

whizzter3 days ago
Automatic conversion using "global" state between "local"/"human" time (5pm where I am now) and points in time (ie with timezone) is one of the biggest sins imho for many libraries/db's/languages. Ran into it a bunch of times using C# as well.
reactordev3 days ago
The two most difficult things in software engineering...

Naming.

Timestamps.

layer83 days ago
It made sense when databases and programs were used almost exclusively locally. It still makes sense for local apps (e.g. local-first or local-only smartphone and desktop apps) who typically will automatically do the right thing that way based on the OS regional settings.

It only started causing widespread issues with the rise of cross-region internet SaaS. Database systems, language runtimes, and OS APIs are keeping the default behavior for backwards compatibility.

tanin2 days ago
Author here

Wow, thank you for testing it. I did test it on my own laptop (Seattle).

> date_add(b.month_start, interval '1 month', 'UTC')

I didn't know this. Thank you.

linasdev4042 days ago
Adding a month is a footgun even without time zones. I was checking a spreadsheet against another implementation month by month and found that HyperFormula 3.4 returns Feb 28 for EDATE(Jan 31 2028, 1). Excel, Google Sheets and Numbers all return Feb 29, because 2028 is a leap year. Jan 31 + 1 month in a leap year is now a test case in anything I write that does month math.
greenflux1 day ago
Thank you for pointing this out with HyperFormula! Appreciate you testing in various environments and sharing the results. I've confirmed the issue and submitted a PR with a fix that should be in the next release.
anitil2 days ago
This is one of those moments that makes me wonder how anything ever works at all
ulrikrasmussen3 days ago
The SQL standard is unfortunately really horrible when it comes to handling of time. The type `timestamp` is not a timestamp at all because it doesn't encode a unique point in time, it just stores a date and a time which has to be interpreted relative to a timezone. It should be called "datetime".

Moving a Java Instant back and forth between a database is also a surprisingly difficult task to do right, and it doesn't help that JDBC is just handling it completely wrong if you use its setTimestamp/getTimestamp methods. Not because it is a bad design with footguns, but because the implementation is just plain wrong and will corrupt your data if you deal with instants whose calendar date is far enough in the past due to it using the legacy date/time API which switches to the Gregorian calendar for dates in the past.

The name `timestamp with time zone` is also misleading because it doesn't actually store a time zone, it stores the number of seconds since epoch like a java.time.Instant (although at a different resolution). The "with time zone" part just refers to the textual format you denote the values in which includes the time zone after the date/time part to uniquely identify a timestamp, but the time zone is thrown away and not stored after the value has been parsed. This is different from e.g. `ZonedDateTime` in Java which will actually store the offset and therefore corresponds to a pair of (Instant, TimeZone).

masklinn3 days ago
A ZonedDateTime is not an instant and a timezone, for the same reason that you can’t unambiguously round trip between arbitrary timezones and UTC:

- zoned datetimes carry ambiguities as to their actual location on the timeline (because they can repeat, or not exist at all)

- future zoned datetime carry outright uncertainty as to their actual location on the timeline (as the zone’s offset can be updated at any point and any number of times until the event has elapsed)

paulddraper3 days ago
A java.time.ZonedDateTime is not simply zoneid+date+time.

It has the zone offset and so is completely unambiguous and invariable.

  private final LocalDateTime dateTime;
  private final ZoneOffset offset;
  private final ZoneId zone;
Converting Instant+ZoneId into a ZonedDateTime can vary when zone rules change. The inverse does not.
marcosdumay3 days ago
Still, the GP is correct about the problems.

Relational databases have exactly 1 type that corresponds to modern data-handling practices: timestamp with time zone, that stores a timestamp. There is no good way to store any other modern type, and the 1980s practices on time handling weren't actually very good.

tshaddox2 days ago
It's always worth noting that future UTC timestamps are also ambiguous for certain operations, most notably computing durations, due to the unpredictability of leap seconds.
rawling3 days ago
Has anyone proposed versioning timezones? Or is this such an edge case it would be overkill? (Either specify your future instant in UTC if you mean to stick to that, or specify it in a timezone and accept that it could change before it happens, or if you need something else get it in a contract and don't trust the computer!)
ulrikrasmussen3 days ago
Ah, yes, you are right! Forgot about those two details
officialchicken3 days ago
SET TIME ZONE 'UTC'; // or GMT, PST, etc.
crote3 days ago
Except you shouldn't do the GMT/PST part, because it can lead to people falsely believing that Postgres can handle time zones, leading to silent data corruption when the time zone definition changes - such as due to abolishing DST.

Postgres can only store 1) unzoned local time, or 2) UTC, with optional conversion on read/write.

tibbar3 days ago
This summer, with the help of AI, I found an inconsistency in the way Postgres handles timestamp vs. timestamptz comparisons under a DST spring-forward gap for the datetime_ops btree family [0]. Essentially there are scenarios where expression B > A and B < C, but also C = A, which can cause queries using a btree index (among other things) to return an incorrect result.

The assessment in the mailing list was that this was a bug, but there were no good ways to fix it.

> Backpatching a behavioral change like this seems awfully scary. For the moment I'm just contemplating what we could potentially change in master. So far I don't like any of the choices :-(

[0] https://www.postgresql.org/message-id/flat/CA%2BCOZaDmCuOds-...

cryptonector2 days ago
The problem really is inherent to DST itself, just as the month math in TFA is inherently wonky in any system. What's January 30th + 1 month? February 28th (or 29th, if a leap year)? March 1st? March 2nd?

UI time elements have to be presented in the user's TZ. In the DB one should store timestamptz in UTC for all things, and maybe also timestamptz in non-UTC TZs for user input (e.g., in a calendaring app).

spacesez-ai3 days ago
Does the same hole exist for range types? If a GiST exclusion constraint on tstzrange is built on the same comparison logic, a booking table could accept two reservations that overlap only inside the DST gap, and nobody would notice until two people show up for the same room. Curious whether you checked that path or only the btree family.
globular-toast3 days ago
> timestamp doesn't contain the timezone information. Technically, it doesn't represent a time in the real world.

Unfortunately this is just as misleading.

It's correct that `timestamp` doesn't contain timezone information, but *neither does `timestamptz`. All `timestamptz` is is UNIX-style Epoch timestamp, that is an absolute point in time in UTC. It says nothing about the timezone because there are no time zones in that context.

The confusion arises because when you insert into a field you can insert "19:00 on the 29th March 2025 in London", but this is simply converted to UTC at insert time and the timezone is lost.

If anything, `timestamp` does represent a time in the "real world" and `timestamptz` represents a time in the universe.

If time zones are important to you, you need to record the time zone in a separate field. That is all. Where you store this depends on context, of course. You could store the user's current time zone in a profile and render all times in that time zone (psql and other clients do this automatically based on the OS time zone btw). Or you could store the time zone with each record if that makes sense, ie. you want to know what the wall clock time was for the user at the time the record was taken.

Read the full thread on Hacker News →

Related stories