Configuración y comprobación de la zona horaria en MySQL - Comportamiento por servidor / conexión / columna
Entender la estructura de tres capas - Servidor / conexión / columna
El comportamiento de la zona horaria en MySQL se determina en 3 capas: el valor predeterminado de todo el servidor, la configuración de sesión de cada conexión de cliente y el tipo de columna (TIMESTAMP o DATETIME). Muchos errores nacen de que los desarrolladores no distinguen estas tres capas y solo piensan en «cambiar la zona horaria de MySQL». Saber con precisión qué hay que cambiar para que cambie qué es el primer paso hacia una operación mantenible.
En el lado del servidor, el valor predeterminado se especifica con default-time-zone en archivos de configuración como my.cnf o /etc/mysql/conf.d/. En el lado del cliente, se puede cambiar después de conectarse con SET time_zone = '+09:00', y en muchos casos los controladores de JDBC o del ORM lo configuran automáticamente al conectarse. En el lado de la columna, el comportamiento predeterminado está fijado para cada tipo, y TIMESTAMP y DATETIME funcionan de manera fundamentalmente distinta.
Cómo comprobar la configuración actual de la zona horaria
Para comprobar la configuración actual, ejecuta SELECT @@global.time_zone, @@session.time_zone;. global es el valor predeterminado de todo el servidor y session es el valor aplicado a la conexión actual. Si aparece SYSTEM, significa que no se usa una configuración propia de MySQL, sino que se sigue la zona horaria del sistema operativo, y con SELECT @@system_time_zone; puedes comprobar qué zona horaria se aplica realmente.
Que se puedan usar nombres de zona horaria IANA como Asia/Tokyo depende de si tzdata se ha cargado en las tablas de zonas horarias de la base de datos mysql. Si SELECT COUNT(*) FROM mysql.time_zone_name; devuelve 0, los datos no están cargados, y SET time_zone = 'Asia/Tokyo' falla con el ERROR 1298 que se describe más adelante. Incluso en ese caso, una especificación de desplazamiento como '+09:00' siempre es válida, así que es fiable aislar el problema en este orden: primero comprobar el valor actual y después comprobar si hay tzdata.
La diferencia esencial entre TIMESTAMP y DATETIME
El tipo TIMESTAMP convierte internamente el valor a UTC para guardarlo y, al leerlo, lo devuelve a la hora local con la time_zone de la sesión. Con este diseño, desde cualquier parte del mundo donde se conecte un cliente, el valor se muestra correctamente en la zona horaria de cada uno. El rango está limitado de 1970-01-01 00:00:01 UTC a 2038-01-19 03:14:07 UTC, lo cual se debe a la representación en 32 bits de los segundos de época Unix. Este límite superior no ha cambiado tampoco en la serie 8.0. Lo que se amplió desde la versión 8.0.28 en entornos de 64 bits es el rango de argumentos que aceptan UNIX_TIMESTAMP() y FROM_UNIXTIME() (hasta el año 3001), y el límite del propio tipo TIMESTAMP sigue siendo el año 2038.
El tipo DATETIME no tiene el concepto de zona horaria y conserva tal cual, sin conversión de zona horaria, el valor introducido «2026-05-20 10:00:00». Su característica es que el valor mostrado no cambia por mucho que cambie la time_zone del cliente, y que su rango también es amplio, de 1000-01-01 a 9999-12-31. Parece sencillo a primera vista, pero en sistemas usados desde varias zonas horarias su significado no queda definido si no se registra aparte «de qué lugar es la hora de este valor», por lo que en la operación resulta, al contrario, un tipo difícil.
Diferencias entre TIMESTAMP y DATETIME - Almacenamiento / lectura / rango
Aspecto
TIMESTAMP
DATETIME
Qué se guarda
La hora convertida internamente a UTC
El valor introducido «2026-05-20 10:00:00» tal cual
Dependencia de la time_zone al leer
Depende. Vuelve a la hora local con la time_zone de la sesión
No depende. Aunque cambie la time_zone, el valor mostrado es el mismo
Rango representable
De 1970-01-01 00:00:01 UTC a 2038-01-19 03:14:07 UTC (se debe a la representación en 32 bits de los segundos de época Unix)
De 1000-01-01 a 9999-12-31
DEFAULT CURRENT_TIMESTAMP
Se puede usar. Se guarda convertido a UTC tomando como referencia la time_zone de la sesión
Se puede usar desde MySQL 5.6. Sin embargo, si se cambia la time_zone del servidor, el significado de los valores guardados también cambia en silencio
Para fijar el significado del valor
No se necesita información adicional, porque el propio tipo representa una hora absoluta
Hay que registrar aparte «de qué lugar es la hora de este valor»
La recomendación para un proyecto nuevo es una configuración que fija la time_zone del servidor en UTC, usa TIMESTAMP y concentra la conversión al mostrar en la capa de aplicación.
La time_zone de la sesión - El centro del comportamiento
Si al conectarse el cliente se especifica algo como SET time_zone = 'Asia/Tokyo', a partir de entonces la lectura y escritura de TIMESTAMP, NOW() y CURRENT_TIMESTAMP() funcionan en la zona horaria especificada. Para usar nombres de zona horaria IANA, hay que haber cargado tzdata en el servidor con mysql_tzinfo_to_sql. La imagen oficial de MySQL para Docker a menudo no tiene instaladas las zonas horarias IANA, y es un ejemplo típico en el que SET time_zone = 'Asia/Tokyo' falla con ERROR 1298.
Si se quiere evitar cargar tzdata, también existe la forma de especificar solo el desplazamiento, como SET time_zone = '+09:00'. Sin embargo, este método no puede representar el horario de verano, por lo que solo es un medio válido de forma limitada para regiones con un desplazamiento fijo todo el año, como Japón. En sistemas que manejan regiones con horario de verano, como Estados Unidos o Australia, se debe cambiar a un diseño que opere siempre con nombres de zona horaria IANA.
Las trampas del controlador JDBC - serverTimezone y connectionTimeZone
En MySQL Connector/J (el controlador JDBC), la zona horaria se indica explícitamente añadiendo el parámetro serverTimezone o connectionTimeZone a la URL de conexión. A partir de Connector/J 8.0 se recomienda connectionTimeZone, que admite SERVER (respeta la configuración del lado del servidor) o cualquier nombre IANA, y su valor predeterminado es LOCAL (la zona horaria predeterminada de la JVM). Si no se indica explícitamente, al escribir en MySQL valores Instant u OffsetDateTime del lado de Java se realiza de forma implícita una conversión de zona horaria basada en el valor predeterminado LOCAL. Además, aunque se especifique connectionTimeZone, la variable de sesión time_zone del lado del servidor no cambia, así que, si se quiere alinear también la sesión, se especifica además forceConnectionTimeZoneToSession=true.
Un accidente frecuente es el caso de un código que funcionaba en el entorno de desarrollo porque tanto el TimeZone de Java como la time_zone de MySQL estaban en JST, pero que provoca un desfase de 9 horas en el servidor UTC de producción. Para evitarlo, hay que indicar explícitamente connectionTimeZone en la URL de JDBC y aplicar de forma rigurosa en toda la aplicación la política de usar Instant u OffsetDateTime. java.util.Date del código heredado guarda internamente información de hora local, por lo que la conversión automática de JDBC introduce desfases con facilidad, y la migración a la API java.time es el remedio de fondo.
El comportamiento de DEFAULT CURRENT_TIMESTAMP
El patrón de configurar DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP en una columna TIMESTAMP se usa mucho para gestionar created_at / updated_at. En este caso, la «hora actual» se convierte internamente a UTC tomando como referencia la time_zone de la sesión del servidor, por lo que, si la time_zone del servidor es UTC, se guarda un valor exacto que corresponde a los segundos de época. En cambio, si el servidor opera con la configuración JST, cambiar después la zona horaria altera la interpretación de los datos pasados, por lo que al cambiar la política de operación hace falta un estudio cuidadoso.
Desde MySQL 5.6 también se puede especificar DEFAULT CURRENT_TIMESTAMP en una columna DATETIME. Sin embargo, DATETIME no conserva la zona horaria, así que, si cambia la time_zone del servidor, el significado de los valores guardados también cambia en silencio. Cambiar la time_zone del servidor en el entorno de producción es una decisión casi irreversible porque afecta a la interpretación de los datos pasados, y son imprescindibles el acuerdo de todo el equipo y pruebas suficientes.
Buenas prácticas - Un diseño unificado en UTC
La recomendación para un proyecto nuevo es una configuración que fija la time_zone del servidor en UTC, usa el tipo TIMESTAMP y maneja Instant u OffsetDateTime en el lado de la aplicación. La conversión de zona horaria al mostrar se concentra en la capa de aplicación, y en la base de datos siempre se guarda la hora absoluta en UTC. Incluso en un despliegue multirregión no surgen variaciones en la interpretación de los datos, y tampoco se producen incoherencias en la replicación.
Si un sistema existente funciona con la combinación del tipo DATETIME y un servidor en JST, la migración total inmediata es difícil. Como medida mínima, se van ajustando las cosas para unificar las columnas nuevas en TIMESTAMP, indicar explícitamente connectionTimeZone de JDBC y dejar siempre el desplazamiento UTC en los registros de la aplicación. A largo plazo, elaborar un plan de migración que sustituya DATETIME por TIMESTAMP y documentar el significado de cada columna con «a qué hora corresponde este valor» es el primer paso hacia una organización que no genere errores de zona horaria.