Show the total sale in Hourly basis - sql

I have to generate a report sales report for current day in which user will select start hour and end hour.
It will be max 24 hours.
Start hour will be 4:00 AM and max end hour can be next day 4:00 AM
The below query will return date-time and amount of sale
select s.StartDate ,
CONVERT(DECIMAL(10,2),sum(OrigionalSubTotal)/100.0) Amt from Sale s
where
s.StartDate
BETWEEN '2015-07-03 04:00:01'
and '2015-07-04 04:00:00'
and s.IsSuspend = 0 and s.IsTrainMode = 0 and wasrefunded=0
and IsCancelled = 0
group by S.StartDate
order by s.StartDate
O/p
StartDate Amt
2015-07-03 17:01:15.780 10.00
2015-07-03 18:45:57.360 10.00
2015-07-03 18:48:41.250 20.00
2015-07-03 19:02:50.850 5.00
2015-07-03 19:04:45.090 15.00
2015-07-03 19:18:38.960 10.00
2015-07-03 21:12:25.700 100.00
2015-07-03 21:16:30.730 20.00
2015-07-03 22:21:09.380 30.00
2015-07-03 23:38:32.050 34.00
2015-07-04 00:39:46.790 200.00
2015-07-04 01:00:14.820 106.00
From this I need to take hourly sales
Consider current day is 03-July-2015
Let say user select 16:00 (04:00 PM) - 04:00 AM (next day 04-July-2015).
Then the desired o/p should be like below
Hour Amount
16:00 - 17:00 0.00 -- No sale row between this time
17:00 - 18:00 10.00 -- sale between 17:00 to 17:59
18:00 - 19:00 30.00
19:00 - 20:00 30.00
20:00 - 21:00 0.00 -- No sale row between this time
21:00 - 22:00 120.00
22:00 - 23:00 30.00
23:00 - 0:00 34:00
0:00 - 1:00 200.00
1:00 - 2:00 106.00
2:00 - 3:00 0.00
3:00 - 4:00 0.00
I tried below query to achieve this
select STUFF(CONVERT(CHAR(13), s.StartDate , 120), 11, 1, ' ') ,
DATEPART(HOUR,s.startdate),
CONVERT(DECIMAL(10,2),sum(OrigionalSubTotal)/100.0) from Sale s
where
s.StartDate
BETWEEN '2015-07-03 04:00:01'
and '2015-07-04 04:00:00'
and s.IsSuspend = 0 and s.IsTrainMode = 0 and wasrefunded=0
and IsCancelled = 0
group by STUFF(CONVERT(CHAR(13), s.StartDate , 120), 11, 1, ' '),DATEPART(HOUR,s.startdate)
order by STUFF(CONVERT(CHAR(13), s.StartDate , 120), 11, 1, ' '),DATEPART(HOUR,s.startdate)
O/P is like below
Date Hour Amt
2015-07-03 17 17 10.00
2015-07-03 18 18 30.00
2015-07-03 19 19 30.00
2015-07-03 21 21 120.00
2015-07-03 22 22 30.00
2015-07-04 23 23 34.00
2015-07-04 0 0 200.00
2015-07-04 01 1 106.00
How can I achieve the desired o/p from this. Please help.
Edited
Table structure
Saleid - Unqiueidentifier eg:- 5D0AC452-2F01-E511-8502-0019178A0F32
startDate - datetime eg:- 2015-05-23 13:37:32.880
OrigionalSubTotal - int eg: 5400 (last two digit is decimal)its table of customized software i cannot change the type
SQL Fiddle

