Transposing Datetime Records in SQL Server - sql-server

I'm working with the following table in SQL Server, which captures the start & end date/time for which an employee worked:
EMP_NO RECORD_DATE START_TIME END_TIME
123456 2020-07-04 10:00:00.0000000 14:30:00.0000000
I need to transpose the date/time values, so that it generates incremental records at 30 minute intervals with the Date/time values concatenated:
Expected Result:
EMP_NO SHIFT_WORKED
123456 2020-07-04 10:00
123456 2020-07-04 10:30
123456 2020-07-04 11:00
123456 2020-07-04 11:30
123456 2020-07-04 12:00
123456 2020-07-04 12:30
123456 2020-07-04 13:00
123456 2020-07-04 13:30
123456 2020-07-04 14:00
Sample code:
CREATE TABLE #HOURS (
EMP_NO INT NOT NULL,
RECORD_DATE DATETIME ,
START_TIME TIME NOT NULL,
END_TIME TIME NOT NULL
)
INSERT INTO #HOURS
VALUES (123456 ,' 2020-07-04',' 10:00',' 14:30')

A tally table will help you here. (I prefer to call it an "integers table"). This is just a table of integers. You can either actually have one persisted somewhere in the database, or create one "one the fly" with a CTE. There are a lot of different ways to create such a thing, some faster than others!
Once you have a tally table, you can generate any values that can be mathematically derived from that. In your case, datetime values in 30 minute increments. You can then join that onto your date range. Here's one way which generates a tally table as a CTE
with integers as (
select top 1000 -- as many as you need
i = -1 + row_number() over (order by a.number) -- start from 0
from master..spt_values a
cross join master..spt_values b
)
select h.emp_no,
shift_worked = dateadd(minute, i * 30, t.startdt)
from #hours h
cross apply ( -- this cross apply just makes the join to integers easier to read
select startdt = h.record_Date + cast(h.start_Time as datetime),
enddt = h.record_date + cast(h.end_time as datetime)
) t
join integers i on dateadd(minute, i * 30, t.startdt) < t.enddt

