EOMONTH в MS SQL: первый и последний день месяца

EOMONTH возвращает последний день месяца. Функции для начала месяца вроде SOMONTH в SQL Server нет, поэтому первый день я считаю через DATEFROMPARTS (на SQL Server 2022+ — через DATETRUNC):

DECLARE @d datetime = '2026-11-04T15:30:00';

SELECT EOMONTH(@d);                           -- 2026-11-30
SELECT DATEFROMPARTS(YEAR(@d), MONTH(@d), 1); -- 2026-11-01

Синтаксис EOMONTH

EOMONTH ( start_date [ , month_to_add ] )
  • start_date — дата: date, datetime, datetime2 или строка, которая приводится к дате.
  • month_to_add — необязательное целое число. EOMONTH сначала сдвигает дату на столько месяцев, потом берёт последний день. Отрицательное число сдвигает назад.
  • Результат всегда date, какой бы тип ни пришёл на вход. Время отбрасывается.

EOMONTH появилась в SQL Server 2012, есть в Azure SQL Database, Azure SQL Managed Instance, Azure Synapse и Microsoft Fabric. В SQL Server 2008 и более ранних версиях её заменяет DATEADD(day, -1, DATEADD(month, DATEDIFF(month, 0, @d) + 1, 0)). Эта формула возвращает datetime.

Последний день текущего, прошлого и следующего месяца

SELECT EOMONTH(GETDATE())     AS this_month,  -- 2026-11-30
       EOMONTH(GETDATE(), -1) AS prev_month,  -- 2026-10-31
       EOMONTH(GETDATE(), 1)  AS next_month;  -- 2026-12-31

В комментариях результат на 4 ноября 2026 года. Високосные годы EOMONTH учитывает: EOMONTH('2024-02-10') вернёт 2024-02-29.

Первый день месяца в SQL Server

Все четыре варианта вернут 1 ноября 2026 года, но типы у них разные:

DECLARE @d datetime = '2026-11-04T15:30:00';

-- date, SQL Server 2012+
SELECT DATEFROMPARTS(YEAR(@d), MONTH(@d), 1);
SELECT DATEADD(day, 1, EOMONTH(@d, -1));

-- тип как у @d, SQL Server 2022+
SELECT DATETRUNC(month, @d);

-- datetime, любая версия
SELECT DATEADD(month, DATEDIFF(month, 0, @d), 0);
  • DATEFROMPARTS читается проще всех, я беру её по умолчанию.
  • DATEADD(day, 1, EOMONTH(@d, -1)) — день после конца прошлого месяца. Пригодится, если в запросе уже есть EOMONTH.
  • DATETRUNC(month, @d) работает в SQL Server 2022, Azure SQL Database, Managed Instance и Fabric. Тип она возвращает тот же, что получила: datetime останется datetime, date — date, а строковый литерал станет datetime2(7). Уровень совместимости 160 ей не нужен: я запускал её в базе с уровнем 100.
  • DATEADD(month, DATEDIFF(month, 0, @d), 0) — классический трюк из времён до SQL Server 2012. 0 здесь означает 1900-01-01: считаем, сколько целых месяцев прошло с этой даты, и прибавляем их обратно.

Первый день прошлого и следующего месяца:

DECLARE @d datetime = '2026-11-04T15:30:00';

-- первый день прошлого месяца: 2026-10-01
SELECT DATEADD(month, -1, DATEFROMPARTS(YEAR(@d), MONTH(@d), 1));

-- первый день следующего месяца: 2026-12-01
SELECT DATEADD(day, 1, EOMONTH(@d));

Как прибавлять и вычитать дни, месяцы и годы через DATEADD, я разбирал в посте про текущую дату минус год, месяц, день или минуту.

Группировка по месяцам

SELECT DATEFROMPARTS(YEAR(OrderDate), MONTH(OrderDate), 1) AS MonthStart,
       SUM(Amount) AS Total
