SQL incorrect syntax in tableau - sql-server

I have this error message when I insert my SQL query in Tableau. How to solve this issue?
Is it SQL Server currently does not allow CTE's inside a subquery??
Below is the Tableau output when I insert query in
[Microsoft][SQL Server Native Client 11.0][SQL Server]Incorrect syntax near the keyword 'WITH'.
[Microsoft][SQL Server Native Client 11.0][SQL Server]Incorrect syntax near the keyword 'with'. If this statement is a common table
expression, an xmlnamespaces clause or a change tracking context
clause, the previous statement must be terminated with a semicolon.
[Microsoft][SQL Server Native Client 11.0][SQL Server]Incorrect syntax near ')'.
Below is my current CTE Query (recursive)
WITH shiftHours AS (
SELECT RowID,
y.EMPLOYEENAME AS EMPLOYEENAME,
-- flatten the first hour to remove the minutes and get the initial current hour
DATEADD(hour, DATEDIFF(hour, 0, ShiftA_Start), 0) AS currentHour,
ShiftA_Start,
ShiftA_End,
DATEPART(hour, ShiftA_Start) AS hourOrdinal,
-- determine how much of the first hour is applicable. if it is minute 0 then the whole hour counts
CAST(CASE
WHEN DATEADD(hour, DATEDIFF(hour, 0, ShiftA_Start), 0) = DATEADD(hour, DATEDIFF(hour, 0, ShiftA_End), 0) THEN DATEDIFF(minute, ShiftA_Start, ShiftA_End) / 60.0
WHEN DATEPART(minute, ShiftA_Start) = 0 THEN 1.0
ELSE (60 - DATEPART(minute, ShiftA_Start)) / 60.0
END AS DECIMAL(5,3)) AS hourValue
FROM (
-- use a ROW_NUMBER() to generate row IDs for the shifts to ensure each row is unique once it gets to the pivot
SELECT ROW_NUMBER() OVER(ORDER BY ShiftA_Start, ShiftA_End) AS RowID,
EMPLOYEENAME,
ShiftA_Start,
ShiftA_End
FROM (
-- this is where the data gets pulled from the source table and where the data types are converted from string to DATETIME
SELECT
EMPLOYEENAME,
CONVERT(DATETIME, LEFT(SHIFTA_start, 17), 103) AS ShiftA_Start,
CONVERT(DATETIME, LEFT(SHIFTA_end, 17), 103) AS ShiftA_End
from [TableName].[dbo].[TMS_People]
where
CONVERT(DATETIME, LEFT(SHIFTA_START, 17), 103) IS NOT NULL AND CONVERT(DATETIME, LEFT(SHIFTA_END, 17), 103) IS NOT NULL
AND CONVERT(DATETIME, LEFT(SHIFTA_START, 17), 103) != CONVERT(DATETIME, LEFT(SHIFTA_END, 17), 103)
-- this is also where you would add any filtering from the source table such as date ranges
) x
) AS y
UNION ALL
SELECT RowID,
EMPLOYEENAME,
-- add an hour to the currentHour each time the recursive CTE is called
DATEADD(hour, 1, currentHour) AS currentHour,
ShiftA_Start,
ShiftA_End,
DATEPART(hour, DATEADD(hour, 1, currentHour)) AS hourOrdinal,
CAST(CASE
-- when this is the last time period determine the amount of the hour that is applicable
WHEN DATEADD(hour, 2, currentHour) > ShiftA_End THEN DATEPART(minute, ShiftA_End) / 60.0
ELSE 1
END AS DECIMAL(5,3)) AS hourValue
from shiftHours
-- contine recursion until the next hour is after the ShiftEnd
WHERE DATEADD(hour, 1, currentHour) < ShiftA_End
)
SELECT *
FROM (
SELECT RowID,
EMPLOYEENAME,
ShiftA_Start,
ShiftA_End,
hourValue,
hourOrdinal
from shiftHours
) AS t
PIVOT (
SUM(hourValue)
FOR hourOrdinal IN ([0], [1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11], [12], [13], [14], [15], [16], [17], [18], [19], [20], [21], [22], [23])
) AS pvt
OPTION (MAXRECURSION 0);

Tableau do not support SQL server CTE feature .
You should try to implement same logic using view or sub query or Stored proc/table valued function .
Please go throw below thread CTE (SQL Server) are not sported in Tableau.
https://community.tableau.com/thread/105965 .
View Sample code. for CTE
If we have user table (userId, userName, managerId)
CREATE VIEW vUSER AS
WITH UserCTE AS (
SELECT userId, userName, managerId,0 AS steps
FROM dbo.Users
WHERE userId = null
UNION ALL
SELECT mgr.userId, mgr.userName, mgr.managerId, usr.steps +1 AS steps
FROM UserCTE AS usr
INNER JOIN dbo.Users AS mgr
ON usr.managerId = mgr.userId
)
SELECT * FROM UserCTE ;
Create an sp ie MyProc and put all your query in it
ie
CREATE procedure dbo.MyProc AS
WITH UserCTE AS (
SELECT userId, userName, managerId,0 AS steps
FROM dbo.Users
WHERE userId = null
UNION ALL
SELECT mgr.userId, mgr.userName, mgr.managerId, usr.steps +1 AS steps
FROM UserCTE AS usr
INNER JOIN dbo.Users AS mgr
ON usr.managerId = mgr.userId
)
SELECT * FROM UserCTE ;
2 Create a link server using sp_addlinkedserver ie i am telling is Local
3 Call that sp in your view using openquery.
CREATE VIEW dbo.MyView
AS(
SELECT *
FROM openquery(Local,'exec mydb.dbo.MyProc'))
http://capnjosh.com/blog/how-to-call-a-stored-procedure-from-a-view-in-sql-server/

