Convertir timestamps Unix en MySQL: FROM_UNIXTIME, UNIX_TIMESTAMP, zonas
MySQL trae dos tipos de fecha casi idénticos, FROM_UNIXTIME y UNIX_TIMESTAMP, más una historia de zonas horarias que sorprende a todo el mundo una vez. En cuanto sabes las reglas, son predecibles.
FROM_UNIXTIME y UNIX_TIMESTAMP cubren el round-trip
FROM_UNIXTIME(seconds) devuelve un DATETIME renderizado en la zona de la sesión. UNIX_TIMESTAMP(datetime) hace lo contrario. Ambas están desde MySQL 4.
```sql SELECT FROM_UNIXTIME(1763500800); /* 2025-11-19 01:00:00 en Europe/Madrid, 2025-11-19 00:00:00 en UTC */
SELECT UNIX_TIMESTAMP('2025-11-19 00:00:00'); /* 1763500800 si la sesión está en UTC */ ```
La zona de la sesión se fija con SET time_zone = '+00:00' por conexión, o globalmente en my.cnf. Si no la fijas, MySQL coge la zona del sistema, que es lo que reporte el SO del servidor. Receta para sufrir en producción.
TIMESTAMP y DATETIME no son lo mismo
DATETIME guarda una hora de pared sin zona pegada. El string que devuelve un SELECT es el mismo da igual la zona de sesión.
TIMESTAMP se guarda internamente como UTC y se convierte a y desde la zona de la sesión al leer y escribir. Es lo que quieres para una columna updated_at que debe reflejar siempre un instante real.
La pega: TIMESTAMP topa el 2038-01-19 03:14:07 UTC porque usa segundos de 32 bits con signo desde 1970. Cualquier timestamp posterior desborda. DATETIME llega hasta 9999. Para datos de aplicación con vida larga, mejor DATETIME y UTC por convención; para columnas de auditoría donde te fías de MySQL para la conversión, TIMESTAMP vale hasta que 2038 empiece a aparecer en el horizonte.
Las tablas de zonas horarias no van por defecto
Funciones como CONVERT_TZ('2025-01-01 00:00:00', 'UTC', 'Europe/Madrid') solo funcionan si las tablas de zonas están pobladas. En una instalación fresca están vacías y cada llamada devuelve NULL en silencio.
Para poblarlas en Linux:
mysql_tzinfo_to_sql /usr/share/zoneinfo | mysql -u root mysql
A partir de ahí, zonas con nombre tipo 'America/New_York' y 'Europe/Madrid' van. Sin eso te limitas a offsets numéricos tipo '+02:00', que no manejan DST. Si tu código llama a CONVERT_TZ y te salen NULLs en prod pero funciona local, es esto.
Milisegundos y microsegundos
MySQL 5.6 añadió soporte de fracciones de segundo hasta microsegundos. DATETIME(3) guarda milisegundos, DATETIME(6) microsegundos. Para epoch JavaScript en milisegundos:
```sql /* JS Date.now() a DATETIME(3) */ SELECT FROM_UNIXTIME(1763500837.421);
/* Round-trip con precisión de milis */ SELECT UNIX_TIMESTAMP(NOW(3)) * 1000; ```
Si necesitas timestamps con fracción de segundo, declara la columna como DATETIME(3) desde el día uno. Un ALTER TABLE para añadir precisión a una tabla con datos es online en 8.0+, pero sigue bloqueando lecturas un instante en tablas grandes.
Ejemplo completo
sql/* Asegúrate de que la sesión va en UTC */
SET time_zone = '+00:00';
/* Epoch en segundos a legible */
SELECT FROM_UNIXTIME(1763500800);
/* 2025-11-19 00:00:00 */
/* Ahora a epoch en milis (equivalente a Date.now en JS) */
SELECT CAST(UNIX_TIMESTAMP(NOW(3)) * 1000 AS UNSIGNED);
/* Filtrar filas de las últimas 24 horas usando epoch Unix */
SELECT id
FROM events
WHERE created_at >= FROM_UNIXTIME(UNIX_TIMESTAMP() - 86400); ¿Solo necesitas el resultado?
Cuando solo quieres saber qué significa 1763500837 sin abrir un cliente MySQL, pega el epoch en el convertidor de timestamps en navegador. Te muestra segundos, milisegundos, ISO 8601, UTC y tu zona local a la vez, con el offset relativo desde ahora, sin viajes al servidor.
Abrir Convertidor de Timestamp Unix →Preguntas frecuentes
¿Mis columnas TIMESTAMP se rompen en 2038?
Sí, si son TIMESTAMP de 32 bits. El arreglo es migrar a DATETIME (sin tope en 2038) y guardar UTC por convención, o esperar al reemplazo de 64 bits que saque Oracle antes de entonces. Planifica ya migraciones para datos que vivan más allá de 2030.
¿Por qué CONVERT_TZ devuelve NULL en producción y no en mi portátil?
Producción no tiene cargadas las tablas de zonas. Lanza mysql_tzinfo_to_sql contra la base mysql una vez. Hasta que lo hagas, las zonas con nombre devuelven NULL y solo van los offsets numéricos, en silencio y fácil de pasar por alto.
¿UTC_TIMESTAMP() o NOW()?
UTC_TIMESTAMP() siempre devuelve UTC sin importar la zona de sesión. NOW() devuelve la hora actual en la zona de sesión. Para código de aplicación usa UTC_TIMESTAMP() y convierte al mostrar; para queries puntuales cualquiera vale si conoces la zona.