Related
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
;
i have a table containing 3 columns. ID,Start_Date,End_Date.i want to print all the days between Start_Date and End_Date along with ID.
For Example i have table
+----+------------+------------+
| ID | Start_Date | End_Date |
+----+------------+------------+
| 1 | 2017-01-01 | 2017-01-05 |
+----+------------+------------+
and i want result like
+----+------------+
| ID | Date |
+----+------------+
| 1 | 2017-01-01 |
| 1 | 2017-01-02 |
| 1 | 2017-01-03 |
| 1 | 2017-01-04 |
| 1 | 2017-04-05 |
+----+------------+
Use Common table expression :
DECLARE #StartDT DATETIME = '2017-01-01'
DECLARE #EndDT DATETIME = '2017-01-05'
DECLARE #Id INT = 1
;WITH CTE (_id , _Date)AS
(
SELECT #Id , #StartDT
UNION ALL
SELECT #Id , DATEADD(DAY,1,_Date)
FROM CTE
WHERE _Date < #EndDT
)
SELECT * FROM CTE
Create a so called tally table (see e. g. here: https://dwaincsql.com/2014/03/27/tally-tables-in-t-sql/) and use it to create all dates between "from" and "to" date.
SELECT TOP 1000000 N=IDENTITY(INT, 1, 1)
INTO dbo.Tally
FROM master.dbo.syscolumns a CROSS JOIN master.dbo.syscolumns b;
go
declare #dateFrom datetime = '20170101';
declare #dateTo datetime = '20170105';
select dateadd(day, N - 1, #dateFrom)
from Tally
where N between 1 and datediff(day, #dateFrom, #dateTo) + 1
Or:
select dateadd(day, t.N - 1, o.DateFrom)
from Tally t
cross join OtherTable o
where t.N between 1 and datediff(day, o.DateFrom, o.DateTo) + 1
A tally table is very useful for such cases, it could also be filled with dates in a second column, starting from 1900-01-01 or so.
DECLARE #StartDT DATETIME = '2017-01-01'
DECLARE #EndDT DATETIME = '2017-01-05'
DECLARE #Id INT = 1
SELECT RANK() OVER (
ORDER BY (SELECT 1)) AS SeqNo
,CAST (Start_Date AS DATE) AS Start_Date
FROM (
SELECT #StartDT + Row_Number() OVER (ORDER BY Rno) - 1 AS Start_Date
FROM (
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT 0)) - 1 AS Rno
FROM master..spt_values
) Dt
) Dt2
WHERE Dt2.Start_Date <= #EndDT
OutPut
SeqNo Start_Date
--------------------
1 2017-01-01
1 2017-01-02
1 2017-01-03
1 2017-01-04
1 2017-01-05
Let's assume I have a table structure like this.
CheckIn
- int checkInId pk
- int companyPositionId
- Date checkInDate
Let's say I want to get a count of all check ins for the last 7 days from a given date. What would be the best way to do this? Right now I'm making a query for each of the 7 dates AND the company positions. Currently this is too slow because there could be many companyPositions * 7 days. How can I roll this up into one query?
Would it be easiest to generate the last 7 days dates and construct a long query? Could I then group count by a date range for each of the 7 days?
An ideal result back could look like:
companyPositionId, date1Count, date2Count, date3Count, date4Count, date5Count, date6Count.
Example Data:
checkInId | companyPositionId | checkInDate
1 | 1 | 1970-01-01
2 | 1 | 1970-01-02
3 | 1 | 1970-01-03
4 | 1 | 1970-01-04
5 | 1 | 1970-01-05
6 | 1 | 1970-01-06
7 | 1 | 1970-01-07
8 | 2 | 1970-01-01
9 | 2 | 1970-01-02
10 | 2 | 1970-01-03
11 | 2 | 1970-01-04
12 | 2 | 1970-01-05
13 | 2 | 1970-01-06
14 | 2 | 1970-01-07*
15 | 2 | 1970-01-07*
My current query is this:
SELECT * FROM CheckIn
WHERE (startDate) <= (inputDate)
AND (inputDate) <= (endDate)
AND companyPositionId = (companyPositionId);
I then loop through each startDate/endDate that is generated from the beginning of the day and end of that day. And then each of the companyPositionId's.
Ideal result:
companyPositionId | date1Count | date2Count | date3Count | date4Count | date5Count | date6Count | date7Count
1 | 1 | 1 | 1 | 1 | 1 | 1 | 1
2 | 1 | 1 | 1 | 1 | 1 | 1 | 2
You can do this with the PIVOT command, or with conditional SUMs:
DECLARE #my_date DATE = CAST(GETDATE() AS DATE)
SELECT
companyPositionId,
SUM(CASE WHEN CAST(checkInDate AS DATE) = DATEADD(DAY, -7, #my_date) THEN 1 ELSE 0 END) AS date1Count,
SUM(CASE WHEN CAST(checkInDate AS DATE) = DATEADD(DAY, -6, #my_date) THEN 1 ELSE 0 END) AS date2Count,
SUM(CASE WHEN CAST(checkInDate AS DATE) = DATEADD(DAY, -5, #my_date) THEN 1 ELSE 0 END) AS date3Count,
SUM(CASE WHEN CAST(checkInDate AS DATE) = DATEADD(DAY, -4, #my_date) THEN 1 ELSE 0 END) AS date4Count,
SUM(CASE WHEN CAST(checkInDate AS DATE) = DATEADD(DAY, -3, #my_date) THEN 1 ELSE 0 END) AS date5Count,
SUM(CASE WHEN CAST(checkInDate AS DATE) = DATEADD(DAY, -2, #my_date) THEN 1 ELSE 0 END) AS date6Count,
SUM(CASE WHEN CAST(checkInDate AS DATE) = DATEADD(DAY, -1, #my_date) THEN 1 ELSE 0 END) AS date7Count
FROM
CheckIn
WHERE
checkInDate BETWEEN DATEADD(DAY, -7, GETDATE()) AND DATEADD(DAY, -1, GETDATE())
GROUP BY
companyPositionId
If your checkInDate has a time component then you'll need to account for that.
I declared the #my_date variable just to avoid having to repeat that expression in the query a bunch of times, but you could replace the variable with that expression and it would work as well. You could also use BETWEEN which might have better performance since the optimizer could then potentially use an index on your checkInDate. Just calculate midnight/11:59:59 of each of the days instead of looking for equality.
Look at
Declare #d date= '2016-05-05'; -- parameter, we need this day and six previous
Select
companyPositionId
,date1Count = count(case checkInDate when #d then checkInId end)
,date2Count = count(case checkInDate when dateadd(d,-1,#d) then checkInId end)
--...
from checkIn
where checkInDate between dateadd(d,-6,#d) and #d
group by companyPositionId
order by companyPositionId;
Here's a quick pivot example assuming you're looking for the previous 7 days of the #inputDate. This should work even with the time component since it goes off a DATEDIFF with days only.
DECLARE #InputDate DATE = '1/8/1970'
;WITH CTE AS (
SELECT *, DATEDIFF(DD, checkInDate, #InputDate) AS DaysAgo FROM #CheckIns
)
SELECT
companyPositionId,
SUM([7]) AS date1Count,
SUM([6]) AS date2Count,
SUM([5]) AS date3Count,
SUM([4]) AS date4Count,
SUM([3]) AS date5Count,
SUM([2]) AS date6Count,
SUM([1]) AS date7Count -- Reversed columns since you wanted 7 to be most recent day
FROM CTE
PIVOT (COUNT(checkInId) FOR DaysAgo IN ([1], [2], [3], [4], [5], [6], [7])) PVT
GROUP BY
companyPositionId
This gives the following results matching your desired output:
companyPositionId date1Count date2Count date3Count date4Count date5Count date6Count date7Count
----------------- ----------- ----------- ----------- ----------- ----------- ----------- -----------
1 1 1 1 1 1 1 1
2 1 1 1 1 1 1 2
Create Data:
CREATE TABLE #checkin (
checkInId INT IDENTITY(1, 1),
companyPositionId int,
checkInDate DATE
)
DECLARE #counter INT = 100
WHILE #counter > 0
BEGIN
INSERT INTO #checkin
( companyPositionId, checkInDate )
VALUES ( RAND() * 10 + 1, -- 10 possible companies?
DATEADD(day, RAND()*-7, GETDATE()) -- random days in the last 2 weeks
)
SET #counter = #counter - 1
END
Logic starts here:
DECLARE
#now DATETIME = GETDATE(),
#days INT = -7 -- Our interval of interest
This logic is partially borrowed from Bogdan Sahlean, his results were getting all '1' for me, so I changed the dayNum calculation
SELECT *
FROM (
SELECT companyPositionId, checkInId, 1 + CAST(DATEDIFF(DAY,checkInDate, #now) AS INT) AS DayNum
FROM #CheckIn
WHERE checkInDate BETWEEN CAST(DATEADD(DAY, #days, #now) AS DATE) AND #now
) AS ps -- Pivot source
PIVOT ( COUNT(checkInId) FOR DayNum IN ([1], [2], [3], [4], [5], [6], [7]) ) AS p
DROP TABLE #checkin
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)
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