sqlpostgresqltimezonetimestamptz

AT TIME ZONE en SQL: convertir UTC almacenado a la hora local del usuario

El doble comportamiento de AT TIME ZONE: sobre timestamptz devuelve la hora de pared local, sobre un timestamp naive lo interpreta en esa zona y devuelve timestamptz.

2 min de lecturaReferencesql · postgresql · timezone · timestamptz · timestamp · clickhouse

AT TIME ZONE es una herramienta con dos caras. Lo que hace depende por completo del tipo de su entrada: para un timestamptz representa el momento como la hora de pared en la zona elegida, mientras que para un timestamp naive hace lo contrario, declara que el valor ya estaba en esa zona y devuelve el instante absoluto.

Dos modos, un operador

La regla es corta: AT TIME ZONE siempre invierte el tipo al opuesto.

  • timestamptz AT TIME ZONE 'zona' -> timestamp (hora de pared en esa zona).
  • timestamp AT TIME ZONE 'zona' -> timestamptz (interpreta el valor naive como hora local de esa zona).
-- timestamptz -> timestamp: what the clock on the wall in Moscow shows
SELECT TIMESTAMPTZ '2026-06-17 12:00:00+00' AT TIME ZONE 'Europe/Moscow';
-- 2026-06-17 15:00:00

-- timestamp -> timestamptz: this naive value WAS Moscow local time
SELECT TIMESTAMP '2026-06-17 15:00:00' AT TIME ZONE 'Europe/Moscow';
-- 2026-06-17 12:00:00+00

Las dos expresiones son espejo una de la otra y se revierten sin perdida. La primera responde "que hora es en Moscu en este momento"; la segunda responde "a que instante absoluto corresponden las 15:00 de Moscu".

Convertir UTC almacenado a la zona del usuario

El patron canonico: almacenas created_at como timestamptz (que internamente es UTC) y quieres mostrarlo en la zona de un usuario concreto.

SELECT
  o.id,
  o.amount,
  o.created_at,                                          -- absolute moment (UTC)
  o.created_at AT TIME ZONE u.tz AS local_created_at     -- wall time for the user
FROM orders o
JOIN users u ON u.id = o.user_id;

Aqui u.tz es una columna de texto con un nombre de zona como 'America/Sao_Paulo'. Usa siempre zonas IANA con nombre, nunca desplazamientos fijos como '+03': un nombre de zona conoce todo el historial de transiciones de horario de verano, un desplazamiento crudo no.

Agrupar pedidos por el "dia local" del usuario es igual de directo, primero convertir, luego truncar:

SELECT
  DATE_TRUNC('day', o.created_at AT TIME ZONE u.tz) AS local_day,
  SUM(o.amount) AS revenue
FROM orders o
JOIN users u ON u.id = o.user_id
WHERE o.status = 'paid'
GROUP BY 1
ORDER BY 1;

El horario de verano, donde de verdad muerde

La razon principal para no sumar desplazamientos a mano es el DST. Toma Nueva York alrededor de la transicion de primavera de 2026, cuando los relojes saltan de las 02:00 directo a las 03:00.

SELECT TIMESTAMPTZ '2026-03-08 06:30:00+00' AT TIME ZONE 'America/New_York' AS before_dst,
       TIMESTAMPTZ '2026-03-08 07:30:00+00' AT TIME ZONE 'America/New_York' AS after_dst;
-- before_dst = 2026-03-08 01:30:00   (offset -05)
-- after_dst  = 2026-03-08 03:30:00   (offset -04, the 02:00 hour never exists)

Paso exactamente una hora de tiempo absoluto entre los dos momentos, pero el reloj de pared salto de la 01:30 a las 03:30, la hora 02:xx simplemente no existe esa noche. Un desplazamiento fijo de -05 daria la respuesta equivocada en la segunda fila.

Trampas

  • Trampa: confusion de modos. Aplicar AT TIME ZONE a un valor que ya es una columna timestamp naive no lo "convierte", declara que estaba en esa zona. Revisa primero el tipo de la columna.
  • Aplicarlo dos veces hace ida y vuelta. (ts AT TIME ZONE 'Europe/Moscow') da un timestamp naive; aplica AT TIME ZONE de nuevo y recuperas un timestamptz, util a veces para "recambiar de zona", pero mas a menudo un error.
  • Usa nombres IANA. Zonas como 'Europe/Moscow' sobreviven a los cambios de reglas de DST; desplazamientos como '+03' no.

MySQL y ClickHouse

Ningun motor tiene la sintaxis AT TIME ZONE. En MySQL usa CONVERT_TZ(ts, 'UTC', 'Europe/Moscow') (las tablas de zonas con nombre deben estar cargadas). En ClickHouse usa toTimeZone(ts, 'Europe/Moscow') para un DateTime, recordando que alli la zona es solo un atributo de visualizacion sobre un timestamp basado en UTC.

Practica con ejercicios reales

Resuelve ejercicios en el entrenador de SQL con corrección instantánea y pistas.

Abrir el entrenador