Tres capas - Servidor, sesión y columna
El comportamiento de zona horaria en MySQL está gobernado por tres capas: el valor predeterminado del servidor configurado en my.cnf, el valor por sesión ajustable mediante SET time_zone, y el tipo de columna en sí (TIMESTAMP frente a DATETIME). La mayoría de los errores de zona horaria provienen de confundir estas capas. Saber cuál cambiar para un síntoma dado es la base de una operación fiable.
El valor predeterminado del servidor se configura con default-time-zone en el archivo my.cnf. El valor de sesión puede cambiarse con SET time_zone = '+09:00' después de conectarse, y la mayoría de los controladores ORM lo hacen automáticamente al establecer una conexión. El comportamiento de columna, en cambio, está determinado enteramente por el tipo elegido en la definición del esquema y no puede cambiarse en tiempo de ejecución.
TIMESTAMP vs DATETIME - La diferencia crítica
TIMESTAMP almacena valores internamente como UTC y los convierte a la time_zone de la sesión al leerlos. Esto significa que clientes en diferentes zonas horarias ven horas locales consistentes para el mismo instante subyacente. El rango válido va desde 1970-01-01 hasta 2038-01-19 porque la implementación original usaba una representación en segundos de época Unix de 32 bits. MySQL 8.0.28 expandió el manejo interno pero mantuvo el rango documentado por compatibilidad.
DATETIME almacena valores como cadenas literales de año-mes-día-hora-minuto-segundo sin ninguna noción de zona horaria. El mismo valor se lee igual independientemente de la configuración de sesión, y el rango válido va desde 1000-01-01 hasta 9999-12-31. DATETIME parece más simple pero se vuelve más difícil de usar correctamente en sistemas multizona porque el significado del valor almacenado depende de un contexto que existe fuera de la base de datos.
La variable de sesión time_zone
Después de conectarse, SET time_zone = 'Asia/Tokyo' hace que las lecturas de TIMESTAMP posteriores, NOW() y CURRENT_TIMESTAMP() usen la zona especificada. Para usar nombres de zona IANA, el servidor debe tener los datos de zona horaria cargados mediante mysql_tzinfo_to_sql. Muchas imágenes Docker oficiales de MySQL no incluyen estos datos, por lo que SET time_zone = 'Asia/Tokyo' falla con ERROR 1298 a menos que se carguen previamente.
Si cargar los datos IANA no es práctico, se puede usar la notación de desplazamiento como SET time_zone = '+09:00' en su lugar. Esto funciona para zonas con desplazamientos estables durante todo el año pero no puede representar el horario de verano. Para sistemas que sirven localidades con horario de verano como Estados Unidos o Australia, los nombres IANA son esenciales y el esfuerzo inicial de cargar los datos de zona vale la pena.
Problemas del controlador JDBC - serverTimezone y connectionTimeZone
MySQL Connector/J acepta un parámetro serverTimezone o, en la versión 8.0+, connectionTimeZone en la URL de JDBC. El valor recomendado en versiones modernas del controlador es connectionTimeZone=SERVER (que respeta la configuración del lado del servidor) o un nombre de zona IANA explícito. Sin él, el controlador adivina basándose en heurísticas, y los valores de Instant u OffsetDateTime de Java pueden desplazarse silenciosamente al escribir.
Un accidente de producción común es código que funcionaba localmente porque el JDK del desarrollador y MySQL coincidían en JST, pero produce desfases de 9 horas en un servidor UTC en producción. La solución es especificar connectionTimeZone explícitamente en la URL de JDBC y migrar todo el uso de java.util.Date a tipos de java.time como Instant u OffsetDateTime. Date lleva semántica oculta de zona horaria local que las capas JDBC convierten de formas sorprendentes.
Semántica de DEFAULT CURRENT_TIMESTAMP
El patrón TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP se usa ampliamente para columnas created_at y updated_at. El tiempo actual se toma de la time_zone de la sesión, luego se almacena internamente como UTC. Si la time_zone del servidor es UTC, los valores almacenados corresponden exactamente al segundo de época Unix. Si es JST, lo mismo es cierto tras la conversión, pero cambiar la time_zone del servidor posteriormente no reinterpreta retroactivamente las filas existentes.
Las columnas DATETIME también pueden tener como predeterminado CURRENT_TIMESTAMP desde MySQL 5.6, pero DATETIME no almacena zona horaria, así que cambiar la time_zone del servidor cambia el significado de cualquier valor escrito antes del cambio. Esto hace que la time_zone del servidor sea una decisión casi irreversible una vez que se han acumulado datos, y cualquier cambio debe planificarse con plena conciencia de la interpretación histórica.
Práctica recomendada - UTC de extremo a extremo
Para proyectos nuevos, configure la time_zone del servidor en UTC, use columnas TIMESTAMP y represente los valores en el código de aplicación como Instant u OffsetDateTime. La capa de presentación es el único lugar que convierte a la zona horaria local del usuario. Esto elimina toda una clase de errores y hace que los despliegues multirregión sean directos, porque cada réplica interpreta los valores almacenados de forma idéntica.
Los sistemas existentes que funcionan con servidores en JST con columnas DATETIME no pueden cambiar de la noche a la mañana. El plan pragmático es usar TIMESTAMP para columnas nuevas, configurar connectionTimeZone explícitamente en las URL de JDBC, registrar marcas temporales con desplazamientos UTC explícitos y documentar el significado previsto de cada columna existente. Una migración de varios trimestres que ajusta el esquema columna por columna es mucho más segura que un corte total en un solo día.