Alright, using PostgreSQL timestamps with time zones in the table solved the problem.
Here are details about where the problem was:
I put timezone-aware timestamps in columns that are not timezone-aware. I assumed that the
timestamp without time zone
column would contain the UTC time (like fortimestamp with time zone
), but it actually contains the timestamp in local time, as described in the documentation:Conversions between timestamp without time zone and timestamp with time zone normally assume that the timestamp without time zone value should be taken or given as timezone local time. A different time zone can be specified for the conversion using AT TIME ZONE.
Thus, 00:00 UTC is stored as
08:00
in a columntimestamp without time zone
when in China.When doing a comparison like "timestamp without time zone" < "timestamp with time zone", I can't find in the documentation what PostgreSQL does. However, what was observed in the question is consistent with the idea that the timezone is first removed in the same way, and the timestamps without timezone are compared. I should not have been surprised by the result: the late time "15:00" is indeed earlier than 13:00 UTC, in China (UTC+8). The key was to know that displayed (and stored) times are in the local time zone.
The moral I get from this is: conversions between timestamps with and without timezones are better handled explicitly.