Fechas · límites temporales

Primer y último día del mes en SQL

Calcula los límites de un mes para informes y filtros sin confundir el último día con el último instante de un timestamp.

Lectura: 9–11 minIntermediaEjemplo comprobado
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

Idea portable
inicio_mes = truncar_al_mes(fecha)
fin_mes    = inicio_del_mes_siguiente - 1 día

truncar_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.

SQLite · ejecutado sobre la base educativa
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;
Resultado comprobado
idfechainicio_mesfin_mes
1012025-05-022025-05-012025-05-31
1112025-06-012025-06-012025-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.

PostgreSQL
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.

MySQL 8.4
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.

SQL Server 2022+
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.

Patrón de filtro
WHERE fecha >= inicio_mes
  AND fecha < inicio_mes_siguiente

Con 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

Intermedia · fechas

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.

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_mes y < inicio_mes_siguiente suele ser más seguro que fabricar un “último instante”.
  • PostgreSQL, MySQL, SQL Server y SQLite expresan estos límites con sintaxis diferentes.

Conceptos relacionados

Fuentes técnicas consultadas