declare #starttime datetime = '2015-07-03 16:00:01'
declare #endtime datetime = '2015-07-04 04:00:00'
;with reporttable as
(
select s.StartDate ,
CONVERT(DECIMAL(10,2),sum(OrigionalSubTotal)/100.0) Amt from Sale s
where
s.StartDate
BETWEEN #starttime
and #endtime
and s.IsSuspend = 0 and s.IsTrainMode = 0 and wasrefunded=0
and IsCancelled = 0
group by S.StartDate
),
CTE
AS
(
SELECT 0 AS HR
UNION ALL
SELECT HR+1 AS HR FROM CTE WHERE HR<23
)
,cte1 as
(
SELECT (select cast(min(startdate) as date) from ReportTable) as [date],c.hr as hr,cast(c.hr as varchar(100))+'-'+cast(c.hr+1 as varchar(100)) as period,sum(isnull(originalsubtotal,0)) as total
FROM CTE c
left join ReportTable RT on c.hr = datepart(hh,rt.startDate) and cast(rt.startdate as date) = (select cast(min(startdate) as date) from ReportTable)
group by c.hr,cast(rt.startdate as date)
union all
SELECT (select cast(max(startdate) as date) from ReportTable) as [date],c.hr as hr,cast(c.hr as varchar(100))+'-'+cast(c.hr+1 as varchar(100)) as period,sum(isnull(originalsubtotal,0)) as total
FROM CTE c
left join ReportTable RT on c.hr = datepart(hh,rt.startDate) and cast(rt.startdate as date) = (select cast(max(startdate) as date) from ReportTable)
group by c.hr,cast(rt.startdate as date)
)
select * from cte1 where (cast([date] as date)=cast(#starttime as date) and hr>=datepart(hh,#starttime)) and (cast([date] as date)=cast(#endtime as date) and hr<=datepart(hh,#endtime))
order by [date],hr

I would create a temp table with the 12 Hours in it and then join that against your aggregated data:
-- declare temporary output table
DECLARE #output table (pos int, HourInt int, [Hour] varchar(20))
-- define some variables
DECLARE #counter int = 0
DECLARE #maxCount int = 12
DECLARE #beginHour int = 16
DECLARE #currentHour int = 0
DECLARE #followHour int = 0
-- loop from 16 o'clock to 4 o'clock
WHILE #counter < #maxCount
BEGIN
SET #currentHour = #beginHour + #counter
IF #currentHour > 23 BEGIN SET #currentHour = #currentHour - 24 END
SET #followHour = #currentHour + 1
IF #followHour > 23 BEGIN SET #followHour = #followHour - 24 END
-- create one row in temp table for this hour
INSERT INTO #output SELECT #counter, #currentHour, CAST(#currentHour AS varchar) + ':00 - ' + CAST(#followHour AS varchar) + ':00'
SET #counter = #counter + 1
END
-- Left Join temp table with sale table, rows with no data shows zero
SELECT o.[Hour], ISNULL(CONVERT(DECIMAL(10,2),SUM(s.OrigionalSubTotal)/100.0),0) AS Amt
FROM #output o LEFT JOIN Sale s ON o.HourInt = DATEPART(HOUR,s.startdate)
WHERE (s.StartDate
BETWEEN '2015-07-03 04:00:01'
AND '2015-07-04 04:00:00')
OR s.StartDate IS NULL
GROUP BY DATEPART(HOUR,s.startdate), o.[Hour], o.pos
ORDER BY o.pos
Here is the fiddle for it

I've approached this in two steps:
Get the range of datetime values using MIN and MAX on the data
Use these values to create the full range of dates and hours in a CTE and join them back on to the data.
The CTE will produce this lookup table to join back on to the main data:
| DateVal | HourVal |
|------------|---------|
| 2015-07-03 | 17 |
| 2015-07-03 | 18 |
| 2015-07-03 | 19 |
| 2015-07-03 | 20 |
| 2015-07-03 | 21 |
| 2015-07-03 | 22 |
| 2015-07-03 | 23 |
| 2015-07-04 | 0 |
| 2015-07-04 | 1 |
Runnable sample:
The sample code is commented to explain what each step is doing.
-- dummy table
CREATE TABLE #Sale
(
[StartDate] DATETIME ,
[Amt] INT
);
-- fill dummy data
INSERT INTO #Sale
( [StartDate], [Amt] )
VALUES ( '2015-07-03 17:01:15', 10.00 ),
( '2015-07-03 18:45:57', 10.00 ),
( '2015-07-03 18:48:41', 20.00 ),
( '2015-07-03 19:02:50', 5.00 ),
( '2015-07-03 19:04:45', 15.00 ),
( '2015-07-03 19:18:38', 10.00 ),
( '2015-07-03 21:12:25', 100.00 ),
( '2015-07-03 21:16:30', 20.00 ),
( '2015-07-03 22:21:09', 30.00 ),
( '2015-07-03 23:38:32', 34.00 ),
( '2015-07-04 00:39:46', 200.00 ),
( '2015-07-04 01:00:14', 106.00 );
DECLARE #minDate DATETIME ,
#maxDate DATETIME
-- set min date
SELECT TOP 1
#minDate = StartDate
FROM #Sale
ORDER BY StartDate
-- set max date
SELECT TOP 1
#maxDate = StartDate
FROM #Sale
ORDER BY StartDate DESC
-- cte to iterate between min and max, to generate unique date and hour vals for range
;WITH cte
AS ( SELECT CONVERT(DATE, StartDate) AS DateVal ,
DATEPART(HOUR, StartDate) AS HourVal
FROM #Sale
WHERE StartDate = #minDate
UNION ALL
SELECT CASE WHEN cte.HourVal + 1 > 23
THEN DATEADD(DAY, 1, cte.DateVal)
ELSE cte.DateVal
END AS DateVal ,
CASE WHEN cte.HourVal + 1 = 24 THEN 0
ELSE cte.HourVal + 1
END AS HourVal
FROM cte
WHERE DATEADD(HOUR, CASE WHEN cte.HourVal + 1 = 24 THEN 0
ELSE cte.HourVal + 1
END,
CONVERT(DATETIME, CASE WHEN cte.HourVal + 1 = 24
THEN DATEADD(DAY, 1,
cte.DateVal)
ELSE cte.DateVal
END)) <= #maxDate
)
-- join results of cte to source data on date and hour with sum/group by
SELECT cte.DateVal ,
cte.HourVal ,
-- covers hours with no sales
COALESCE(SUM(s.Amt), 0) AS Amt
FROM cte
LEFT JOIN #Sale s ON cte.DateVal = CONVERT(DATE, s.StartDate)
AND cte.HourVal = DATEPART(HOUR, s.StartDate)
GROUP BY cte.DateVal ,
cte.HourVal
ORDER BY cte.DateVal ,
cte.HourVal
DROP TABLE #Sale
Output
| DateVal | HourVal | Amt |
|------------|---------|-----|
| 2015-07-03 | 17 | 10 |
| 2015-07-03 | 18 | 30 |
| 2015-07-03 | 19 | 30 |
| 2015-07-03 | 20 | 0 |
| 2015-07-03 | 21 | 120 |
| 2015-07-03 | 22 | 30 |
| 2015-07-03 | 23 | 34 |
| 2015-07-04 | 0 | 200 |
| 2015-07-04 | 1 | 106 |
SQL Fiddle Demo
I've ignored the outliers in the above, as there is no data outside the range of the min/max dates. If you need this, you can of course tweak the min/max values as shown in the code below. This modified version will take user input for the date range to produce your desired output:
DECLARE #minDate DATETIME ,
#maxDate DATETIME
-- set min date
SET #minDate = '2015-07-03 16:00:00'
-- set max date
SET #maxDate = '2015-07-04 04:00:00'
-- cte to iterate between min and max, to generate unique date and hour vals for range
;WITH cte
AS ( SELECT CONVERT(DATE, #minDate) AS DateVal ,
DATEPART(HOUR, #minDate) AS HourVal
UNION ALL
SELECT CASE WHEN cte.HourVal + 1 > 23
THEN DATEADD(DAY, 1, cte.DateVal)
ELSE cte.DateVal
END AS DateVal ,
CASE WHEN cte.HourVal + 1 = 24 THEN 0
ELSE cte.HourVal + 1
END AS HourVal
FROM cte
WHERE DATEADD(HOUR, CASE WHEN cte.HourVal + 1 = 24 THEN 0
ELSE cte.HourVal + 1
END,
CONVERT(DATETIME, CASE WHEN cte.HourVal + 1 = 24
THEN DATEADD(DAY, 1,
cte.DateVal)
ELSE cte.DateVal
END)) <= #maxDate
)
-- join results of cte to source data on date and hour with sum/group by
SELECT cte.DateVal ,
CONVERT(NVARCHAR(2),cte.HourVal) + ':00 -' +
CONVERT(NVARCHAR(2),cte.HourVal+1) + ':00' AS [Hours],
-- covers hours with no sales
COALESCE(SUM(s.Amt), 0) AS Amt
FROM cte
LEFT JOIN #Sale s ON cte.DateVal = CONVERT(DATE, s.StartDate)
AND cte.HourVal = DATEPART(HOUR, s.StartDate)
GROUP BY cte.DateVal ,
cte.HourVal
ORDER BY cte.DateVal ,
cte.HourVal
Ouput
| DateVal | Hours | Amt |
|------------|--------------|-----|
| 2015-07-03 | 16:00 -17:00 | 0 |
| 2015-07-03 | 17:00 -18:00 | 10 |
| 2015-07-03 | 18:00 -19:00 | 30 |
| 2015-07-03 | 19:00 -20:00 | 30 |
| 2015-07-03 | 20:00 -21:00 | 0 |
| 2015-07-03 | 21:00 -22:00 | 120 |
| 2015-07-03 | 22:00 -23:00 | 30 |
| 2015-07-03 | 23:00 -24:00 | 34 |
| 2015-07-04 | 0:00 -1:00 | 200 |
| 2015-07-04 | 1:00 -2:00 | 106 |
| 2015-07-04 | 2:00 -3:00 | 0 |
| 2015-07-04 | 3:00 -4:00 | 0 |
| 2015-07-04 | 4:00 -5:00 | 0 |
SQL Fiddle Demo

Select cast('2015-07-03 17:01:15.780' as datetime) as StartDate , 10.00 as Amt
into #temp
union all
Select '2015-07-03 18:45:57.360', 10.00
union all
Select '2015-07-03 18:48:41.250', 20.00
union all
Select '2015-07-03 19:02:50.850', 5.00
union all
Select '2015-07-03 19:04:45.090', 15.00
union all
Select '2015-07-03 19:18:38.960', 10.00
union all
Select '2015-07-03 21:12:25.700', 100.00
union all
Select '2015-07-03 21:16:30.730', 20.00
union all
Select '2015-07-03 22:21:09.380', 30.00
union all
Select '2015-07-03 23:38:32.050', 34.00
union all
Select '2015-07-04 00:39:46.790', 200.00
union all
Select '2015-07-04 01:00:14.820', 106.00
declare #cntr int, #maxCounter int
set #cntr = 0
set #maxCounter = (SELECT datediff(hh,DATEADD(hh,DATEDIFF(hh,0,min(startdate)),0),DATEADD(hh,DATEDIFF(hh,0,Max(startdate))+1,0)) from #temp)
Create table #hrsrange
(
DateID int,
StartDate datetime,
EndDate datetime
)
WHILE (#cntr <= #maxCounter)
BEGIN
Insert into #hrsrange
SELECT distinct #cntr+1 as DateID,
DateAdd(hh,#cntr,(SELECT DATEADD(hh,DATEDIFF(hh,0,min(startdate)),0) from #temp)) as StartDate,
DateAdd(hh,#cntr+1,(SELECT DATEADD(hh,DATEDIFF(hh,0,min(startdate)),0) from #temp)) as EndDate
from #temp
SET #cntr = #cntr + 1
End
select cast(DATEPART(hh,hr.StartDate) as varchar)+' - '+ cast(DATEPART(hh,hr.EndDate) as varchar)
,sum(s.Amt)/100.0 as Amt
from #temp s
left join #hrsrange hr
on cast(s.StartDate as datetime) between hr.StartDate and hr.EndDate
group by DATEPART(hh,hr.StartDate),DATEPART(hh,hr.EndDate)

Related

Use CTE to capture volume counts by day and hour

I want to see current patient volumes by days of the week and by hour based off of their registered Start date and Discharge date.
Ex: John doe Start date: 01-01-2022 13:00:00 ; End date 01-01-2022 16:25:00
I would like the data to show each Hour John doe is in the Facility. So output would look like something like this:
John Doe 01-01-2022 ( Hour) 13
John Doe 01-01-2022 ( Hour) 14
John Doe 01-01-2022 ( Hour) 15
John Doe 01-01-2022 ( Hour) 16
I have my start date and discharge dates in a temp table and thought I could use a CTE to get this done, but not sure how to link the CTE results to my table.
How do I get the breakdown of volumes by hour so I can count how many people are in the facility each hour based off of the start and discharge dates?
DECLARE #minDateTime AS DATETIME;
DECLARE #maxDateTime AS DATETIME;
SET #minDateTime = '2022-05-01 05:28:05.000';
SET #maxDateTime = '2022-05-02 06:50:00.000';
;
WITH Dates_CTE
AS
(SELECT #minDateTime AS Dates
UNION ALL
SELECT Dateadd(hh, 1, Dates)
FROM Dates_CTE
WHERE Dates < Dateadd(hh, -1, #maxDateTime)
)
SELECT --Convert(VARCHAR,Year,Dates)
Dates
,Year(Dates) as 'Year'
,Month(Dates) as 'Month'
,Day(Dates) as 'day'
,Datename(DW,Dates) as 'DayName'
,DATEPART(HOUR,Dates) as 'hh'
FROM Dates_CTE
OPTION (MAXRECURSION 0)
Sample Data
AccountNumber ServiceDateTime RegistrationTypeDischargeDateTime
G111 2021-05-07 10:44:19.000 2021-05-07 14:30:00.000
G222 2021-05-08 09:59:00.000 2021-05-08 10:56:00.000
G333 2021-07-02 11:35:07.000 2021-07-02 11:53:00.000
G444 2021-07-07 07:57:16.000 2021-07-07 13:35:00.000
If we have the enter and leave datestamp for each patient in another table we can join you calendar table and group by hour to find the id's of patients present and count them.
create table inTreatment(
patientid int,
enter datetime,
leave datetime
);
insert into inTreatment values
(1,'2022-05-01 09:00:00','2022-05-01 18:00:00'),
(2,'2022-05-01 11:00:00','2022-05-01 14:00:00'),
(3,'2022-05-01 12:00:00','2022-05-02 15:00:00')
GO
3 rows affected
DECLARE #minDateTime AS DATETIME;
DECLARE #maxDateTime AS DATETIME;
SET #minDateTime = '2022-05-01 05:00:00.000';
SET #maxDateTime = '2022-05-02 06:00:00.000';
;
WITH Dates_CTE
AS
(SELECT #minDateTime AS Dates
UNION ALL
SELECT Dateadd(hh, 1, Dates)
FROM Dates_CTE
WHERE Dates < Dateadd(hh, -1, #maxDateTime)
)
SELECT --Convert(VARCHAR,Year,Dates)
string_agg(patientid,',') patients,
count(patientid) no_pats,
Dates
--,Year(Dates) as 'Year'
--,Month(Dates) as 'Month'
--,Day(Dates) as 'day'
----,Datename(DW,Dates) as 'DayName'
--,DATEPART(HOUR,Dates) as 'hh'
FROM Dates_CTE d
left join InTreatment i
on enter <= Dates and leave >= Dates
group by dates
OPTION (MAXRECURSION 0)
GO
patients | no_pats | Dates
:------- | ------: | :----------------------
null | 0 | 2022-05-01 05:00:00.000
null | 0 | 2022-05-01 06:00:00.000
null | 0 | 2022-05-01 07:00:00.000
null | 0 | 2022-05-01 08:00:00.000
1 | 1 | 2022-05-01 09:00:00.000
1 | 1 | 2022-05-01 10:00:00.000
1,2 | 2 | 2022-05-01 11:00:00.000
1,2,3 | 3 | 2022-05-01 12:00:00.000
1,2,3 | 3 | 2022-05-01 13:00:00.000
1,2,3 | 3 | 2022-05-01 14:00:00.000
1,3 | 2 | 2022-05-01 15:00:00.000
1,3 | 2 | 2022-05-01 16:00:00.000
1,3 | 2 | 2022-05-01 17:00:00.000
1,3 | 2 | 2022-05-01 18:00:00.000
3 | 1 | 2022-05-01 19:00:00.000
3 | 1 | 2022-05-01 20:00:00.000
3 | 1 | 2022-05-01 21:00:00.000
3 | 1 | 2022-05-01 22:00:00.000
3 | 1 | 2022-05-01 23:00:00.000
3 | 1 | 2022-05-02 00:00:00.000
3 | 1 | 2022-05-02 01:00:00.000
3 | 1 | 2022-05-02 02:00:00.000
3 | 1 | 2022-05-02 03:00:00.000
3 | 1 | 2022-05-02 04:00:00.000
3 | 1 | 2022-05-02 05:00:00.000
db<>fiddle here
Given this table and sample data:
CREATE TABLE dbo.Admissions
(
AccountNumber char(4),
ServiceDateTime datetime,
RegistrationTypeDischargeDateTime datetime
);
INSERT dbo.Admissions VALUES
('G111','20210507 10:44:19','20210507 14:30:00');
Here's how I would do it:
DECLARE #min datetime = '20210507 05:28:05',
#max datetime = '20210508 06:50:00';
DECLARE #d tinyint = DATEDIFF(HOUR, #min, #max),
#floor datetime = SMALLDATETIMEFROMPARTS
(YEAR(#min), MONTH(#min), DAY(#min), DATEPART(HOUR, #min), 0);
; -- see sqlblog.org/cte
WITH hours(h) AS
(
SELECT #floor UNION ALL
SELECT DATEADD(HOUR, 1, h)
FROM hours WHERE h <= #max
)
SELECT a.AccountNumber, Date = CONVERT(date, hours.h),
Hour = DATEPART(HOUR, hours.h)
FROM hours INNER JOIN dbo.Admissions AS a
ON a.ServiceDateTime < DATEADD(HOUR, 1, hours.h)
AND a.RegistrationTypeDischargeDateTime >= hours.h
OPTION (MAXRECURSION 32767);
Output:
AccountNumber
Date
Hour
G111
2021-05-07
10
G111
2021-05-07
11
G111
2021-05-07
12
G111
2021-05-07
13
G111
2021-05-07
14
Example db<>fiddle
You may need to tweak <=/</>=/> depending on how you want to handle edge cases (e.g. entry or exit right on the hour, or entry and exit < 1 hour).
For a fast, simple way (vs CTE) CROSS APPLY using a numbers table or a tally function. In this case I'm using dbo.fnTally
select a.AccountNumber, cast(a.ServiceDateTime as date) [Date],
datepart(hour, dateadd(hour, fn.N, cast(a.ServiceDateTime as time))) hr
from #Admissions a
cross apply dbo.fnTally(0, datediff(hour,
a.ServiceDateTime,
a.RegistrationTypeDischargeDateTime)) fn;
dbo.fnTally
CREATE FUNCTION [dbo].[fnTally]
/**********************************************************************************************************************
Jeff Moden Script on SSC: https://www.sqlservercentral.com/scripts/create-a-tally-function-fntally
**********************************************************************************************************************/
(#ZeroOrOne BIT, #MaxN BIGINT)
RETURNS TABLE WITH SCHEMABINDING AS
RETURN WITH
H2(N) AS ( SELECT 1
FROM (VALUES
(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
,(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
,(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
,(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
,(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
,(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
,(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
,(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
,(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
,(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
,(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
,(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
,(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
,(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
,(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
,(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1),(1)
)V(N)) --16^2 or 256 rows
, H4(N) AS (SELECT 1 FROM H2 a, H2 b) --16^4 or 65,536 rows
, H8(N) AS (SELECT 1 FROM H4 a, H4 b) --16^8 or 4,294,967,296 rows
SELECT N = 0 WHERE #ZeroOrOne = 0 UNION ALL
SELECT TOP(#MaxN)
N = ROW_NUMBER() OVER (ORDER BY N)
FROM H8
;

SQL Gaps/Islands Question - Determine if someone has worked for X years without a Y days break

Working on problem for a company in Japan. The government has some rules such as... If you are on a work visa:
You cannot work for more than 3 years at a company without taking 30 days off
You cannot work for the same staffing company for more than 5 years without taking 6 months off
So we want to figure out if anyone will be violating either rule in the next 30/60/90 days.
Sample data (list of contracts):
if object_id('tempdb..#sampleDates') is not null drop table #sampleDates
create table #sampleDates (UserId int, CompanyID int, WorkPeriodStart datetime, WorkPeriodEnd datetime)
insert #sampleDates (UserId, CompanyID, WorkPeriodStart, WorkPeriodEnd) values (27809, 972, '2019-10-10', '2020-10-10')
insert #sampleDates (UserId, CompanyID, WorkPeriodStart, WorkPeriodEnd) values (27853, 484, '2019-10-10', '2020-10-10')
insert #sampleDates (UserId, CompanyID, WorkPeriodStart, WorkPeriodEnd) values (27856, 172, '2019-10-10', '2020-10-10')
insert #sampleDates (UserId, CompanyID, WorkPeriodStart, WorkPeriodEnd) values (27857, 1234, '2015-01-01', '2015-12-31')
insert #sampleDates (UserId, CompanyID, WorkPeriodStart, WorkPeriodEnd) values (27857, 1234, '2016-01-01', '2017-02-28')
insert #sampleDates (UserId, CompanyID, WorkPeriodStart, WorkPeriodEnd) values (27857, 1234, '2017-01-01', '2017-12-31')
insert #sampleDates (UserId, CompanyID, WorkPeriodStart, WorkPeriodEnd) values (27857, 1234, '2018-01-01', '2018-12-31')
insert #sampleDates (UserId, CompanyID, WorkPeriodStart, WorkPeriodEnd) values (27857, 1234, '2019-01-01', '2020-01-31')
insert #sampleDates (UserId, CompanyID, WorkPeriodStart, WorkPeriodEnd) values (27857, 1234, '2020-01-01', '2020-12-31')
insert #sampleDates (UserId, CompanyID, WorkPeriodStart, WorkPeriodEnd) values (27897, 179, '2019-10-10', '2020-10-10')
My first issue is possibly overlapping dates. I am close to a solution on that already, but until I know how to solve the Working X years/ Y Days off issue, I'm not sure what the output of my cte or temp table should look like.
I don't expect anyone to do the work for me, but I want to find an article that can tell me:
How can I determine if someone has taken any breaks in the time period, and for how long (gaps between date ranges)?
How can I figure if they will have worked for 3/5 years without a 30/180 days break in the next 30/60/90 days?
This seemed so simple until I started coding the procedure.
Thanks for any help in advance.
EDIT:
For what it's worth, here's my second working attempt at eliminating overlapping dates (first version used a dense_rank approach and it worked until I screwed something up, went with something simple):
;with CJ as (
select UserId, CompanyID, WorkPeriodStart, WorkPeriodEnd from #sampleDates c
)
select
c.CompanyID,
c.WorkPeriodStart,
min(t1.WorkPeriodEnd) as EndDate
from CJ c
inner join CJ t1 on c.WorkPeriodStart <= t1.WorkPeriodEnd and c.UserId = t1.UserId and c.CompanyID = t1.CompanyID
and not exists(select * from CJ t2 where t1.UserId = t2.UserId and t1.CompanyID = t2.CompanyID and t1.WorkPeriodEnd >= t2.WorkPeriodStart AND t1.WorkPeriodEnd < t2.WorkPeriodEnd)
where not exists(select * from CJ c2 where c.UserId = c2.UserId and c.CompanyID = c2.CompanyID and c.WorkPeriodStart > c2.WorkPeriodStart AND c.WorkPeriodStart <= c2.WorkPeriodEnd)
group by c.UserId, c.CompanyID, c.WorkPeriodStart
order by c.UserId, c.WorkPeriodStart
Disclaimer: This is an incomplete answer.
I can continue later, but this shows how to compute the islands. Then identifying the offender ones shouldn't be that complicated.
See augmented example. I added user 27897 that has three islands: 0, 1, and 2. See below:
create table t (UserId int, CompanyID int, WorkPeriodStart date, WorkPeriodEnd date);
insert t (UserId, CompanyID, WorkPeriodStart, WorkPeriodEnd) values
(27809, 972, '2019-10-10', '2020-10-10'),
(27853, 484, '2019-10-10', '2020-10-10'),
(27856, 172, '2019-10-10', '2020-10-10'),
(27857, 1234, '2015-01-01', '2015-12-31'),
(27857, 1234, '2016-01-01', '2017-02-28'),
(27857, 1234, '2017-01-01', '2017-12-31'),
(27857, 1234, '2018-01-01', '2018-12-31'),
(27857, 1234, '2019-01-01', '2020-01-31'),
(27857, 1234, '2020-01-01', '2020-12-31'),
(27897, 179, '2015-05-28', '2015-09-30'),
(27897, 179, '2017-03-11', '2017-04-30'),
(27897, 188, '2017-02-20', '2017-07-07'),
(27897, 179, '2019-10-10', '2020-10-10');
With this data, the query that computes the island for each row can look like:
select *,
sum(hop) over(partition by UserId order by WorkPeriodStart) as island
from (
select *,
case when WorkPeriodStart > dateadd(day, 1, max(WorkPeriodEnd)
over(partition by UserId
order by WorkPeriodStart
rows between unbounded preceding and 1 preceding))
then 1 else 0 end as hop
from t
) x
order by UserId, WorkPeriodStart
Result:
UserId CompanyID WorkPeriodStart WorkPeriodEnd hop island
------ --------- --------------- ------------- --- ------
27809 972 2019-10-10 2020-10-10 0 0
27853 484 2019-10-10 2020-10-10 0 0
27856 172 2019-10-10 2020-10-10 0 0
27857 1234 2015-01-01 2015-12-31 0 0
27857 1234 2016-01-01 2017-02-28 0 0
27857 1234 2017-01-01 2017-12-31 0 0
27857 1234 2018-01-01 2018-12-31 0 0
27857 1234 2019-01-01 2020-01-31 0 0
27857 1234 2020-01-01 2020-12-31 0 0
27897 179 2015-05-28 2015-09-30 0 0
27897 188 2017-02-20 2017-07-07 1 1
27897 179 2017-03-11 2017-04-30 0 1
27897 179 2019-10-10 2020-10-10 1 2
Now, we can augment this query to get the "worked days" for each island, and the "days off" before each island, by doing:
select *,
datediff(day, s, e) + 1 as worked,
datediff(day, lag(e) over(partition by UserId order by island), s) as prev_days_off
from (
select UserId, island, min(WorkPeriodStart) as s, max(WorkPeriodEnd) as e
from (
select *,
sum(hop) over(partition by UserId order by WorkPeriodStart) as island
from (
select *,
case when WorkPeriodStart > dateadd(day, 1, max(WorkPeriodEnd)
over(partition by UserId
order by WorkPeriodStart
rows between unbounded preceding and 1 preceding))
then 1 else 0 end as hop
from t
) x
) y
group by UserId, island
) x
order by UserId, island
Result:
UserId island s e worked prev_days_off
------ ------ ---------- ---------- ------ -------------
27809 0 2019-10-10 2020-10-10 367 <null>
27853 0 2019-10-10 2020-10-10 367 <null>
27856 0 2019-10-10 2020-10-10 367 <null>
27857 0 2015-01-01 2020-12-31 2192 <null>
27897 0 2015-05-28 2015-09-30 126 <null>
27897 1 2017-02-20 2017-07-07 138 509
27897 2 2019-10-10 2020-10-10 367 825
This result is much close to what you need. That data is actually useful to filter rows according to your criteria.
This script merges any overlapping work periods and then calculates the total days worked within the previous 3 and 5 year periods. Then takes this value and determines if this is more than the maximum working days allowed within that period by UserId and CompanyId for the 3 year limit, and just by UserId for the 5 year limit. (Is this a correct interpretation of the rules in your question?)
From this it then simply adds on 30, 60 and 90 days to that total, to see if that larger value would be over the respective limits. Given the different grouping rules, this would be cleaner as 2 queries (no duplication of UserId for 5 year rule) but the result is still a flag against any offending UserId.
In the example below you can see UserId = 27857 only violating the 5 year rule at present, but then also violating the 3 year rule should they stay on for another 60 days. In addition, UserId = 27858 is currently okay, but will violate the 5 year rule in 60 days.
I have made some assumptions about how you define a year and whether or not your WorkPeriodEnd values are inclusive or not, so do check that your required logic is properly applied.
Script
if object_id('tempdb..#sampleDates') is not null drop table #sampleDates
create table #sampleDates (UserId int, CompanyId int, WorkPeriodStart datetime, WorkPeriodEnd datetime)
insert #sampleDates values
(27809, 972, '2019-10-10', '2020-10-10')
,(27853, 484, '2019-10-10', '2020-10-10')
,(27856, 172, '2019-10-10', '2020-10-10')
,(27857, 1234, '2015-01-01', '2015-12-31')
,(27857, 1234, '2016-01-01', '2017-02-28')
,(27857, 1234, '2017-01-01', '2017-12-31')
,(27857, 1234, '2018-01-01', '2018-12-31')
,(27857, 1234, '2019-01-01', '2020-01-31')
,(27857, 1234, '2020-01-01', '2020-05-31')
,(27858, 1234, '2015-01-01', '2015-12-31')
,(27858, 1234, '2016-01-01', '2017-02-28')
,(27858, 1234, '2017-01-01', '2017-12-31')
,(27858, 1234, '2018-01-01', '2018-12-31')
,(27858, 1234, '2019-09-01', '2020-01-31')
,(27858, 1234, '2020-01-01', '2020-08-31')
,(27859, 12345, '2015-01-01', '2015-12-31')
,(27859, 12346, '2016-01-01', '2017-02-28')
,(27859, 12347, '2017-01-01', '2017-12-31')
,(27859, 12348, '2018-01-01', '2018-12-31')
,(27859, 12349, '2019-01-01', '2020-01-31')
,(27859, 12340, '2020-01-01', '2020-12-31')
,(27897, 179, '2019-10-10', '2020-10-10')
;
declare #3YearsAgo date = dateadd(year,-3,getdate());
declare #3YearWorkingDays int = (365*3)-30;
declare #5YearsAgo date = dateadd(year,-5,getdate());
declare #5YearWorkingDays int = (365*5)-(365/2);
with p as
(
select UserId
,CompanyId
,min(WorkPeriodStart) as WorkPeriodStart
,max(WorkPeriodEnd) as WorkPeriodEnd
from(select l.*,
sum(case when dateadd(day,1,l.PrevEnd) < l.WorkPeriodStart then 1 else 0 end) over (partition by l.UserId, l.CompanyId order by l.WorkPeriodStart rows unbounded preceding) as grp
from(select d.*,
lag(d.WorkPeriodEnd) over (partition by d.UserId, d.CompanyId order by d.WorkPeriodEnd) as PrevEnd
from #sampleDates as d
) as l
) as g
group by grp
,UserId
,CompanyId
)
,d as
(
select UserId
,CompanyId
,sum(case when #3YearsAgo < WorkPeriodEnd
then datediff(day
,case when #3YearsAgo between WorkPeriodStart and WorkPeriodEnd then #3YearsAgo else WorkPeriodStart end
,WorkPeriodEnd
)
else 0
end
) as WorkingDays3YearsToToday
,sum(case when #5YearsAgo < WorkPeriodEnd
then datediff(day
,case when #5YearsAgo between WorkPeriodStart and WorkPeriodEnd then #5YearsAgo else WorkPeriodStart end
,WorkPeriodEnd
)
else 0
end
) as WorkingDays5YearsToToday
from p
group by UserId
,CompanyId
)
select UserId
,CompanyId
,#3YearWorkingDays as Limit3Year
,#5YearWorkingDays as Limit5Year
,WorkingDays3YearsToToday
,WorkingDays5YearsToToday
,case when WorkingDays3YearsToToday > #3YearWorkingDays then 1 else 0 end as Violation3YearNow
,case when sum(WorkingDays5YearsToToday) over (partition by UserId) > #5YearWorkingDays then 1 else 0 end as Violation5YearNow
,case when WorkingDays3YearsToToday + 30 > #3YearWorkingDays then 1 else 0 end as Violation3Year30Day
,case when sum(WorkingDays5YearsToToday) over (partition by UserId) + 30 > #5YearWorkingDays then 1 else 0 end as Violation5Year30Day
,case when WorkingDays3YearsToToday + 60 > #3YearWorkingDays then 1 else 0 end as Violation3Year60Day
,case when sum(WorkingDays5YearsToToday) over (partition by UserId) + 60 > #5YearWorkingDays then 1 else 0 end as Violation5Year60Day
,case when WorkingDays3YearsToToday + 90 > #3YearWorkingDays then 1 else 0 end as Violation3Year90Day
,case when sum(WorkingDays5YearsToToday) over (partition by UserId) + 90 > #5YearWorkingDays then 1 else 0 end as Violation5Year90Day
from d
order by UserId
,CompanyId;
Output
+--------+-----------+------------+------------+--------------------------+--------------------------+-------------------+-------------------+---------------------+---------------------+---------------------+---------------------+---------------------+---------------------+
| UserId | CompanyId | Limit3Year | Limit5Year | WorkingDays3YearsToToday | WorkingDays5YearsToToday | Violation3YearNow | Violation5YearNow | Violation3Year30Day | Violation5Year30Day | Violation3Year60Day | Violation5Year60Day | Violation3Year90Day | Violation5Year90Day |
+--------+-----------+------------+------------+--------------------------+--------------------------+-------------------+-------------------+---------------------+---------------------+---------------------+---------------------+---------------------+---------------------+
| 27809 | 972 | 1065 | 1643 | 366 | 366 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 27853 | 484 | 1065 | 1643 | 366 | 366 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 27856 | 172 | 1065 | 1643 | 366 | 366 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
| 27857 | 1234 | 1065 | 1643 | 1029 | 1760 | 0 | 1 | 0 | 1 | 1 | 1 | 1 | 1 |
| 27858 | 1234 | 1065 | 1643 | 877 | 1608 | 0 | 0 | 0 | 0 | 0 | 1 | 0 | 1 |
| 27859 | 12340 | 1065 | 1643 | 365 | 365 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 |
| 27859 | 12345 | 1065 | 1643 | 0 | 147 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 |
| 27859 | 12346 | 1065 | 1643 | 0 | 424 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 |
| 27859 | 12347 | 1065 | 1643 | 147 | 364 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 |
| 27859 | 12348 | 1065 | 1643 | 364 | 364 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 |
| 27859 | 12349 | 1065 | 1643 | 395 | 395 | 0 | 1 | 0 | 1 | 0 | 1 | 0 | 1 |
| 27897 | 179 | 1065 | 1643 | 366 | 366 | 0 | 0 | 0 | 0 | 0 | 0 | 0 | 0 |
+--------+-----------+------------+------------+--------------------------+--------------------------+-------------------+-------------------+---------------------+---------------------+---------------------+---------------------+---------------------+---------------------+
Here is what I ended up with.
<UselessExplanation>
The issues I kept facing were:
How can I handle any and all date range overlaps and determine just the days within the contract date ranges.
The client is STILL using SQL 2008, so I need some old(er) school tsql.
Ensure that the break times (times between contracts) is accurately calculated.
So I chose to come up with my own solution which is probably dumb, being that it needs to generate a record in memory for every Workday/Candidate combination. I do not see the contracts table going beyond the 5-10k record range. Only reason I'm going this direction.
I created a calendar table with every date in it from 1/1/1980 - 12/31/2050
I then left joined the contract ranges against the calendar table by CandidateId. These will be the dates worked.
Any dates in the calendar table that do not match a date within a contract range is a Break Day.
</UselessExplanation>
Calendar table
if object_id('CalendarTable') is not null drop table CalendarTable
go
create table CalendarTable (pk int identity, CalendarDate date )
declare #StartDate date = cast('1980-01-01' as date)
declare #EndDate date = cast('2050-12-31' as date)
while #StartDate <= #EndDate
begin
insert into CalendarTable ( CalendarDate ) values ( #StartDate )
set #StartDate = dateadd(dd, 1, #StartDate)
end
go
Query for 5 year violations (working 5 years without a 6 month cool off period)
declare #enddate date = dateadd(dd, 30, getdate())
declare #beginDate date = dateadd(dd, -180, dateadd(year, -5, getdate()))
select poss.CandidateId,
min(work.CalendarDate) as FirstWorkDate,
count(work.CandidateId) as workedDays,
sum(case when work.CandidateId is null then 1 else 0 end) as breakDays,
case when count(work.CandidateId) > (365*5) and sum(case when work.CandidateId is null then 1 else 0 end) < (365/2) then 1 else 0 end as Year5Violation,
case when count(work.CandidateId) > (365*5) and sum(case when work.CandidateId is null then 1 else 0 end) < (365/2) then DATEADD(year, 5, min(work.CalendarDate)) else null end as ViolationDate
from
(
select cand.CandidateId, cal.CalendarDate
from CalendarTable cal
join (select distinct c.CandidateId from contracts c where c.WorkPeriodStart is not null and c.WorkPeriodEnd is not null and c.Deleted = 0) cand on 1 = 1
where cal.CalendarDate between #beginDate and #enddate
) as poss
left join
(
select distinct c.CandidateId, cal.CalendarDate
from contracts c
join CalendarTable cal on cal.CalendarDate between c.WorkPeriodStart and c.WorkPeriodEnd
where c.WorkPeriodStart is not null and c.WorkPeriodEnd is not null and c.Deleted = 0
) as work on work.CandidateId = poss.CandidateId and work.CalendarDate = poss.CalendarDate
group by poss.CandidateId

Getting duplicate dates while repeating the rows

I'm trying to rotate or repeat the sfhitId(1,2) between the date range for each employee.
Everything is working fine but I don't know how to stop getting duplicate dates means why I am getting dublicate dates and how can I rid from it...
Can anyone help me with this?
My only requirement is if any employeeid has 1 or more than 1 shift then shiftId should repeat between given date range for each employee.
DECLARE #TempTable TABLE (EmployeeId int, ShiftId int)
INSERT INTO #TempTable
SELECT 1 , 1
UNION ALL
SELECT 1, 3
UNION ALL
SELECT 2, 3
DECLARE #StartDate datetime = '2020-03-05',
#EndDate datetime = '2020-03-09';
WITH theDates AS
(
SELECT #StartDate AS theDate
UNION ALL
SELECT DATEADD(DAY, 1, theDate)
FROM theDates
WHERE DATEADD(DAY, 1, theDate) <= #EndDate
)
SELECT theDate, EmployeeID, SHiftId
FROM theDates
CROSS APPLY #TempTable
ORDER BY EmployeeId, theDate
OPTION (MAXRECURSION 0);
and I want result like this...
theDate EmployeeID SHiftId
2020-03-05 1 1
2020-03-06 1 3
2020-03-07 1 1
2020-03-08 1 3
2020-03-09 1 1
2020-03-05 2 3
2020-03-06 2 3
2020-03-07 2 3
2020-03-08 2 3
2020-03-09 2 3
Use window functions to join the 2 tables:
DECLARE #TempTable TABLE (EmployeeId int, ShiftId int)
INSERT INTO #TempTable
SELECT 1 , 1
UNION ALL
SELECT 1, 3
UNION ALL
SELECT 2, 3
DECLARE #StartDate datetime = '2020-03-05',
#EndDate datetime = '2020-03-09';
WITH
theDates AS (
SELECT 1 rn, #StartDate AS theDate
UNION ALL
SELECT rn + 1, DATEADD(DAY, 1, theDate)
FROM theDates
WHERE DATEADD(DAY, 1, theDate) <= #EndDate
),
theShifts AS (
SELECT *,
ROW_NUMBER() OVER (PARTITION BY EmployeeId ORDER BY ShiftId) rn,
COUNT(*) OVER (PARTITION BY EmployeeId) counter
FROM #TempTable
)
SELECT d.theDate, s.EmployeeID, s.ShiftId
FROM theDates d INNER JOIN theShifts s
ON s.rn % s.counter = d.rn % s.counter
ORDER BY s.EmployeeId, d.theDate
OPTION (MAXRECURSION 0);
See the demo.
Results:
> theDate | EmployeeID | ShiftId
> :---------------------- | ---------: | ------:
> 2020-03-05 00:00:00.000 | 1 | 1
> 2020-03-06 00:00:00.000 | 1 | 3
> 2020-03-07 00:00:00.000 | 1 | 1
> 2020-03-08 00:00:00.000 | 1 | 3
> 2020-03-09 00:00:00.000 | 1 | 1
> 2020-03-05 00:00:00.000 | 2 | 3
> 2020-03-06 00:00:00.000 | 2 | 3
> 2020-03-07 00:00:00.000 | 2 | 3
> 2020-03-08 00:00:00.000 | 2 | 3
> 2020-03-09 00:00:00.000 | 2 | 3

How to return same row multiple times with multiple conditions

My knowledge is pretty basic so your help would be highly appreciated.
I'm trying to return the same row multiple times when it meets the condition (I only have access to select query).
I have a table of more than 500000 records with Customer ID, Start Date and End Date, where end date could be null.
I am trying to add a new column called Week_No and list all rows accordingly. For example if the date range is more than one week, then the row must be returned multiple times with corresponding week number. Also I would like to count overlapping days, which will never be more than 7 (week) per row and then count unavailable days using second table.
Sample data below
t1
ID | Start_Date | End_Date
000001 | 12/12/2017 | 03/01/2018
000002 | 13/01/2018 |
000003 | 02/01/2018 | 11/01/2018
...
t2
ID | Unavailable
000002 | 14/01/2018
000003 | 03/01/2018
000003 | 04/01/2018
000003 | 08/01/2018
...
I cannot pass the stage of adding week no. I have tried using CASE and UNION ALL but keep getting errors.
declare #week01start datetime = '2018-01-01 00:00:00'
declare #week01end datetime = '2018-01-07 00:00:00'
declare #week02start datetime = '2018-01-08 00:00:00'
declare #week02end datetime = '2018-01-14 00:00:00'
...
SELECT
ID,
'01' as Week_No,
'2018' as YEAR,
Start_Date,
End_Date
FROM t1
WHERE (Start_Date <= #week01end and End_Date >= #week01start)
or (Start_Date <= #week01end and End_Date is null)
UNION ALL
SELECT
ID,
'02' as Week_No,
'2018' as YEAR,
Start_Date,
End_Date
FROM t1
WHERE (Start_Date <= #week02end and End_Date >= #week02start)
or (Start_Date <= #week02end and End_Date is null)
...
The new table should look like this
ID | Week_No | Year | Start_Date | End_Date | Overlap | Unavail_Days
000001 | 01 | 2018 | 12/12/2017 | 03/01/2018 | 3 |
000002 | 02 | 2018 | 13/01/2018 | | 2 | 1
000003 | 01 | 2018 | 02/01/2018 | 11/01/2018 | 6 | 2
000003 | 02 | 2018 | 02/01/2018 | 11/01/2018 | 4 | 1
...
business wise i cannot understand what you are trying to achieve. You can use the following code though to calculate your overlapping days etc. I did it the way you asked, but i would recommend a separate table, like a Time dimension to produce a "cleaner" solution
/*sample data set in temp table*/
select '000001' as id, '2017-12-12'as start_dt, ' 2018-01-03' as end_dt into #tmp union
select '000002' as id, '2018-01-13 'as start_dt, null as end_dt union
select '000003' as id, '2018-01-02' as start_dt, '2018-01-11' as end_dt
/*calculate week numbers and week diff according to dates*/
select *,
DATEPART(WK,start_dt) as start_weekNumber,
DATEPART(WK,end_dt) as end_weekNumber,
case
when DATEPART(WK,end_dt) - DATEPART(WK,start_dt) > 0 then (DATEPART(WK,end_dt) - DATEPART(WK,start_dt)) +1
else (52 - DATEPART(WK,start_dt)) + DATEPART(WK,end_dt)
end as WeekDiff
into #tmp1
from
(
SELECT *,DATEADD(DAY, 2 - DATEPART(WEEKDAY, start_dt), CAST(start_dt AS DATE)) [start_dt_Week_Start_Date],
DATEADD(DAY, 8 - DATEPART(WEEKDAY, start_dt), CAST(start_dt AS DATE)) [startdt_Week_End_Date],
DATEADD(DAY, 2 - DATEPART(WEEKDAY, end_dt), CAST(end_dt AS DATE)) [end_dt_Week_Start_Date],
DATEADD(DAY, 8 - DATEPART(WEEKDAY, end_dt), CAST(end_dt AS DATE)) [end_dt_Week_End_Date]
from #tmp
) s
/*cte used to create duplicates when week diff is over 1*/
;with x as
(
SELECT TOP (10) rn = ROW_NUMBER() --modify the max you want
OVER (ORDER BY [object_id])
FROM sys.all_columns
ORDER BY [object_id]
)
/*final query*/
select --*
ID,
start_weekNumber+ (r-1) as Week,
DATEPART(YY,start_dt) as [YEAR],
start_dt,
end_dt,
null as Overlap,
null as unavailable_days
from
(
select *,
ROW_NUMBER() over (partition by id order by id) r
from
(
select d.* from x
CROSS JOIN #tmp1 AS d
WHERE x.rn <= d.WeekDiff
union all
select * from #tmp1
where WeekDiff is null
) a
)a_ext
order by id,start_weekNumber
--drop table #tmp1,#tmp
The above will produce the results you want except the overlap and unavailable columns. Instead of just counting weeks, i added the number of week in the year using start_dt, but you can change that if you don't like it:
ID Week YEAR start_dt end_dt Overlap unavailable_days
000001 50 2017 2017-12-12 2018-01-03 NULL NULL
000001 51 2017 2017-12-12 2018-01-03 NULL NULL
000001 52 2017 2017-12-12 2018-01-03 NULL NULL
000002 2 2018 2018-01-13 NULL NULL NULL
000003 1 2018 2018-01-02 2018-01-11 NULL NULL
000003 2 2018 2018-01-02 2018-01-11 NULL NULL

T-SQL between periods gaps

I have some data on my table like:
DAY | QTY | Name
1/1/2010 | 1 | jack
5/1/2010 | 5 | jack
2/1/2010 | 3 | wendy
5/1/2010 | 2 | wendy
my goal is to have a SP requesting a period of time (example: '2010-1-1' to '2010-1-5'), and get no gaps.
Output example:
DAY | QTY | Name
1/1/2010 | 1 | jack
2/1/2010 | 0 | jack
3/1/2010 | 0 | jack
4/1/2010 | 0 | jack
5/1/2010 | 5 | jack
1/1/2010 | 3 | wendy
2/1/2010 | 0 | wendy
3/1/2010 | 0 | wendy
4/1/2010 | 2 | wendy
5/1/2010 | 0 | wendy
Any gaps is filled with 0-
I know that I can create a loop to will solve me the problem, but is very slow.
Does anyone have any ideas how to optimize this?
WITH DateRangeCTE([d]) AS
(
SELECT
CONVERT(DATETIME, '2010-01-01') AS [d]
UNION ALL
SELECT
DATEADD(d, 1, [d]) AS [d]
FROM
DateRangeCTE
WHERE [d] < DATEADD(d, -1, CONVERT(DATETIME, '2010-1-31'))
)
SELECT
DateRangeCTE.d, YourTable.Qty, YourTable.Name
FROM DateRangeCTE
LEFT JOIN YourTable ON DateRangeCTE.d = YourTable.DAY
If you get the error "The statement terminated. The maximum recursion 100 has been exhausted before statement completion." then use the maxrecursion hint.
Here's a solution that you can use if you don't know the date range in advance. It derives the date range based on the data. The solution uses a numbers table, which uses an existing table in the master database (spt_values).
WITH MinMax AS
( SELECT DISTINCT [Name],
MIN([DAY]) OVER () AS min_day, MAX([DAY]) OVER () AS max_day
FROM mytable
)
, DateRange AS
( SELECT MinMax.[Name], DATEADD(mm, n.number, MinMax.min_day) AS [Date]
FROM MinMax
JOIN master.dbo.spt_values n ON n.type = 'P'
AND DATEADD(mm, n.number, MinMax.min_day) <= MinMax.max_day
)
SELECT dr.[Name], COALESCE(mt.[qty], 0) AS [QTY], dr.Date
FROM DateRange dr
LEFT OUTER JOIN MyTable mt ON dr.Name = mt.Name AND mt.Day = dr.Date
ORDER BY dr.Name, dr.Date ;
Here's another way:
DECLARE #output TABLE (
DateValue datetime,
Qty varchar(50),
LastName varchar(25)
PRIMARY KEY (DateValue, LastName)
)
DECLARE #minMonth datetime, #maxMonth datetime, #lastName varchar(25)
-- whatever your business logic dictates for these
SET #minMonth = '01/01/2010'
SET #maxMonth = '12/01/2010';
with cte as (
SELECT #minMonth AS DateValue
UNION ALL
SELECT DATEADD(month, 1, DateValue)
FROM cte
WHERE DATEADD(month, 1, DateValue) <= #maxMonth
)
INSERT INTO #output (DateValue, Qty, LastName)
SELECT cte.DateValue,
ISNULL(tbl.Alias,0),
tbl.Name
FROM cte LEFT JOIN dbo.YourTable tbl ON tbl.[Day] = cte.Mth
UPDATE #output SET
LastName = CASE WHEN LastName IS NULL THEN #lastName ELSE LastName END,
#lastName = LastName
FROM #output
SELECT * FROM #output
I leave were the correct answer based on the help of everyone
-- dummy data
declare #table table
(
DAY datetime,
QTY int,
Name nvarchar (500) NULL
)
insert #table values('2010-1-1', 1, 'jack')
insert #table values('2010-1-3', 5, 'jack')
insert #table values('2010-1-2', 3 , 'wendy')
insert #table values('2010-1-6', 2 , 'wendy')
-- algorithm
DECLARE #output TABLE (
DAY datetime,
Qty int,
Name varchar(25)
)
DECLARE #minMonth datetime, #maxMonth datetime, #lastName varchar(25)
SET #minMonth = '2010-1-1'
SET #maxMonth = '2010-1-6';
WITH cte AS (
SELECT #minMonth AS DateValue
UNION ALL
SELECT DATEADD(day, 1, DateValue)
FROM cte
WHERE DATEADD(day, 1, DateValue) <= #maxMonth
)
INSERT INTO #output
SELECT
cte.DateValue,
ISNULL(tbl.qty,0),
tbl.Name
FROM
cte cross JOIN
#table tbl
update #output
set qty = 0
where cast(DAY as nvarchar)+'#'+cast(Qty as nvarchar)+'#'+Name in
(
select cast(DAY as nvarchar)+'#'+cast(Qty as nvarchar)+'#'+Name from #output
except
select cast(DAY as nvarchar)+'#'+cast(Qty as nvarchar)+'#'+Name from #table
)
SELECT DAY, sum(qty) as qty, Name
FROM #output
GROUP BY DAY, Name
order by 3,1
and the output that I pretend
2010-01-01 00:00:00.000 1 jack
2010-01-02 00:00:00.000 0 jack
2010-01-03 00:00:00.000 5 jack
2010-01-04 00:00:00.000 0 jack
2010-01-05 00:00:00.000 0 jack
2010-01-06 00:00:00.000 0 jack
2010-01-01 00:00:00.000 0 wendy
2010-01-02 00:00:00.000 3 wendy
2010-01-03 00:00:00.000 0 wendy
2010-01-04 00:00:00.000 0 wendy
2010-01-05 00:00:00.000 0 wendy
2010-01-06 00:00:00.000 2 wendy
Although the solution is correct, doesn't fit my need because recursion limitation.
Hopefully this script will help anyone with similar questions
Thank you to all