Contenido de esta guía
La idea: obtener dos límites, no “formatear una fecha”
Cuando necesitas el primer y el último día de un mes, la pregunta no es solo cómo mostrar una fecha. Estás calculando los límites de un periodo de calendario. El inicio suele ser el día 1 del mismo mes; el final depende de cuántos días tenga ese mes.
Este patrón aparece al cerrar ventas mensuales, generar periodos de facturación, validar vencimientos o preparar filtros. La intención es común, pero la sintaxis cambia entre PostgreSQL, MySQL, SQL Server y SQLite.
Patrón mental
inicio_mes = truncar_al_mes(fecha)
fin_mes = inicio_del_mes_siguiente - 1 díatruncar_al_mes no es una función SQL universal. Representa la operación que devuelve el comienzo del mes en tu motor. Para el final, algunos motores ofrecen una función directa; otros parten del mes siguiente y retroceden un día.
Ejemplo comprobado en SQLite
La base educativa contiene pedidos de mayo y junio de 2025. SQLite permite llevar una fecha al comienzo de su mes con el modificador 'start of month'. Para obtener el último día, avanza un mes y retrocede un día.
SELECT id,
fecha,
date(fecha, 'start of month') AS inicio_mes,
date(fecha, 'start of month', '+1 month', '-1 day') AS fin_mes
FROM pedidos
WHERE id IN (101, 111)
ORDER BY id;| id | fecha | inicio_mes | fin_mes |
|---|---|---|---|
| 101 | 2025-05-02 | 2025-05-01 | 2025-05-31 |
| 111 | 2025-06-01 | 2025-06-01 | 2025-06-30 |
El mismo patrón funciona en febrero sin tener que memorizar si el año es bisiesto: el motor calcula el comienzo del mes siguiente y después resta un día.
Cómo cambia entre motores
PostgreSQL: date_trunc + intervalo
date_trunc('month', fecha) lleva el valor al comienzo del mes. Para obtener una fecha sin hora puedes convertir el resultado a date. El último día se obtiene avanzando un mes y restando un día.
SELECT date_trunc('month', fecha)::date AS inicio_mes,
(date_trunc('month', fecha)
+ INTERVAL '1 month'
- INTERVAL '1 day')::date AS fin_mes
FROM pedidos;Si fecha es timestamp with time zone, la zona horaria usada para truncar puede cambiar qué instante pertenece a cada día. Define la zona de negocio antes de construir periodos.
MySQL: DATE_FORMAT + LAST_DAY
MySQL ofrece LAST_DAY() para el final del mes. Para el inicio, una forma explícita es construir el día 01 con DATE_FORMAT y convertirlo de nuevo a DATE.
SELECT CAST(DATE_FORMAT(fecha, '%Y-%m-01') AS DATE) AS inicio_mes,
LAST_DAY(fecha) AS fin_mes
FROM pedidos;DATE_FORMAT produce texto formateado; el CAST deja claro que el resultado final debe volver a tratarse como fecha.
SQL Server 2022+: DATETRUNC + EOMONTH
SQL Server 2022 introdujo DATETRUNC, que permite truncar al mes. EOMONTH devuelve directamente el último día del mes y también está disponible en versiones anteriores.
SELECT CAST(DATETRUNC(month, fecha) AS date) AS inicio_mes,
EOMONTH(fecha) AS fin_mes
FROM pedidos;En SQL Server anterior a 2022 puedes formar el inicio con DATEFROMPARTS(YEAR(fecha), MONTH(fecha), 1) y conservar EOMONTH(fecha) para el final.
SQLite: modificadores de date()
SQLite aplica los modificadores temporales de izquierda a derecha. 'start of month' retrocede al comienzo del mes; '+1 month' y '-1 day' llevan al último día del mes original.
Para filtrar timestamps, piensa en el mes siguiente
El último día es útil para mostrar periodos, pero un filtro sobre timestamps necesita más cuidado. Si escribes un límite equivalente a 2025-05-31 00:00:00, puedes excluir casi todo el 31 de mayo. Inventar 23:59:59 tampoco es robusto cuando existe precisión de fracciones de segundo.
Una forma más segura es usar un intervalo semiabierto: incluye el inicio del mes y excluye el inicio del siguiente.
WHERE fecha >= inicio_mes
AND fecha < inicio_mes_siguienteCon fechas puras, un rango inclusivo puede ser suficiente. Con timestamps, el intervalo >= inicio y < siguiente_mes expresa el periodo completo sin depender de la precisión temporal del motor.
Errores frecuentes
Restar 30 días para encontrar el mes anterior
Los meses no tienen una duración fija. Febrero, abril y julio no pueden modelarse correctamente como “30 días”. Trabaja con unidades de calendario del motor.
Confundir último día con último instante
2025-05-31 representa un día; no define por sí solo hasta qué fracción de segundo debe aceptar un filtro sobre timestamps. Para filtros mensuales, usa preferentemente el inicio del mes siguiente como límite exclusivo.
Copiar una función entre motores
EOMONTH, LAST_DAY, date_trunc y los modificadores de SQLite no son una sintaxis portable única. Conserva el modelo mental y adapta la expresión al dialecto real.
Comprueba lo aprendido
Obtén los meses presentes en los pedidos
En SQLite, devuelve una fila por mes presente en pedidos con las columnas inicio_mes y fin_mes. Ordena por inicio_mes.
Criterio de éxito: obtienes exactamente dos filas —mayo y junio de 2025— sin escribir manualmente cuántos días tiene cada mes.
date(fecha, 'start of month').'+1 month' y después '-1 day'.DISTINCT para dejar una sola fila por par de límites.Solución razonada
SELECT DISTINCT
date(fecha, 'start of month') AS inicio_mes,
date(fecha, 'start of month', '+1 month', '-1 day') AS fin_mes
FROM pedidos
ORDER BY inicio_mes;| inicio_mes | fin_mes |
|---|---|
| 2025-05-01 | 2025-05-31 |
| 2025-06-01 | 2025-06-30 |
DISTINCT elimina repeticiones porque todos los pedidos de un mismo mes producen los mismos límites. La lógica del calendario queda en las funciones de fecha, no en números de días escritos a mano.
Qué debes recordar
- El inicio del mes es una operación de truncado o reconstrucción del día 1.
- El final puede obtenerse con una función específica o como inicio del siguiente mes menos un día.
- Los meses no duran siempre 30 días.
- Para filtrar timestamps,
>= inicio_mesy< inicio_mes_siguientesuele ser más seguro que fabricar un “último instante”. - PostgreSQL, MySQL, SQL Server y SQLite expresan estos límites con sintaxis diferentes.