Applying different time period Groupings to a set of data - sql

The following was a pattern I started to use two years ago and it is repeated over and over in my legacy code.
It effectively groups the same data using different time periods.
Is there a standard way I should be approaching this or is this long winded method as good as I'll get?
Another way of putting this question is how can the following be made more concise?
All 4 queries come out of the same data source and all four go into the same output table can these 4 queries be amalgamated into 1 shorter script?
DECLARE #myDate DATETIME = CONVERT(DATETIME,CONVERT(VARCHAR(11),GETDATE(),106));
DECLARE #myFirstDateLastMth CHAR(8) =CONVERT(CHAR(6),DATEADD(mm,-1,#myDate-1),112) + '01';
DECLARE #myFirstDateCurrentMth CHAR(8) =CONVERT(CHAR(6),DATEADD(mm,0,#myDate-1),112) + '01';
DELETE FROM WH.dbo.tb_myTable
--day on day==========
INSERT INTO WH.dbo.tb_myTable
SELECT
TimePeriod =
CASE
WHEN x.DateKey = CONVERT(VARCHAR(11),#myDate - 1,112) THEN 'Day'
WHEN x.DateKey = CONVERT(VARCHAR(11),#myDate - 2,112) THEN 'Day-1'
END,
Game = x.Name,
Score = SUM(x.Score),
Ticks = SUM(x.Ticks),
ScorePerTick = SUM(x.Score)/SUM(x.Ticks)
FROM #LimitedBetinfo x
WHEREx.DateKey >= CONVERT(VARCHAR(11),#myDate - 2,112)
GROUP BY
CASE
WHEN x.DateKey = CONVERT(VARCHAR(11),#myDate - 1,112) THEN 'Day'
WHEN x.DateKey = CONVERT(VARCHAR(11),#myDate - 2,112) THEN 'Day-1'
END,
x.Name;
--wk on wk==========
INSERT INTO WH.dbo.tb_myTable
SELECT
TimePeriod =
CASE
WHEN x.DateKey >= CONVERT(VARCHAR(11),#myDate - 7,112) THEN 'Week'
WHEN x.DateKey < CONVERT(VARCHAR(11),#myDate - 7,112)
AND x.DateKey >= CONVERT(VARCHAR(11),#myDate - 14,112)
THEN 'Week-1'
END,
Game = x.Name,
Score = SUM(x.Score),
Ticks = SUM(x.Ticks),
ScorePerTick = SUM(x.Score)/SUM(x.Ticks)
FROM #LimitedBetinfo x
WHERE x.DateKey >= CONVERT(VARCHAR(11),#myDate - 14,112)
GROUP BY
CASE
WHEN x.DateKey >= CONVERT(VARCHAR(11),#myDate - 7,112) THEN 'Week'
WHEN x.DateKey < CONVERT(VARCHAR(11),#myDate - 7,112)
AND x.DateKey >= CONVERT(VARCHAR(11),#myDate - 14,112)
THEN 'Week-1'
END,
g.Name;
--mth on mth==========
INSERT INTO WH.dbo.tb_myTable
SELECT
TimePeriod =
CASE
WHEN x.DateKey >= CONVERT(VARCHAR(11),#myDate - 28,112) THEN 'Month'
WHEN x.DateKey < CONVERT(VARCHAR(11),#myDate - 28,112)
AND x.DateKey >= CONVERT(VARCHAR(11),#myDate - 56,112)
THEN 'Month-1'
END,
Game = x.Name,
Score = SUM(x.Score),
Ticks = SUM(x.Ticks),
ScorePerTick = SUM(x.Score)/SUM(x.Ticks)
FROM #LimitedBetinfo x
WHERE x.DateKey >= CONVERT(VARCHAR(11),#myDate - 56,112)
GROUP BY
CASE
WHEN x.DateKey >= CONVERT(VARCHAR(11),#myDate - 28,112) THEN 'Month'
WHEN x.DateKey < CONVERT(VARCHAR(11),#myDate - 28,112)
AND x.DateKey >= CONVERT(VARCHAR(11),#myDate - 56,112)
THEN 'Month-1'
END,
g.Name;
--MTD and PrevCalMonth==========
INSERT INTO WH.dbo.tb_myTable
SELECT
TimePeriod
= CASE
WHEN x.DateKey >= #myFirstDateCurrentMth THEN 'MTD'
WHEN x.DateKey < #myFirstDateCurrentMth
AND x.DateKey >=#myFirstDateLastMth THEN 'PrevCalMonth'
END,
Game = x.Name,
Score = SUM(x.Score),
Ticks = SUM(x.Ticks),
ScorePerTick = SUM(x.Score)/SUM(x.Ticks)
FROM #LimitedBetinfo x
WHERE x.DateKey >= CONVERT(CHAR(6),DATEADD(mm,-1,#myDate-1),112) + '01'
GROUP BY
CASE
WHEN x.DateKey >= #myFirstDateCurrentMth THEN 'MTD'
WHEN x.DateKey < #myFirstDateCurrentMth
AND x.DateKey >=#myFirstDateLastMth THEN 'PrevCalMonth'
END,
g.Name;

I would make it a single insert statement.
Would prefer for now not to use the group by grouping sets, cube, or rollup as that I don't see how I could limit the rows calculated over individual day groups from being less than those calculated over larger time period groups.
So, to keep that from happening you could create a common-table-expression (;WITH mycte AS (...subquery...)), temp table, table variable, or XML formatted text object that would contain the time periods, one row/element for each.
This script can also be run with more or less time periods defined to get all results with only one trip from the app to the server.
Here's an example with temp table, that could also be easily made into a table variable:
--Define time periods
CREATE TABLE #TempTimePeriods (
TimePeriod VARCHAR(20) PRIMARY KEY,
TPBegin VARCHAR(11) NOT NULL,
TPEnd VARCHAR(11) NULL
);
DECLARE #myDate DATETIME = '2012-10-10';
DECLARE #myDateMinusOne DATETIME = DATEADD(dd, -1, #myDate);
INSERT INTO #TempTimePeriods ( TimePeriod, TPBegin, TPEnd )
SELECT [TimePeriod], CONVERT(VARCHAR(11), TPBegin, 112) TPBegin, CONVERT(VARCHAR(11), TPEnd, 112) TPEnd
FROM (
SELECT 'Day' [TimePeriod], #myDate - 1 TPBegin, #myDate - 1 TPEnd UNION ALL
SELECT 'Day-1' [TimePeriod], #myDate - 2 TPBegin, #myDate - 2 TPEnd UNION ALL
SELECT 'Week' [TimePeriod], #myDate - 7 TPBegin, NULL TPEnd UNION ALL
SELECT 'Week-1' [TimePeriod], #myDate - 14 TPBegin, #myDate - 8 TPEnd UNION ALL
SELECT 'Month' [TimePeriod], #myDate - 28 TPBegin, NULL TPEnd UNION ALL
SELECT 'Month-1' [TimePeriod], #myDate - 56 TPBegin, #myDate - 29 TPEnd UNION ALL
SELECT 'MTD' [TimePeriod], DATEADD(dd, -1 * DAY(#myDateMinusOne) + 1, #myDateMinusOne) TPBegin, NULL TPEnd UNION ALL
SELECT 'PrevCalMonth' [TimePeriod], DATEADD(mm,-1,DATEADD(dd, -1 * DAY(#myDateMinusOne) + 1, #myDateMinusOne)) TPBegin, DATEADD(dd, -1 * DAY(#myDateMinusOne), #myDateMinusOne) TPEnd
) TT;
And here is the main query...
--compute/insert results
INSERT INTO WH.dbo.tb_myTable
SELECT TimePeriods.TimePeriod,
x.Name Game,
SUM(x.Score) Score,
SUM(x.Ticks) Ticks,
CASE WHEN SUM(x.Ticks) != 0 THEN SUM(x.Score)/SUM(x.Ticks) END ScorePerTick
FROM #TempTimePeriods TimePeriods
--for periods with no data use left outer join to return 0-value results, otherwise inner join
LEFT OUTER JOIN #LimitedBetInfo x
ON x.DateKey >= [TimePeriods].TPBegin
AND (
[TimePeriods].TPEnd IS NULL
OR x.DateKey <= [TimePeriods].TPEnd
)
GROUP BY TimePeriods.TimePeriod, x.Name
You could also eliminate the the #TempTimePeriods table using a Common-Table-Expression below:
DECLARE #myDate DATETIME = '2012-10-10';
DECLARE #myDateMinusOne DATETIME = DATEADD(dd, -1, #myDate);
;WITH TimePeriods AS (
SELECT [TimePeriod], CONVERT(VARCHAR(11), TPBegin, 112) TPBegin, CONVERT(VARCHAR(11), TPEnd, 112) TPEnd
FROM (
SELECT 'Day' [TimePeriod], #myDate - 1 TPBegin, #myDate - 1 TPEnd UNION ALL
SELECT 'Day-1' [TimePeriod], #myDate - 2 TPBegin, #myDate - 2 TPEnd UNION ALL
SELECT 'Week' [TimePeriod], #myDate - 7 TPBegin, NULL TPEnd UNION ALL
SELECT 'Week-1' [TimePeriod], #myDate - 14 TPBegin, #myDate - 8 TPEnd UNION ALL
SELECT 'Month' [TimePeriod], #myDate - 28 TPBegin, NULL TPEnd UNION ALL
SELECT 'Month-1' [TimePeriod], #myDate - 56 TPBegin, #myDate - 29 TPEnd UNION ALL
SELECT 'MTD' [TimePeriod], DATEADD(dd, -1 * DAY(#myDateMinusOne) + 1, #myDateMinusOne) TPBegin, NULL TPEnd UNION ALL
SELECT 'PrevCalMonth' [TimePeriod], DATEADD(mm,-1,DATEADD(dd, -1 * DAY(#myDateMinusOne) + 1, #myDateMinusOne)) TPBegin, DATEADD(dd, -1 * DAY(#myDateMinusOne), #myDateMinusOne) TPEnd
) TT
)
INSERT INTO WH.dbo.tb_myTable
SELECT TimePeriods.TimePeriod,
x.Name Game,
SUM(x.Score) Score,
SUM(x.Ticks) Ticks,
CASE WHEN SUM(x.Ticks) != 0 THEN SUM(x.Score)/SUM(x.Ticks) END ScorePerTick
FROM [TimePeriods]
--for periods with no data use left outer join to return 0-value results, otherwise inner join
LEFT OUTER JOIN #LimitedBetInfo x
ON x.DateKey >= [TimePeriods].TPBegin
AND (
[TimePeriods].TPEnd IS NULL
OR x.DateKey <= [TimePeriods].TPEnd
)
GROUP BY [TimePeriods].TimePeriod, x.Name
And lastly you could define the time periods in an XML string-handy for passing to a stored procedure if that's your preference and proceed as follows:
--example XML string with time period definitions
DECLARE #TimePeriodsXml NVARCHAR(MAX) = '
<TimePeriod name="Day" tpbegin="20121010" tpend="20121010" />
<TimePeriod name="Day-1" tpbegin="20121009" tpend="20121009" />
<TimePeriod name="Week" tpbegin="20121004"/>
<TimePeriod name="Week-1" tpbegin="20120927" tpend="20121004" />
<TimePeriod name="Month" tpbegin="20120913" />
<TimePeriod name="Month-1" tpbegin="20120815" tpend="20120912" />
<TimePeriod name="MTD" tpbegin="20121001" />
<TimePeriod name="PrevCalMonth" tpbegin="20120901" tpend="20120930" />
';
and the main query modified to read the XML:
SELECT TimePeriods.TimePeriod,
x.Name Game,
SUM(x.Score) Score,
SUM(x.Ticks) Ticks,
CASE WHEN SUM(x.Ticks) != 0 THEN SUM(x.Score)/SUM(x.Ticks) END ScorePerTick
FROM (
SELECT
E.TimePeriod.value('./#name', 'VARCHAR(20)') TimePeriod,
E.TimePeriod.value('./#tpbegin', 'VARCHAR(20)') TPBegin,
E.TimePeriod.value('./#tpend', 'VARCHAR(20)') TPEnd
FROM (
SELECT CAST(#TimePeriodsXml AS XML) tpxml
) TT
CROSS APPLY tpxml.nodes('/TimePeriod') AS E(TimePeriod)
) TimePeriods
--for periods with no data use left outer join to return 0-value results, otherwise inner join
LEFT OUTER JOIN #LimitedBetInfo x
ON x.DateKey >= [TimePeriods].TPBegin
AND (
[TimePeriods].TPEnd IS NULL
OR x.DateKey <= [TimePeriods].TPEnd
)
GROUP BY TimePeriods.TimePeriod, x.Name
For an example of how the XML stringed query could be turned into a procedure, to support a single parameter of 1 or more time periods:
CREATE PROCEDURE dbo.GetTimePeriodAggregates
#TimePeriodsXmlString NVARCHAR(MAX)
AS
BEGIN
SET NOCOUNT ON;
SELECT TimePeriods.TimePeriod,
x.Name Game,
SUM(x.Score) Score,
SUM(x.Ticks) Ticks,
CASE WHEN SUM(x.Ticks) != 0 THEN SUM(x.Score)/SUM(x.Ticks) END ScorePerTick
FROM (
SELECT
E.TimePeriod.value('./#name', 'VARCHAR(20)') TimePeriod,
E.TimePeriod.value('./#tpbegin', 'VARCHAR(20)') TPBegin,
E.TimePeriod.value('./#tpend', 'VARCHAR(20)') TPEnd
FROM (
SELECT CAST(#TimePeriodsXml AS XML) tpxml
) TT
CROSS APPLY tpxml.nodes('/TimePeriod') AS E(TimePeriod)
) TimePeriods
LEFT OUTER JOIN #LimitedBetInfo x
ON x.DateKey BETWEEN TimePeriods.TPBegin AND TimePeriods.TPEnd
GROUP BY TimePeriods.TimePeriod, x.Name
END
Which could be run as:
--This declare is just an example, it could be instead a parameter passed from an application
DECLARE #ThisExecutionsXmlString NVARCHAR(MAX) = N'
<TimePeriod name="Day" tpbegin="20121010" tpend="20121010" />
<TimePeriod name="Day-1" tpbegin="20121009" tpend="20121009" />
<TimePeriod name="Week" tpbegin="20121004"/>
<TimePeriod name="Week-1" tpbegin="20120927" tpend="20121004" />
<TimePeriod name="Month" tpbegin="20120913" />
<TimePeriod name="Month-1" tpbegin="20120815" tpend="20120912" />
<TimePeriod name="MTD" tpbegin="20121001" />
<TimePeriod name="PrevCalMonth" tpbegin="20120901" tpend="20120930" />
';
INSERT INTO WH.dbo.tb_myTable
EXEC dbo.GetTimePeriodAggregates #TimePeriodsXmlString=#ThisExecutionsXmlString

You can create this stored procedure
CREATE PROCEDURE InsertData
#minLimit date,
#maxLimit date,
#minTerm nvarchar(50),
#maxTerm nvarchar(50)
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO tb_myTable
SELECT
[TimePeriod] = CASE WHEN x.DateKey >= #maxLimit THEN #maxTerm ELSE #minTerm END,
[Game] = x.Name,
[Score] = SUM(x.[Score]),
[Ticks] = SUM(x.[Ticks]),
[ScorePerTick] = SUM(x.[Score])/SUM(x.[Ticks])
FROM #LimitedBetinfo x
WHERE x.DateKey >= #minLimit
GROUP BY
CASE WHEN x.DateKey >= #maxLimit THEN #maxTerm ELSE #minTerm END,
x.Name
END
GO
And use like this
TRUNCATE TABLE tb_myTable
DECLARE #today date = cast(getdate() as date)
DECLARE #yesterday date = dateadd(day, -1, #today)
EXECUTE dbo.InsertData #yesterday, #today, N'Day-1', N'Day'
DECLARE #thisweek date = DATEADD(ww, DATEDIFF(ww,0,GETDATE()), 0)
DECLARE #lastweek date = DATEADD(ww, -1, #thisweek)
EXECUTE dbo.InsertData #lastweek, #thisweek, N'Week-1', N'Week'
DECLARE #prev28 date = dateadd(day, -28, #today)
DECLARE #prev56 date = dateadd(day, -56, #today)
EXECUTE dbo.InsertData #prev56, #prev28, N'Month-1', N'Month'
DECLARE #thismonth date = DATEADD(mm, DATEDIFF(mm,0,GETDATE()), 0)
DECLARE #lastmonth date = DATEADD(mm, -1, #thismonth)
EXECUTE dbo.InsertData #lastmonth, #thismonth, N'PrevCalMonth', N'MTD'

Use parameters - VALUES As a Table Source and apply them as parameters in CROSS APPLY with derived table
DECLARE #myDate datetime = CAST(GETDATE() AS date);
IF OBJECT_ID('WH.dbo.tb_myTable') IS NOT NULL DROP TABLE WH.dbo.tb_myTable
SELECT TimePeriod, Game, Score, Ticks, ScorePerTicks
INTO WH.dbo.tb_myTable
FROM (VALUES('Day', DATEADD(day, -1, #myDate), #myDate),
('Day-1', DATEADD(day, -2, #myDate), DATEADD(day, -2, #myDate)),
('Week', DATEADD(day, -7, #myDate), #myDate),
('Week-1', DATEADD(day, -14, #myDate), DATEADD(day, -8, #myDate)),
('Month', DATEADD(day, -28, #myDate), #myDate),
('Month-1', DATEADD(day, -56, #myDate), DATEADD(day, -29, #myDate)),
('MTD', DATEADD(DAY, 1 - DAY(#myDate), #myDate), #myDate),
('PrevCalMonth', DATEADD(DAY, 1 - DAY(#myDate), DATEADD(MONTH, -1, #myDate)), DATEADD(DAY, - DAY(#myDate), #myDate)))
RParameters(TimePeriod, BDate, EDate)
CROSS APPLY (SELECT x.Name AS Game,
SUM(x.Score) AS Score,
SUM(x.Ticks) AS Ticks,
SUM(x.Score) / SUM(x.Ticks) AS ScorePerTicks
FROM #LimitedBetinfo x
WHERE DateKey BETWEEN RParameters.BDate AND RParameters.EDate
GROUP BY Name) AS o
Demo on SQLFiddle

A possible improvement on fred's answer. Not in terms of speed, just readability / modifiability by removing the extra CASE. As a suggestion, I also replaced the passing of both strings (e.g. DAY and DAY-1) with a single string and to have the other just be a concat; this would however cause PrevCalMonth to be displayed as MTD-1 instead (though there are some work-arounds for this).
CREATE PROCEDURE InsertData
#minLimit date, #maxLimit date, #string nvarchar(50)
AS
INSERT INTO tb_myTable
SELECT TimePeriod, Name, SUM(Score) Score, SUM(Ticks) Ticks,
SUM(Score)/SUM(Ticks) ScorePerTick
FROM
(
SELECT *, /* or 'Name, Score, Ticks,' */
TimePeriod = CASE WHEN x.DateKey >= #maxLimit THEN #string ELSE #string+'-1' END
FROM #LimitedBetinfo x
WHERE x.DateKey >= #minLimit
) A
GROUP BY TimePeriod, Name
GO
And use like this:
TRUNCATE TABLE tb_myTable
DECLARE #today date = cast(getdate() as date)
DECLARE #yesterday date = dateadd(day, -1, #today)
EXECUTE dbo.InsertData #yesterday, #today, N'Day'
DECLARE #thisweek date = DATEADD(ww, DATEDIFF(ww,0,GETDATE()), 0)
DECLARE #lastweek date = DATEADD(ww, -1, #thisweek)
EXECUTE dbo.InsertData #lastweek, #thisweek, N'Week'
DECLARE #prev28 date = dateadd(day, -28, #today)
DECLARE #prev56 date = dateadd(day, -56, #today)
EXECUTE dbo.InsertData #prev56, #prev28, N'Month'
DECLARE #thismonth date = DATEADD(mm, DATEDIFF(mm,0,GETDATE()), 0)
DECLARE #lastmonth date = DATEADD(mm, -1, #thismonth)
EXECUTE dbo.InsertData #lastmonth, #thismonth, N'MTD'

It seems that this may be the job for CUBE groupings.
Sorry, I will not give you exact solution to your problem, but the MOCKUP form of select should be like:
select * from
(
select *,count(*) amount from
(
select datepart(HOUR, login_time) as hour,
datepart(MINUTE, login_time) as minute,
cmd as name
from sys.sysprocesses
) tmp
group by cube(tmp.hour, tmp.minute, tmp.name)
) tmp2
where tmp2.name is not null and
(
(tmp2.hour is not null and tmp2.minute is null) or
(tmp2.hour is null and tmp2.minute is not null)
)
One minus - that cube generates too much data for your problem here. So it needs to be filtered out. A big plus would be that you will only need just ONE select into temporary table.

Related

Count how many times a time occurs within a date range

I could not think of a good way to phrase this question to search properly if its already been asked.
I'm looking for a way in SQL 2008 R2 to count how many times 6pm occurs between two datetime values.
For example between '2017-04-17 19:00:00' and '2017-04-19 17:00:00' 6pm only occurs once even though the times span 3 different days.
Between '2017-04-17 18:00:00' and '2017-04-19 18:00:00' it occurs 3 times whilst also spanning 3 days.
Heres a really silly made up expression of what I want for illustration.
timecount(hh, 6, min(datefield), max(datefield))
Thank you
A simple query to count:
DECLARE #StartDate datetime = '2017-04-17 18:00:00'
DECLARE #EndDate datetime = '2017-04-19 18:00:00'
SELECT
CASE
WHEN CAST(#StartDate AS time) <= '18:00' AND CAST(#EndDate AS time) >= '18:00'
THEN datediff(day, #StartDate, #EndDate) + 1
WHEN CAST(#StartDate AS time) <= '18:00' AND CAST(#EndDate AS time) < '18:00'
THEN datediff(day, #StartDate, #EndDate)
WHEN CAST(#StartDate AS time) > '18:00' AND CAST(#EndDate AS time) >= '18:00'
THEN datediff(day, #StartDate, #EndDate)
ELSE datediff(day, #StartDate, #EndDate) - 1
END AS TotalCount
This will give you each hour and the number of occurences:
select datepart(hh, DateColumn) as TheHours, count(*) as occurs
from MyTable
where DateColumn between #SomeDate and #SomeOtherDate
group by datepart(hh, DateColumn)
Or just for 6pm:
select count(*)
from MyTable
where datepart(hh, DateColumn) = 18
and DateColumn between #SomeDate and #SomeOtherDate
DECLARE
#Time time = '18:00',
#From datetime = '2017-04-17 18:00:00',
#To datetime = '2017-04-19 18:00:00'
SELECT
CASE
-- Same date
WHEN DATEDIFF(DAY, #From, #To) = 0 THEN
CASE WHEN CAST(CAST(#From AS date) AS datetime) + #Time BETWEEN #From AND #To THEN 1 ELSE 0 END
-- Not same date
WHEN #From <= #To THEN
CASE WHEN #Time >= CAST(#From AS time) THEN 1 ELSE 0 END
+ DATEDIFF(DAY, #From, #To) - 1
+ CASE WHEN #Time <= CAST(#To AS time) THEN 1 ELSE 0 END
-- Invalid range
ELSE 0
END AS CountOfTime
Try below formula I have tried with different scenario and it works, let me know if I miss any scenario and not work as per your requirement.
DECLARE #firstDate Datetime='17-Apr-2017 17:00:00'
DECLARE #secondDate Datetime='17-Apr-2017 18:59:00'
SELECT
CASE WHEN DATEDIFF(day,#firstDate,#secondDate)=0
THEN IIF(CAST(#firstDate AS TIME) <='18:00' AND DATEPART(hh,#secondDate) >=18,1,0)
ELSE
CASE WHEN
(
CAST(#firstDate AS TIME) <='18:00' AND CAST(#secondDate AS TIME) <'18:00'
OR
CAST(#firstDate AS TIME) >'18:00' AND CAST(#secondDate AS TIME) >='18:00'
)
THEN DATEDIFF(day,#firstDate,#secondDate)
WHEN CAST(#firstDate AS TIME) <='18:00' AND CAST(#secondDate AS TIME) >='18:00' THEN DATEDIFF(day,#firstDate,#secondDate)+1
ELSE DATEDIFF(day,#firstDate,#secondDate)-1
END
END AS TotalCount
Try the below script, using CTE
DECLARE #F_DATE AS DATETIME = '2017-04-17 19:00:00'
,#T_DATE AS DATETIME = '2017-04-19 17:00:00'
;WITH CTE
AS (
SELECT (CASE WHEN DATEPART(HH,#F_DATE) <= 18
THEN #F_DATE
ELSE (CASE WHEN DATEDIFF(DAY,#F_DATE,#T_DATE) > 0
THEN DATEADD(DAY,1,#F_DATE) END)
END) AS CDATE
UNION ALL
SELECT DATEADD(DAY,1,CDATE)
FROM CTE
WHERE DATEADD(DAY,1,CDATE) <= #T_DATE
)
SELECT COUNT(CDATE) DATE_COUNT
FROM CTE
OPTION ( MAXRECURSION 0 )
Here's the count of every 6pm between two datetime:
DECLARE #StartDate datetime
DECLARE #EndDate datetime
set #StartDate = '2017-04-17 19:00:00'
set #EndDate = '2017-04-19 17:00:00'
;WITH cte1 (S) AS (
SELECT 1 FROM (VALUES (1), (1), (1), (1), (1), (1), (1), (1), (1), (1)) n (S)
),
cte2 (S) AS (SELECT 1 FROM cte1 AS cte1 CROSS JOIN cte1 AS cte2),
cte3 (S) AS (SELECT 1 FROM cte1 AS cte1 CROSS JOIN cte2 AS cte2)
select count(datepart(hour,result)) as count from
(SELECT TOP (DATEDIFF(hour, #StartDate, #EndDate) + 1)
result = DATEADD(hour, ROW_NUMBER() OVER(ORDER BY S) - 1, #StartDate)
FROM cte3) as res where datepart(hour,result) = 18
Here's the detailed view of 6pm between two datetime:
DECLARE #StartDate datetime
DECLARE #EndDate datetime
set #StartDate = '2017-04-17 19:00:00'
set #EndDate = '2017-04-19 17:00:00'
;WITH cte1 (S) AS (
SELECT 1 FROM (VALUES (1), (1), (1), (1), (1), (1), (1), (1), (1), (1)) n (S)
),
cte2 (S) AS (SELECT 1 FROM cte1 AS cte1 CROSS JOIN cte1 AS cte2),
cte3 (S) AS (SELECT 1 FROM cte1 AS cte1 CROSS JOIN cte2 AS cte2)
select result,datepart(hour,result) from
(SELECT TOP (DATEDIFF(hour, #StartDate, #EndDate) + 1)
result = DATEADD(hour, ROW_NUMBER() OVER(ORDER BY S) - 1, #StartDate)
FROM cte3) as res where datepart(hour,result) = 18
Here gives the count between any date ranges
declare #time datetime='06:00:00'
declare #startDate datetime='04/20/2017 05:00:00'
declare #enddate datetime='04/21/2017 05:00:00'
SELECT
case
WHEN datediff(ss,#time, convert(time(0),#startDate)) <=0 and datediff(ss,#time, convert(time(0),#enddate)) >=0
THEN datediff(dd,#startDate,#enddate) +1
WHEN (datediff(ss,#time, convert(time(0),#startDate)) <=0 and
datediff(ss,#time, convert(time(0),#enddate)) <=0
OR
datediff(ss,#time, convert(time(0),#startDate))> 0 and
datediff(ss,#time, convert(time(0),#enddate)) >0
OR
datediff(ss,#time, convert(time(0),#startDate))> 0 and datediff(ss,#time, convert(time(0),#enddate)) <=0
)
then datediff(dd,#startDate,#enddate)
ELSE
datediff(dd,#startDate,#enddate)-1
END

How to avoid repetitive CASE statements in SQL WHILE loop doing INSERT

Is there a way to simplify this script so that the CASE statements are not duplicated? It can look acceptable in this shortened example but in reality the CASE statement is much longer as I have cases for "every 2 weeks, "every 4 weeks", "monthly", etc. I am using SQL Server and a WHILE statement for performance reason. Would a CTE or MERGE help?
DECLARE #theStartDate DATE
DECLARE #Interval INT
DECLARE #eventCharges TABLE
(
[EventDate] [datetime],
Person_Id int
)
SET #today = GETDATE()
SET #Interval = 0
-- delete event charges from previous user
DELETE FROM #eventCharges
-- Insert the calculated transactions
WHILE #Interval < 100
BEGIN
SET #Interval = #Interval + 1
INSERT INTO #eventCharges
SELECT
CASE
WHEN pcc.Recurrence = 'Daily'
THEN DATEADD(DAY, #Interval, #theStartDate)
WHEN pcc.Recurrence = 'Weekly'
THEN DATEADD(WEEK, #Interval, #theStartDate)
END AS EventDate
,pcc.Person_Id
FROM #personChargeCurrent pcc
WHERE CASE
WHEN pcc.Recurrence = 'Daily'
THEN DATEADD(DAY, #Interval, #theStartDate)
WHEN pcc.Recurrence = 'Weekly'
THEN DATEADD(WEEK, #Interval, #theStartDate)
END <= #today
AND NOT EXISTS(SELECT 1 FROM dbo.PersonChargeTransaction pct
WHERE pct.Person_Id = pcc.Person_Id
AND pct.PersonCharge_Id = pcc.Id
AND pct.TransactionDate =
CASE
WHEN pcc.Recurrence = 'Daily'
THEN DATEADD(DAY, #Interval, #theStartDate)
WHEN pcc.Recurrence = 'Weekly'
THEN DATEADD(WEEK, #Interval, #theStartDate)
END)
ORDER BY StartDate
END
You can wrap this in a function:
Create Function dbo.IntervalEnd(
#recurrence varchar(10),
#interval int,
#startDate date -- or whatever data type you're using for dates
) returns date as
begin
return case
when #recurrence = 'Daily' then dateadd(day, #interval, #startDate)
when #recurrence = 'Weekly' then dateadd(week, #interval, #startDate)
end
end
Then
Insert Into #eventCharges
Select
dbo.IntervalEnd(pcc.Recurrence, #Interval, #theStartDate) as EventDate,
pcc.Person_Id
From
#personChargeCurrent pcc
Where
dbo.IntervalEnd(pcc.Recurrence, #Interval, #theStartDate) <= #today And
Not Exists (
Select
1
From
dbo.PersonChargeTransaction pct
Where
pct.Person_Id = pcc.Person_Id And
pct.PersonCharge_Id = pcc.Id And
pct.TransactionDate
= dbo.IntervalEnd(pcc.Recurrence, #Interval, #theStartDate)
)
There are overheads for using a function. You'll have to decide if the slightly reduced performance is worth the tradeoff for increased readibility.
Yes, a CTE should help. Try changing your INSERT statement to:
WITH cte as
(SELECT CASE
WHEN Recurrence = 'Daily'
THEN DATEADD(DAY, #Interval, #theStartDate)
WHEN Recurrence = 'Weekly'
THEN DATEADD(WEEK, #Interval, #theStartDate)
END AS EventDate,
p.*
FROM #personChargeCurrent p)
INSERT INTO #eventCharges
SELECT cte.EventDate, cte.Person_Id
FROM cte
WHERE cte.EventDate <= #today AND
NOT EXISTS
(SELECT 1
FROM dbo.PersonChargeTransaction pct
WHERE pct.Person_Id = cte.Person_Id AND
pct.PersonCharge_Id = cte.Id AND
pct.TransactionDate = cte.EventDate)
ORDER BY StartDate
You could create a temporary table in which to insert the calculated fields and then use that table with a join to insert the data / apply the conditions

T-SQL get number of working days between 2 dates

I want to calculate the number of working days between 2 given dates. For example if I want to calculate the working days between 2013-01-10 and 2013-01-15, the result must be 3 working days (I don't take into consideration the last day in that interval and I subtract the Saturdays and Sundays). I have the following code that works for most of the cases, except the one in my example.
SELECT (DATEDIFF(day, '2013-01-10', '2013-01-15'))
- (CASE WHEN DATENAME(weekday, '2013-01-10') = 'Sunday' THEN 1 ELSE 0 END)
- (CASE WHEN DATENAME(weekday, DATEADD(day, -1, '2013-01-15')) = 'Saturday' THEN 1 ELSE 0 END)
How can I accomplish this? Do I have to go through all the days and check them? Or is there an easy way to do this.
Please, please, please use a calendar table. SQL Server doesn't know anything about national holidays, company events, natural disasters, etc. A calendar table is fairly easy to build, takes an extremely small amount of space, and will be in memory if it is referenced enough.
Here is an example that creates a calendar table with 30 years of dates (2000 -> 2029) but requires only 200 KB on disk (136 KB if you use page compression). That is almost guaranteed to be less than the memory grant required to process some CTE or other set at runtime.
CREATE TABLE dbo.Calendar
(
dt DATE PRIMARY KEY, -- use SMALLDATETIME if < SQL Server 2008
IsWorkDay BIT
);
DECLARE #s DATE, #e DATE;
SELECT #s = '2000-01-01' , #e = '2029-12-31';
INSERT dbo.Calendar(dt, IsWorkDay)
SELECT DATEADD(DAY, n-1, '2000-01-01'), 1
FROM
(
SELECT TOP (DATEDIFF(DAY, #s, #e)+1) ROW_NUMBER()
OVER (ORDER BY s1.[object_id])
FROM sys.all_objects AS s1
CROSS JOIN sys.all_objects AS s2
) AS x(n);
SET DATEFIRST 1;
-- weekends
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE DATEPART(WEEKDAY, dt) IN (6,7);
-- Christmas
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE MONTH(dt) = 12
AND DAY(dt) = 25
AND IsWorkDay = 1;
-- continue with other holidays, known company events, etc.
Now the query you're after is quite simple to write:
SELECT COUNT(*) FROM dbo.Calendar
WHERE dt >= '20130110'
AND dt < '20130115'
AND IsWorkDay = 1;
More info on calendar tables:
http://web.archive.org/web/20070611150639/http://sqlserver2000.databases.aspfaq.com/why-should-i-consider-using-an-auxiliary-calendar-table.html
More info on generating sets without loops:
http://www.sqlperformance.com/tag/date-ranges
Also beware of little things like relying on the English output of DATENAME. I've seen several applications break because some users had a different language setting, and if you're relying on WEEKDAY be sure you set your DATEFIRST setting appropriately...
For stuff like this i tend to maintain a calendar table that also includes bank holidays etc.
The script i use for this is as follows (Note that i didnt write it # i forget where i found it)
SET DATEFIRST 1
SET NOCOUNT ON
GO
--Create ISO week Function (thanks BOL)
CREATE FUNCTION ISOweek ( #DATE DATETIME )
RETURNS INT
AS
BEGIN
DECLARE #ISOweek INT
SET #ISOweek = DATEPART(wk, #DATE) + 1 - DATEPART(wk, CAST(DATEPART(yy, #DATE) AS CHAR(4)) + '0104')
--Special cases: Jan 1-3 may belong to the previous year
IF ( #ISOweek = 0 )
SET #ISOweek = dbo.ISOweek(CAST(DATEPART(yy, #DATE) - 1 AS CHAR(4)) + '12' + CAST(24 + DATEPART(DAY, #DATE) AS CHAR(2))) + 1
--Special case: Dec 29-31 may belong to the next year
IF ( ( DATEPART(mm, #DATE) = 12 )
AND ( ( DATEPART(dd, #DATE) - DATEPART(dw, #DATE) ) >= 28 )
)
SET #ISOweek = 1
RETURN(#ISOweek)
END
GO
--END ISOweek
--CREATE Easter algorithm function
--Thanks to Rockmoose (http://www.sqlteam.com/forums/topic.asp?TOPIC_ID=45689)
CREATE FUNCTION fnDLA_GetEasterdate ( #year INT )
RETURNS CHAR(8)
AS
BEGIN
-- Easter date algorithm of Delambre
DECLARE #A INT ,
#B INT ,
#C INT ,
#D INT ,
#E INT ,
#F INT ,
#G INT ,
#H INT ,
#I INT ,
#K INT ,
#L INT ,
#M INT ,
#O INT ,
#R INT
SET #A = #YEAR % 19
SET #B = #YEAR / 100
SET #C = #YEAR % 100
SET #D = #B / 4
SET #E = #B % 4
SET #F = ( #B + 8 ) / 25
SET #G = ( #B - #F + 1 ) / 3
SET #H = ( 19 * #A + #B - #D - #G + 15 ) % 30
SET #I = #C / 4
SET #K = #C % 4
SET #L = ( 32 + 2 * #E + 2 * #I - #H - #K ) % 7
SET #M = ( #A + 11 * #H + 22 * #L ) / 451
SET #O = 22 + #H + #L - 7 * #M
IF #O > 31
BEGIN
SET #R = #O - 31 + 400 + #YEAR * 10000
END
ELSE
BEGIN
SET #R = #O + 300 + #YEAR * 10000
END
RETURN #R
END
GO
--END fnDLA_GetEasterdate
--Create the table
CREATE TABLE MyDateTable
(
FullDate DATETIME NOT NULL
CONSTRAINT PK_FullDate PRIMARY KEY CLUSTERED ,
Period INT ,
ISOWeek INT ,
WorkingDay VARCHAR(1) CONSTRAINT DF_MyDateTable_WorkDay DEFAULT 'Y'
)
GO
--End table create
--Populate table with required dates
DECLARE #DateFrom DATETIME ,
#DateTo DATETIME ,
#Period INT
SET #DateFrom = CONVERT(DATETIME, '20000101')
--yyyymmdd (1st Jan 2000) amend as required
SET #DateTo = CONVERT(DATETIME, '20991231')
--yyyymmdd (31st Dec 2099) amend as required
WHILE #DateFrom <= #DateTo
BEGIN
SET #Period = CONVERT(INT, LEFT(CONVERT(VARCHAR(10), #DateFrom, 112), 6))
INSERT MyDateTable
( FullDate ,
Period ,
ISOWeek
)
SELECT #DateFrom ,
#Period ,
dbo.ISOweek(#DateFrom)
SET #DateFrom = DATEADD(dd, +1, #DateFrom)
END
GO
--End population
/* Start of WorkingDays UPDATE */
UPDATE MyDateTable
SET WorkingDay = 'B' --B = Bank Holiday
--------------------------------EASTER---------------------------------------------
WHERE FullDate = DATEADD(dd, -2, CONVERT(DATETIME, dbo.fnDLA_GetEasterdate(DATEPART(yy, FullDate)))) --Good Friday
OR FullDate = DATEADD(dd, +1, CONVERT(DATETIME, dbo.fnDLA_GetEasterdate(DATEPART(yy, FullDate))))
--Easter Monday
GO
UPDATE MyDateTable
SET WorkingDay = 'B'
--------------------------------NEW YEAR-------------------------------------------
WHERE FullDate IN ( SELECT MIN(FullDate)
FROM MyDateTable
WHERE DATEPART(mm, FullDate) = 1
AND DATEPART(dw, FullDate) NOT IN ( 6, 7 )
GROUP BY DATEPART(yy, FullDate) )
---------------------MAY BANK HOLIDAYS(Always Monday)------------------------------
OR FullDate IN ( SELECT MIN(FullDate)
FROM MyDateTable
WHERE DATEPART(mm, FullDate) = 5
AND DATEPART(dw, FullDate) = 1
GROUP BY DATEPART(yy, FullDate) )
OR FullDate IN ( SELECT MAX(FullDate)
FROM MyDateTable
WHERE DATEPART(mm, FullDate) = 5
AND DATEPART(dw, FullDate) = 1
GROUP BY DATEPART(yy, FullDate) )
--------------------AUGUST BANK HOLIDAY(Always Monday)------------------------------
OR FullDate IN ( SELECT MAX(FullDate)
FROM MyDateTable
WHERE DATEPART(mm, FullDate) = 8
AND DATEPART(dw, FullDate) = 1
GROUP BY DATEPART(yy, FullDate) )
--------------------XMAS(Move to next working day if on Sat/Sun)--------------------
OR FullDate IN ( SELECT CASE WHEN DATEPART(dw, FullDate) IN ( 6, 7 ) THEN DATEADD(dd, +2, FullDate)
ELSE FullDate
END
FROM MyDateTable
WHERE DATEPART(mm, FullDate) = 12
AND DATEPART(dd, FullDate) IN ( 25, 26 ) )
GO
---------------------------------------WEEKENDS--------------------------------------
UPDATE MyDateTable
SET WorkingDay = 'N'
WHERE DATEPART(dw, FullDate) IN ( 6, 7 )
GO
/* End of WorkingDays UPDATE */
--SELECT * FROM MyDateTable ORDER BY 1
DROP FUNCTION fnDLA_GetEasterdate
DROP FUNCTION ISOweek
--DROP TABLE MyDateTable
SET NOCOUNT OFF
Once you have created the table, finding the number of working days is easy peasy:
SELECT COUNT(FullDate) AS WorkingDays
FROM dbo.tbl_WorkingDays
WHERE WorkingDay = 'Y'
AND FullDate >= CONVERT(DATETIME, '10/01/2013', 103)
AND FullDate < CONVERT(DATETIME, '15/01/2013', 103)
Note that this script includes UK bank holidays, i'm not sure what region you're in.
Here's a simple function that counts working days not including Saturday and Sunday (when counting holidays isn't necessary):
CREATE FUNCTION dbo.udf_GetBusinessDays (
#START_DATE DATE,
#END_DATE DATE
)
RETURNS INT
WITH EXECUTE AS CALLER
AS
BEGIN
DECLARE #NUMBER_OF_DAYS INT = 0;
DECLARE #DAY_COUNTER INT = 0;
DECLARE #BUSINESS_DAYS INT = 0;
DECLARE #CURRENT_DATE DATE;
DECLARE #DAYNAME NVARCHAR(9)
SET #NUMBER_OF_DAYS = DATEDIFF(DAY, #START_DATE, #END_DATE);
WHILE #DAY_COUNTER <= #NUMBER_OF_DAYS
BEGIN
SET #CURRENT_DATE = DATEADD(DAY, #DAY_COUNTER, #START_DATE)
SET #DAYNAME = DATENAME(WEEKDAY, #CURRENT_DATE)
SET #DAY_COUNTER += 1
IF #DAYNAME = N'Saturday' OR #DAYNAME = N'Sunday'
BEGIN
CONTINUE
END
ELSE
BEGIN
SET #BUSINESS_DAYS += 1
END
END
RETURN #BUSINESS_DAYS
END
GO
This is the method I normally use (When not using a calendar table):
DECLARE #T TABLE (Date1 DATE, Date2 DATE);
INSERT #T VALUES ('20130110', '20130115'), ('20120101', '20130101'), ('20120611', '20120701');
SELECT Date1, Date2, WorkingDays
FROM #T t
CROSS APPLY
( SELECT [WorkingDays] = COUNT(*)
FROM Master..spt_values s
WHERE s.Number BETWEEN 1 AND DATEDIFF(DAY, t.date1, t.Date2)
AND s.[Type] = 'P'
AND DATENAME(WEEKDAY, DATEADD(DAY, s.number, t.Date1)) NOT IN ('Saturday', 'Sunday')
) wd
If like I do you have a table with holidays in you can add this in too:
SELECT Date1, Date2, WorkingDays
FROM #T t
CROSS APPLY
( SELECT [WorkingDays] = COUNT(*)
FROM Master..spt_values s
WHERE s.Number BETWEEN 1 AND DATEDIFF(DAY, t.date1, t.Date2)
AND s.[Type] = 'P'
AND DATENAME(WEEKDAY, DATEADD(DAY, s.number, t.Date1)) NOT IN ('Saturday', 'Sunday')
AND NOT EXISTS
( SELECT 1
FROM HolidayTable ht
WHERE ht.Date = DATEADD(DAY, s.number, t.Date1)
)
) wd
The above will only work if your dates are within 2047 days of each other, if you are likely to be calculating larger date ranges you can use this:
SELECT Date1, Date2, WorkingDays
FROM #T t
CROSS APPLY
( SELECT [WorkingDays] = COUNT(*)
FROM ( SELECT [Number] = ROW_NUMBER() OVER(ORDER BY s.number)
FROM Master..spt_values s
CROSS JOIN Master..spt_values s2
) s
WHERE s.Number BETWEEN 1 AND DATEDIFF(DAY, t.date1, t.Date2)
AND DATENAME(WEEKDAY, DATEADD(DAY, s.number, t.Date1)) NOT IN ('Saturday', 'Sunday')
) wd
I did my code in SQL SERVER 2008 (MS SQL) . It works fine for me. I hope it will help you.
DECLARE #COUNTS int,
#STARTDATE date,
#ENDDATE date
SET #STARTDATE ='01/21/2013' /*Start date in mm/dd/yyy */
SET #ENDDATE ='01/26/2013' /*End date in mm/dd/yyy */
SET #COUNTS=0
WHILE (#STARTDATE<=#ENDDATE)
BEGIN
/*Check for holidays*/
IF ( DATENAME(weekday,#STARTDATE)<>'Saturday' and DATENAME(weekday,#STARTDATE)<>'Sunday')
BEGIN
SET #COUNTS=#COUNTS+1
END
SET #STARTDATE=DATEADD(day,1,#STARTDATE)
END
/* Display the no of working days */
SELECT #COUNTS
By Combining #Aaron Bertrand's answer and the Easter Calculation from #HeavenCore's and adding some code of my own, this code creates a calendar from 2000 to 2049 that includes UK (England) Bank Holidays. Usage and notes as per Aaron's answer:
DECLARE #s DATE, #e DATE;
SELECT #s = '2000-01-01' , #e = '2049-12-31';
-- Insert statements for procedure here
CREATE TABLE dbo.Calendar
(
dt DATE PRIMARY KEY, -- use SMALLDATETIME if < SQL Server 2008
IsWorkDay BIT
);
INSERT dbo.Calendar(dt, IsWorkDay)
SELECT DATEADD(DAY, n-1, '2000-01-01'), 1
FROM
(
SELECT TOP (DATEDIFF(DAY, #s, #e)+1) ROW_NUMBER()
OVER (ORDER BY s1.[object_id])
FROM sys.all_objects AS s1
CROSS JOIN sys.all_objects AS s2
) AS x(n);
SET DATEFIRST 1;
-- weekends
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE DATEPART(WEEKDAY, dt) IN (6,7);
-- Christmas
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE IsWorkDay = 1 and MONTH(dt) = 12 and
(DAY(dt) in (25,26) or
(DAY(dt) in (27, 28) and DATEPART(WEEKDAY, dt) IN (1,2)) );
-- New Year
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE IsWorkDay = 1 and MONTH(dt) = 1 AND
( DAY(dt) = 1 or (DAY(dt) IN (2,3) AND DATEPART(WEEKDAY, dt)=1 ));
-- Easter
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE dt = DATEADD(dd, -2, CONVERT(DATETIME, dbo.fnDLA_GetEasterdate(DATEPART(yy, dt)))) --Good Friday
OR dt = DATEADD(dd, +1, CONVERT(DATETIME, dbo.fnDLA_GetEasterdate(DATEPART(yy, dt)))) --Easter Monday
-- May Day (first Monday in May)
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE MONTH(dt) = 5 AND DATEPART(WEEKDAY, dt)=1 and DAY(DT)<8;
-- Spring Bank Holiday (last Monday in May apart from 2022 when moved to include Platinum Jubilee bank holiday)
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE
(YEAR(dt)=2022 and MONTH(dt) = 6 AND DAY(dt) IN (2,3)) OR
(YEAR(dt)<>2022 and MONTH(dt) = 5 AND DATEPART(WEEKDAY, dt)=1 and DAY(DT)>24);
-- Summer Bank Holiday (last Monday in August)
UPDATE dbo.Calendar SET IsWorkDay = 0
WHERE MONTH(dt) = 8 AND DATEPART(WEEKDAY, dt)=1 and DAY(DT)>24;

How to split date ranges based on months in SQL Server 2005

Ex: date range is 01-01-2011 to 01-01-2012, I want the output like this :
01-01-2011 31-01-2011
01-02-2011 28-02-2011
How can I do this ? I'm using SQL Server 2005..
Thanks
Using CTE:
DECLARE #Begin DATETIME
DECLARE #End DATETIME
SELECT #Begin = '20110101', #End = '20120101'
;WITH ranges(DateFrom, DateTo) AS
(
SELECT #Begin, DATEADD(DAY, -1, DATEADD(MONTH, 1, #begin))
UNION ALL
SELECT DATEADD(month, 1, DateFrom), DATEADD(DAY, -1, DATEADD(MONTH, 2, DateFrom))
FROM ranges
WHERE DateFrom < #End
)
SELECT * FROM ranges
OPTION(MAXRECURSION 0)
And not using CTE:
DECLARE #Begin DATETIME
DECLARE #End DATETIME
SELECT #Begin = '20110101', #End = '20120101'
SELECT DATEADD(MONTH, n.Number, #Begin) DateFrom, DATEADD(day, -1, DATEADD(MONTH, n.Number+1, #Begin)) DateTo
FROM master.dbo.spt_values n
WHERE
n.Number < DATEDIFF(MONTH, #begin, #end)
AND n.Type = 'P'
If you need to include January 2012 too, use this
DECLARE #Begin DATETIME
DECLARE #End DATETIME
SELECT #Begin = '20110101', #End = '20120101'
SELECT DATEADD(MONTH, n.Number, #Begin) DateFrom, DATEADD(day, -1, DATEADD(MONTH, n.Number+1, #Begin)) DateTo
FROM master.dbo.spt_values n
WHERE
n.Number <= DATEDIFF(MONTH, #begin, #end)
AND n.Type = 'P'
And CTE:
DECLARE #Begin DATETIME
DECLARE #End DATETIME
SELECT #Begin = '20110101', #End = '20120101'
;WITH ranges(DateFrom, DateTo) AS
(
SELECT #Begin, DATEADD(DAY, -1, DATEADD(MONTH, 1, #begin))
UNION ALL
SELECT DATEADD(month, 1, DateFrom), DATEADD(DAY, -1, DATEADD(MONTH, 2, DateFrom))
FROM ranges
WHERE DATEADD(month, 1, DateFrom) < #End
)
SELECT * FROM ranges
OPTION(MAXRECURSION 0)
>On Providing the Start and End Date range, it can be split into months. This might help.
DECLARE #StartDate datetime
DECLARE #EndDate datetime
DECLARE #tmpStart datetime
DECLARE #tmpEnd datetime
Select #StartDate = '2012-01-01'
Select #EndDate = '2009-01-01'
Select #StartDate = DATEADD(MONTH,1,Convert(DATETIME,Convert(VARCHAR(25), Datepart (YEAR,#StartDate)) + '-' + Convert(VARCHAR(25), Datepart(MONTH, #StartDate)) + '-' + Convert(VARCHAR(25), 1)))
Select #EndDate = DATEADD(MONTH,1,Convert(DATETIME,Convert(VARCHAR(25), Datepart(YEAR, #EndDate)) + '-' + Convert(VARCHAR(25), Datepart(MONTH, #EndDate)) + '-' + Convert(VARCHAR(25), 1)))
Select #EndDate = DATEADD(DD,-1, DATEADD(MM,1,#EndDate))
Select #tmpEnd = #EndDate
Select #tmpStart = Convert(DATETIME,Convert(VARCHAR(25), Datepart(YEAR, #tmpEnd)) + '-' +
Convert(VARCHAR(25), Datepart(MONTH, #tmpEnd)) + '-' + Convert(VARCHAR(25), 1))
Select #StartDate 'Start' , #EndDate 'End'
While #tmpStart <= #StartDate
BEGIN
Select #tmpStart 'tmpStart' , #tmpEnd 'tmpEnd'
Select #tmpStart = DATEADD(MM,1,#tmpStart)
Select #tmpEnd = DATEADD(DD,-1,DATEADD (MM, 1 , #tmpStart))
END

Calculating number of full months between two dates in SQL

I need to calculate the number of FULL month in SQL, i.e.
2009-04-16 to 2009-05-15 => 0 full month
2009-04-16 to 2009-05-16 => 1 full month
2009-04-16 to 2009-06-16 => 2 full months
I tried to use DATEDIFF, i.e.
SELECT DATEDIFF(MONTH, '2009-04-16', '2009-05-15')
but instead of giving me full months between the two date, it gives me the difference of the month part, i.e.
1
anyone know how to calculate the number of full months in SQL Server?
The original post had some bugs... so I re-wrote and packaged it as a UDF.
CREATE FUNCTION FullMonthsSeparation
(
#DateA DATETIME,
#DateB DATETIME
)
RETURNS INT
AS
BEGIN
DECLARE #Result INT
DECLARE #DateX DATETIME
DECLARE #DateY DATETIME
IF(#DateA < #DateB)
BEGIN
SET #DateX = #DateA
SET #DateY = #DateB
END
ELSE
BEGIN
SET #DateX = #DateB
SET #DateY = #DateA
END
SET #Result = (
SELECT
CASE
WHEN DATEPART(DAY, #DateX) > DATEPART(DAY, #DateY)
THEN DATEDIFF(MONTH, #DateX, #DateY) - 1
ELSE DATEDIFF(MONTH, #DateX, #DateY)
END
)
RETURN #Result
END
GO
SELECT dbo.FullMonthsSeparation('2009-04-16', '2009-05-15') as MonthSep -- =0
SELECT dbo.FullMonthsSeparation('2009-04-16', '2009-05-16') as MonthSep -- =1
SELECT dbo.FullMonthsSeparation('2009-04-16', '2009-06-16') as MonthSep -- =2
select case when DATEPART(D,End_dATE) >=DATEPART(D,sTAR_dATE)
THEN ( case when DATEPART(M,End_dATE) = DATEPART(M,sTAR_dATE) AND DATEPART(YYYY,End_dATE) = DATEPART(YYYY,sTAR_dATE)
THEN 0 ELSE DATEDIFF(M,sTAR_dATE,End_dATE)END )
ELSE DATEDIFF(M,sTAR_dATE,End_dATE)-1 END
What's your definition of a month? Technically a month can be 28,29,30 or 31 days depending on the month and leap years.
It seems you're considering a month to be 30 days since in your example you disregarded that May has 31 days, so why not just do the following?
SELECT DATEDIFF(DAY, '2009-04-16', '2009-05-15')/30
, DATEDIFF(DAY, '2009-04-16', '2009-05-16')/30
, DATEDIFF(DAY, '2009-04-16', '2009-06-16')/30
The dateadd function can be used to offset to the beginning of the month. If the endDate has a day part less than startDate, it will get pushed to the previous month, thus datediff will give the correct number of months.
DATEDIFF(MONTH, DATEADD(DAY,-DAY(startDate)+1,startDate),DATEADD(DAY,-DAY(startDate)+1,endDate))
This is for ORACLE only and not for SQL-Server:
months_between(to_date ('2009/05/15', 'yyyy/mm/dd'),
to_date ('2009/04/16', 'yyyy/mm/dd'))
And for full month:
round(months_between(to_date ('2009/05/15', 'yyyy/mm/dd'),
to_date ('2009/04/16', 'yyyy/mm/dd')))
Can be used in Oracle 8i and above.
I know this is an old question, but as long as the dates are >= 01-Jan-1753 I use:
DATEDIFF(MONTH, DATEADD(DAY,-DAY(#Start)+1,#Start),DATEADD(DAY,-DAY(#Start)+1,#End))
DATEDIFF() is designed to return the number boundaries crossed between the two dates for the span specified. To get it to do what you want, you need to make an additional adjustment to account for when the dates cross a boundary but don't complete the full span.
WITH
-- Count how many months must be added to #StartDate to exceed #DueDate
MONTHS_SINCE(n, [Month_hence], [IsFull], [RemainingDays] ) AS (
SELECT
1 as n,
DATEADD(Day, -1, DATEADD(Month, 1, #StartDate)) AS Month_hence
,CASE WHEN (DATEADD(Day, -1, DATEADD(Month, 1, #StartDate)) <= #LastDueDate)
THEN 1
ELSE 0
END AS [IsFull]
,DATEDIFF(day, #StartDate, #LastDueDate) as [RemainingDays]
UNION ALL
SELECT
n+1,
--DateAdd(Month, 1, Month_hence) as Month_hence -- No, causes propagation of short month discounted days
DATEADD(Day, -1, DATEADD(Month, n+1, #StartDate)) as Month_hence
,CASE WHEN (DATEADD(Day, -1, DATEADD(Month, n+1, #StartDate)) <= #LastDueDate)
THEN 1
ELSE 0
END AS [IsFull]
,DATEDIFF(day, DATEADD(Day, -1, DATEADD(Month, n, #StartDate)), #LastDueDate)
FROM MONTHS_SINCE
WHERE Month_hence<( #LastDueDate --WHERE Period= 1
)
), --SELECT * FROM MONTHS_SINCE
MONTH_TALLY (full_months_over_all_terms, months_over_all_terms, days_in_incomplete_month ) AS (
SELECT
COALESCE((SELECT MAX(n) FROM MONTHS_SINCE WHERE isFull = 1),1) as full_months_over_all_terms,
(SELECT MAX(n) FROM MONTHS_SINCE ) as months_over_all_terms,
COALESCE((SELECT [RemainingDays] FROM MONTHS_SINCE WHERE isFull = 0),0) as days_in_incomplete_month
) SELECT * FROM MONTH_TALLY;
Is not necesary to create the function only the #result part. For example:
Select Name,
(SELECT CASE WHEN
DATEPART(DAY, '2016-08-28') > DATEPART(DAY, '2016-09-29')
THEN DATEDIFF(MONTH, '2016-08-28', '2016-09-29') - 1
ELSE DATEDIFF(MONTH, '2016-08-28', '2016-09-29') END) as NumberOfMonths
FROM
tableExample;
This answer follows T-SQL format. I conceptualize this problem as one of a linear-time distance between two date points in datetime format, call them Time1 and Time2; Time1 should be aligned to the 'older in time' value you are dealing with (say a Birth date or a widget Creation date or a journey Start date) and Time2 should be aligned with the 'newer in time' value (say a snapshot date or a widget completion date or a journey checkpoint-reached date).
DECLARE #Time1 DATETIME
SET #Time1 = '12/14/2015'
DECLARE #Time2 DATETIME
SET #Time2 = '12/15/2016'
The solution leverages simple measurement, conversion and calculations of the serial intersections of multiple cycles of different lengths; here: Century,Decade,Year,Month,Day (Thanks Mayan Calendar for the concept!). A quick note of thanks: I thank other contributors to Stack Overflow for showing me some of the component functions in this process that I've stitched together. I've positively rated these in my time on this forum.
First, construct a horizon that is the linear set of the intersections of the Century,Decade,Year,Month cycles, incremental by month. Use the cross join Cartesian function for this. (Think of this as creating the cloth from which we will cut a length between two 'yyyy-mm' points in order to measure distance):
SELECT
Linear_YearMonths = (centuries.century + decades.decade + years.[year] + months.[Month]),
1 AS value
INTO #linear_months
FROM
(SELECT '18' [century] UNION ALL
SELECT '19' UNION ALL
SELECT '20') centuries
CROSS JOIN
(SELECT '0' [decade] UNION ALL
SELECT '1' UNION ALL
SELECT '2' UNION ALL
SELECT '3' UNION ALL
SELECT '4' UNION ALL
SELECT '5' UNION ALL
SELECT '6' UNION ALL
SELECT '7' UNION ALL
SELECT '8' UNION ALL
SELECT '9') decades
CROSS JOIN
(SELECT '1' [year] UNION ALL
SELECT '2' UNION ALL
SELECT '3' UNION ALL
SELECT '4' UNION ALL
SELECT '5' UNION ALL
SELECT '6' UNION ALL
SELECT '7' UNION ALL
SELECT '8' UNION ALL
SELECT '9' UNION ALL
SELECT '0') years
CROSS JOIN
(SELECT '-01' [month] UNION ALL
SELECT '-02' UNION ALL
SELECT '-03' UNION ALL
SELECT '-04' UNION ALL
SELECT '-05' UNION ALL
SELECT '-06' UNION ALL
SELECT '-07' UNION ALL
SELECT '-08' UNION ALL
SELECT '-09' UNION ALL
SELECT '-10' UNION ALL
SELECT '-11' UNION ALL
SELECT '-12') [months]
ORDER BY 1
Then, convert your Time1 and Time2 date points into the 'yyyy-mm' format (Think of these as the coordinate cut points on the whole cloth). Retain the original datetime versions of the points as well:
SELECT
Time1 = #Time1,
[YYYY-MM of Time1] = CASE
WHEN LEFT(MONTH(#Time1),1) <> '1' OR MONTH(#Time1) = '1'
THEN (CAST(YEAR(#Time1) AS VARCHAR) + '-' + '0' + CAST(MONTH(#Time1) AS VARCHAR))
ELSE (CAST(YEAR(#Time1) AS VARCHAR) + '-' + CAST(MONTH(#Time1) AS VARCHAR))
END,
Time2 = #Time2,
[YYYY-MM of Time2] = CASE
WHEN LEFT(MONTH(#Time2),1) <> '1' OR MONTH(#Time2) = '1'
THEN (CAST(YEAR(#Time2) AS VARCHAR) + '-' + '0' + CAST(MONTH(#Time2) AS VARCHAR))
ELSE (CAST(YEAR(#Time2) AS VARCHAR) + '-' + CAST(MONTH(#Time2) AS VARCHAR))
END
INTO #datepoints
Then, Select the ordinal distance of 'yyyy-mm' units, less one to convert to cardinal distance (i.e. cut a piece of cloth from the whole cloth at the identified cut points and get its raw measurement):
SELECT
d.*,
Months_Between = (SELECT (SUM(l.value) - 1) FROM #linear_months l
WHERE l.[Linear_YearMonths] BETWEEN d.[YYYY-MM of Time1] AND d.[YYYY-MM of Time2])
FROM #datepoints d
Raw Output:
I call this a 'raw distance' because the month component of the 'yyyy-mm' cardinal distance may be one too many; the day cycle components within the month need to be compared to see if this last month value should count. In this example specifically, the raw output distance is '12'. But this wrong as 12/14 is before 12/15, so therefore only 11 full months have lapsed--its just one day shy of lapsing through the 12th month. We therefore have to bring in the intra-month day cycle to get to a final answer. Insert a 'month,day' position comparison between the to determine if the latest date point month counts nominally, or not:
SELECT
d.*,
Months_Between = (SELECT (SUM(l.value) - 1) FROM AZ_VBP.[MY].[edg_Linear_YearMonths] l
WHERE l.[Linear_YearMonths] BETWEEN d.[YYYY-MM of Time1] AND d.[YYYY-MM of Time2])
+ (CASE WHEN DAY(Time1) < DAY(Time2)
THEN -1
ELSE 0
END)
FROM #datepoints d
Final Output:
The correct answer of '11' is now our output. And so, I hope this helps. Thanks!
select CAST(DATEDIFF(MONTH, StartDate, EndDate) AS float) -
(DATEPART(dd,StartDate) - 1.0) / DATEDIFF(DAY, StartDate, DATEADD(MONTH, 1, StartDate)) +
(DATEPART(dd,EndDate)*1.0 ) / DATEDIFF(DAY, EndDate, DATEADD(MONTH, 1, EndDate))
I realize this is an old post, but I created this interesting solution that I think is easy to implement using a CASE statement.
Estimate the difference using DATEDIFF, and then test the months before and after using DATEADD to find the best date. This assumes Jan 31 to Feb 28 is 1 month (because it is).
DECLARE #First date = '2015-08-31'
DECLARE #Last date = '2016-02-28'
SELECT
#First as [First],
#Last as [Last],
DateDiff(Month, #First, #Last) as [DateDiff Thinks],
CASE
WHEN DATEADD(Month, DATEDIFF(Month, #First, #Last) +1, #First) <= #Last Then DATEDIFF(Month, #First, #Last) +1
WHEN DATEADD(Month, DATEDIFF(Month, #First, #Last) , #First) <= #Last Then DATEDIFF(Month, #First, #Last)
WHEN DATEADD(Month, DATEDIFF(Month, #First, #Last) -1, #First) <= #Last Then DATEDIFF(Month, #First, #Last) -1
END as [Actual Months Apart]
SIMPLE AND EASY WAY, Just Copy and Paste this FULL code to MS SQL and Execute :
declare #StartDate date='2019-01-31'
declare #EndDate date='2019-02-28'
SELECT
DATEDIFF(MONTH, #StartDate, #EndDate)+
(
case
when format(#StartDate,'yyyy-MM') != format(#EndDate,'yyyy-MM') AND DATEPART(DAY,#StartDate) > DATEPART(DAY,#EndDate) AND DATEPART(DAY,#EndDate) = DATEPART(DAY,EOMONTH(#EndDate)) then 0
when format(#StartDate,'yyyy-MM') != format(#EndDate,'yyyy-MM') AND DATEPART(DAY,#StartDate) > DATEPART(DAY,#EndDate) then -1
else 0
end
)
as NumberOfMonths
All you need to do is deduct the additional month if the end date has not yet passed the day of the month in the start date.
DECLARE #StartDate AS DATE = '2019-07-17'
DECLARE #EndDate AS DATE = '2019-09-15'
DECLARE #MonthDiff AS INT = DATEDIFF(MONTH,#StartDate,#EndDate)
SELECT #MonthDiff -
CASE
WHEN FORMAT(#StartDate,'dd') > FORMAT(#EndDate,'dd') THEN 1
ELSE 0
END
You can create this function to calculate absolute difference between two dates.
As I found using DATEDIFF inbuilt system function we will get the difference only in months, days and years. For example : Let say there are two dates 18-Jan-2018 and 15-Jan-2019. So the difference between those dates will be given by DATEDIFF in month as 12 months where as it is actually 11 Months 28 Days. So using the function given below, we can find absolute difference between two dates.
CREATE FUNCTION GetDurationInMonthAndDays(#First_Date DateTime,#Second_Date DateTime)
RETURNS VARCHAR(500)
AS
BEGIN
DECLARE #RESULT VARCHAR(500)=''
DECLARE #MONTHS TABLE(MONTH_ID INT,MONTH_NAME VARCHAR(100),MONTH_DAYS INT)
INSERT INTO #MONTHS
SELECT 1,'Jan',31
union SELECT 2,'Feb',28
union SELECT 3,'Mar',31
union SELECT 4,'Apr',30
union SELECT 5,'May',31
union SELECT 6,'Jun',30
union SELECT 7,'Jul',31
union SELECT 8,'Aug',31
union SELECT 9,'Sep',30
union SELECT 10,'Oct',31
union SELECT 11,'Nov',30
union SELECT 12,'Jan',31
IF(#Second_Date>#First_Date)
BEGIN
declare #month int=0
declare #days int=0
declare #first_year int
declare #second_year int
SELECT #first_year=Year(#First_Date)
SELECT #second_year=Year(#Second_Date)+1
declare #first_month int
declare #second_month int
SELECT #first_month=Month(#First_Date)
SELECT #second_month=Month(#Second_Date)
if(#first_month=2)
begin
IF((#first_year%100<>0) AND (#first_year%4=0) OR (#first_year%400=0))
BEGIN
SELECT #days=29-day(#First_Date)
END
else
begin
SELECT #days=28-day(#First_Date)
end
end
else
begin
SELECT #days=(SELECT MONTH_DAYS FROM #MONTHS WHERE MONTH_ID=#first_month)-day(#First_Date)
end
SELECT #first_month=#first_month+1
WHILE #first_year<#second_year
BEGIN
if(#first_month=13)
begin
set #first_month=1
end
WHILE #first_month<13
BEGIN
if(#first_year=Year(#Second_Date))
begin
if(#first_month=#second_month)
begin
SELECT #days=#days+DAY(#Second_Date)
break;
end
else
begin
SELECT #month=#month+1
end
end
ELSE
BEGIN
SELECT #month=#month+1
END
SET #first_month=#first_month+1
END
SET #first_year = #first_year + 1
END
select #month=#month+(#days/30)
select #days=#days%30
if(#days>0)
begin
SELECT #RESULT=CAST(#month AS VARCHAR)+' Month '+CAST(#days AS VARCHAR)+' Days '
end
else
begin
SELECT #RESULT=CAST(#month AS VARCHAR)+' Month '
end
END
ELSE
BEGIN
SELECT #RESULT='ERROR'
END
RETURN #RESULT
END
SELECT dateadd(dd,number,DATEADD(yy, DATEDIFF(yy,0,getdate()), 0)) AS gun FROM master..spt_values
WHERE type = 'p'
AND year(dateadd(dd,number,DATEADD(yy, DATEDIFF(yy,0,getdate()), 0)))=year(DATEADD(yy, DATEDIFF(yy,0,getdate()), 0))
CREATE FUNCTION ufFullMonthDif (#dStart DATE, #dEnd DATE)
RETURNS INT
AS
BEGIN
DECLARE #dif INT,
#dEnd2 DATE
SET #dif = DATEDIFF(MONTH, #dStart, #dEnd)
SET #dEnd2 = DATEADD (MONTH, #dif, #dStart)
IF #dEnd2 > #dEnd
SET #dif = #dif - 1
RETURN #dif
END
GO
SELECT dbo.ufFullMonthDif ('2009-04-30', '2009-05-01')
SELECT dbo.ufFullMonthDif ('2009-04-30', '2009-05-29')
SELECT dbo.ufFullMonthDif ('2009-04-30', '2009-05-30')
SELECT dbo.ufFullMonthDif ('2009-04-16', '2009-05-15')
SELECT dbo.ufFullMonthDif ('2009-04-16', '2009-05-16')
SELECT dbo.ufFullMonthDif ('2009-04-16', '2009-06-16')
SELECT dbo.ufFullMonthDif ('2019-01-31', '2019-02-28')
Making Some changes to the Above function worked for me.
CREATE FUNCTION [dbo].[FullMonthsSeparation]
(
#DateA DATETIME,
#DateB DATETIME
)
RETURNS INT
AS
BEGIN
DECLARE #Result INT
DECLARE #DateX DATETIME
DECLARE #DateY DATETIME
IF(#DateA < #DateB)
BEGIN
SET #DateX = #DateA
SET #DateY = #DateB
END
ELSE
BEGIN
SET #DateX = #DateB
SET #DateY = #DateA
END
SET #Result = (
SELECT
CASE
WHEN DATEPART(DAY, #DateX) > DATEPART(DAY, #DateY)
THEN DATEDIFF(MONTH, #DateX, #DateY) - iif(EOMONTH(#DateY) = #DateY, 0, 1)
ELSE DATEDIFF(MONTH, #DateX, #DateY)
END
)
RETURN #Result
END
Declare #FromDate datetime, #ToDate datetime,
#TotalMonth int ='2021-10-01', #TotalDay='2021-12-31' int,
#Month int = 0
WHILE #ToDate > DATEADD(MONTH,#Month,#FromDate)
BEGIN
SET #Month = #Month +1
END
SET #TotalMonth = #Month -1
SET #TotalDay = DATEDIFF(DAY, DATEADD(MONTH,#TotalMonth, #FromDate),#ToDate) +1
IF(#TotalDay = DAY(EOMONTH(#ToDate)))
BEGIN
SET #TotalMonth = #TotalMonth +1
SET #TotalDay =0
END
Result #TotalMonth = 3, #TotalDay=0
if you are using PostGres only --
SELECT (DATE_PART('year', '2012-01-01'::date) - DATE_PART('year', '2011-10-02'::date)) * 12 +
(DATE_PART('month', '2012-01-01'::date) - DATE_PART('month', '2011-10-02'::date));
There are a lot of answers here that did not satisfy all the corner cases so I set about to fix them. This handles:
01/05/2021 - 02/04/2021 = 0 months
01/31/2021 - 02/28/2021 = 1 months
09/01/2021 - 10/31/2021 = 2 months
I think this generally handles all the cases needed.
declare #dateX date = '01/1/2022'
declare #datey date = '02/28/2022'
-- select datediff(month, #dateX, #datey) --Here for comparison
SELECT
CASE
WHEN DATEPART(DAY, #DateX) = 1 and DATEPART(DAY, #DateY) = DATEPART(DAY, eomonth(#DateY))
THEN DATEDIFF(MONTH, #DateX, #DateY) + 1
WHEN DATEPART(DAY, #DateX) > DATEPART(DAY, #DateY) and DATEPART(DAY, #DateY) != DATEPART(DAY, eomonth(#DateY))
THEN DATEDIFF(MONTH, #DateX, #DateY) - 1
ELSE DATEDIFF(MONTH, #DateX, #DateY)
END
I believe it is important to note that the question specifically asks for "full months between" AND that in the examples given each date is treated as "the START point of that date". This latter item is important because some comments state that year-01-31 to year-02-28 is a result of zero. This is correct. 1 complete day in January, plus 27 complete days in February (02-28 is the start of that day, so incomplete) is zero "full" months.
With that in mind I believe the following would meet the requirements IF StartDate is <= EndDate
(DATEPART(YEAR, EndDate) - DATEPART(YEAR, StartDate)) * 12
+ (DATEPART(MONTH, EndDate) - DATEPART(MONTH, StartDate))
- CASE WHEN DATEPART(DAY,EndDate) < DATEPART(DAY,StartDate) THEN 1 ELSE 0 END
To accommodate the possibility that the dates may be in any order then:
, CASE WHEN StartDate <= EndDate THEN
(DATEPART(YEAR, EndDate) - DATEPART(YEAR, StartDate)) * 12
+ (DATEPART(MONTH, EndDate) - DATEPART(MONTH, StartDate))
- CASE WHEN DATEPART(DAY,EndDate) < DATEPART(DAY,StartDate) THEN 1 ELSE 0 END
ELSE
(DATEPART(YEAR, StartDate) - DATEPART(YEAR, EndDate)) * 12
+ (DATEPART(MONTH, StartDate) - DATEPART(MONTH, EndDate))
- CASE WHEN DATEPART(DAY,StartDate) < DATEPART(DAY,EndDate) THEN 1 ELSE 0 END
END AS FullMnthsBtwn
For this sample:
select
StartDate, EndDate
into mytable
from (
values
(cast(getdate() as date),cast(getdate() as date)) -- both same date
-- original
,('2009-04-16','2009-05-15') -- > 0 full month
,('2009-04-16','2009-05-16') -- > 1 full month
,('2009-04-16','2009-06-16') -- > 2 full months
-- '1/31/2018' and endDate of '3/1/2018', I get a 0 – Eugene
, ('2018-01-31','2018-03-01')
-- some extras mentioned in comments, both of these should return 0 (in my opinion)
,('2009-01-31','2009-02-28')
,('2012-12-31','2013-02-28')
,('2022-05-15','2022-04-16') -- > 0 full month
,('2022-05-16','2022-04-16') -- > 1 full month
,('2021-06-16','2022-04-16') -- > 10 full months
) d (StartDate, EndDate)
query
select
StartDate
, EndDate
, CASE WHEN StartDate <= EndDate THEN
(DATEPART(YEAR, EndDate) - DATEPART(YEAR, StartDate)) * 12
+ (DATEPART(MONTH, EndDate) - DATEPART(MONTH, StartDate))
- CASE WHEN DATEPART(DAY,EndDate) < DATEPART(DAY,StartDate) THEN 1 ELSE 0 END
ELSE
(DATEPART(YEAR, StartDate) - DATEPART(YEAR, EndDate)) * 12
+ (DATEPART(MONTH, StartDate) - DATEPART(MONTH, EndDate))
- CASE WHEN DATEPART(DAY,StartDate) < DATEPART(DAY,EndDate) THEN 1 ELSE 0 END
END AS FullMnthsBtwn
from mytable
order by 1
result
+------------+------------+---------------+
| StartDate | EndDate | FullMnthsBtwn |
+------------+------------+---------------+
| 2009-01-31 | 2009-02-28 | 0 |
| 2009-04-16 | 2009-05-15 | 0 |
| 2009-04-16 | 2009-05-16 | 1 |
| 2009-04-16 | 2009-06-16 | 2 |
| 2012-12-31 | 2013-02-28 | 1 |
| 2018-01-31 | 2018-03-01 | 1 |
| 2021-06-16 | 2022-04-16 | 10 |
| 2022-05-15 | 2022-04-16 | 0 |
| 2022-05-16 | 2022-04-16 | 1 |
| 2022-07-09 | 2022-07-09 | 0 |
+------------+------------+---------------+
See db<>fiddle here (compares some other responses as well)
I got some ideas from the other answers, but none of them gave me exactly what I wanted.
The problem boils down to what I perceive a "month between" to be, which may be what others are also looking for also.
For example 25th February to 25th March would be one month to me, even though it is only 28 days. I would also consider 25th March to 25th April as one month at 31 days.
Also, I would consider 31st January to 2nd March as 1 month and 2 days even though it is 30 days between.
Also, fractions of a month are a bit meaningless as it depends on the length of a month and which month in the range do you choose to take a fraction of.
So, with that in mind, I came up with this function. It returns a decimal, the integer part is the number of months and the decimal part is the number of days, so a return value of 3.07 would mean 3 months and 7 days.
CREATE FUNCTION MonthsAndDaysBetween (#fromDt date, #toDt date)
RETURNS decimal(10,2)
AS
BEGIN
DECLARE #d1 date, #d2 date, #numM int, #numD int, #trc varchar(10);
IF(#fromDt < #toDt)
BEGIN
SET #d1 = #fromDt;
SET #d2 = #toDt;
END
ELSE
BEGIN
SET #d1 = #toDt;
SET #d2 = #fromDt;
END
IF DAY(#d1)>DAY(#d2)
SET #numM = year(#d2)*12+month(#d2)-year(#d1)*12-month(#d1)-1;
ELSE
SET #numM = year(#d2)*12+month(#d2)-year(#d1)*12-month(#d1);
IF YEAR(#d1) < YEAR(#d2) OR (YEAR(#d1) = YEAR(#d2) AND MONTH(#d1) < MONTH(#d2))
BEGIN
IF DAY(#d2) < DAY(#d1)
SET #numD = DAY(#d2) + DAY(EOMONTH(DATEADD(month,-1,#d2))) - DAY(#d1);
ELSE
SET #numD = DAY(#d2)-DAY(#d1);
END
ELSE
SET #numD = DAY(#d2)-DAY(#d1);
RETURN #numM + ABS(#numD) / 100.0;
END
In sql server, this formula works for going backward and forward in time.
DATEDIFF(month,#startdate, #enddate) + iif(#startdate <=#enddate,IIF(DAY(#startdate) > DAY(#enddate),-1,0),IIF(DAY(#startdate) < DAY(#enddate),+1, 0)))
SELECT 12 * (YEAR(end_date) - YEAR(start_date)) +
((MONTH(end_date) - MONTH(start_date))) +
SIGN(DAY(end_date) / DAY(start_date));
This works fine for me on SQL SERVER 2000.
Try:
trunc(Months_Between(date2, date1))
UPDATED
Right now, I just use
SELECT DATEDIFF(MONTH, '2019-01-31', '2019-02-28')
and SQL server returns the exact result (1).
I googled over internet.
And suggestion I found is to add +1 to the end.
Try do it like this:
Declare #Start DateTime
Declare #End DateTime
Set #Start = '11/1/07'
Set #End = '2/29/08'
Select DateDiff(Month, #Start, #End + 1)