you can do this:
CREATE TABLE temp (EMP_NO INT ,SHIFT_WORKED datetime)
DECLARE #START_TIME TIME,#END_TIME TIME,#shift TIME,#emp int
SELECT #START_TIME=START_TIME,#END_TIME=END_TIME,#emp=EMP_NO FROM #HOURS
SET #shift=#START_TIME
WHILE DATEADD(MINUTE, 30, #shift) <=#END_TIME
BEGIN
INSERT INTO temp VALUES(#EMP,#shift)
SET #Shift = DATEADD(MINUTE, 30, #shift)
END

Related

SQL query with function

I have a table of data which i am using a count statement to get the amount of records for the submission date
example
AuditId Date Crew Shift Cast ObservedBy 2ndObserver AuditType Product
16 2017-06-27 3 Day B1974, B1975 Glen Mason NULL Identification Billet
20 2017-06-29 1 Day 9879 Corey Lundy NULL Identification Billet
21 2017-06-29 4 Day T9627, T9625 Joshua Dwyer NULL ShippingPad Tee
22 2017-06-29 4 Day NULL Joshua Dwyer NULL Identification Billet
23 2017-06-29 4 Day S9874 Joshua Dwyer NULL ShippingPad Slab
24 2017-06-29 4 Day Bay 40 Joshua Dwyer NULL Identification Billet
Basically I am using the following code to get my results
SELECT YEAR([Date]) as YEAR, CAST([Date] as nvarchar(25)) AS [Date], COUNT(*) as "Audit Count"
FROM AuditResults
where AuditType = 'Identification' AND Product = 'Billet'
group by Date
this returns example
YEAR Date Audit Count
2017 2017-06-27 1
2017 2017-06-29 3
Now I want to be able to retrieve all dates even if blank
so I would like the return to be
YEAR Date Audit Count
2017 2017-06-27 1
2017 2017-06-28 0
2017 2017-06-29 3
I have the following function I am trying to use:
ALTER FUNCTION [dbo].[fnGetDatesInRange]
(
#FromDate datetime,
#ToDate datetime
)
RETURNS #DateList TABLE (Dt date)
AS
BEGIN
DECLARE #TotalDays int, #DaysCount int
SET #TotalDays = DATEDIFF(dd,#FromDate,#ToDate)
SET #DaysCount = 0
WHILE #TotalDays >= #DaysCount
BEGIN
INSERT INTO #DateList
SELECT (#ToDate - #DaysCount) AS DAT
SET #DaysCount = #DaysCount + 1
END
RETURN
END
How do I use my select statement with this function? or is there a better way?
cheers
Try this;
ALTER FUNCTION [dbo].[fnGetDatesInRange]
(
#FromDate datetime,
#ToDate datetime
)
RETURNS #YourData TABLE ([Year] int, DateText nvarchar(25),[Audit Count] int)
AS
begin
insert into #YourData
SELECT
YEAR(allDates.[Date]) as YEAR,
CAST(allDates.[Date] as nvarchar(25)) AS [Date],
COUNT(r.Product) as "Audit Count"
from
(
SELECT
[date]=convert(datetime, CONVERT(float,d.Seq))
FROM
(
select top 100000 row_number() over(partition by 1 order by A.name) as Seq
from syscolumns A, syscolumns B
)d
)allDates
left join
AuditResults r on r.[Date]=allDates.[date] and r.AuditType = 'Identification' AND r.Product = 'Billet'
where
allDates.[Date]>=#FromDate and allDates.[Date]<=#ToDate
group by
allDates.[Date]
return
end
The key is the 'allDates' section ;
SELECT
[date]=convert(datetime, CONVERT(float,d.Seq))
FROM
(
select top 100000 row_number() over(partition by 1 order by A.name) as Seq
from syscolumns A, syscolumns B
)d
This will return all dates between 1900 and 2173 (in this example). Limit that as you need but a nice option. A ton of different ways to approach this clearly
you have to create another table calendar as (Mysql)- idea is the same on all RDBMS-
CREATE TABLE `calendar` (
`dt` DATE NOT NULL,
UNIQUE INDEX `calendar_dt_unique` (`dt`)
)
COLLATE='utf8_general_ci'
ENGINE=InnoDB
;
and fill with date data.
more details

Time and Date Clash - Sql Server

stuck on a project. I wrote this code in sql server which finds the duplicate date matches for a staff member, but I'm stuck when trying to expand it to narrow it down to when the time ranges overlap each other also.
So there is a table called 'Rosters' with columns 'StaffID', 'Date', 'Start', 'End'
SELECT
y.[Date],y.StaffID,y.Start,y.[End]
FROM Rosters y
INNER JOIN (SELECT
[Date],StaffID, COUNT(*) AS CountOf
FROM Rosters
GROUP BY [Date],StaffID
HAVING COUNT(*)>1)
dd ON y.[Date]=dd.[Date] and y.StaffID=dd.StaffID
It returns all duplicate dates for each staff member, I wish to add the logic-
y.Start <= dd.[End] && dd.Start <= y.[End]
Is it possible with the way I'm currently doing it? Any help would be appreciated.
#TT. Sorry, below is probably a better visual explanation -
e.g This would be the roster table
ID Date Start End
1 01/01/2000 8:00 12:00
1 01/01/2000 9:00 11:00
2 01/01/2000 10:00 14:00
2 01/01/2000 8:00 9:00
3 01/01/2000 14:00 18:00
3 02/02/2002 13:00 19:00
And I'm trying to return what is below for the example as they are the only 2 rows that clash for ID, Date, and the Time range (start - end)
ID Date Start End
1 01/01/2000 8:00 12:00
1 01/01/2000 9:00 11:00
This is the logic that you would need to filter your results to overlapping time ranges, though I think this can be handled without your intermediate step of finding the duplicates. If you simply post your source table schema with some test data and your desired output, you will get a much better answer:
declare #t table (RowID int
,ID int
,DateValue date --\
,StartTime Time -- > Avoid using reserved words for your object names.
,EndTime Time --/
);
insert into #t values
(1,1, '01/01/2000', '8:00','12:00' )
,(2,1, '01/01/2000', '9:00','11:00' )
,(3,2, '01/01/2000', '10:00','14:00')
,(4,2, '01/01/2000', '8:00','9:00' )
,(5,3, '01/01/2000', '14:00','18:00')
,(6,3, '02/02/2002', '13:00','19:00');
select t1.*
from #t t1
inner join #t t2
on(t1.RowID <> t2.RowID -- If you don't have a unique ID for your rows, you will need to specify all columns so as no to match on the same row.
and t1.ID = t2.ID
and t1.DateValue = t2.DateValue
and t1.StartTime <= t2.EndTime
and t1.EndTime >= t2.StartTime
)
order by t1.RowID
Try this
with cte as
(
SELECT ROW_NUMBER() over (order by StaffID,Date,Start,End) as rno
,StaffID, Date, Start, End
FROM Rosters
)
select distinct t1.*
from cte t1
inner join cte t2
on(t1.rno <> t2.rno
and t1.StaffID = t2.StaffID
and t1.Date = t2.Date
and t1.Start <= t2.End
and t1.End >= t2.Start
)
order by t1.rno
Made some changes in #iamdave's Answer
If you use SQL Server 2012 up, you can try below script:
declare #roster table (StaffID int,[Date] date,[Start] Time,[End] Time);
insert into #roster values
(1, '01/01/2000', '9:00','11:00' )
,(1, '01/01/2000', '8:00','12:00' )
,(2, '01/01/2000', '10:00','14:00')
,(2, '01/01/2000', '8:00','9:00' )
,(3, '01/01/2000', '14:00','18:00')
,(3, '02/02/2002', '13:00','19:00');
SELECT t.StaffID,t.Date,t.Start,t.[End] FROM (
SELECT y.StaffID,y.Date,y.Start,y.[End]
,CASE WHEN y.[End] BETWEEN
LAG(y.Start)OVER(PARTITION BY y.StaffID,y.Date ORDER BY y.Start) AND LAG(y.[End])OVER(PARTITION BY y.StaffID,y.Date ORDER BY y.Start) THEN 1 ELSE 0 END
+CASE WHEN LEAD(y.[End])OVER(PARTITION BY y.StaffID,y.Date ORDER BY y.Start) BETWEEN y.Start AND y.[End] THEN 1 ELSE 0 END AS IsOverlap
,COUNT (0)OVER(PARTITION BY y.StaffID,y.Date) AS cnt
FROM #roster AS y
) t WHERE t.cnt>1 AND t.IsOverlap>0
StaffID Date Start End
----------- ---------- ---------------- ----------------
1 2000-01-01 08:00:00.0000000 12:00:00.0000000
1 2000-01-01 09:00:00.0000000 11:00:00.0000000

SQL query to join tables - where one table's datetime does not match the other ones

Joined just to ask this because it is killing me :) Great forum with a lot of great minds!
I simply need to join these two tables, i can only join them on date and time no other columns are available.
Table 1 has for example following columns
Film -----------------Datetime……………………………Duration(minutes)
TITANIC------------2016-01-01 01:00:00-----------------60
Armageddon---------2016-01-01 02:00:00-----------------60
Table 2 has following columns
Date----------------Time
2016-01-01……….01:00:00
2016-01-01……….01:01:00
2016-01-01……….01:02:00
2016-01-01……….01:03:00
…and so on
Table 2 contains info for every minute but table one only one specific time and date per event. so i need to match for every minute in table two with what i got from table one.
Any ideas? i'll take anything that works :) btw sorry for the formatting!
Edit:
Desired result would be something like
TITANIC------------2016-01-01 01:00:00-----------------60
TITANIC------------2016-01-01 01:01:00-----------------60
TITANIC------------2016-01-01 01:02:00-----------------60
Armageddon---------2016-01-01 02:00:00-----------------60
Armageddon---------2016-01-01 02:01:00-----------------60
Armageddon---------2016-01-01 02:02:00-----------------60
And so on...
convert the table 2 date and time to a datetime and see if it is between table 1 datetime and datetime + duration.
SELECT *
FROM table1 t1
JOIN table2 t2 ON CAST(t2.Date AS DATETIME)
+ CAST(t2.Time AS DATETIME) >= t1.Datetime
AND CAST(t2.Date AS DATETIME)
+ CAST(t2.Time AS DATETIME) < DATEADD(MIN,
t1.Duration,
t1.Datetime)
I'm going to make some assumptions as you haven't outlined the table structure.
Your film table is of the following structure:
CREATE TABLE Films ([Film] NVARCHAR(128), [DateTime] DATETIME, Duration INT)
GO
Your Date/Time values table is of the following structure:
CREATE TABLE DateTimeValues ([Date] DATE, [Time] TIME)
GO
Lets insert your values:
--Insert Values for Films
INSERT INTO Films
VALUES ('TITANIC', '2016-01-01T01:00:00', 60),
('Armageddon', '2016-01-01 02:00:00', 60)
GO
--Insert Values for every minute of 2016-01-01
DECLARE #DATETIMEBEGIN DATETIME
SET #DATETIMEBEGIN = '2016-01-01'
DECLARE #DATETIMEEND DATETIME
SELECT #DATETIMEEND = '2016-01-02'
;WITH CTE AS (SELECT DATEADD(day, 0, DATEDIFF(day, 0, #DATETIMEBEGIN)) DateTimeValues
UNION ALL
SELECT DATEADD(MINUTE, 1, DateTimeValues) AS DateTimeValues
FROM CTE
WHERE DateTimeValues < #DATETIMEEND
)
INSERT INTO DateTimeValues
SELECT CONVERT(DATE, DateTimeValues) "Date",
CONVERT(TIME, DateTimeValues) "Time"
FROM CTE
OPTION (MAXRECURSION 0)
GO
Lets make it simple and lets caluclate the start time/end time for each film according to the duration. The query you will need for your desired output is:
;WITH CTEFilms AS
(
SELECT Film,
CONVERT(DATE, [DateTime]) "Date",
CONVERT(TIME, [DateTime]) "StartTime",
CONVERT(TIME, DATEADD(MINUTE,Duration,[DateTime])) "EndTime",
Duration
FROM Films
)
SELECT f.Film,
CAST(dtv."Date" AS DATETIME) + dtv."Time" "DateTime",
Duration
FROM CTEFilms f
INNER JOIN DateTimeValues dtv
ON f.[Date] = dtv.[Date]
AND dtv.[Time] >= f.StartTime
AND dtv.[Time] < f.EndTime
ORDER BY Film, Time
Your Results:
Film DateTime Duration
Armageddon 2016-01-01 02:00:00.000 60
Armageddon 2016-01-01 02:01:00.000 60
Armageddon 2016-01-01 02:02:00.000 60
Armageddon 2016-01-01 02:03:00.000 60
Armageddon 2016-01-01 02:04:00.000 60
...
Armageddon 2016-01-01 02:55:00.000 60
Armageddon 2016-01-01 02:56:00.000 60
Armageddon 2016-01-01 02:57:00.000 60
Armageddon 2016-01-01 02:58:00.000 60
Armageddon 2016-01-01 02:59:00.000 60
TITANIC 2016-01-01 01:00:00.000 60
TITANIC 2016-01-01 01:01:00.000 60
TITANIC 2016-01-01 01:02:00.000 60
TITANIC 2016-01-01 01:03:00.000 60
TITANIC 2016-01-01 01:04:00.000 60
...
TITANIC 2016-01-01 01:55:00.000 60
TITANIC 2016-01-01 01:56:00.000 60
TITANIC 2016-01-01 01:57:00.000 60
TITANIC 2016-01-01 01:58:00.000 60
TITANIC 2016-01-01 01:59:00.000 60
#Eddie B, you did not give out your table structure, i.e. column data types, so I just come up with the following assumed code/data
use tempdb
drop table dbo.t1, dbo.t2;
create table dbo.T1 (film varchar(10), dt datetime, duration int)
create table dbo.T2 (dt varchar(10), tm varchar(10));
go
-- populate the tables with SOME sample data
insert into dbo.T1(film, dt, duration)
values ('Titanic', '2016-01-01 01:00:00', 5), ('Amageddon', '2016-01-01 02:00:00', 4)
insert into dbo.T2 (dt, tm)
values
('2016-01-01', '01:00:00')
, ('2016-01-01', '01:01:00')
, ('2016-01-01', '01:02:00')
, ('2016-01-01', '01:03:00')
, ('2016-01-01', '01:04:00')
, ('2016-01-01', '01:05:00')
, ('2016-01-01', '02:00:00')
, ('2016-01-01', '02:01:00')
, ('2016-01-01', '02:02:00')
, ('2016-01-01', '02:03:00')
, ('2016-01-01', '02:04:00')
, ('2016-01-01', '02:05:00')
, ('2016-01-01', '03:00:00');
go
-- here is the result
select t1.film, [DateTime]= convert(datetime, t2.dt + ' ' + t2.tm), t1.duration
from dbo.t1
inner join dbo.t2
on convert(datetime, t2.dt + ' ' + t2.tm) >= t1.dt
and convert(datetime, t2.dt + ' ' + t2.tm) <= dateadd(minute, t1.duration, t1.dt)
go
Here is the result:

How to get a derive 'N' Date Rows from a single record with From / To date columns?

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.

SQL Query to return 24 hour, hourly count even when no values exist?

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.

Resources