How to prevent clustered index scan? - sql-server

I am operating in an SAP B1 database, so modifying the structure of the database is not allowed.
I have a table with 4 columns.
Table name: HLD1
Column Name Type
1 HldCode nvarchar
2 StrDate datetime
3 EndDate datetime
4 Rmrks nvarchar
Some of the data looks like this:
HldCode StrDate EndDate Rmrks
2016 Holidays 2016-09-05 00:00:00.000 2016-09-05 00:00:00.000 Labor Day
2016 Holidays 2016-11-24 00:00:00.000 2016-11-25 00:00:00.000 Thankgiving
2016 Holidays 2016-12-26 00:00:00.000 2016-12-26 00:00:00.000 Christmas
2017 Holidays 2017-01-02 00:00:00.000 2017-01-02 00:00:00.000 New Years Day
2017 Holidays 2017-05-29 00:00:00.000 2017-05-29 00:00:00.000 Memorial Day
2017 Holidays 2017-07-04 00:00:00.000 2017-07-04 00:00:00.000 Indepenance Day
Notice that there is no primary key in this table.
I have a function that I have created to find the number of days between two dates, excluding holidays (as provided by the HLD1 table above) and weekends. While the function works as expected, it also takes ~.75 seconds per row that it's used in, and we're attempting to return 50000 rows to later summarize in Crystal Reports.
The piece of the function that is referencing the HLD1 table (and is causing the Clustered Index in the execution plan) looks like this:
CREATE FUNCTION [dbo].[dateDiffHolidays] (
declare #START DATE
declare #END DATE
)
RETURNS INT
AS
BEGIN
SELECT #AddDays =
(select sum(datediff(dd,strdate,enddate) + 1) from hld1
where strdate between #START and #END)
+
(SELECT
(DATEDIFF(wk, #Start, #End) * 2)
+(CASE WHEN DATENAME(dw, #Start) = 'Sunday' THEN 1 ELSE 0 END)
+(CASE WHEN DATENAME(dw, #End) = 'Saturday' THEN 1 ELSE 0 END))
RETURN #AddDays
END
GO
Specifically, the first part. #START and #END are the parameters passed to the function.
When I check the execution plan for the function, everything looks speedy quick, except for this piece. It gives me the following info:
All of the sources that I've found on the web about how to prevent or fix this kind of slowdown suggest adding indices, not referencing certain columns, etc, but since I can't modify the database, I've been unable to find any methodology on how to help in my situation.
Any suggestions?
EDIT 1:
Added Table Schema info from SQL Management
EDIT 2:
Added full text of function, just in case:
CREATE FUNCTION [dbo].[dateDiffHolidays] (
#startdaytime DATETIME,
#enddaytime DATETIME
)
RETURNS INT
AS
BEGIN
DECLARE #answer INT, #START Date, #END Date, #AddDays int
SET #answer = 0
-- Strip Times
SELECT #START = dateadd(dd,0, datediff(dd,0,#StartDayTime)), #END =
dateadd(dd,0, datediff(dd,0,#EndDayTime))
SELECT #AddDays = (select sum(datediff(dd,strdate,enddatE) + 1) from hld1
where strdate between #START and #END
order by HldCode, StrDate, EndDate) + (
SELECT
(DATEDIFF(wk, #Start, #End) * 2)
+(CASE WHEN DATENAME(dw, #Start) = 'Sunday' THEN 1 ELSE 0 END)
+(CASE WHEN DATENAME(dw, #End) = 'Saturday' THEN 1 ELSE 0 END))
-- handle end conditions
DECLARE #firstWeekDayInRange datetime, #lastWeekDayInRange datetime;
SELECT #firstWeekDayInRange = #START, #lastWeekDayInRange = #END
WHILE #firstWeekDayInRange in (select cast( DATEADD(day, t.N - 1, StrDate)
as date) as ResultDate
from HLD1 s join cteTally t on t.N <= DATEDIFF(day, StrDate, EndDate) + 1)
or datepart(dw,#firstWeekDayInRange) in (1,7)
BEGIN
SELECT #firstWeekDayInRange =
CASE
WHEN #firstWeekDayInRange in (select cast( DATEADD(day, t.N - 1, StrDate) as
date) from HLD1 s join cteTally t on t.N <= DATEDIFF(day, StrDate, EndDate)
+ 1)
or datepart(dw,#firstWeekDayInRange) in (1,7)
THEN dateadd(DAY,1,#firstWeekDayInRange)
ELSE #firstWeekDayInRange
END
END
WHILE #lastWeekDayInRange in (select cast( DATEADD(day, t.N - 1, StrDate) as
date) as ResultDate
from HLD1 s join cteTally t on t.N <= DATEDIFF(day, StrDate, EndDate) + 1)
or datepart(dw,#lastWeekDayInRange) in (1,7)
BEGIN
SELECT #lastWeekDayInRange =
CASE
WHEN #lastWeekDayInRange in (select cast( DATEADD(day, t.N - 1, StrDate) as
date) from HLD1 s join cteTally t on t.N <= DATEDIFF(day, StrDate, EndDate)
+ 1)
or datepart(dw,#lastWeekDayInRange) in (1,7)
THEN dateadd(DAY,-1,#lastWeekDayInRange)
ELSE #lastWeekDayInRange
END
END
-- add one day to answer (to count Friday) if enddate was on a weekend
SELECT #answer = #answer +
CASE
-- triggered if start and end date are on same weekend
WHEN dateDiff(DAY,#firstWeekDayInRange,#lastWeekDayInRange) < 0 THEN
(#answer * -1)
-- otherwise count the days and substract 2 days per weekend in between dates
ELSE (DateDiff(DAY, #firstWeekDayInRange, #lastWeekDayInRange) - #AddDays)
END
RETURN #answer
END
GO

You can try by add an ORDER BY clause.
CREATE TABLE HLD1
(
HldCode nvarchar(20),
StrDate datetime,
EndDate datetime,
Rmrks nvarchar(50)
)
create unique index id_hld1 on HLD1 (HldCode, StrDate, EndDate);
GO
INSERT INTO HLD1
VALUES ('2016 Holidays', '2016-09-05 00:00:00.000', '2016-09-05 00:00:00.000', 'Labor Day'),
('2016 Holidays', '2016-11-24 00:00:00.000', '2016-11-25 00:00:00.000', 'Thanksgiving'),
('2016 Holidays', '2016-12-26 00:00:00.000', '2016-12-26 00:00:00.000', 'Christmas'),
('2017 Holidays', '2017-01-02 00:00:00.000', '2017-01-02 00:00:00.000', 'New Years Day'),
('2017 Holidays', '2017-05-29 00:00:00.000', '2017-05-29 00:00:00.000', 'Memorial Day'),
('2017 Holidays', '2017-07-04 00:00:00.000', '2017-07-04 00:00:00.000', 'Independence Day');
GO
6 rows affected
DECLARE #StrDate datetime = '2017-01-01';
DECLARE #EndDate datetime = '2018-01-01'
set statistics profile on;
SELECT HldCode, StrDate, EndDate, Rmrks
FROM HLD1
WHERE StrDate >= #StrDate
AND EndDate < #EndDate;
set statistics profile off;
GO
Output:
HldCode | StrDate | EndDate | Rmrks
:------------ | :------------------ | :------------------ | :--------------
2017 Holidays | 02/01/2017 00:00:00 | 02/01/2017 00:00:00 | New Years Day
2017 Holidays | 29/05/2017 00:00:00 | 29/05/2017 00:00:00 | Memorial Day
2017 Holidays | 04/07/2017 00:00:00 | 04/07/2017 00:00:00 | Indepenance Day
Rows | Executes | StmtText | StmtId | NodeId | Parent | PhysicalOp | LogicalOp | Argument | DefinedValues | EstimateRows | EstimateIO | EstimateCPU | AvgRowSize | TotalSubtreeCost | OutputList | Warnings | Type | Parallel | EstimateExecutions
> :--- | :------- | :----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -----: | -----: | -----: | :--------- | :--------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------- | :--------- | :---------- | ---------: | :--------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------- | :------- | :------- | :-----------------
> 3 | 1 | SELECT HldCode, StrDate, EndDate, Rmrks<br>from HLD1<br>WHERE StrDate >= #StrDate<br>AND EndDate < #EndDate | 1 | 1 | 0 | <em>null</em> | <em>null</em> | <em>null</em> | <em>null</em> | 1 | <em>null</em> | <em>null</em> | <em>null</em> | 0.0032886 | <em>null</em> | <em>null</em> | SELECT | False | <em>null</em>
> 3 | 1 | |--Table Scan(OBJECT:([fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1]), WHERE:([fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1].[StrDate]>=[#StrDate] AND [fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1].[EndDate]<[#EndDate])) | 1 | 2 | 1 | Table Scan | Table Scan | OBJECT:([fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1]), WHERE:([fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1].[StrDate]>=[#StrDate] AND [fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1].[EndDate]<[#EndDate]) | [fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1].[HldCode], [fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1].[StrDate], [fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1].[EndDate], [fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1].[Rmrks] | 1 | 0.003125 | 0.0001636 | 99 | 0.0032886 | [fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1].[HldCode], [fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1].[StrDate], [fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1].[EndDate], [fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1].[Rmrks] | <em>null</em> | PLAN_ROW | False | 1
DECLARE #StrDate datetime = '2017-01-01';
DECLARE #EndDate datetime = '2018-01-01'
set statistics profile on;
SELECT HldCode, StrDate, EndDate, Rmrks
FROM HLD1
WHERE StrDate >= #StrDate
AND EndDate < #EndDate
ORDER BY HldCode, StrDate, EndDate;
set statistics profile off;
GO
Output:
HldCode | StrDate | EndDate | Rmrks
:------------ | :------------------ | :------------------ | :--------------
2017 Holidays | 02/01/2017 00:00:00 | 02/01/2017 00:00:00 | New Years Day
2017 Holidays | 29/05/2017 00:00:00 | 29/05/2017 00:00:00 | Memorial Day
2017 Holidays | 04/07/2017 00:00:00 | 04/07/2017 00:00:00 | Indepenance Day
Rows | Executes | StmtText | StmtId | NodeId | Parent | PhysicalOp | LogicalOp | Argument | DefinedValues | EstimateRows | EstimateIO | EstimateCPU | AvgRowSize | TotalSubtreeCost | OutputList | Warnings | Type | Parallel | EstimateExecutions
:--- | :------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -----: | -----: | -----: | :----------- | :--------- | :--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :-------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------- | :--------- | :---------- | ---------: | :--------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------- | :------- | :------- | :-----------------
3 | 1 | SELECT HldCode, StrDate, EndDate, Rmrks<br>from HLD1<br>WHERE StrDate >= #StrDate<br>AND EndDate < #EndDate<br>ORDER BY HldCode, StrDate, EndDate | 1 | 1 | 0 | <em>null</em> | <em>null</em> | <em>null</em> | <em>null</em> | 1 | <em>null</em> | <em>null</em> | <em>null</em> | 0.00658116 | <em>null</em> | <em>null</em> | SELECT | False | <em>null</em>
3 | 1 | |--Nested Loops(Inner Join, OUTER REFERENCES:([Bmk1000])) | 1 | 2 | 1 | Nested Loops | Inner Join | OUTER REFERENCES:([Bmk1000]) | <em>null</em> | 1 | 0 | 4.18E-06 | 99 | 0.00658116 | [fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1].[HldCode], [fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1].[StrDate], [fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1].[EndDate], [fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1].[Rmrks] | <em>null</em> | PLAN_ROW | False | 1
3 | 1 | |--Index Scan(OBJECT:([fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1].[id_hld1]), WHERE:([fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1].[StrDate]>=[#StrDate] AND [fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1].[EndDate]<[#EndDate]) ORDERED FORWARD) | 1 | 3 | 2 | Index Scan | Index Scan | OBJECT:([fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1].[id_hld1]), WHERE:([fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1].[StrDate]>=[#StrDate] AND [fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1].[EndDate]<[#EndDate]) ORDERED FORWARD | [Bmk1000], [fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1].[HldCode], [fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1].[StrDate], [fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1].[EndDate] | 1 | 0.003125 | 0.0001636 | 55 | 0.0032886 | [Bmk1000], [fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1].[HldCode], [fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1].[StrDate], [fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1].[EndDate] | <em>null</em> | PLAN_ROW | False | 1
3 | 3 | |--RID Lookup(OBJECT:([fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1]), SEEK:([Bmk1000]=[Bmk1000]) LOOKUP ORDERED FORWARD) | 1 | 5 | 2 | RID Lookup | RID Lookup | OBJECT:([fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1]), SEEK:([Bmk1000]=[Bmk1000]) LOOKUP ORDERED FORWARD | [fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1].[Rmrks] | 1 | 0.003125 | 0.0001581 | 61 | 0.0032831 | [fiddle_9f66021924d842d39e112d909afc0794].[dbo].[HLD1].[Rmrks] | <em>null</em> | PLAN_ROW | False | 1
dbfiddle here
UPDATE
As far as you need a stored procedure, you can try;
WITH (INDEX(IndexName))
CREATE FUNCTION [dbo].[dateDiffHolidays] (#START DATE, #END DATE)
RETURNS INT
AS
BEGIN
DECLARE #AddDays int;
SELECT #AddDays = (SELECT sum(datediff(dd, StrDate, EndDate) + 1)
FROM hld1 WITH (INDEX(HLD1_PRIMARY))
WHERE StrDate BETWEEN #START AND #END)
+
(SELECT (DATEDIFF(wk, #Start, #End) * 2)
+ (CASE WHEN DATENAME(dw, #Start) = 'Sunday' THEN 1 ELSE 0 END)
+ (CASE WHEN DATENAME(dw, #End) = 'Saturday' THEN 1 ELSE 0 END))
RETURN #AddDays
END
DECLARE #StrDate datetime = '2017-01-01';
DECLARE #EndDate datetime = '2018-01-01';
DECLARE #NumDays int = 0;
set statistics profile on;
EXEC #NumDays = [dbo].[dateDiffHolidays] #StrDate, #EndDate;
set statistics profile off;
SELECT #NumDays;
Rows | Executes | StmtText | StmtId | NodeId | Parent | PhysicalOp | LogicalOp | Argument | DefinedValues | EstimateRows | EstimateIO | EstimateCPU | AvgRowSize | TotalSubtreeCost | OutputList | Warnings | Type | Parallel | EstimateExecutions
:--- | :------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | -----: | -----: | -----: | :--------------- | :------------- | :---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | :----------- | :--------- | :---------- | ---------: | :--------------- | :--------------------------------------------------------------------------------------------------------------------------------- | :------- | :------- | :------- | :-----------------
1 | 1 | SELECT #AddDays = (SELECT sum(datediff(dd, StrDate, EndDate) + 1) <br> FROM hld1 WITH (INDEX(HLD1_PRIMARY))<br> WHERE StrDate BETWEEN #START AND #END) <br> + <br> (SELECT (DATEDIFF(wk, #Start, #End) * 2)<br> + (CASE WHEN DATENAME(dw, #Start) = 'Sunday' THEN 1 ELSE 0 END)<br> + (CASE WHEN DATENAME(dw, #End) = 'Saturday' THEN 1 ELSE 0 END)) | 1 | 1 | 0 | null | null | null | null | 1 | null | null | null | 0.00329658 | null | null | SELECT | False | null
0 | 0 | |--Compute Scalar(DEFINE:([Expr1006]=[Expr1003]+(datediff(week,CONVERT_IMPLICIT(datetimeoffset(7),[#START],0),CONVERT_IMPLICIT(datetimeoffset(7),[#END],0))*(2)+CASE WHEN datename(weekday,[#START])=N'Sunday' THEN (1) ELSE (0) END+CASE WHEN datename(weekday,[#END])=N'Saturday' THEN (1) ELSE (0) END))) | 1 | 2 | 1 | Compute Scalar | Compute Scalar | DEFINE:([Expr1006]=[Expr1003]+(datediff(week,CONVERT_IMPLICIT(datetimeoffset(7),[#START],0),CONVERT_IMPLICIT(datetimeoffset(7),[#END],0))*(2)+CASE WHEN datename(weekday,[#START])=N'Sunday' THEN (1) ELSE (0) END+CASE WHEN datename(weekday,[#END])=N'Saturday' THEN (1) ELSE (0) END)) | [Expr1006]=[Expr1003]+(datediff(week,CONVERT_IMPLICIT(datetimeoffset(7),[#START],0),CONVERT_IMPLICIT(datetimeoffset(7),[#END],0))*(2)+CASE WHEN datename(weekday,[#START])=N'Sunday' THEN (1) ELSE (0) END+CASE WHEN datename(weekday,[#END])=N'Saturday' THEN (1) ELSE (0) END) | 1 | 0 | 1E-07 | 11 | 0.00329658 | [Expr1006] | null | PLAN_ROW | False | 1
0 | 0 | |--Compute Scalar(DEFINE:([Expr1003]=CASE WHEN [Expr1012]=(0) THEN NULL ELSE [Expr1013] END)) | 1 | 3 | 2 | Compute Scalar | Compute Scalar | DEFINE:([Expr1003]=CASE WHEN [Expr1012]=(0) THEN NULL ELSE [Expr1013] END) | [Expr1003]=CASE WHEN [Expr1012]=(0) THEN NULL ELSE [Expr1013] END | 1 | 0 | 0 | 11 | 0.00329648 | [Expr1003] | null | PLAN_ROW | False | 1
1 | 1 | |--Stream Aggregate(DEFINE:([Expr1012]=COUNT_BIG([Expr1007]), [Expr1013]=SUM([Expr1007]))) | 1 | 4 | 3 | Stream Aggregate | Aggregate | null | [Expr1012]=COUNT_BIG([Expr1007]), [Expr1013]=SUM([Expr1007]) | 1 | 0 | 2.3E-06 | 11 | 0.00329648 | [Expr1012], [Expr1013] | null | PLAN_ROW | False | 1
0 | 0 | |--Compute Scalar(DEFINE:([Expr1007]=datediff(day,[fiddle_c7abc2eb9b3f49599be6803069c6aa56].[dbo].[HLD1].[StrDate],[fiddle_c7abc2eb9b3f49599be6803069c6aa56].[dbo].[HLD1].[EndDate])+(1))) | 1 | 5 | 4 | Compute Scalar | Compute Scalar | DEFINE:([Expr1007]=datediff(day,[fiddle_c7abc2eb9b3f49599be6803069c6aa56].[dbo].[HLD1].[StrDate],[fiddle_c7abc2eb9b3f49599be6803069c6aa56].[dbo].[HLD1].[EndDate])+(1)) | [Expr1007]=datediff(day,[fiddle_c7abc2eb9b3f49599be6803069c6aa56].[dbo].[HLD1].[StrDate],[fiddle_c7abc2eb9b3f49599be6803069c6aa56].[dbo].[HLD1].[EndDate])+(1) | 3 | 0 | 3E-07 | 11 | 0.00329418 | [Expr1007] | null | PLAN_ROW | False | 1
3 | 1 | |--Index Scan(OBJECT:([fiddle_c7abc2eb9b3f49599be6803069c6aa56].[dbo].[HLD1].[HLD1_PRIMARY]), WHERE:([fiddle_c7abc2eb9b3f49599be6803069c6aa56].[dbo].[HLD1].[StrDate]>=[#START] AND [fiddle_c7abc2eb9b3f49599be6803069c6aa56].[dbo].[HLD1].[StrDate]<=[#END])) | 1 | 6 | 5 | Index Scan | Index Scan | OBJECT:([fiddle_c7abc2eb9b3f49599be6803069c6aa56].[dbo].[HLD1].[HLD1_PRIMARY]), WHERE:([fiddle_c7abc2eb9b3f49599be6803069c6aa56].[dbo].[HLD1].[StrDate]>=[#START] AND [fiddle_c7abc2eb9b3f49599be6803069c6aa56].[dbo].[HLD1].[StrDate]<=[#END]), FORCEDINDEX | [fiddle_c7abc2eb9b3f49599be6803069c6aa56].[dbo].[HLD1].[StrDate], [fiddle_c7abc2eb9b3f49599be6803069c6aa56].[dbo].[HLD1].[EndDate] | 3 | 0.003125 | 0.0001636 | 23 | 0.0032886 | [fiddle_c7abc2eb9b3f49599be6803069c6aa56].[dbo].[HLD1].[StrDate], [fiddle_c7abc2eb9b3f49599be6803069c6aa56].[dbo].[HLD1].[EndDate] | null | PLAN_ROW | False | 1
| (No column name) |
| ---------------: |
| 108 |
dbfiddle here

Since the HLD1_PRIMARY index includes the following columns: HldCode, StrDate, EndDate try adding a condition that filters by HldCode to your aggregate select.
For example if you only want to include rows with HldCode = '2016 Holidays' and HldCode = '2017 Holidays':
CREATE FUNCTION [dbo].[dateDiffHolidays] (
declare #START DATE
declare #END DATE
)
RETURNS INT
AS
BEGIN
SELECT #AddDays = (
select
sum(datediff(dd,strdate,enddate) + 1)
from
hld1
where
HldCode in ('2016 Holidays', '2017 Holidays')
and strdate between #START and #END
)
+
(
SELECT
(DATEDIFF(wk, #Start, #End) * 2)
+(CASE WHEN DATENAME(dw, #Start) = 'Sunday' THEN 1 ELSE 0 END)
+(CASE WHEN DATENAME(dw, #End) = 'Saturday' THEN 1 ELSE 0 END)
)
RETURN #AddDays
END
UPDATE
You won't get any better than an Index Seek for that particular query.
Then, after looking at the full code, I suspect the issue is not on that query but on the while loops used to calculate #firstWeekDayInRange and #lastWeekDayInRange:
WHILE #firstWeekDayInRange in (
select
cast(DATEADD(day, t.N - 1, StrDate) as date) as ResultDate
from
HLD1 s
join cteTally t
on t.N <= DATEDIFF(day, StrDate, EndDate) + 1
)
or datepart(dw, #firstWeekDayInRange) in (1,7)
BEGIN
SELECT #firstWeekDayInRange =
CASE
WHEN #firstWeekDayInRange in (
select
cast(DATEADD(day, t.N - 1, StrDate) as date)
from
HLD1 s
join cteTally t on
t.N <= DATEDIFF(day, StrDate, EndDate) + 1
)
or datepart(dw,#firstWeekDayInRange) in (1,7) THEN
dateadd(DAY,1,#firstWeekDayInRange)
ELSE
#firstWeekDayInRange
END
END
At best these queries result in the Clustered Index Scan and at worst on a Table Scan on the HLD1 table for each iteration of the loop, and we don't know much about cteTally, so it could be worse.

Do you must use a function? how about create a date calendar kind of table?
CREATE TABLE #Datecalendar
(DateId DATE PRIMARY KEY
, IsWeekend BIT DEFAULT 0
, IsHoliday BIT DEFAULT 0
);
CREATE TABLE #HLD1
(HldCode nvarchar(20)
, StrDate datetime
, EndDate datetime
, Rmrks nvarchar(50)
);
INSERT INTO #HLD1
VALUES ('2016 Holidays', '2016-09-05 00:00:00.000', '2016-09-05 00:00:00.000', 'Labor Day'),
('2016 Holidays', '2016-11-24 00:00:00.000', '2016-11-25 00:00:00.000', 'Thanksgiving'),
('2016 Holidays', '2016-12-26 00:00:00.000', '2016-12-26 00:00:00.000', 'Christmas'),
('2017 Holidays', '2017-01-02 00:00:00.000', '2017-01-02 00:00:00.000', 'New Years Day'),
('2017 Holidays', '2017-05-29 00:00:00.000', '2017-05-29 00:00:00.000', 'Memorial Day'),
('2017 Holidays', '2017-07-04 00:00:00.000', '2017-07-04 00:00:00.000', 'Independence Day');
WITH cte AS (SELECT CAST('2016-01-01' AS DATE) AS DateId
UNION ALL
SELECT DATEADD(dd, 1, DateId)
FROM cte
WHERE DATEADD(dd, 1, DateId) <= '2019-01-01'
)
INSERT INTO #Datecalendar
(DateId
, IsWeekend
, IsHoliday
)
SELECT DateId
,CASE WHEN DATEPART(WEEKDAY,DateId) =1 OR DATEPART(WEEKDAY,DateId) = 7 THEN 1 ELSE 0 END
,CASE WHEN h.HldCode IS NOT NULL THEN 1 ELSE 0 END
FROM cte cte
LEFT JOIN #HLD1 h
ON cte.DateId BETWEEN h.StrDate AND h.EndDate
OPTION (MAXRECURSION 0);
SELECT COUNT(*)
FROM #Datecalendar
WHERE DateId BETWEEN '2016-05-06' AND '2017-02-24'
AND IsWeekend = 0
AND IsHoliday = 0;
DROP TABLE #HLD1;
DROP TABLE #Datecalendar;

Related

Pivot table with custom values

i have table say FIDDLE HERE
+----+------+------+-----+-----+
| id | year | sell | buy | own |
+----+------+------+-----+-----+
| 1 | 2016 | 9 | 2 | 10 |
| 1 | 2017 | 9 | | 10 |
| 1 | 2018 | | 2 | 10 |
| 2 | 2016 | 7 | 2 | 11 |
| 2 | 2017 | 2 | | |
| 2 | 2018 | | | 18 |
+----+------+------+-----+-----+
create table test(id varchar(20), year varchar(20),
sell varchar(20), buy varchar(20),
own varchar(20));
insert into test values('1', '2016','9','2','10' )
insert into test values('1', '2017','9',NULL,'10' )
insert into test values('1', '2018',NULL,'2','10' )
insert into test values('2', '2016','7','2','11' )
insert into test values('2', '2017','2',NULL,'17' )
insert into test values('2', '2018','5','2','18' )
I'm trying to PIVOT but instead of aggregate the values, i wanted to keep some letters if it is not null (S-Sell,B-Buy,O-Own). If there are values for all columns for particular year then i need S_B_O for that year. If there are values only for sell and buy then S_B etc., so Expected output is
+----+-------+------+------+
| ID | 2016 | 2017 | 2018 |
+----+-------+------+------+
| 1 | S_B_O | S_O | B_O |
+----+-------+------+------+
| 2 | S_B_O | S | O |
+----+-------+------+------+
The closest i have got is using conditional aggrgarion( MAX and concat) instead of PIVOT but this is also giving null if any one is NULL. Please suggest a solution.
select ID,
MAX(CASE WHEN Year = '2016' AND sell is not null THEN 'S_' END +
CASE WHEN Year = '2016' AND buy is not null THEN 'B_' END +
CASE WHEN Year = '2016' AND own is not null THEN 'O' END)
AS [2016],
MAX(CASE WHEN Year = '2017' AND sell is not null THEN 'S_' END +
CASE WHEN Year = '2017' AND buy is not null THEN 'B_' END +
CASE WHEN Year = '2017' AND own is not null THEN 'O' END)
AS [2017]
/* ......for all year */
from test
group by id
FIDDLE HERE
You can use CONCAT function, which will handle NULLs automatically.
select ID,
CONCAT(MAX(CASE WHEN Year = '2016' AND sell is not null THEN 'S_' END) ,
MAX(CASE WHEN Year = '2016' AND buy is not null THEN 'B_' END) ,
MAX(CASE WHEN Year = '2016' AND buy is not null THEN 'O' END))
AS [2016],
CONCAT(MAX(CASE WHEN Year = '2017' AND sell is not null THEN 'S_' END) ,
MAX(CASE WHEN Year = '2017' AND buy is not null THEN 'B_' END) ,
MAX(CASE WHEN Year = '2017' AND buy is not null THEN 'O' END))
AS [2017]
from test
group by id
+----+-------+------+
| ID | 2016 | 2017 |
+----+-------+------+
| 1 | S_B_O | S_ |
| 2 | S_B_O | S_ |
+----+-------+------+
UPDATE Dynamic query. As #Larnu told, You should have asked this as separate question. You should not change the requirement.
DECLARE #lst_Years NVARCHAR(MAX) , #query NVARCHAR(MAX)
SET #lst_Years = STUFF((SELECT distinct ',' + QUOTENAME([Year])
FROM test
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
SET #query = 'SELECT * FROM
(
select ID, [Year],
CONCAT(MAX(CASE WHEN sell is not null THEN ''S_'' END) ,
MAX(CASE WHEN buy is not null THEN ''B_'' END) ,
MAX(CASE WHEN buy is not null THEN ''O'' END))
AS [Value]
from test
group by id, [year]) as t
pivot
(
max(value) FOR YEAR IN (' + #lst_Years + ')
) as pvt'
EXEC(#query)
+----+-------+------+-------+
| ID | 2016 | 2017 | 2018 |
+----+-------+------+-------+
| 1 | S_B_O | S_ | B_O |
| 2 | S_B_O | S_ | S_B_O |
+----+-------+------+-------+

TSQL: Find Groups of Records in a Sequence of Records

Sorry for the title if you find it incorrect, I really wasn't sure how to name this question. There is probably a term for this type of query/pattern.
I have a sequence of records that need to be ordered by date, the records have a condition I would like to "group" by (SomeCondition) to get the earliest start date and latest end date (taking NULL's into account) but I'm unsure how to accomplish the query (if it's even possible). The original records in the table look something like;
-----------------------------------------------------------
| AbcID | XyzID | StartDate | EndDate | SomeCondition |
-----------------------------------------------------------
| 1 | 1 | 2018-01-01 | 2018-03-05 | 1 |
| 2 | 1 | 2018-04-20 | 2018-05-01 | 1 |
| 3 | 1 | 2018-05-02 | 2018-05-15 | 0 |
| 4 | 1 | 2018-06-01 | 2018-07-01 | 1 |
| 5 | 1 | 2018-08-01 | NULL | 1 |
| 6 | 2 | 2018-01-01 | 2018-06-30 | 1 |
| 7 | 2 | 2018-07-01 | 2018-08-31 | 0 |
-----------------------------------------------------------
The result I'm going for would be;
-----------------------------------
| XyzID | StartDate | EndDate |
-----------------------------------
| 1 | 2018-01-01 | 2018-05-01 |
| 1 | 2018-06-01 | NULL |
| 2 | 2018-01-01 | 2018-06-30 |
-----------------------------------
Thanks for any help/insight, even if it's "not possible".
Solving this problem requires you to solve it piece by piece. Here are the steps that I used to do that:
Determine when the island begins (when SomeCondition is false)
Create an "ID" number for each island (within each XyzID) by summing the number of IslandBegins while considering the records in AbcID order
Determine the first and last AbcID within each XyzID/IslandNumber combination where SomeCondition is true
Use the previous step as a guide as to what StartDate / EndDate you should get for each record in the result set
Sample Data:
declare #sample_data table
(
AbcID int
, XyzID int
, StartDate date
, EndDate date
, SomeCondition bit
)
insert into #sample_data
values (1, 1, '2018-01-01', '2018-03-05', 1)
, (2, 1, '2018-04-20', '2018-05-01', 1)
, (3, 1, '2018-05-02', '2018-05-15', 0)
, (4, 1, '2018-06-01', '2018-07-01', 1)
, (5, 1, '2018-08-01', NULL, 1)
, (6, 2, '2018-01-01', '2018-06-30', 1)
, (7, 2, '2018-07-01', '2018-08-31', 0)
Answer:
The comments in the code show which step each part of the CTE is accomplishing.
with island_bgn as
(
--Step 1
select d.AbcID
, d.XyzID
, d.StartDate
, d.EndDate
, d.SomeCondition
, case when d.SomeCondition = 0 then 1 else 0 end as IslandBegin
from #sample_data as d
)
, island_nbr as
(
--Step 2
select b.AbcID
, b.XyzID
, b.StartDate
, b.EndDate
, b.SomeCondition
, b.IslandBegin
, sum(b.IslandBegin) over (partition by b.XyzID order by b.AbcID asc) as IslandNumber
from island_bgn as b
)
, prelim as
(
--Step 3
select n.XyzID
, n.IslandNumber
, min(n.AbcID) as AbcIDMin
, max(n.AbcID) as AbcIDMax
from island_nbr as n
where 1=1
and n.SomeCondition = 1
group by n.XyzID
, n.IslandNumber
)
--Step 4
select p.XyzID
, a.StartDate
, b.EndDate
from prelim as p
inner join #sample_data as a on p.AbcIDMin = a.AbcID
inner join #sample_data as b on p.AbcIDMax = b.AbcID
order by p.XyzID
, a.StartDate
, b.EndDate
Results:
+-------+------------+------------+
| XyzID | StartDate | EndDate |
+-------+------------+------------+
| 1 | 2018-01-01 | 2018-05-01 |
+-------+------------+------------+
| 1 | 2018-06-01 | NULL |
+-------+------------+------------+
| 2 | 2018-01-01 | 2018-06-30 |
+-------+------------+------------+

How to add biweekly periods in a year to a table? (SQL Server)

This SQL Statement is not enough to add the right biweekly periods.
What i want is something like this:
Column 1 (period) / Column 2 (start period) / Column 3 (end period)
20160115 / 2016-01-01 / 2016-01-15
20160131 / 2016-01-15 / 2016-01-31
20160215 / 2016-02-01 / 2016-02-15
20160229 / 2016-02-16 / 2016-02-29
and so on...
This is what I have now, which is wrong / incomplete.
the value is being inserted correctly in the table columns but the gap is wrong since months don't have the same amount of days
Can someone help me? Thank you very much!
DECLARE #d date= '20020101'
WHILE #d<'20030101'
BEGIN
INSERT INTO [TABLE]
VALUES ((SELECT CONVERT(VARCHAR(8), #d, 112) AS [YYYYMMDD]), #d, #d, 'Fechado', '1')
SET #d=DATEADD(DAY,15,#d)
END
using a common table expression for an adhoc numbers table, and union all for two queries with some date math using dateadd():
declare #startdate date = '2016-01-01'
declare #enddate date = '2016-12-31'
;with cte as (
select top (datediff(month,#startdate,#enddate)+1)
Month = convert(date,dateadd(month,row_number() over (order by (select 1))-1,#startdate))
from master..spt_values
order by month
)
select
Period = convert(char(8), dateadd(day,14,month), 112)
, PeriodStart = month
, PeriodEnd = dateadd(day,14,month)
from cte
union all
select
Period = convert(char(8), eomonth(month), 112)
, PeriodStart = dateadd(day,14,month)
, PeriodEnd = eomonth(month)
from cte
order by period
rextester demo: http://rextester.com/BSYIXW7864
returns:
+----------+-------------+------------+
| Period | PeriodStart | PeriodEnd |
+----------+-------------+------------+
| 20160115 | 2016-01-01 | 2016-01-15 |
| 20160131 | 2016-01-15 | 2016-01-31 |
| 20160215 | 2016-02-01 | 2016-02-15 |
| 20160229 | 2016-02-15 | 2016-02-29 |
| 20160315 | 2016-03-01 | 2016-03-15 |
| 20160331 | 2016-03-15 | 2016-03-31 |
| 20160415 | 2016-04-01 | 2016-04-15 |
| 20160430 | 2016-04-15 | 2016-04-30 |
| 20160515 | 2016-05-01 | 2016-05-15 |
| 20160531 | 2016-05-15 | 2016-05-31 |
| 20160615 | 2016-06-01 | 2016-06-15 |
| 20160630 | 2016-06-15 | 2016-06-30 |
| 20160715 | 2016-07-01 | 2016-07-15 |
| 20160731 | 2016-07-15 | 2016-07-31 |
| 20160815 | 2016-08-01 | 2016-08-15 |
| 20160831 | 2016-08-15 | 2016-08-31 |
| 20160915 | 2016-09-01 | 2016-09-15 |
| 20160930 | 2016-09-15 | 2016-09-30 |
| 20161015 | 2016-10-01 | 2016-10-15 |
| 20161031 | 2016-10-15 | 2016-10-31 |
| 20161115 | 2016-11-01 | 2016-11-15 |
| 20161130 | 2016-11-15 | 2016-11-30 |
| 20161215 | 2016-12-01 | 2016-12-15 |
| 20161231 | 2016-12-15 | 2016-12-31 |
+----------+-------------+------------+
Create a function, then call in your query:
Function:
create FUNCTION advance_biweekly
(
#current date
)
RETURNS date
AS
BEGIN
--if it's the first day of month, advance two weeks
if (DAY(#current) = 1)
return DATEADD(WEEK, 2, #current)
else
--if its last day of month, advance one day
if (DAY(DATEADD(d, -1, DATEADD(m, DATEDIFF(m, 0, #current) + 1, 0))) = DAY(#current))
return DATEADD(DAY, 1, #current)
else
--else, it's the middle of the month, go to end
return dateadd(month,((YEAR(#current)-1900)*12)+MONTH(#current)-1,DAY(DATEADD(d, -1, DATEADD(m, DATEDIFF(m, 0, #current) + 1, 0)))-1)
return null
END
GO
code:
DECLARE #d date= '20020101'
WHILE #d<'20030101'
begin
set #d = dbo.advance_biweekly(#d)
print #d
end
result:
2002-01-15
2002-01-31
2002-02-01
2002-02-15
2002-02-28
2002-03-01
2002-03-15
2002-03-31
2002-04-01
2002-04-15
2002-04-30
2002-05-01

Get all days between two dates with all day hours in SQL Server

I have to generate a result set of a SQL query which should match the following, but let me explain both inputs and outputs:
I have a table named Orders and this table has some orders in some days at some hours, then, I have been requested to provide a result-set which should get all days between two dates (i.e. 2017-10-01 and 2017-10-07), with all 24 hours for each day, even if that day or that hour had no orders, but it should be appeared with 0 value.
+------------+------+-------------+
| Day | Hour | TotalOrders |
+------------+------+-------------+
| 2017-10-01 | 0 | 0 |
+------------+------+-------------+
| 2017-10-01 | 1 | 3 |
+------------+------+-------------+
| 2017-10-01 | 2 | 4 |
+------------+------+-------------+
| 2017-10-01 | 3 | 0 |
+------------+------+-------------+
| 2017-10-01 | 4 | 7 |
+------------+------+-------------+
| 2017-10-01 | 5 | 0 |
+------------+------+-------------+
| 2017-10-01 | 6 | 0 |
+------------+------+-------------+
| 2017-10-01 | 7 | 9 |
+------------+------+-------------+
| 2017-10-01 | 8 | 0 |
+------------+------+-------------+
| 2017-10-01 | 9 | 0 |
+------------+------+-------------+
| 2017-10-01 | 10 | 0 |
+------------+------+-------------+
| 2017-10-01 | 11 | 0 |
+------------+------+-------------+
| 2017-10-01 | 12 | 0 |
+------------+------+-------------+
| 2017-10-01 | 13 | 0 |
+------------+------+-------------+
| 2017-10-01 | 14 | 0 |
+------------+------+-------------+
| 2017-10-01 | 15 | 0 |
+------------+------+-------------+
| 2017-10-01 | 16 | 0 |
+------------+------+-------------+
| 2017-10-01 | 17 | 0 |
+------------+------+-------------+
| 2017-10-01 | 18 | 0 |
+------------+------+-------------+
| 2017-10-01 | 19 | 0 |
+------------+------+-------------+
| 2017-10-01 | 20 | 0 |
+------------+------+-------------+
| 2017-10-01 | 21 | 0 |
+------------+------+-------------+
| 2017-10-01 | 22 | 0 |
+------------+------+-------------+
| 2017-10-01 | 23 | 0 |
+------------+------+-------------+
| 2017-10-02 | 0 | 0 |
+------------+------+-------------+
| 2017-10-02 | 1 | 0 |
+------------+------+-------------+
| 2017-10-02 | 2 | 0 |
+------------+------+-------------+
| 2017-10-02 | 3 | 0 |
+------------+------+-------------+
| 2017-10-02 | 4 | 0 |
+------------+------+-------------+
| 2017-10-02 | 5 | 0 |
+------------+------+-------------+
| 2017-10-02 | 6 | 0 |
+------------+------+-------------+
| 2017-10-02 | 7 | 0 |
+------------+------+-------------+
| and so on .................. |
+------------+------+-------------+
So, the above result set should contain every day between the given two dates, and each day should have all 24 hours, irrespective off that day had orders and the same for hour (either it had orders or not)
I did it using a nested CTE:
DECLARE #MinDate DATE = '20171001',
#MaxDate DATE = '20171006';
;WITH INNER_CTE as(
SELECT TOP (DATEDIFF(DAY, #MinDate, #MaxDate) + 1)
Date = DATEADD(DAY, ROW_NUMBER() OVER(ORDER BY a.object_id) - 1, #MinDate)
FROM sys.all_objects a
CROSS JOIN sys.all_objects b) ,
OUTER_CTE as (
select * from INNER_CTE
cross apply (
SELECT TOP (24) n = ROW_NUMBER() OVER (ORDER BY [object_id]) -1
FROM sys.all_objects ORDER BY n)) t4
)
select t1.Date, t1.n [Hour], ISNULL(t2.TotalORders,0) TotalOrders from
OUTER_CTE t1
LEFT JOIN orders t2 on t1.Date = t2.[Day] and t1.n = t2.[Hour]
Good Reading about generating sequences using a query here: https://sqlperformance.com/2013/01/t-sql-queries/generate-a-set-1
I prefer to do this with a tally table instead of using loops. The performance is much better. I keep a tally on my system as a view like this.
create View [dbo].[cteTally] as
WITH
E1(N) AS (select 1 from (values (1),(1),(1),(1),(1),(1),(1),(1),(1),(1))dt(n)),
E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --10E+4 or 10,000 rows max
cteTally(N) AS
(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E4
)
select N from cteTally
GO
Now that we have our tally table we can use some basic math to get the desired output. Something along these lines.
declare #Date1 datetime = '2017-10-01';
declare #Date2 datetime = '2017-10-07';
select Day = convert(date, DATEADD(hour, t.N, #Date1))
, Hour = t.N - 1
, TotalOrders = COUNT(o.OrderID)
from cteTally t
left join Orders o on o.OrderDate = DATEADD(hour, t.N, #Date1)
where t.N <= DATEDIFF(hour, #Date1, #Date2)
group by DATEDIFF(hour, #Date1, #Date2)
, t.N
The simplest way is to just use a temporary table or table variable to fill the desired result set, and then count the number of Orders for each row.
declare #Date1 date = '2017-10-01';
declare #Date2 date = '2017-10-07';
declare #Hour int;
declare #Period table (Day Date, Hour Time);
while #Date1 <= #Date2
begin
set #Hour = 0;
while #Hour < 24
begin
insert into #Period (Day, Hour) values (#Date1, TimeFromParts(#Hour,0,0,0,0));
set #Hour = #Hour + 1;
end
set #Date1 = DateAdd(Day, 1, #Date1);
end
select Day, Hour,
(select count(*)
from Orders
where Orders.Day = Period.Day and Orders.Hour = Period.Hour) as TotalOrders
from #Period as Period;

SQL - how do I generate rows for each month based on date ranges in existing dataset?

assume I have a dataset:
rowID | dateStart | dateEnd | Year | Month
121 | 2013-10-03 | 2013-12-03 | NULL | NULL
143 | 2013-12-11 | 2014-03-11 | NULL | NULL
322 | 2014-01-02 | 2014-02-11 | NULL | NULL
And I want sql to generate the following datasource based on the dateStart and the dateEnd. Note the year and month grouping.
rowID | dateStart | dateEnd | Year | Month
121 | 2013-10-03 | 2013-12-03 | 2013 | 10
121 | 2013-10-03 | 2013-12-03 | 2013 | 11
121 | 2013-10-03 | 2013-12-03 | 2013 | 12
143 | 2013-12-11 | 2014-03-11 | 2013 | 12
143 | 2013-12-11 | 2014-03-11 | 2014 | 1
143 | 2013-12-11 | 2014-03-11 | 2014 | 2
143 | 2013-12-11 | 2014-03-11 | 2014 | 3
322 | 2014-01-02 | 2014-02-11 | 2014 | 1
322 | 2014-01-02 | 2014-02-11 | 2014 | 2
I'm having a hard time wrapping my head around this one. Any ideas?
I find it easiest to approach these problems by creating a list of integers and then using that to increment the dates. Here is an example:
with nums as (
select 0 as n
union all
select n + 1 as n
from nums
where n < 11
)
select rowid, datestart, dateend,
year(dateadd(month, n.n, datestart)) as yr,
month(dateadd(month, n.n, datestart)) as mon
from table t join
nums n
on dateadd(month, n.n - 1, datestart) <= dateend;
First, create a tabled-valued function that takes the 2 dates and returns the year and month as a table:
create function dbo.YearMonths(#StartDate DateTime, #EndDate DateTime)
returns #YearMonths table
([Year] int,
[Month] int)
as
begin
set #EndDate = DATEADD(month, 1, #EndDate)
while (#StartDate < #EndDate)
begin
insert into #YearMonths
select YEAR(#StartDate), MONTH(#StartDate)
set #StartDate = DATEADD(month, 1, #StartDate)
end
return
end
As an example the following:
select *
from dbo.YearMonths('1/1/2014', '5/1/2014')
returns:
Then you would join to it like this to get what you wanted:
select m.*, ym.Year, ym.Month
from myTable m
cross apply dbo.YearMonths(dateStart, dateEnd) ym
Try this:
declare #months table(mth int)
insert into #months values(1),(2),(3),(4),(5),(6),(7),(8),(9),(10),(11),(12)
declare #calendar table(yr int,mth int)
insert into #calendar
select distinct year(datestart),mth
from tbl cross join #months
union
select distinct year(dateend),mth
from tbl cross join #months
select t.rowID, t.datestart, t.dateend, y.yr [Year], y.mth [Month]
from
yourtable t
inner join #calendar y on year(datestart) = yr or year(dateend) = yr
where
(mth >= month(datestart) and mth <= month(dateend) and year(datestart) = year(dateend))
or
(year(datestart) < year(dateend))
and
(year(datestart) = yr and mth >= month(datestart) --All months of start year
or
(year(dateend) = yr and mth <= month(dateend))) -- All months of end year
order by t.rowID, [Year],[Month]
We create a 'Calendar table' which lists all the month and year combinations present in the source table. Then, we join the source table to the calendar table based on the year, and filter as required.

Resources