Related

How to count number of report runs last 7 days? Year to date? All time?

I'm trying to create an SSRS report that looks similar to the table below:
Report
Earliest Run
Recent Run
Runs Last 7 days
Runs YTD
Runs All Time
Report 1
3/3/19 1:30
7/8/22 2:45
8
86
233
I know how to query the last 3 columns individually, but is it possible to get all 3 columns using 1 query? I have tried the query below to show my line of thinking but its not working as desired.
SELECT Report
,Min(TimeStart) AS EarliestRun
,Max(TimeStart) AS RecentRun
,CASE WHEN TimeStart BETWEEN GETDATE()-7 AND GETDATE() THEN COUNT(Report) END AS RunsLast7Days
FROM ReportHistory
WHERE TimeStart BETWEEN '1/1/2019 00:00' AND GETDATE()
GROUP BY Report
Yes - use conditional aggregation. Don't filter the query at all since you need an "all time" value. Instead, use sum with a conditional expression for the periods of interest.
select ...
sum(case when TimeStart >= dateadd(day, -7, getdate()) then 1 else 0 end) as [Runs Last 7 days],
sum(case when TimeStart >= datefromparts(year(getdate()), 1, 1) then 1 else 0 end) as [Runs YTD],
...
from dbo.ReportHistory
order by ...;
I was going to propose using CROSS APPLY but SMor has done it with less code
CREATE TABLE #Reports (
ReportId INT NOT NULL,
ReportName VARCHAR(20) NOT NULL
);
INSERT INTO #Reports(ReportId, ReportName)
VALUES(1, 'Report 1');
CREATE TABLE #ReportRun (
ReportId INT,
RunDateTime DATETIME2(2)
);
INSERT INTO #ReportRun(ReportId, RunDateTime)
VALUES
(1, '20220508 10:00:00'),
(1, '20220502 10:00:00'),
(1, '20220101 10:00:00'),
(1, '20210501 10:00:00'),
(1, '20210209 10:00:00'),
(1, '20200509 10:00:00'),
(1, '20190509 10:00:00');
GO
-- SELECT * FROM #Reports
-- SELECT * FROM #ReportRun
SELECT R.ReportName, B.RunLast7Days, C.RunYearToDate, D.RunAllTime
FROM #Reports AS R
CROSS APPLY (
SELECT TOP 1 RunDateTime
FROM #ReportRun
WHERE ReportId = R.ReportId
ORDER BY RunDateTime DESC
) AS ER
CROSS APPLY (
SELECT COUNT(*) AS RunLast7Days
FROM #ReportRun
WHERE ReportId = R.ReportId
AND RunDateTime >= DATEADD(day, -7, CONVERT(date, GETDATE())) -- best to set it to the start of the day
GROUP BY ReportId
) AS B
CROSS APPLY (
SELECT COUNT(*) AS RunYearToDate
FROM #ReportRun
WHERE ReportId = R.ReportId
AND RunDateTime >= DATEADD(yy, DATEDIFF(yy, 0, GETDATE()), 0)
GROUP BY ReportId
) AS C
CROSS APPLY (
SELECT COUNT(*) AS RunAllTime
FROM #ReportRun
WHERE ReportId = R.ReportId
GROUP BY ReportId
) AS D

Count number of occurrences per hour between two dates SQL Server

