Use CTE to capture volume counts by day and hour - sql

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
;

Related

Selecting DateTime Values with intervals greater than 45 days

I have a list of ID's and Dates ordered from oldest to newest. I'd like to select all ID's and Dates where the difference to the next previous date (with matching ID) is 45 days or greater.
202185, 2021-10-01 09:35:000
202185, 2021-10-02 09:36:000
202185, 2021-10-03 09:14:000
202185, 2022-02-01 09:22:000
202185, 2022-02-02 09:23:000
301133, 2021-11-01 09:35:000
301133, 2021-11-02 09:36:000
301133, 2021-11-03 09:14:000
301133, 2021-12-06 09:22:000
301133, 2022-01-25 09:23:000
SELECTION RETURNS:
202185, 2022-02-01 09:22:000
301133, 2022-01-25 09:23:000
Is there an efficient way to handle this using SQL Server?
Thanks!
select id, date
from (
select id, [date],
datediff(day, lag([date]) over (partition by id order by [date]), [date]) as daydiff
from [MyTable]
) t
where t.dayDiff >= 45
order by id, [date];
See it here:
https://dbfiddle.uk/?rdbms=sqlserver_2019&fiddle=69b5b976154d52a04333fbb1b771b3e1
SELECT A.* FROM MyTable A
WHERE A.date >
( SELECT DATEADD(day, 45, MAX(b.date))
FROM MyTable B
WHERE B.ID = A.ID
AND B.date < A.date
)
See it here:
https://dbfiddle.uk/?rdbms=sqlserver_2019&fiddle=038a3d02461f0ea54a14140e6be22370
RHere is a solution using LAG() in a CTE.
CREATE TABLE idz(
ID INT,
DAT DATETIME);
INSERT INTO idz VALUES
(202185, '2021-10-01 09:35:000'),
(202185, '2021-10-02 09:36:000'),
(202185, '2021-10-03 09:14:000'),
(202185, '2022-02-01 09:22:000'),
(202185, '2022-02-02 09:23:000'),
(301133, '2021-11-01 09:35:000'),
(301133, '2021-11-02 09:36:000'),
(301133, '2021-11-03 09:14:000'),
(301133, '2021-12-06 09:22:000'),
(301133, '2022-01-25 09:23:000');
WITH i AS
( SELECT
ID,
DAT,
LAG(DAT) OVER ( PARTITION BY ID
ORDER BY DAT) DAT_1
FROM idz)
SELECT
ID,
DAT,
DAT_1,
DATEDIFF(DAY,DAT_1,DAT) DD
FROM i
WHERE DATEDIFF(DAY,DAT_1,DAT)>= 45
ID | DAT | DAT_1 | DD
-----: | :---------------------- | :---------------------- | --:
202185 | 2022-02-01 09:22:00.000 | 2021-10-03 09:14:00.000 | 121
301133 | 2022-01-25 09:23:00.000 | 2021-12-06 09:22:00.000 | 50
db<>fiddle here
Credit due to shawnt00

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

Show the total sale in Hourly basis

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)

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