TIMESTAMPDIFF
La funcion TIMESTAMPDIFF calcula la diferencia entre dos valores datetime y la expresa en la unidad que tú elijas: segundos, minutos, horas, días, semanas, meses, trimestres o años. Es la función más versátil de MySQL para medir intervalos de tiempo y la respuesta correcta siempre que necesites saber "cuántos meses han pasado", "cuántos años tiene" o "cuántas horas transcurrieron" entre dos fechas.
Sintaxis
TIMESTAMPDIFF(unidad, datetime1, datetime2)El primer argumento es la unidad en la que deseas el resultado. Las unidades válidas son SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, QUARTER y YEAR. Los dos argumentos siguientes son valores DATE o DATETIME. El resultado es datetime2 - datetime1 expresado como un entero en la unidad especificada.
Observa que el orden de los argumentos es diferente al de DATEDIFF y TIMEDIFF: aquí la fecha más antigua va primero y la más reciente después. Si datetime2 es anterior a datetime1, el resultado será negativo.
Comportamiento básico
Calcular la diferencia en días entre dos fechas:
SELECT TIMESTAMPDIFF(DAY, '2026-01-01', '2026-03-15') AS dias;| dias |
|---|
| 73 |
Para obtener la misma diferencia en semanas, meses o años, solo cambias la unidad:
SELECT
TIMESTAMPDIFF(DAY, '2024-06-15', '2026-02-14') AS dias,
TIMESTAMPDIFF(WEEK, '2024-06-15', '2026-02-14') AS semanas,
TIMESTAMPDIFF(MONTH, '2024-06-15', '2026-02-14') AS meses,
TIMESTAMPDIFF(YEAR, '2024-06-15', '2026-02-14') AS anios;| dias | semanas | meses | anios |
|---|---|---|---|
| 610 | 87 | 19 | 1 |
TIMESTAMPDIFF trunca el resultado al entero inferior. En el ejemplo, aunque han pasado 19 meses y algunos días, la unidad MONTH devuelve 19, no 20. Del mismo modo, YEAR devuelve 1 porque aún no se han completado 2 años enteros.
La función también trabaja con precisión horaria cuando usas unidades de tiempo:
SELECT
TIMESTAMPDIFF(HOUR, '2026-03-15 09:00:00', '2026-03-16 14:30:00') AS horas,
TIMESTAMPDIFF(MINUTE, '2026-03-15 09:00:00', '2026-03-15 11:45:00') AS minutos,
TIMESTAMPDIFF(SECOND, '2026-03-15 09:00:00', '2026-03-15 09:05:30') AS segundos;| horas | minutos | segundos |
|---|---|---|
| 29 | 165 | 330 |
Las horas se truncan a 29 (no 29.5) porque TIMESTAMPDIFF siempre devuelve un entero.
Caso práctico: calcular la edad de una persona
Calcular la edad en años es el caso de uso emblemático de TIMESTAMPDIFF. Es la forma más fiable de hacerlo en MySQL porque maneja correctamente los años bisiestos y las variaciones en la cantidad de días por mes:
SELECT
nombre,
fecha_nacimiento,
TIMESTAMPDIFF(YEAR, fecha_nacimiento, CURDATE()) AS edad
FROM empleados
ORDER BY edad DESC
LIMIT 5;| nombre | fecha_nacimiento | edad |
|---|---|---|
| Roberto Díaz | 1968-05-22 | 57 |
| Elena Ruiz | 1975-11-08 | 50 |
| Javier Moreno | 1980-03-15 | 45 |
| María García | 1985-09-30 | 40 |
| Carlos López | 1990-07-12 | 35 |
Este cálculo es correcto porque TIMESTAMPDIFF(YEAR, ...) solo cuenta un año cuando ha transcurrido completo. Si una persona nació el 15 de marzo de 1990, el 14 de marzo de 2026 aún tendrá 35 años, y el 15 de marzo cumplirá 36.
Para mayor detalle, puedes mostrar la edad desglosada:
SELECT
nombre,
fecha_nacimiento,
TIMESTAMPDIFF(YEAR, fecha_nacimiento, CURDATE()) AS anios,
TIMESTAMPDIFF(MONTH, fecha_nacimiento, CURDATE()) % 12 AS meses_adicionales
FROM empleados
WHERE id = 1;| nombre | fecha_nacimiento | anios | meses_adicionales |
|---|---|---|---|
| María García | 1985-09-30 | 40 | 4 |
El operador módulo % 12 extrae los meses restantes después de los años completos, dando como resultado "40 años y 4 meses".
Caso práctico: antigüedad laboral y beneficios
En sistemas de recursos humanos, la antigüedad determina beneficios como días de vacaciones o bonificaciones:
SELECT
e.nombre,
e.fecha_contratacion,
TIMESTAMPDIFF(YEAR, e.fecha_contratacion, CURDATE()) AS anios_antiguedad,
TIMESTAMPDIFF(MONTH, e.fecha_contratacion, CURDATE()) AS meses_totales,
CASE
WHEN TIMESTAMPDIFF(YEAR, e.fecha_contratacion, CURDATE()) >= 10 THEN 30
WHEN TIMESTAMPDIFF(YEAR, e.fecha_contratacion, CURDATE()) >= 5 THEN 25
WHEN TIMESTAMPDIFF(YEAR, e.fecha_contratacion, CURDATE()) >= 2 THEN 20
ELSE 15
END AS dias_vacaciones
FROM empleados e
WHERE e.activo = 1
ORDER BY anios_antiguedad DESC
LIMIT 5;| nombre | fecha_contratacion | anios_antiguedad | meses_totales | dias_vacaciones |
|---|---|---|---|---|
| Roberto Díaz | 2010-03-01 | 15 | 191 | 30 |
| Elena Ruiz | 2015-08-15 | 10 | 126 | 30 |
| Javier Moreno | 2018-01-10 | 8 | 97 | 25 |
| María García | 2020-06-01 | 5 | 68 | 25 |
| Carlos López | 2023-09-15 | 2 | 28 | 20 |
La combinación de TIMESTAMPDIFF con CASE permite asignar beneficios graduales basados en la antigüedad, un patrón muy común en aplicaciones de RRHH.
Caso práctico: métricas de tiempo por etapa
En un sistema de gestión de proyectos o pipeline de ventas, medir cuánto tiempo permanece un registro en cada etapa permite identificar cuellos de botella:
SELECT
o.id AS oportunidad_id,
o.cliente,
o.etapa,
o.fecha_cambio_etapa,
TIMESTAMPDIFF(DAY, o.fecha_cambio_etapa, CURDATE()) AS dias_en_etapa,
TIMESTAMPDIFF(HOUR, o.fecha_cambio_etapa, NOW()) AS horas_en_etapa
FROM oportunidades_venta o
WHERE o.etapa != 'cerrada'
ORDER BY dias_en_etapa DESC
LIMIT 5;| oportunidad_id | cliente | etapa | fecha_cambio_etapa | dias_en_etapa | horas_en_etapa |
|---|---|---|---|---|---|
| 78 | Empresa ABC | negociacion | 2026-01-05 10:00:00 | 40 | 965 |
| 112 | Corp XYZ | propuesta | 2026-01-20 14:30:00 | 25 | 601 |
| 145 | Servicios JKL | contacto | 2026-02-01 09:00:00 | 13 | 319 |
| 189 | Tech MNO | demo | 2026-02-07 16:00:00 | 7 | 167 |
| 201 | Grupo PQR | contacto | 2026-02-10 11:15:00 | 4 | 100 |
Una oportunidad que lleva 40 días en la etapa de negociación podría requerir intervención. La granularidad en horas (horas_en_etapa) añade precisión cuando los días no son suficientes.
Para un resumen agregado por etapa:
SELECT
etapa,
COUNT(*) AS total,
ROUND(AVG(TIMESTAMPDIFF(DAY, fecha_cambio_etapa, CURDATE())), 1) AS promedio_dias,
MAX(TIMESTAMPDIFF(DAY, fecha_cambio_etapa, CURDATE())) AS maximo_dias
FROM oportunidades_venta
WHERE etapa != 'cerrada'
GROUP BY etapa
ORDER BY promedio_dias DESC;| etapa | total | promedio_dias | maximo_dias |
|---|---|---|---|
| negociacion | 8 | 28.5 | 40 |
| propuesta | 12 | 18.3 | 25 |
| demo | 15 | 9.2 | 15 |
| contacto | 23 | 5.8 | 13 |
Manejo de NULL
Si cualquiera de los argumentos datetime es NULL, el resultado es NULL:
SELECT
TIMESTAMPDIFF(DAY, NULL, '2026-03-15') AS primero_nulo,
TIMESTAMPDIFF(DAY, '2026-03-15', NULL) AS segundo_nulo;| primero_nulo | segundo_nulo |
|---|---|
| NULL | NULL |
En la práctica, las columnas de fecha que aún no tienen valor (como fecha_finalizacion de un proyecto en curso) son NULL. Usa COALESCE o IFNULL para sustituirlas:
SELECT
nombre_proyecto,
TIMESTAMPDIFF(DAY, fecha_inicio,
COALESCE(fecha_finalizacion, CURDATE())
) AS dias_transcurridos
FROM proyectos;Esto calcula los días desde el inicio hasta la finalización, o hasta hoy si el proyecto aún no ha terminado.
Combinación con otras funciones
TIMESTAMPDIFF se integra naturalmente con funciones de agregación para generar métricas estadísticas:
SELECT
ROUND(AVG(TIMESTAMPDIFF(HOUR, fecha_creacion, fecha_cierre)), 1) AS promedio_horas,
MIN(TIMESTAMPDIFF(HOUR, fecha_creacion, fecha_cierre)) AS minimo_horas,
MAX(TIMESTAMPDIFF(HOUR, fecha_creacion, fecha_cierre)) AS maximo_horas
FROM tickets_soporte
WHERE fecha_cierre IS NOT NULL
AND fecha_creacion >= DATE_SUB(CURDATE(), INTERVAL 30 DAY);| promedio_horas | minimo_horas | maximo_horas |
|---|---|---|
| 18.5 | 1 | 96 |
También puedes usarla dentro de expresiones condicionales para crear segmentaciones:
SELECT
CASE
WHEN TIMESTAMPDIFF(MONTH, fecha_registro, CURDATE()) < 3 THEN 'Nuevo'
WHEN TIMESTAMPDIFF(MONTH, fecha_registro, CURDATE()) < 12 THEN 'Regular'
ELSE 'Veterano'
END AS segmento,
COUNT(*) AS total_clientes
FROM clientes
GROUP BY segmento;| segmento | total_clientes |
|---|---|
| Nuevo | 145 |
| Regular | 312 |
| Veterano | 543 |
Esta segmentación por antigüedad es fundamental para análisis de marketing y retención de clientes.
En el siguiente artículo veremos TIMESTAMPADD para sumar intervalos en unidades específicas.
Escrito por Eduardo Lázaro
