The same phrase has two directions.
PostgreSQL documents two opposite behaviors for AT TIME ZONE. Applied to timestamp without time zone, it assumes the named zone and returns timestamptz. Applied to timestamptz, it shows that instant in the named zone and returns timestamp without time zone.[2]
The words stay the same while the type decides the direction. That is enough to make a plausible patch wrong. A reviewer who sees only 'UTC' has not seen the operation.
Ask what type goes in and what type comes out.
The session timezone is an input.
When PostgreSQL compares a timestamp without a zone to one with a zone, it assumes the zone-less value belongs to the session's TimeZone setting. The documentation states this conversion rule directly.[2] The query can therefore change meaning when the session setting changes.
A current field report about monthly time-series joins ran into this class of problem. The article is useful because it shows how an explicit UTC conversion can still return a different type than the author expected.[1] Its Hacker News discussion also caught an overstatement in the post. Mixed timestamp comparisons are not always false. PostgreSQL converts the zone-less side through the session timezone, so the result depends on that setting.[4]
Storage and display are separate jobs.
The PostgreSQL wiki advises against storing UTC instants in timestamp without time zone. That type cannot identify a real instant by itself. It can still be right for a wall-clock value such as "every day at 09:00," where no instant exists until a date and zone are supplied.[3]
timestamptz does not preserve the original zone name. PostgreSQL stores an instant internally and renders it using the current session timezone.[3] If the original civil zone matters, store its name in a separate field.
Calendar arithmetic needs a named calendar.
A month is not a fixed duration. PostgreSQL 16 and later provide date_add(timestamptz, interval, zone). The third argument tells PostgreSQL which zone governs times of day and daylight-saving adjustments. If it is omitted, PostgreSQL uses the session TimeZone.[2]
That does not make every monthly query correct. It makes one hidden input visible. Decide whether the rule follows UTC, a customer's civil zone, or a stored billing calendar. Then test a month end and a daylight-saving boundary.
Make the query carry its witness.
- Inspect the column type with
pg_typeof. Do not infer it from the name. - Record
current_setting('TimeZone')in the test output. - State whether the value is an instant or a wall-clock reading.
- Assert the output type after conversion or arithmetic.
- Run one month-end case and one daylight-saving case in every supported zone.
The reliable review is not "convert it to UTC." It is a short receipt that names every assumption PostgreSQL would otherwise supply for you.