I've created a temp table #MB that has a record ID (119 rows), start and end date (partial screenshot of the list is below):
I'm trying to get a count of occurrences that happened each hour for each record ID during the start and end date (or number of occurrences each hour when ID was active between two dates).
I've used this code:
SELECT *
FROM
(SELECT
ISNULL(CAST(part AS VARCHAR(5)), 'Total') AS part,
COUNT(*) AS part_count
FROM
(SELECT DATEPART([HOUR], [Start]) AS part
FROM #MB) grp
GROUP BY
GROUPING SETS((part),())
) pre
PIVOT
(MAX(part_count)
FOR part IN ([0], [1], [2], [3], [4], [5], [6], [7], [8],
[9], [10], [11], [12], [13], [14], [15], [16],
[17], [18], [19], [20], [21], [22], [23], Total)
) pvt;
but it counts only records based on the start date (don't count each hour between two dates) and I stuck on how to generate occurrences per hour for each ID between two dates that I can later use to pre-aggregate and pivot.
first, you need to generate the list of rows for each hour
here i am using a recursive cte query to do it
; with MB as
(
select ID, [Start], [End], [Date] = [Start]
from #MB
union all
select ID, [Start], [End], [Date] = dateadd(hour, 1, convert(date, c.[Date]))
from MB c
where dateadd(hour, 1, c.[Date]) < [End]
)
select *
from MB
so in your pivot query , just change to this
; with MB as
(
select ID, [Start], [End], [Date] = [Start]
from #MB
union all
select ID, [Start], [End], [Date] = DATEADD(HH,DATEPART(HH,[Start]),CAST(CAST([Start] AS DATE) AS DATETIME))
from MB c
where dateadd(hour, 1, c.[Date]) < [End]
)
SELECT *
FROM (
SELECT ISNULL(CAST(part AS VARCHAR(5)), 'Total') AS part,
COUNT(*) AS part_count
FROM (
SELECT DATEPART([HOUR], [Date]) AS part
FROM MB -- changed to the cte
) grp
GROUP BY
GROUPING SETS((part),())
) pre
PIVOT (MAX(part_count) FOR part IN (
[0],[1],[2],[3],[4],[5],[6],[7],[8],
[9],[10],[11],[12],[13],[14],[15],[16],
[17],[18],[19],[20],[21],[22],[23], Total)) pvt;

Aggregate over the column that is not in group by list

For example I have the following table:
declare #table table(val int, dt datetime)
insert into #table values
(10, '2018-3-20 16:00'),
(12, '2018-3-20 14:00'),
(14, '2018-3-20 12:00'),
(16, '2018-3-20 10:00'),
(10, '2018-3-19 14:00'),
(12, '2018-3-19 12:00'),
(14, '2018-3-19 10:00'),
(10, '2018-3-18 12:00'),
(12, '2018-3-18 10:00')
I try to aggregate using the column in group by, it is okay:
select day, MAX(val) as max_by_value from
(
select DATEPART(DAY, dt) as day, val from #table
) q
group by day
It returns:
day max_by_value
18 12
19 14
20 16
Now I need max value by time of the day, so I need 10 as result for each day.
I try to use over but it say Column '#table.dt' is invalid in the select list because it is not contained in either an aggregate function or the GROUP BY clause.
select DATEPART(DAY, dt), MAX(val) as max_by_value
,ROW_NUMBER() over (partition by DATEPART(DAY, dt) order by dt desc) as max_by_date
from #table
group by DATEPART(DAY, dt)
I understand why I get this error but don't understand how to fix my issue. Could you please help to find some way to fill [max_by_date] column?
As result I expect the following output:
day max_by_value max_by_time
18 12 10
19 14 10
20 16 10
Starting from 2012 version, you can use the First_value window function:
SELECT DISTINCT DATEPART(DAY, dt),
MAX(val) OVER (partition by DATEPART(DAY, dt)) as max_by_value,
FIRST_VALUE(val) OVER (partition by DATEPART(DAY, dt) order by dt desc) as max_by_date
FROM #table
Note: I've used the OVER clause for the MAX function instead of using group by.
With 2008 version, you can use a subquery instead:
SELECT DISTINCT DATEPART(DAY, dt),
MAX(val) OVER (partition by DATEPART(DAY, dt)) as max_by_value,
(
SELECT TOP 1 val
FROM #table as t1
WHERE DATEPART(DAY, t1.dt) = DATEPART(DAY, t0.dt)
ORDER BY dt DESC
) as max_by_date
FROM #table as t0

Update Query with Closest DateTime Matching with Inner Join

I have the following two tables
CREATE TABLE Ep
([E] varchar(9), [M] varchar(9), [DTE] DATETIME)
;
INSERT INTO Ep
([E], [M], [DTE])
VALUES
('1595861-1', '1595861-1', CONVERT(datetime, '2002-11-26 14:18:00', 20)),
('1595904-1', '1595904-1', CONVERT(datetime, '2002-11-24 15:15:00', 20)),
('1596298-1', '1596298-1', CONVERT(datetime, '2002-12-17 11:12:00', 20)),
('1596357-1', '1596357-1', CONVERT(datetime, '2002-12-09 19:57:00', 20)),
('1596369-1', '1596369-1', CONVERT(datetime, '2002-12-11 06:00:00', 20)),
('1596370-1', '1596370-1', CONVERT(datetime, '2002-12-19 12:31:00', 20)),
('1596473-2', '1596473-1', CONVERT(datetime, '2002-12-15 08:39:00', 20)),
('1596473-3', '1596473-1', CONVERT(datetime, '2002-12-20 08:39:00', 20)),
('1596473-4', '1596473-1', CONVERT(datetime, '2002-12-13 08:39:00', 20)),
('1596473-5', '1596473-1', CONVERT(datetime, '2002-12-16 08:39:00', 20)),
('1596473-1', '1596473-1', CONVERT(datetime, '2002-12-14 08:39:00', 20))
;
CREATE TABLE Mp
([E] varchar(9), [M] varchar(9), [DTE] DATETIME)
;
INSERT INTO Mp
([E], [M], [DTE])
VALUES
('', '1595861-1', CONVERT(datetime, '2002-11-26 14:18:00', 20)),
('', '1595904-1', CONVERT(datetime, '2002-11-24 15:15:00', 20)),
('', '1596298-1', CONVERT(datetime, '2002-12-17 11:12:00', 20)),
('', '1596357-1', CONVERT(datetime, '2002-12-09 19:57:00', 20)),
('', '1596369-1', CONVERT(datetime, '2002-12-11 06:00:00', 20)),
('', '1596370-1', CONVERT(datetime, '2002-12-19 12:31:00', 20)),
('', '1596473-1', CONVERT(datetime, '2002-12-17 08:39:00', 20))
;
Currently I am updating the [E] field in the Mp table via a match on [M] where the DTE field (in Mp) is within a certian range (say +-3 days). The query to do this is currently
UPDATE [Mp]
SET [E] = [Ep].[E]
FROM [Mp] INNER JOIN [Ep]
ON [Mp].[M] = [Ep].[M]
WHERE [Mp].[DTE] BETWEEN [Ep].[DTE] - 3 AND [Ep].[DTE] + 3;
This updates [Mp].[E] for [Mp].[M] = N'1596473-1' to 1596473-2. Essentailly the first entry SQL Server finds that is valid. However, I want to update this query so that SQL Server matches on the [M] field in the required date range (as it does now), but for the [Ep].[DTE] values that is closest to that in the [Mp].[DTE] value of 2002-12-17 08:39:00.
I have looked at adding a DATEDIFF clause, in the following way
UPDATE [Mp]
SET [E] = [Ep].[E]
FROM [Mp] INNER JOIN [Ep]
ON [Mp].[M] = [Ep].[M]
WHERE [Mp].[DTE] BETWEEN [Ep].[DTE] - 3 AND [Ep].[DTE] + 3
ORDER BY DATEDIFF(minutes, [Mp].[DTE], [Ep].[DTE]);
Clearly I can't do this, but I am unsure how to ammend this so that it works. The final data for [Mp] after the update should be
1595861-1 1595861-1 2002-11-26 14:18:00.000
1595904-1 1595904-1 2002-11-24 15:15:00.000
1596298-1 1596298-1 2002-12-17 11:12:00.000
1596357-1 1596357-1 2002-12-09 19:57:00.000
1596369-1 1596369-1 2002-12-11 06:00:00.000
1596370-1 1596370-1 2002-12-19 12:31:00.000
**1596473-5** 1596473-1 2002-12-17 08:39:00.000
Thanks for your time.
Please first check which data following CTE produces as output
For nearest data, I used SQL ROW_NUMBER function with Partition By clause according to the time difference calculation fetched by DATEDIFF() function. To eliminate previous and following records, I used ABS() mathematical function.
select
*,
ROW_NUMBER() over (partition by [Mp].[M] order by abs(datediff(mi, [Mp].[DTE], [Ep].[DTE]))) as rn,
abs(datediff(mi, [Mp].[DTE], [Ep].[DTE])) diff
from Mp
left join Ep
on [Mp].[M] = [Ep].[M]
WHERE [Mp].[DTE] BETWEEN dateadd(dd,-3,[Ep].[DTE]) AND dateadd(dd,3,[Ep].[DTE])
Then using the same CTE expression in an UPDATE command as following, you can populate the desired data into your target database table
;with cte as (
select
[Mp].[M] as M,
[Ep].E as E,
ROW_NUMBER() over (partition by [Mp].[M] order by abs(datediff(mi, [Mp].[DTE], [Ep].[DTE]))) as rn,
abs(datediff(mi, [Mp].[DTE], [Ep].[DTE])) diff
from Mp
left join Ep
on [Mp].[M] = [Ep].[M]
WHERE [Mp].[DTE] BETWEEN dateadd(dd,-3,[Ep].[DTE]) AND dateadd(dd,3,[Ep].[DTE])
)
update [Mp]
set [E] = cte.E
from [Mp]
inner join cte on [Mp].M = cte.M and cte.rn = 1
where
cte.E is not null
After execution of the UPDATE statement, the target table data is as follows
I hope this is what you require

Creating time slots in SQL Server

We are trying to do an analysis of how long our staff have been working on a hourly basis for trending and forecasting purposes.
We have both the clock in (SHIFTA_Start) and clock out (SHIFTA_End) of the employees.
Then we did a datepart into 4 sections:
Start Time_Hour
Start Time_min
End Time_Hour
End Time_min
[I have included the current output I have, and the desired outcome I hope to get in this image]
http://i.stack.imgur.com/1uhq0.png
Given start time and end time:
e.g.
Start time – 940am (0940)
End time – 615pm (1815)
It can populate very well in the respective hour slots as it is straightforward.
However if the employee work overnight, given start time and end time:
e.g.
Start time – 930pm (2130)
End time – 7am (0700)
The hour slots cannot be populated.
To make it short, this is part of my case statement from 0hr to 1hr
SELECT
--b.*,
b.EMPLOYEENAME,
B.DEPARTMENT,
CONVERT(datetime, LEFT(b.SHIFTA_start,17),103) AS SHIFTA_start,
CONVERT(datetime, LEFT(b.ShiftA_End,17),103) as ShiftA_End,
b.StartTime_HOUR,
b.StartTime_min,
b.EndTime_HOUR,
b.EndTime_min,
CASE WHEN b.[0H_START] < b.[0H_END] THEN b.[0H_START] ELSE b.[0H_END] END AS [0],
CASE WHEN b.[1H_START] < b.[1H_END] THEN b.[1H_START] ELSE b.[1H_END] END AS [1]
from
(
/*Step 2 - calculating minutes from starttime and endtime */
select a.*,
/**Calculating the number of minutes worked from start_time MIN **/
CASE WHEN a.StartTime_HOUR = 0 and a.[0] = 1 AND a.StartTime_min !=0 THEN cast(cast((60-a.StartTime_min) as decimal(10,2))/60 as decimal(10,2)) ELSE a.[0] END AS [0H_START],
CASE WHEN a.StartTime_HOUR = 1 and a.[1] = 1 AND a.StartTime_min !=0 THEN cast(cast((60-a.StartTime_min) as decimal(10,2))/60 as decimal(10,2)) ELSE a.[1] END AS [1H_START],
/**Calculating the number of minutes worked from END_time MIN **/
CASE WHEN a.EndTime_HOUR = 0 and a.[0] = 1 AND a.EndTime_min !=0 THEN cast(cast((a.EndTime_min) as decimal(10,2))/60 as decimal(10,2)) ELSE a.[0] END AS [0H_END],
CASE WHEN a.EndTime_HOUR = 1 and a.[1] = 1 AND a.EndTime_min !=0 THEN cast(cast((a.EndTime_min) as decimal(10,2))/60 as decimal(10,2)) ELSE a.[1] END AS [1H_END]
from
(--Step 1:
/*to determine 1 or 0 using the start and end hour
If time falls in the respective hour = 1
if time doesnt fall in the respective hours = 0*/
SELECT
[EMPLOYEENAME],
[DEPARTMENT],
[SHIFTA_start],
CASE WHEN [SHIFTA_START] !='' OR SHIFTA_START != NULL THEN CONVERT(datetime, LEFT([SHIFTA_START],17),103) ELSE NULL END AS SHIFTA_START_con,
CASE WHEN [SHIFTA_START] !='' OR SHIFTA_START != NULL THEN DATEPART(hh,CONVERT(datetime, LEFT([SHIFTA_START],17),103)) ELSE NULL END AS StartTime_HOUR,
CASE WHEN [SHIFTA_START] !='' OR SHIFTA_START != NULL THEN DATEPART(mi,CONVERT(datetime, LEFT([SHIFTA_START],17),103)) ELSE NULL END AS StartTime_min,
[SHIFTA_end],
CASE WHEN [SHIFTA_END] !='' OR SHIFTA_end != NULL THEN CONVERT(datetime, LEFT([SHIFTA_END],17),103) ELSE NULL END AS SHIFTA_END_con,
CASE WHEN [SHIFTA_END] !='' OR SHIFTA_end != NULL THEN DATEPART(hh,CONVERT(datetime, LEFT([SHIFTA_end],17),103)) ELSE NULL END AS EndTime_HOUR,
CASE WHEN [SHIFTA_END] !='' OR SHIFTA_end != NULL THEN DATEPART(mi,CONVERT(datetime, LEFT([SHIFTA_end],17),103)) ELSE NULL END AS EndTime_min,
CASE WHEN [SHIFTA_START] !='' AND 0 BETWEEN DATEPART(hh,CONVERT(datetime, LEFT([SHIFTA_START],17),103)) AND DATEPART (hh,CONVERT(datetime, LEFT([SHIFTA_end],17),103)) THEN 1 ELSE 0 END AS [0],
CASE WHEN [SHIFTA_START] !='' AND 1 BETWEEN DATEPART(hh,CONVERT(datetime, LEFT([SHIFTA_START],17),103)) AND DATEPART (hh,CONVERT(datetime, LEFT([SHIFTA_end],17),103)) THEN 1 ELSE 0 END AS [1]
from [DatabaseTable].[dbo].[ATTENDANCE]
where ShiftA_Start != '' and ShiftA_End !='' and shiftA_start != shiftA_End
)a
)b
This is the output #Mike
http://i.stack.imgur.com/laSKX.png
My current SQL Statement is
DECLARE #WORKINGHOURS TABLE (
ID INT IDENTITY(1,1) NOT NULL,
SHIFTA_START DATETIME NOT NULL,
SHIFTA_END DATETIME NOT NULL
);
WITH WORKINGHOURS AS (
SELECT TOP 1000 ID,
-- flatten the first hour to remove the minutes and get the initial current hour
DATEADD(hour, DATEDIFF(hour, 0, CONVERT(datetime, LEFT(SHIFTA_start,17),103)), 0) AS currentHour,
CONVERT(datetime, LEFT(SHIFTA_start,17),103) AS [SHIFTA_START],
CONVERT(datetime, LEFT(SHIFTA_END,17),103) AS [SHIFTA_END],
DATEPART(hour, CONVERT(datetime, LEFT(SHIFTA_start,17),103)) AS HourOrdinal,
-- determine how much of the first hour is applicable. if it is minute 0 then the whole hour counts
CAST(CASE DATEPART(minute, CONVERT(datetime, LEFT(SHIFTA_start,17),103))
WHEN 0 THEN 1.0
ELSE (60 - DATEPART(minute, CONVERT(datetime, LEFT(SHIFTA_start,17),103))) / 60.0
END AS DECIMAL(5,3)) AS HourValue
FROM [TableName].[dbo].[Attendance]
UNION ALL
SELECT ID,
-- add an hour to the currentHour each time the recursive CTE is called
DATEADD(hour, 1, currentHour) AS currentHour,
CONVERT(datetime, LEFT(SHIFTA_start,17),103) AS [SHIFTA_START],
CONVERT(datetime, LEFT(SHIFTA_END,17),103) AS [SHIFTA_END],
DATEPART(hour, DATEADD(hour, 1, currentHour)) AS hourOrdinal,
CAST(CASE
-- when this is the last time period determine the amount of the hour that is applicable
WHEN DATEADD(hour, 2, currentHour)
> CONVERT(datetime, LEFT(SHIFTA_END,17),103)
THEN DATEPART(minute, CONVERT(datetime, LEFT(SHIFTA_END,17),103)) / 60.0
ELSE 1
END AS DECIMAL(5,3)) AS HourValue
FROM WORKINGHOURS
-- contine recursion until the next hour is after the ShiftEnd
WHERE DATEADD(hour, 1, currentHour) < CONVERT(datetime, LEFT(SHIFTA_END,17),103)
)
SELECT *
FROM (
SELECT ID,
CONVERT(datetime, LEFT(SHIFTA_start,17),103) AS [SHIFTA_START],
CONVERT(datetime, LEFT(SHIFTA_END,17),103) AS [SHIFTA_END],
HourValue,
HourOrdinal
FROM WORKINGHOURS
) AS t
PIVOT (
SUM(HourValue)
FOR HourOrdinal IN ([0], [1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11], [12], [13], [14], [15], [16], [17], [18], [19], [20], [21], [22], [23])
) AS pvt
OPTION (MAXRECURSION 0);
this should gives you what you wanted.
;
with cte as
(
select *,
hr_st1 = case when datepart(hour, SHIFTA_Start) < datepart(hour, SHIFTA_End)
then datepart(hour, SHIFTA_Start)
else 0
end,
hr_en1 = datepart(hour, SHIFTA_End),
hr_st2 = case when datepart(hour, SHIFTA_Start) > datepart(hour, SHIFTA_End)
then datepart(hour, SHIFTA_Start)
end,
hr_en2 = case when datepart(hour, SHIFTA_Start) > datepart(hour, SHIFTA_End)
then 23
end
from #shift s
)
select *,
CASE WHEN 0 BETWEEN hr_st1 and hr_en1
OR 0 BETWEEN hr_st2 and hr_en2
THEN 1
ELSE 0 END AS [0],
CASE WHEN 1 BETWEEN hr_st1 and hr_en1
OR 1 BETWEEN hr_st2 and hr_en2
THEN 1
ELSE 0 END AS [1],
CASE WHEN 2 BETWEEN hr_st1 and hr_en1
OR 2 BETWEEN hr_st2 and hr_en2
THEN 1
ELSE 0 END AS [2],
CASE WHEN 3 BETWEEN hr_st1 and hr_en1
OR 3 BETWEEN hr_st2 and hr_en2
THEN 1
ELSE 0 END AS [3]
from cte
Because of the need to get a column for each hour this is a good candidate for using the TSQL PIVOT operator. Pivot takes rows of data and turns it into columns.
The first step is to create all of our rows: one for each hour and also calculate how much of that hour was applicable. I've done this by using a recursive CTE to generate all of the numbers from 0 to 23. A recursive CTE is a query that keeps calling itself until the anchor condition is met. The anchor condition is the WHERE clause of the 2nd SQL statement in the UNION.
Then I join on that using the ShiftStart_Hour and ShiftStart_End columns but you need to take into account when one of them is on the next day (ShiftStart_Hour > ShiftEndHour):
INNER JOIN hourOrdinals AS h ON (
t.ShiftStart_Hour < t.ShiftEnd_Hour
AND h.hr BETWEEN t.ShiftStart_Hour AND t.ShiftEnd_Hour
) OR (
t.ShiftStart_Hour > t.ShiftEnd_Hour
AND (
h.hr BETWEEN 0 AND t.ShiftEnd_Hour
OR
h.hr BETWEEN t.ShiftStart_Hour AND 23
)
)
That join creates a row for each hour. Now we need to calculate how much of that hour was applicable using a CASE statement in the SELECT clause:
CAST(CASE h.hr
WHEN ShiftStart_Hour THEN (60 - ShiftStart_Minute) / 60.0
WHEN ShiftEnd_Hour THEN ShiftEnd_Minute / 60.0
ELSE 1.0
END AS DECIMAL(5,3)) AS hourValue
After that we can finally use the PIVOT operator to get the data in the final format we're looking for. The full working example is:
/* CREATE TEST TABLE & DATA */
CREATE TABLE #dataTable (
RowID INT IDENTITY(1,1) NOT NULL,
ShiftStart DATETIME NOT NULL,
ShiftStart_Hour AS DATEPART(hour, ShiftStart),
ShiftStart_Minute AS DATEPART(minute, ShiftStart),
ShiftEnd DATETIME NOT NULL,
ShiftEnd_Hour AS DATEPART(hour, ShiftEnd),
ShiftEnd_Minute AS DATEPART(minute, ShiftEnd)
);
INSERT INTO #dataTable (
ShiftStart,
ShiftEnd
)
VALUES (
'2015-08-18 07:00:00',
'2015-08-18 21:00:00'
),(
'2015-08-20 09:40:00',
'2015-08-20 18:15:00'
),(
'2015-08-20 21:30:00',
'2015-08-21 07:00:00'
),(
'2015-08-25 11:00:00',
'2015-08-27 11:00:00'
);
/* END OF TEST DATA CREATION */
WITH hourOrdinals AS (
SELECT 0 AS hr
UNION ALL
SELECT hr+1
FROM hourOrdinals
WHERE hr<23
)
SELECT *
FROM (
SELECT RowId,
ShiftStart,
ShiftEnd,
h.hr AS hourOrdinal,
CAST(CASE h.hr
WHEN ShiftStart_Hour THEN (60 - ShiftStart_Minute) / 60.0
WHEN ShiftEnd_Hour THEN ShiftEnd_Minute / 60.0
ELSE 1.0
END AS DECIMAL(5,3)) AS hourValue
FROM #dataTable AS t
INNER JOIN hourOrdinals AS h ON (
t.ShiftStart_Hour < t.ShiftEnd_Hour
AND h.hr BETWEEN t.ShiftStart_Hour AND t.ShiftEnd_Hour
) OR (
t.ShiftStart_Hour > t.ShiftEnd_Hour
AND (
h.hr BETWEEN 0 AND t.ShiftEnd_Hour
OR
h.hr BETWEEN t.ShiftStart_Hour AND 23
)
)
) AS shiftHours
PIVOT (
SUM(hourValue)
FOR hourOrdinal IN ([0], [1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11], [12], [13], [14], [15], [16], [17], [18], [19], [20], [21], [22], [23])
) AS pvt;
DROP TABLE #dataTable;
But that's not good enough...
There is a problem with this method. Because we're only actually looking at the hours and not the dates it isn't going to get the right answer if some super hard worker goes on a 24+ hour shift. But there is a better way.
Instead of using the recursive CTE to generate the hours we want to match against we can use it to iterate all of the hours between the ShiftStart and ShiftEnd values. While at the same time calculating how much of each hour is applicable. In this case the anchor condition has to do with the actual data from the table instead of just simple arithmetic. Then just PIVOT again to get the result set in the correct format.
Note: This query likely won't scale for more than a few thousand rows. Date range filtering and other limits on the dataset should help with that. But if you need to process more than that you will need to persist these calculations somewhere.
This example works for shifts spanning multiple days:
/* CREATE TEST TABLE AND DATA */
DECLARE #dataTable TABLE (
RowID INT IDENTITY(1,1) NOT NULL,
ShiftStart DATETIME NOT NULL,
ShiftEnd DATETIME NOT NULL
);
INSERT INTO #dataTable (
ShiftStart,
ShiftEnd
)
VALUES (
'2015-08-18 07:00:00',
'2015-08-18 21:00:00'
),(
'2015-08-20 09:40:00',
'2015-08-20 18:15:00'
),(
'2015-08-20 21:30:00',
'2015-08-21 07:00:00'
),(
'2015-08-25 11:00:00',
'2015-08-27 11:00:00'
),(
'2015-08-01 12:00:00',
'2015-08-02 06:00:00'
),(
'2015-08-02 12:15:00',
'2015-08-04 07:45:00'
),(
'2015-08-11 12:00:00',
'2015-08-11 12:59:00'
),(
'1900-01-01 12:00:00',
'1900-01-01 12:00:00'
),(
'2015-08-11 12:00:00',
'2015-08-11 12:15:00'
);
/* END OF TEST DATA CREATION */
WITH shiftHours AS (
SELECT RowID,
-- flatten the first hour to remove the minutes and get the initial current hour
DATEADD(hour, DATEDIFF(hour, 0, ShiftStart), 0) AS currentHour,
ShiftStart,
ShiftEnd,
DATEPART(hour, ShiftStart) AS hourOrdinal,
-- determine how much of the first hour is applicable. if it is minute 0 then the whole hour counts
CAST(CASE
WHEN DATEADD(hour, DATEDIFF(hour, 0, ShiftStart), 0) = DATEADD(hour, DATEDIFF(hour, 0, ShiftEnd), 0) THEN DATEDIFF(minute, ShiftStart, ShiftEnd) / 60.0
WHEN DATEPART(minute, ShiftStart) = 0 THEN 1.0
ELSE (60 - DATEPART(minute, ShiftStart)) / 60.0
END AS DECIMAL(5,3)) AS hourValue
FROM #dataTable AS t
UNION ALL
SELECT RowID,
-- add an hour to the currentHour each time the recursive CTE is called
DATEADD(hour, 1, currentHour) AS currentHour,
ShiftStart,
ShiftEnd,
DATEPART(hour, DATEADD(hour, 1, currentHour)) AS hourOrdinal,
CAST(CASE
-- when this is the last time period determine the amount of the hour that is applicable
WHEN DATEADD(hour, 2, currentHour) > ShiftEnd THEN DATEPART(minute, ShiftEnd) / 60.0
ELSE 1
END AS DECIMAL(5,3)) AS hourValue
FROM shiftHours
-- contine recursion until the next hour is after the ShiftEnd
WHERE DATEADD(hour, 1, currentHour) < ShiftEnd
)
SELECT *
FROM (
SELECT RowID,
ShiftStart,
ShiftEnd,
hourValue,
hourOrdinal
FROM shiftHours
) AS t
PIVOT (
SUM(hourValue)
FOR hourOrdinal IN ([0], [1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11], [12], [13], [14], [15], [16], [17], [18], [19], [20], [21], [22], [23])
) AS pvt
OPTION (MAXRECURSION 0);
Data type conversion issues...
This answer has already gone way beyond what is a good fit for StackOverflow but let's just iron out the last detail. Your table stores dates as strings. That is a terrible design decision that will cause you lots of pain when trying to do reports from this database.
I can't see how your strings are formatted so I've just made the assumption that your conversion function is correct: CONVERT(datetime, LEFT(b.SHIFTA_start,17),103) If it isn't working then none of this code will work and you'll need to fix the two calls in table expression x below. I've also made the assumption that the table name you've given in your updated query above is correct. If not you'll need to fix that in the same table expression. It should be obvious what needs to be changed.
WITH shiftHours AS (
SELECT RowID,
-- flatten the first hour to remove the minutes and get the initial current hour
DATEADD(hour, DATEDIFF(hour, 0, ShiftStart), 0) AS currentHour,
ShiftStart,
ShiftEnd,
DATEPART(hour, ShiftStart) AS hourOrdinal,
-- determine how much of the first hour is applicable. if it is minute 0 then the whole hour counts
CAST(CASE
WHEN DATEADD(hour, DATEDIFF(hour, 0, ShiftStart), 0) = DATEADD(hour, DATEDIFF(hour, 0, ShiftEnd), 0) THEN DATEDIFF(minute, ShiftStart, ShiftEnd) / 60.0
WHEN DATEPART(minute, ShiftStart) = 0 THEN 1.0
ELSE (60 - DATEPART(minute, ShiftStart)) / 60.0
END AS DECIMAL(5,3)) AS hourValue
FROM (
-- use a ROW_NUMBER() to generate row IDs for the shifts to ensure each row is unique once it gets to the pivot
SELECT ROW_NUMBER() OVER(ORDER BY ShiftStart, ShiftEnd) AS RowID,
ShiftStart,
ShiftEnd
FROM (
-- this is where the data gets pulled from the source table and where the data types are converted from string to DATETIME
SELECT CONVERT(DATETIME, LEFT(SHIFTA_start, 17), 103) AS ShiftStart,
CONVERT(DATETIME, LEFT(SHIFTA_end, 17), 103) AS ShiftEnd
FROM WORKINGHOURS
-- this is also where you would add any filtering from the source table such as date ranges
) x
) AS y
UNION ALL
SELECT RowID,
-- add an hour to the currentHour each time the recursive CTE is called
DATEADD(hour, 1, currentHour) AS currentHour,
ShiftStart,
ShiftEnd,
DATEPART(hour, DATEADD(hour, 1, currentHour)) AS hourOrdinal,
CAST(CASE
-- when this is the last time period determine the amount of the hour that is applicable
WHEN DATEADD(hour, 2, currentHour) > ShiftEnd THEN DATEPART(minute, ShiftEnd) / 60.0
ELSE 1
END AS DECIMAL(5,3)) AS hourValue
FROM shiftHours
-- contine recursion until the next hour is after the ShiftEnd
WHERE DATEADD(hour, 1, currentHour) < ShiftEnd
)
SELECT *
FROM (
SELECT RowID,
ShiftStart,
ShiftEnd,
hourValue,
hourOrdinal
FROM shiftHours
) AS t
PIVOT (
SUM(hourValue)
FOR hourOrdinal IN ([0], [1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11], [12], [13], [14], [15], [16], [17], [18], [19], [20], [21], [22], [23])
) AS pvt
OPTION (MAXRECURSION 0);
You are making this way harder than it needs to be. Use the DateTime as is and use a calculated column to give you minutes worked (or hours).
create table WorkTimes
(
Id int not null identity,
SHIFTA_Start DateTime not null,
SHIFTA_End DateTime,
MinutesWorked AS CASE WHEN SHIFTA_End IS NULL THEN NULL
ELSE DATEDIFF(MINUTE, SHIFTA_Start, SHIFTA_End)
END,
constraint WorkTimes_Check_Shift
check (SHIFTA_End IS NULL OR SHIFTA_Start < SHIFTA_End),
constraint WorkTimes_Check_Shift_Too_Long
check (SHIFTA_End IS NULL OR datediff(hour, SHIFTA_Start, SHIFTA_End) < 22)
)
So the MinutesWorked will always be correct. No need to have code to account for daylight savings time, year change, working overnight, etc.
Now that you have the correct data type you can calculate what you want.
declare #start datetime = '4/27/2016 22:10', #stop datetime = '4/28/2016 05:20'
select h.[Hour],
round(cast(case when #start > h.hour then 60 - DATEPART(MINUTE, #start)
when #stop < h.hour then DATEPART(minute, #stop)
else 60 end as decimal(5, 2)) / 60, 2) PercentHour
from [dbo].HoursBetween(#start, #stop) h
order by h.[Hour]
This creates:
The function I used is:
create function [dbo].[HoursBetween](#Start datetime, #Stop datetime)
returns #Hours TABLE
(
[Hour] DateTime
)
begin
declare #temp datetime
set #Start = cast(CONVERT(VARCHAR(13), #Start, 120) + ':00' as datetime)
set #temp = cast(CONVERT(VARCHAR(13), #Stop, 120) + ':00' as datetime)
if(#temp <> #Stop)
set #Stop = DATEADD(hour, 1, #temp)
while(#Start <= #Stop)
begin
insert into #Hours
values(#Start)
set #Start = DATEADD(hour, 1, #Start)
end
return;
end
GO

Resources