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