DATE_SUB
La funcion DATE_SUB resta un intervalo de tiempo a una fecha o valor datetime. Es la operación inversa de DATE_ADD y resulta esencial para calcular fechas pasadas: encontrar registros anteriores a cierto periodo, determinar inicios de periodos de retención o construir ventanas de tiempo hacia atrás desde la fecha actual.
Sintaxis
DATE_SUB(fecha, INTERVAL expresion unidad)La estructura es idéntica a DATE_ADD. El primer argumento es un valor de tipo DATE, DATETIME o una cadena con formato de fecha válido. La palabra clave INTERVAL introduce la cantidad y unidad de tiempo que deseas restar. Las unidades disponibles son las mismas: SECOND, MINUTE, HOUR, DAY, WEEK, MONTH, QUARTER, YEAR y todas las compuestas como DAY_HOUR, YEAR_MONTH, etc.
Comportamiento básico
Restar días a una fecha es la operación más común:
SELECT DATE_SUB('2026-03-15', INTERVAL 10 DAY) AS resultado;| resultado |
|---|
| 2026-03-05 |
MySQL retrocede diez días desde el 15 de marzo. Para restar meses:
SELECT DATE_SUB('2026-03-31', INTERVAL 1 MONTH) AS resultado;| resultado |
|---|
| 2026-02-28 |
Igual que DATE_ADD, cuando el día del mes no existe en el mes destino, MySQL ajusta al último día válido. Marzo tiene 31 días, pero febrero de 2026 solo 28, así que el resultado se ajusta automáticamente.
Restar años también contempla los años bisiestos:
SELECT DATE_SUB('2024-02-29', INTERVAL 1 YEAR) AS resultado;| resultado |
|---|
| 2023-02-28 |
Al retroceder un año desde el 29 de febrero de 2024 (bisiesto), el resultado se ajusta al 28 de febrero de 2023 porque ese día no existe en un año no bisiesto.
Para restar horas, minutos o segundos necesitas un valor datetime:
SELECT
DATE_SUB('2026-03-15 14:30:00', INTERVAL 3 HOUR) AS menos_3h,
DATE_SUB('2026-03-15 14:30:00', INTERVAL 45 MINUTE) AS menos_45m,
DATE_SUB('2026-03-15 14:30:00', INTERVAL 90 SECOND) AS menos_90s;| menos_3h | menos_45m | menos_90s |
|---|---|---|
| 2026-03-15 11:30:00 | 2026-03-15 13:45:00 | 2026-03-15 14:28:30 |
Las unidades WEEK y QUARTER funcionan como atajos:
SELECT
DATE_SUB('2026-06-15', INTERVAL 2 WEEK) AS menos_2_semanas,
DATE_SUB('2026-06-15', INTERVAL 1 QUARTER) AS menos_1_trimestre;| menos_2_semanas | menos_1_trimestre |
|---|---|
| 2026-06-01 | 2026-03-15 |
Equivalencia con DATE_ADD y valores negativos
Es importante saber que DATE_SUB es equivalente a usar DATE_ADD con un valor negativo. Las siguientes dos expresiones producen exactamente el mismo resultado:
SELECT
DATE_SUB('2026-06-15', INTERVAL 30 DAY) AS con_date_sub,
DATE_ADD('2026-06-15', INTERVAL -30 DAY) AS con_date_add_negativo;| con_date_sub | con_date_add_negativo |
|---|---|
| 2026-05-16 | 2026-05-16 |
Puedes usar cualquiera de las dos formas según lo que te resulte más legible. DATE_SUB expresa la intención con mayor claridad cuando el objetivo es retroceder en el tiempo, mientras que DATE_ADD con valores negativos puede ser útil cuando la dirección depende de una variable calculada.
De la misma forma, el operador - INTERVAL también es válido:
SELECT '2026-06-15' - INTERVAL 30 DAY AS resultado;| resultado |
|---|
| 2026-05-16 |
Caso práctico: registros de los últimos N días
Uno de los usos más habituales de DATE_SUB es filtrar registros recientes. Para obtener todos los pedidos de los últimos 30 días:
SELECT
id,
cliente_id,
total,
fecha_creacion
FROM pedidos
WHERE fecha_creacion >= DATE_SUB(CURDATE(), INTERVAL 30 DAY)
ORDER BY fecha_creacion DESC;| id | cliente_id | total | fecha_creacion |
|---|---|---|---|
| 1024 | 45 | 259.99 | 2026-02-10 |
| 1023 | 112 | 1899.99 | 2026-02-08 |
| 1022 | 78 | 89.97 | 2026-02-05 |
| 1021 | 203 | 549.00 | 2026-02-01 |
| 1020 | 34 | 124.98 | 2026-01-28 |
Esta construcción >= DATE_SUB(CURDATE(), INTERVAL N DAY) es un patrón que verás constantemente en consultas de reportes, dashboards y análisis de ventas recientes.
Caso práctico: periodos de retención y limpieza de datos
En sistemas que manejan grandes volúmenes de datos, es frecuente implementar políticas de retención. Por ejemplo, identificar registros de logs anteriores a 90 días para archivarlos o eliminarlos:
SELECT COUNT(*) AS registros_antiguos
FROM logs_acceso
WHERE fecha_acceso < DATE_SUB(CURDATE(), INTERVAL 90 DAY);| registros_antiguos |
|---|
| 45832 |
Para una política más sofisticada que archive los logs de actividad por trimestre:
SELECT
YEAR(fecha_acceso) AS anio,
QUARTER(fecha_acceso) AS trimestre,
COUNT(*) AS total_registros
FROM logs_acceso
WHERE fecha_acceso < DATE_SUB(CURDATE(), INTERVAL 6 MONTH)
GROUP BY YEAR(fecha_acceso), QUARTER(fecha_acceso)
ORDER BY anio, trimestre;| anio | trimestre | total_registros |
|---|---|---|
| 2025 | 1 | 12450 |
| 2025 | 2 | 15230 |
| 2025 | 3 | 18152 |
Caso práctico: clientes inactivos
Encontrar clientes que no han realizado un pedido en los últimos 6 meses es un análisis clave para campañas de reactivación:
SELECT
c.id,
c.nombre,
c.email,
MAX(p.fecha_creacion) AS ultimo_pedido
FROM clientes c
LEFT JOIN pedidos p ON c.id = p.cliente_id
GROUP BY c.id, c.nombre, c.email
HAVING ultimo_pedido < DATE_SUB(CURDATE(), INTERVAL 6 MONTH)
OR ultimo_pedido IS NULL
ORDER BY ultimo_pedido
LIMIT 5;| id | nombre | ultimo_pedido | |
|---|---|---|---|
| 156 | Roberto Díaz | roberto.diaz@email.com | 2025-05-12 |
| 89 | Lucía Fernández | lucia.fernandez@email.com | 2025-06-03 |
| 234 | Pedro Navarro | pedro.navarro@email.com | 2025-06-18 |
| 67 | Elena Ruiz | elena.ruiz@email.com | 2025-07-01 |
| 312 | Javier Moreno | javier.moreno@email.com | 2025-07-22 |
La condición HAVING filtra después de la agregación, lo que permite comparar la fecha máxima de pedido con el umbral de inactividad. El OR ultimo_pedido IS NULL captura también a los clientes que nunca han comprado.
Manejo de NULL
Al igual que DATE_ADD, cuando cualquiera de los argumentos es NULL, el resultado es NULL:
SELECT
DATE_SUB(NULL, INTERVAL 5 DAY) AS fecha_nula,
DATE_SUB('2026-03-15', INTERVAL NULL DAY) AS intervalo_nulo;| fecha_nula | intervalo_nulo |
|---|---|
| NULL | NULL |
En consultas sobre columnas que pueden contener valores nulos, protege el resultado con IFNULL o COALESCE:
SELECT
nombre,
IFNULL(
DATE_SUB(fecha_ultimo_acceso, INTERVAL 30 DAY),
'Sin registro'
) AS inicio_periodo
FROM empleados;Combinación con otras funciones
DATE_SUB es especialmente útil combinada con funciones de agregación para construir rangos de análisis temporal:
SELECT
DATE_SUB(CURDATE(), INTERVAL 7 DAY) AS inicio_semana,
CURDATE() AS fin_semana,
SUM(total) AS ventas_semana
FROM pedidos
WHERE fecha_creacion BETWEEN DATE_SUB(CURDATE(), INTERVAL 7 DAY) AND CURDATE();| inicio_semana | fin_semana | ventas_semana |
|---|---|---|
| 2026-02-07 | 2026-02-14 | 4523.94 |
También puedes combinarla con DATE_FORMAT para generar etiquetas legibles en reportes:
SELECT
DATE_FORMAT(DATE_SUB(CURDATE(), INTERVAL n.num MONTH), '%M %Y') AS mes,
COUNT(p.id) AS total_pedidos
FROM (SELECT 0 AS num UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5) n
LEFT JOIN pedidos p
ON YEAR(p.fecha_creacion) = YEAR(DATE_SUB(CURDATE(), INTERVAL n.num MONTH))
AND MONTH(p.fecha_creacion) = MONTH(DATE_SUB(CURDATE(), INTERVAL n.num MONTH))
GROUP BY n.num
ORDER BY n.num;Esta consulta genera un resumen de los últimos 6 meses mostrando el nombre del mes y el conteo de pedidos, lo que resulta ideal para gráficas de tendencia.
En el siguiente artículo veremos ADDTIME para sumar tiempo a un valor datetime.
Escrito por Eduardo Lázaro
