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/
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
I'm trying to extract the number of sales operations for every month for a number of variable sale centers.
Using the following TSQL...
;WITH Months(m) AS
(
SELECT 1 m
UNION ALL
SELECT m+1 FROM Months WHERE m < 12
)
SELECT t.Center,m Month, t.Sales FROM Months
CROSS APPLY
(
SELECT C.Center, COUNT(1) Sales FROM Operations C
LEFT JOIN Centers A ON A.Code=C.Center
WHERE Date BETWEEN '01/'+ CONVERT(VARCHAR(2),Months.m) + '/2013' AND DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,'01/'+ CONVERT(VARCHAR(2),Months.m) + '/2013')+1,0))
GROUP BY C.Center
) t
So I get the following output:
Center Month Sales
-----------------------
A 1 20
B 1 30
A 2 25
B 2 30
....
And what I want to end with is:
Center 1 2 ...
----------------------
A 20 25 ...
B 30 30 ...
I'm studying pivot with xmlpath, but It's so complicated that I can't make it work. Anyone has a solution?
Maybe I am missing something with your question but since there are only 12 months in the year, there shouldn't be a reason to use dynamic SQL as you will only ever have 12 columns.
This could easily be accomplished using the following query:
select center,
[1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11], [12]
from
(
select o.center, month(date) month
from operations o
inner join centers c
on o.center = c.code
where c.date >= '2013-01-01'
and c.date <= '2013-12-31'
) d
pivot
(
count(month)
for month in ([1], [2], [3], [4], [5], [6], [7], [8], [9],
[10], [11], [12])
) piv
See SQL Fiddle with Demo.
If you want to do this dynamically, then you could use the following:
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX),
#startdate datetime,
#enddate datetime
set #startdate = '2013-01-01'
set #enddate = '2013-12-31'
;WITH Months(m) AS
(
SELECT 1 m
UNION ALL
SELECT m+1 FROM Months WHERE m < 12
)
select #cols = STUFF((SELECT ',' + QUOTENAME(m)
from Months
group by m
order by m
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT center, ' + #cols + '
from
(
select o.center, month(date) month
from operations o
inner join centers c
on o.center = c.code
where c.date >= '''+convert(varchar(10), #startdate, 120)+'''
and c.date <= '''+convert(varchar(10), #enddate, 120)+'''
) x
pivot
(
count(month)
for month in (' + #cols + ')
) p '
execute sp_executesql #query;
See SQL Fiddle with Demo
Don't do it. Why fight Sql Server into something it wasn't meant to do ?
Let your query return data vertically, just like you have now.
Transpose it on the application level.
It will be much easier to write, debug, maintain, and handle special business rules which will undoubtedly pop up.
Also, it will make it easier to paste data into Excel for troubleshooting and pivoting.
I finally implemented the first suggestion of BlueFeet, with slight changes:
;WITH Months(m) AS
(
SELECT 1 m
UNION ALL
SELECT m+1 FROM Months WHERE m < 12
)
SELECT * FROM Months
CROSS APPLY
(
SELECT C.Center, COUNT(1) Sales FROM Operations C
LEFT JOIN Centers A ON A.Code=C.Center
WHERE Date BETWEEN '01/'+ CONVERT(VARCHAR(2),Months.m) + '/2013' AND DATEADD(s,-1,DATEADD(mm, DATEDIFF(m,0,'01/'+ CONVERT(VARCHAR(2),Months.m) + '/2013')+1,0))
GROUP BY C.Center
) t
pivot
(
max(Sales)
for m in ([1],[2],[3],[4],[5],[6],[7],[8],[9],[10],[11],[12])
) as PIV
What I was looking for it's not to write the month numbers on the pivot statement, so I could only return a quarter, for example. But that makes the work required.
Thanks.
I have a table with columns sales(int), month(int). I want to retrieve sum of sales corresponding to every month. I need ouput in form of 12 columns corresponding to each month in which there will be a single record containing sales for for each column(month).
You should take a look at PIVOT for switching rows with columns. This prevents a select statement for each month. Something like this:
DECLARE #salesTable TABLE
(
[month] INT,
sales INT
)
-- Note that I use SQL Server 2008 INSERT syntax here for inserting
-- multiple rows in one statement!
INSERT INTO #salesTable
VALUES (0, 2) ,(0, 2) ,(1, 2) ,(1, 2) ,(2, 2)
,(3, 2) ,(3, 2) ,(4, 2) ,(4, 2) ,(5, 2)
,(6, 2) ,(6, 2) ,(7, 2) ,(8, 2) ,(8, 2)
,(9, 2) ,(10, 2) ,(10, 2) ,(11, 2) ,(11, 2)
SELECT [0], [1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11]
FROM
(
SELECT [month], sales
FROM #salesTable
) AS SourceTable
PIVOT
(
SUM(sales)
FOR [month] IN ([0], [1], [2], [3], [4], [5], [6], [7], [8], [9], [10], [11])
) AS PivotTable
Not pretty... but this works well
SELECT
(SELECT SUM(Sales) FROM SalesTable WHERE [Month] = 1) [Sales1],
(SELECT SUM(Sales) FROM SalesTable WHERE [Month] = 2) [Sales2],
(SELECT SUM(Sales) FROM SalesTable WHERE [Month] = 3) [Sales3],
(SELECT SUM(Sales) FROM SalesTable WHERE [Month] = 4) [Sales4],
(SELECT SUM(Sales) FROM SalesTable WHERE [Month] = 5) [Sales5],
(SELECT SUM(Sales) FROM SalesTable WHERE [Month] = 6) [Sales6],
(SELECT SUM(Sales) FROM SalesTable WHERE [Month] = 7) [Sales7],
(SELECT SUM(Sales) FROM SalesTable WHERE [Month] = 8) [Sales8],
(SELECT SUM(Sales) FROM SalesTable WHERE [Month] = 9) [Sales9],
(SELECT SUM(Sales) FROM SalesTable WHERE [Month] = 10) [Sales10],
(SELECT SUM(Sales) FROM SalesTable WHERE [Month] = 11) [Sales11],
(SELECT SUM(Sales) FROM SalesTable WHERE [Month] = 12) [Sales12]
Here's an alternate way to write the pivot that gives you a little more control (especially over the column names). It's also a little easier to generate dynamic SQL for.
It's similar to Robin's answer, but has the advantage of only hitting the table once:
select
Sales1 = sum( case when Month = 1 then Sales end )
, Sales2 = sum( case when Month = 2 then Sales end )
, Sales3 = sum( case when Month = 3 then Sales end )
-- etc..
from SalesTable;
I did some investigation, and it seems like the new pivot operator is just syntax sugar for this type of query. The query plans end up looking identical.
As an interesting aside, the unpivot operator seems to also just be syntax sugar. For example:
If you have a table like:
Create Table Sales ( JanSales int, FebSales int, MarchSales int...)
You can write:
select unpivoted.monthName, unpivoted.sales
from Sales s
outer apply (
select 'Jan', JanSales union all
select 'Feb', FebSales union all
select 'March', MarchSales
) unpivoted( monthName, sales );
And get the unpivoted data...
You can do it with OLAP. Here is another link to MSDN documentation on the topic.
With OLAP, you can create a cube with the information you have, with the layout you need.
If you do not want to go that way, you will have to create summary tables with .NET, Java, TransacSQL, or your preferred language to manipulate SQLServer data.
To easily transpose columns into rows with its names you should use XML. In my blog I was described this with example: Link