Test Data Setup:
create table #Data(ID INT IDENTITY PRIMARY KEY CLUSTERED, EventDateTime datetime, Instance int)
insert into #Data(EventDateTime)
values
('2018-10-16T01:37:23.173'),
('2018-10-16T01:37:31.447'),
('2018-10-16T01:37:36.577'),
('2018-10-16T01:37:45.457'),
('2018-10-16T01:37:48.860'),
('2018-10-16T01:38:06.407'),
('2018-10-16T01:38:11.030'),
('2018-10-16T01:38:15.470'),
('2018-10-16T01:38:19.133'),
('2018-10-16T01:38:27.830')
Desired Output (which I can get):
Did this using quirky update:
declare #Instance int = 1
declare #StartDate datetime
select top 1 #StartDate = EventDateTime from #Data order by id asc
update t
set #Instance = Instance = case when ABS(datediff(MILLISECOND,t.EventDateTime, #StartDate)) < 10000 then #Instance else #Instance+1 end
, #StartDate = case when ABS(datediff(MILLISECOND,t.EventDateTime, #StartDate)) < 10000 then #StartDate else EventDateTime end
from #Data t
This is what it's doing in this specific example and it's the logic required:
Check row 1, if it’s within 10 seconds of #StartDate (Yes) then keep both the #InstanceID and #StartDate the same and set Instance to #InstanceID
Check row 2, if it’s within 10 seconds of #StartDate (still the original one, so Yes) then keep both the #InstanceID and #StartDate the same and set Instance to #InstanceID
Check row 3, if it’s within 10 seconds of #StartDate (still the original one, so No) then increment #instanceID by one and set Instance to that, and reset #StartDate to the Transactiondate of this record
Check row 4, if it’s within 10 seconds of #StartDate (now the one from row 3, so Yes) then keep both the #InstanceID and #StartDate the same and set Instance to #InstanceID
Check row 5, if it’s within 10 seconds of #StartDate (still the one from row 3, so No) then increment #instanceID by one and set Instance to that, and reset #StartDate to the Transactiondate of this record
etc
etc
The Question:
This can be done using a while loop, a cursor, or a quirky update. Is there a truly set based method for doing this (i.e none of the mentioned methods)?
Recursive CTE query can be a solution. No updates required at all.
DECLARE #OffsetMsec int = 10000;
WITH d1 (ID, EventDateTime, IsStarted, StartDate)
AS (
SELECT TOP 1 ID, EventDateTime, 1, EventDateTime FROM #Data
UNION ALL
SELECT d2.ID, d2.EventDateTime,
CASE WHEN ABS(DATEDIFF(MILLISECOND, d1.StartDate, d2.EventDateTime)) < #OffsetMsec
THEN 0 ELSE 1 END AS IsStarted,
CASE WHEN ABS(DATEDIFF(MILLISECOND, d1.StartDate, d2.EventDateTime)) < #OffsetMsec
THEN d1.StartDate ELSE d2.EventDateTime END AS StartDate
FROM #Data d2 INNER JOIN d1 ON d2.ID = d1.ID + 1
),
d2 (ID, EventDateTime, Instance)
AS (
SELECT ID, EventDateTime, IsStarted, StartDate,
(SELECT SUM(IsStarted) FROM d1 d11 WHERE d11.ID <= d1.ID) AS Instance
FROM d1
)
SELECT * FROM d2
Result
ID EventDateTime Instance
----------- ----------------------- -----------
1 2018-10-16 01:37:23.173 1
2 2018-10-16 01:37:31.447 1
3 2018-10-16 01:37:36.577 2
4 2018-10-16 01:37:45.457 2
5 2018-10-16 01:37:48.860 3
6 2018-10-16 01:38:06.407 4
7 2018-10-16 01:38:11.030 4
8 2018-10-16 01:38:15.470 4
9 2018-10-16 01:38:19.133 5
10 2018-10-16 01:38:27.830 5
Related
I'm writing a function in SQL Server 2012 that will need to know the number of 3 specific days of the month that have passed since a given date. I can do this with a while loop, but its slow and I was looking for a better way.
Here is what I have so far:
Let's assume that GETDATE() = '11/14/2016' and #productDate = '10/1/2016'
--Get the number of "units" that have passed since the date on the label
DECLARE #unitCount INT = 0;
DECLARE #countingDate DATE
SET #countingDate = DATEADD(DAY,1,#productDate);--add 1 to prevent counting the date on the label as the first unit
WHILE (#countingDate < CAST(GETDATE() As date ))
BEGIN
SELECT #unitCount = #unitCount +
CASE
WHEN DAY(#countingDate) = 1 OR DAY(#countingDate) = 10 OR DAY(#countingDate) = 20 THEN 1
ELSE 0
END
SET #countingDate = DATEADD(DAY,1,#countingDate);
END
This will result in #unitCount = 4
GETDATE() of '11/20/2016' would result in #unitCount = 5
Without using a numbers table
create function dbo.fn_DateCounter
(
#datefrom date,
#dateto date
)
returns int
as
begin
return
-- number of complete months
3 *
(
(DATEPART(YYYY, #dateto) * 12 + DATEPART(MM, #dateto))
-(DATEPART(YYYY, #datefrom) * 12 + DATEPART(MM, #datefrom))
- 1
)
-- add on the extras from the first month
+ case when DATEPART(DD, #datefrom) < 10 then 2
when DATEPART(DD, #datefrom) < 20 then 1
else 0
end
-- add on the extras from the last month
+ case when DATEPART(DD, #dateto) > 20 then 3
when DATEPART(DD, #dateto) > 10 then 2
else 1
end
end
go
select dbo.fn_DateCounter('01-jan-2000','01-jan-2000') -- 0
select dbo.fn_DateCounter('01-jan-2000','10-jan-2000') -- 0
select dbo.fn_DateCounter('01-jan-2000','11-jan-2000') -- 1
select dbo.fn_DateCounter('01-jan-2000','20-jan-2000') -- 1
select dbo.fn_DateCounter('01-jan-2000','21-jan-2000') -- 2
select dbo.fn_DateCounter('11-jan-2000','21-jan-2000') -- 1
select dbo.fn_DateCounter('11-jan-2000','21-feb-2000') -- 4
select dbo.fn_DateCounter('01-jan-2000','01-jan-2001') -- 36
select dbo.fn_DateCounter('01-jan-2000','11-jan-2001') -- 37
You can use a combination of sum, case, and the dbo.spt_values table:
declare #productDate datetime = '11/01/2016',
#unitCount int
;with nums as ( -- use a CTE to build a number list
select top 1000 number from master..spt_values
)
select #unitCount = sum(
case when day(dateadd(day, n, #productDate)) in (1, 10, 20)
then 1 else 0 end
) -- add 1 for each 1,10,20 we find
from (
select n = row_number() over (order by nums.number)
from nums cross join nums as num -- 1000*1000 = 1 million rows
) n
where dateadd(day, n, #productDate) < getdate()
select #unitCount
This will grab each date between #productDate and getdate(). The case statement will select 1 for each 1/10/20, and 0 for every other date. Finally, we take the sum of the result.
For 11/1 - 11/11, it returns 1.
For 1/1 - 11/11, the result is 31.
EDIT: In the CTE (with nums as...), we select 1-1000, and then we do a cross join which gives us a million records to work with. The answer is still limited, but now you can go ~2700 years with this.
Title sounds confusing but let me please explain:
I have a table that has two columns that provide a date range, and one column that provides a value. I need to query that table and "detail" the data such as this
Is it possible to do only using TSQL?
Additional Info
The table in question is about 2-3million records long (and growing)
Assuming the range of dates is fairly narrow, an alternative is to use a recursive CTE to create a list of all dates in the range and then join interpolate to it:
WITH LastDay AS
(
SELECT MAX(Date_To) AS MaxDate
FROM MyTable
),
Days AS
(
SELECT MIN(Date_From) AS TheDate
FROM MyTable
UNION ALL
SELECT DATEADD(d, 1, TheDate) AS TheDate
FROM Days CROSS JOIN LastDay
WHERE TheDate <= LastDay.MaxDate
)
SELECT mt.Item_ID, mt.Cost_Of_Item, d.TheDate
FROM MyTable mt
INNER JOIN Days d
ON d.TheDate BETWEEN mt.Date_From AND mt.Date_To;
I've also assumed an that date from and date to represent an inclusive range (i.e. includes both edges) - it is unusual to use inclusive BETWEEN on dates.
SqlFiddle here
Edit
The default MAXRECURSION on a recursive CTE in Sql Server is 100, which will limit the date range in the query to a span of 100 days. You can adjust this to a maximum of 32767.
Also, if you are filtering just a smaller range of dates in your large table, you can adjust the CTE to limit the number of days in the range:
WITH DateRange AS
(
SELECT CAST('2014-01-01' AS DATE) AS MinDate,
CAST('2014-02-16' AS DATE) AS MaxDate
),
Days AS
(
SELECT MinDate AS TheDate
FROM DateRange
UNION ALL
SELECT DATEADD(d, 1, TheDate) AS TheDate
FROM Days CROSS APPLY DateRange
WHERE TheDate <= DateRange.MaxDate
)
SELECT mt.Item_ID, mt.Cost_Of_Item, d.TheDate
FROM MyTable mt
INNER JOIN Days d
ON d.TheDate BETWEEN mt.Date_From AND mt.Date_To
OPTION (MAXRECURSION 0);
Update Fiddle
This can be achieved using Cursors.
I've simulated the test data provided and created another table with the name "DesiredTable" to store the data inside, and created the following cusror which achieved exactly what you are looking for:
SET NOCOUNT ON;
DECLARE #ITEM_ID int, #COST_OF_ITEM Money,
#DATE_FROM date, #DATE_TO date;
DECLARE #DateDiff INT; -- holds number of days between from & to columns
DECLARE #counter INT = 0; -- for loop counter
PRINT '-------- Begin the Date Expanding Cursor --------';
-- defining the cursor target statement
DECLARE Date_Expanding_Cursor CURSOR FOR
SELECT [ITEM_ID]
,[COST_OF_ITEM]
,[DATE_FROM]
,[DATE_TO]
FROM [dbo].[OriginalTable]
-- openning the cursor
OPEN Date_Expanding_Cursor
-- fetching next row data into the declared variables
FETCH NEXT FROM Date_Expanding_Cursor
INTO #ITEM_ID, #COST_OF_ITEM, #DATE_FROM, #DATE_TO
-- if next row is found
WHILE ##FETCH_STATUS = 0
BEGIN
-- calculate the number of days in between the date columns
SELECT #DateDiff = DATEDIFF(day,#DATE_FROM,#DATE_TO)
-- reset the counter to 0 for the next loop
set #counter = 0;
WHILE #counter <= #DateDiff
BEGIN
-- inserting rows inside the new table
insert into DesiredTable
Values (#COST_OF_ITEM, DATEADD(day,#counter,#DATE_FROM))
set #counter = #counter +1
END
-- fetching next row
FETCH NEXT FROM Date_Expanding_Cursor
INTO #ITEM_ID, #COST_OF_ITEM, #DATE_FROM, #DATE_TO
END
-- cleanup code
CLOSE Date_Expanding_Cursor;
DEALLOCATE Date_Expanding_Cursor;
The code fetches every row from your original table, then it calculates the number of days between DATE_FROM and DATE_TO columns, then using this number the script will create identical rows to be inserted inside the new table DesiredTable.
give it a try and let me know of the results.
You can generate an increment table and join it to your date From:
Query:
With inc(n) as (
Select ROW_NUMBER() over (order by (select 1)) -1 From (
Select 1 From (values(1), (1), (1), (1), (1), (1), (1), (1), (1), (1)) as x1(n)
Cross Join (values(1), (1), (1), (1), (1), (1), (1), (1), (1), (1)) as x2(n)
) as x(n)
)
Select item_id, cost, DATEADD(day, n, dateFrom), n From #dates d
Inner Join inc i on n <= DATEDIFF(day, dateFrom, dateTo)
Order by item_id
Output:
item_id cost Date n
1 100 2014-01-01 00:00:00.000 0
1 100 2014-01-02 00:00:00.000 1
1 100 2014-01-03 00:00:00.000 2
2 105 2014-01-08 00:00:00.000 2
2 105 2014-01-07 00:00:00.000 1
2 105 2014-01-06 00:00:00.000 0
2 105 2014-01-09 00:00:00.000 3
3 102 2014-02-14 00:00:00.000 3
3 102 2014-02-15 00:00:00.000 4
3 102 2014-02-16 00:00:00.000 5
3 102 2014-02-11 00:00:00.000 0
3 102 2014-02-12 00:00:00.000 1
3 102 2014-02-13 00:00:00.000 2
Sample Data:
declare #dates table(item_id int, cost int, dateFrom datetime, dateTo datetime);
insert into #dates(item_id, cost, dateFrom, dateTo) values
(1, 100, '20140101', '20140103')
, (2, 105, '20140106', '20140109')
, (3, 102, '20140211', '20140216');
Yet another way is to create and maintain calendar table, containing all dates for many years (in our app we have table for 30 years or so, extending every year). Then you can just link to calendar:
select <whatever you need>, calendar.day
from <your tables> inner join calendar on calendar.day between <min date> and <max date>
This approach allows to include additional information (holidays etc) in calendar table - sometimes very helpful.
Let's start with a sample of the data I'm working with:
Policy No | start date
1 | 2/15/2006
1 | 2/15/2009
1 | 2/15/2012
2 | 3/15/2006
3 | 3/19/2006
3 | 3/19/2012
4 | 3/31/2006
4 | 3/31/2009
I'm trying to write code in SQL Server 2008 that counts a few things. The principle is that the policyholder's earliest start date is when the policy began. Every three years an increase is offered to the client. If they agree to the increase, the start date is refreshed with the same date as the original, three years later. If they decline, nothing is added to the database at all.
I'm trying to not only count the number of times a customer accepted the offer (or increased the start date by three years), but separate it out by first offer or second offer. Taking the original start date and dividing the number of days between now and then by 1095 gets me the total number of offers, so I've gotten that far. What I really want it to do is compare each policy number to the one before it to see if it's the same (it's already ordered by policy number), then count the date change in a new "accepted" column and count the times it didn't change but could have as "declined".
Is this a case where I would need to self-join the table to itself to compare the dates? Or is there an easier way?
are you looking for this :-
Set Nocount On;
Declare #Test Table
(
PolicyNo Int
,StartDate Date
)
Declare #PolicyWithInc Table
(
RowId Int Identity(1,1) Primary Key
,PolicyNo Int
,StartDate Date
)
Insert Into #Test(PolicyNo,StartDate) Values
(1,'2/15/2006')
,(1,'2/15/2009')
,(1,'2/15/2012')
,(2,'3/15/2006')
,(3,'3/19/2006')
,(3,'3/19/2012')
,(4,'3/31/2006')
,(4,'3/31/2009')
Insert Into #PolicyWithInc(PolicyNo,StartDate)
Select t.PolicyNo
,t.StartDate
From #Test As t
Select pw.PolicyNo
,Sum(Case When Datediff(Year,t.StartDate, pw.StartDate) = 3 Then 1 Else 0 End) As DateArrived
,Sum(Case When Datediff(Year,t.StartDate, pw.StartDate) > 3 Then 1 Else 0 End) As DateNotArrived
,Sum(Case When Isnull(Datediff(Year,t.StartDate,pw.StartDate),0) = 3 Then 1 Else 0 End) As Years3IncrementCount
From #PolicyWithInc As pw
Left Join #PolicyWithInc As t On pw.PolicyNo = t.PolicyNo And pw.RowId = (t.RowId + 1)
Group By pw.PolicyNo
Probably below could help:
Set Nocount On;
Declare #Test Table
(
PolicyNo Int
,StartDate Date
)
Insert Into #Test(PolicyNo,StartDate) Values
(1,'2/15/2006')
,(1,'2/15/2009')
,(1,'2/15/2012')
,(2,'3/15/2006')
,(3,'3/19/2006')
,(3,'3/19/2012')
,(4,'3/31/2006')
,(4,'3/31/2009')
select PolicyNo, StartDate, dateadd(yy, 3, StartDate)Offer1, dateadd(yy, 6, StartDate)Offer2, dateadd(yy, 9, StartDate)Offer3 from
(select * , row_number() over (partition by PolicyNo order by StartDate) rn from #Test)A
where rn = 1
select
count(*) * 3 TotalOffersMade,
count(Data1.StartDate) FirstOfferAccepted,
count(Data2.StartDate) SecondOfferAccepted,
count(Data3.StartDate) ThirdOfferAccepted,
count(*) - count(Data1.StartDate) FirstOfferDeclined,
count(*) - count(Data2.StartDate) SecondOfferDeclined,
count(*) - count(Data3.StartDate) ThirdOfferDeclined
from
(
select PolicyNo, StartDate, dateadd(yy, 3, StartDate)Offer1, dateadd(yy, 6, StartDate)Offer2, dateadd(yy, 9, StartDate)Offer3 from
(select * , row_number() over (partition by PolicyNo order by StartDate) rn from #Test)A
where rn = 1
)Offers
LEFT JOIN
#Test Data1
on Offers.PolicyNo = Data1.PolicyNo and Offers.Offer1 = Data1.StartDate
LEFT JOIN
#Test Data2
on Offers.PolicyNo = Data2.PolicyNo and Offers.Offer2 = Data2.StartDate
LEFT JOIN
#Test Data3
on Offers.PolicyNo = Data3.PolicyNo and Offers.Offer3 = Data3.StartDate
I have an ASP.NET website with a C# back-end using MSSQL SQL Server 2008 for its content.
I have written the following stored procedure which checks for any records within the last 7 days and then returns what it finds.
ALTER PROCEDURE [dbuser].[GetResponses]
(
#QUEST_ID int
)
AS
SELECT DateAdded, SUM(Responses) AS responseCount
FROM ActiveResponses
WHERE #QUEST_ID = QuestionnaireID AND DateAdded >= dateadd(day,datediff(day,0,GetDate())- 6,0)
GROUP BY DateAdded
RETURN
My problem here is that if no record exists for any of those last 7 days then my method over on the website back-end side will fail as it required 7 records. For example:
Lets say I have the following records in my table
-DateAdded--------Responses
2012-02-12 4
2012-02-11 5
2012-02-10 8
2012-02-08 7
2012-02-07 3
Notice that there are no records for both 2012-02-13(today) and 2012-02-09
How can I create an SQL statement that checks the last 7 days for the number of responses and if no records are found for any one of those days it creates a record with a response of 0 in the correct position?
This is a good application of a numbers table (ex: http://www.projectdmx.com/tsql/tblnumbers.aspx)
Assuming you have a numbers table dbo.Nums that has at least 6 numbers in it, you can try the following:
CREATE TABLE #Dates
(
[Date] DATETIME
)
INSERT INTO #Dates
(
[Date]
)
SELECT
DATEADD(DD, DATEDIFF(DD, 0, GETDATE()) - ([n] - 1), 0)
FROM
[dbo].[Nums] WITH (NOLOCK)
WHERE
[n] < 7
SELECT
[Date],
ISNULL(SUM([Responses]), 0) AS [responseCount]
FROM
#Dates AS d
LEFT OUTER JOIN
ActiveResponses AS a
ON
a.[DateAdded] = d.[Date]
WHERE
#QUEST_ID = QuestionnaireID
ORDER BY
[Date] ASC
This demonstrates getting summary data for each day in a week, even if some of the days have no data:
declare #Data as table ( DateAdded date, Responses int )
insert into #Data ( DateAdded, Responses ) values ( '2/10/2012', 5 ), ( '2/13/2012', 9 )
; with James as (
select cast( SysDateTime() as date ) as StartOfDay, 7 as DaysLeft
union all
select DateAdd( d, -1, StartOfDay ), DaysLeft - 1
from James
where DaysLeft > 1
)
select J.StartOfDay, DateAdd( ms, -3, cast( DateAdd( day, 1, J.StartOfDay ) as DateTime ) ) as EndOfDay, Coalesce( D.Responses, 0 ) as Responses
from James as J left outer join
#Data as D on D.DateAdded = J.StartOfDay
order by J.StartOfDay desc
Left as an exercise is mating this with your questionnaire data.
Note that the time closest to midnight represented by DateTime values is 3ms before midnight. You can use the StartOfDay and EndOfDay values to drop any DateAdded into the correct date.
Declare a table variable with the last seven dates and include it with your query:
ALTER PROCEDURE [dbuser].[GetResponses]
(
#QUEST_ID int
)
AS
DECLARE #i INT=0;
DECLARE #today DATE=getdate();
DECLARE #last7 TABLE(DateAdded DATE);
WHILE #i>-7 BEGIN
INSERT INTO #last7 VALUES (DATEADD(DAY,#i,#today));
SET #i -= 1;
END
;WITH a AS (
SELECT ar.DateAdded, count(ar.Responses) as responseCount
FROM ActiveResponses ar
INNER JOIN #last7 z ON z.DateAdded=ar.DateAdded
WHERE #QUEST_ID = ar.QuestionnaireID
GROUP BY ar.DateAdded
)
SELECT DateAdded=ISNULL(a.DateAdded,z.DateAdded)
, responseCount=ISNULL(a.responseCount,0)
FROM #last7 z
LEFT JOIN a ON a.DateAdded=z.DateAdded;
RETURN;
GO
Results:
DateAdded responseCount
---------- -------------
2012-02-13 0
2012-02-12 4
2012-02-11 5
2012-02-10 8
2012-02-09 0
2012-02-08 7
2012-02-07 3
I've written a query that groups the number of rows per hour, based on a given date range.
SELECT CONVERT(VARCHAR(8),TransactionTime,101) + ' ' + CONVERT(VARCHAR(2),TransactionTime,108) as TDate,
COUNT(TransactionID) AS TotalHourlyTransactions
FROM MyTransactions WITH (NOLOCK)
WHERE TransactionTime BETWEEN CAST(#StartDate AS SMALLDATETIME) AND CAST(#EndDate AS SMALLDATETIME)
AND TerminalId = #TerminalID
GROUP BY CONVERT(VARCHAR(8),TransactionTime,101) + ' ' + CONVERT(VARCHAR(2),TransactionTime,108)
ORDER BY TDate ASC
Which displays something like this:
02/11/20 07 4
02/11/20 10 1
02/11/20 12 4
02/11/20 13 1
02/11/20 14 2
02/11/20 16 3
Giving the number of transactions and the given hour of the day.
How can I display all hours of the day - from 0 to 23, and show 0 for those which have no values?
Thanks.
UPDATE
Using the tvf below works for me for one day, however I'm not sure how to make it work for a date range.
Using the temp table of 24 hours:
-- temp table to store hours of the day
DECLARE #tmp_Hours TABLE ( WhichHour SMALLINT )
DECLARE #counter SMALLINT
SET #counter = -1
WHILE #counter < 23
BEGIN
SET #counter = #counter + 1
--print
INSERT INTO #tmp_Hours
( WhichHour )
VALUES ( #counter )
END
SELECT MIN(CONVERT(VARCHAR(10),[dbo].[TerminalTransactions].[TransactionTime],101)) AS TDate, [#tmp_Hours].[WhichHour], CONVERT(VARCHAR(2),[dbo].[TerminalTransactions].[TransactionTime],108) AS TheHour,
COUNT([dbo].[TerminalTransactions].[TransactionId]) AS TotalTransactions,
ISNULL(SUM([dbo].[TerminalTransactions].[TransactionAmount]), 0) AS TransactionSum
FROM [dbo].[TerminalTransactions] RIGHT JOIN #tmp_Hours ON [#tmp_Hours].[WhichHour] = CONVERT(VARCHAR(2),[dbo].[TerminalTransactions].[TransactionTime],108)
GROUP BY [#tmp_Hours].[WhichHour], CONVERT(VARCHAR(2),[dbo].[TerminalTransactions].[TransactionTime],108), COALESCE([dbo].[TerminalTransactions].[TransactionAmount], 0)
Gives me a result of:
TDate WhichHour TheHour TotalTransactions TransactionSum
---------- --------- ------- ----------------- ---------------------
02/16/2010 0 00 4 40.00
NULL 1 NULL 0 0.00
02/14/2010 2 02 1 10.00
NULL 3 NULL 0 0.00
02/14/2010 4 04 28 280.00
02/14/2010 5 05 11 110.00
NULL 6 NULL 0 0.00
02/11/2010 7 07 4 40.00
NULL 8 NULL 0 0.00
02/24/2010 9 09 2 20.00
So how can I get this to group properly?
The other issue is that for some days there will be no transactions, and these days also need to appear.
Thanks.
You do this by building first the 23 hours table, the doing an outer join against the transactions table. I use, for same purposes, a table valued function:
create function tvfGetDay24Hours(#date datetime)
returns table
as return (
select dateadd(hour, number, cast(floor(cast(#date as float)) as datetime)) as StartHour
, dateadd(hour, number+1, cast(floor(cast(#date as float)) as datetime)) as EndHour
from master.dbo.spt_values
where number < 24 and type = 'p');
Then I can use the TVF in queries that need to get 'per-hour' basis data, even for missing intervals in the data:
select h.StartHour, t.TotalHourlyTransactions
from tvfGetDay24Hours(#StartDate) as h
outer apply (
SELECT
COUNT(TransactionID) AS TotalHourlyTransactions
FROM MyTransactions
WHERE TransactionTime BETWEEN h.StartHour and h.EndHour
AND TerminalId = #TerminalID) as t
order by h.StartHour
Updated
Example of a TVF that returns 24hours between any arbitrary dates:
create function tvfGetAnyDayHours(#dateFrom datetime, #dateTo datetime)
returns table
as return (
select dateadd(hour, number, cast(floor(cast(#dateFrom as float)) as datetime)) as StartHour
, dateadd(hour, number+1, cast(floor(cast(#dateFrom as float)) as datetime)) as EndHour
from master.dbo.spt_values
where type = 'p'
and number < datediff(hour,#dateFrom, #dateTo) + 24);
Note that since master.dbo.spt_values contains only 2048 numbers, the function will not work between dates further apart than 2048 hours.
You have just discovered the value of the NUMBERS table. You need to create a table with a single column containing the numbers 0 to 23 in it. Then you join again this table using an OUTER join to ensure you always get 24 rows returned.
So going back to using Remus' original function, I've re-used it in a recursive call and storing the results in a temp table:
DECLARE #count INT
DECLARE #NumDays INT
DECLARE #StartDate DATETIME
DECLARE #EndDate DATETIME
DECLARE #CurrentDay DATE
DECLARE #tmp_Transactions TABLE
(
StartHour DATETIME,
TotalHourlyTransactions INT
)
SET #StartDate = '2000/02/10'
SET #EndDate = '2010/02/13'
SET #count = 0
SET #NumDays = DateDiff(Day, #StartDate, #EndDate)
WHILE #count < #NumDays
BEGIN
SET #CurrentDay = DateAdd(Day, #count, #StartDate)
INSERT INTO #tmp_Transactions (StartHour, TotalHourlyTransactions)
SELECT h.StartHour ,
t.TotalHourlyTransactions
FROM tvfGetDay24Hours(#CurrentDay) AS h
OUTER APPLY ( SELECT COUNT(TransactionID) AS TotalHourlyTransactions
FROM [dbo].[TerminalTransactions]
WHERE TransactionTime BETWEEN h.StartHour AND h.EndHour
AND TerminalId = 4
) AS t
ORDER BY h.StartHour
SET #count = #Count + 1
END
SELECT *
FROM #tmp_Transactions
group by datepart('hour', thetime). to show those hours with no values you'd have to left join a table of times against the grouping (coalesce(transaction.amount, 0))
I've run into a version of this problem before. The suggestion that worked the best was to setup a table (temporary, or not) with the hours of the day, then do an outer join to that table and group by datepart('h', timeOfRecord).
I don't remember why, but probably due to lack of flexibility because of the need for the other table, I ended up using a method where I group by whatever datepart I want and order by the datetime, then loop through and fill any spaces that are skipped with a 0. This approach worked well for me because I'm not reliant on the database to do all my work for me, and it's also MUCH easier to write an automated test for it.
Step 1, Create #table or a CTE to generate a hours days table. Outer loop for days and inner loop hours 0-23. This should be 3 columns Date, Days, Hours.
Step 2, Write your main query to also have days and hours columns and alias it so you can join it. CTE's have to be above this main query and pivots should be inside CTE's for it to work naturally.
Step 3, Do a select from step 1 table and Left join this Main Query table
ON A.[DATE] = B.[DATE]
AND A.[HOUR] = B.[HOUR]
You can also create a order by if your date columns like
ORDER BY substring(CONVERT(VARCHAR(15), A.[DATE], 105),4,2)
Guidlines
This will then give you all data for hours and days and including zeros for hours with no matches to do that use isnull([col1],0) as [col1].
You can now graph facts against days and hours.