FROM dbo.Orders
GROUP BY DATEFROMPARTS(YEAR(OrderDate), MONTH(OrderDate), 1)
ORDER BY MonthStart;

На SQL Server 2022 короче написать GROUP BY DATETRUNC(month, OrderDate). GROUP BY EOMONTH(OrderDate) тоже работает, если месяц удобнее подписывать последним днём.

Подвох: <= EOMONTH для столбца datetime

EOMONTH возвращает date, а при сравнении со столбцом datetime эта дата превращается в полночь последнего дня. Если в OrderDate хранится время, условие OrderDate <= EOMONTH(@d) теряет всё, что было после 00:00 этого дня. В моей тестовой таблице так пропал заказ от 31 октября в 18:45. Фильтруйте полуоткрытым интервалом:

DECLARE @d date = '2026-10-15';

SELECT SUM(Amount) AS Total
FROM dbo.Orders
WHERE OrderDate >= DATEFROMPARTS(YEAR(@d), MONTH(@d), 1)
  AND OrderDate <  DATEADD(day, 1, EOMONTH(@d));

Такой фильтр ещё и работает с индексом по OrderDate. С этим индексом в плане Index Seek:

CREATE INDEX IX_Orders_OrderDate
    ON dbo.Orders (OrderDate) INCLUDE (Amount);

А WHERE EOMONTH(OrderDate) = '2026-10-31' оборачивает столбец в функцию, и тот же индекс читается целиком (Index Scan). Без INCLUDE (Amount) оптимизатор и с диапазоном читал всю таблицу на 500 000 строк: за Amount для каждой из примерно 6000 строк месяца пришлось бы ходить в таблицу (Key Lookup), а это дороже.

SOMONTH: своя функция по образцу EOMONTH

В 2016 году я написал для этого функцию SOMONTH. У неё был параметр datetime, из-за которого она падала на датах раньше 1753 года, и лишняя промежуточная переменная того же типа. Исправленная версия:

CREATE OR ALTER FUNCTION dbo.SOMONTH (@d date)
RETURNS date
AS
BEGIN
    RETURN DATEFROMPARTS(YEAR(@d), MONTH(@d), 1);
END;

CREATE OR ALTER есть начиная с SQL Server 2016 SP1, в более старых версиях пишите CREATE FUNCTION. Вызывать функцию нужно со схемой: SELECT dbo.SOMONTH(GETDATE());

Скалярная функция в GROUP BY тормозит

До SQL Server 2019 скалярная UDF вызывается для каждой строки и запрещает параллельный план. В SQL Server 2019 появился инлайнинг скалярных функций (scalar UDF inlining) на уровне совместимости 150 и выше: оптимизатор подставляет тело функции в запрос. Но если функция стоит в GROUP BY или ORDER BY, инлайнинга не будет. Я проверил на SQL Server 2022: с GROUP BY dbo.SOMONTH(OrderDate) план по-прежнему вызывает функцию и не распараллеливается.

Посчитайте месяц в CROSS APPLY и группируйте по получившемуся столбцу. На уровне совместимости 150 и выше такой запрос инлайнится:

SELECT m.MonthStart, SUM(o.Amount) AS Total
FROM dbo.Orders AS o
CROSS APPLY (SELECT dbo.SOMONTH(o.OrderDate) AS MonthStart) AS m
GROUP BY m.MonthStart;

На больших таблицах и в версиях до 2019 обходитесь без функции: пишите DATEFROMPARTS в самом запросе, как в примере с группировкой. Условие WHERE dbo.SOMONTH(OrderDate) = ... инлайнинг тоже не спасает: столбец всё равно обёрнут в функцию, поэтому фильтруйте по диапазону дат.

Проверить, может ли SQL Server встроить вашу функцию:

SELECT is_inlineable
FROM sys.sql_modules
WHERE object_id = OBJECT_ID('dbo.SOMONTH');  -- 1
Консультирую по продукту, собираю MVP и учу делать продукты самостоятельно. Поработать со мной →