To simplify this, we can reduce both requirements to exactly one operation from the start of the current month:
The first day of the prior month is exactly one month before the first day of the current month.
DATEADD(month, -1, {StartOfMonth})
The last day of the prior month is exactly one day before the first day of the current month.
DATEADD(day, -1, {StartOfMonth})
So how do we find the first day of the current month? There are threefour valid ways:
The old DateAdd/DateDiff from the beginning of time trick for the month interval
DATEADD(month, DATEDIFF(month, 0, current_timestamp), 0)
Use DateFromParts()
and Year()
/Month()
for day 1
DATEFROMPARTS(year(current_timestamp), month(current_timestamp), 1)
Use EOMonth()
(one day after one month before the end of the current month)
DATEADD(month, -1, dateadd(day, 1, EOMonth(current_timestamp)))
Which I just learned today can be simplified like this (thanks again Aaron):
DATEADD(day, 1, EOMonth(current_timestamp, -1))
DateTrunc()
, the new hotness in SQL Server 2022
DATETRUNC(month, current_timestamp)
I'm comfortable with any of those. While certainly one is faster than the others, they're all reasonably performant and they all use reliable date math; they do NOT construct the value from a string, which is something you should avoid.
If you have SQL Server 2022, I'd probably pick the last one (DATETRUNC()
), because it's easily the shortest and simplest to understand. But otherwise take whichever of these you want and plug them in for the StartOfMonth
expressions above.
Finally, I'll add that if you intend to use these as boundaries for a range query, you're typically better off using a half-open range, where the end boundary is an exclusive test for the day after your range closes; that is, the first day of the current month.
So instead of eventually doing this:
WHERE MyDateColumn BETWEEN @WorkMonthBeginDate AND @WorkMonthEndDate
You should be trying for something like this:
WHERE MyDateColumn >= @WorkMonthBeginDate
AND MyDateColumn < @NextMonthBeginDate
This way, in the case where the column may change to start also storing time values, the range still covers the entire month, and doesn't leave out values after the initial tick of the final day. It doesn't always matter, but this also helps you tend to naturally end up in a better place regarding index use.
DATETRUNC(month, ...)
function useful. Once you have that, you can subtract one month or one day to get the needed range. Side note: If working with date/time ranges, range logic often works better with half-open intervals where the start date/time is inclusive, and the end date/time is exclusive. Example, date/times in the month of August could be selected usingdt >= '2024-08-01' AND dt < '2024-09-01'
. This saves having to truncate dates or mess with inexact23:59
,23:59:59
,23:59:59.997
, or23:59:59.9999999
end-conditions.