I have read the stuff on MS pivot tables and I am still having problems getting this correct.
I have a temp table that is being created, we will say that column 1 is a Store number, and column 2 is a week number and lastly column 3 is a total of some type. Also the Week numbers are dynamic, the store numbers are static.
Store Week xCount
------- ---- ------
102 1 96
101 1 138
105 1 37
109 1 59
101 2 282
102 2 212
105 2 78
109 2 97
105 3 60
102 3 123
101 3 220
109 3 87
I would like it to come out as a pivot table, like this:
Store 1 2 3 4 5 6....
-----
101 138 282 220
102 96 212 123
105 37
109
Store numbers down the side and weeks across the top.
If you are using SQL Server 2005+, then you can use the PIVOT function to transform the data from rows into columns.
It sounds like you will need to use dynamic sql if the weeks are unknown but it is easier to see the correct code using a hard-coded version initially.
First up, here are some quick table definitions and data for use:
CREATE TABLE yt
(
[Store] int,
[Week] int,
[xCount] int
);
INSERT INTO yt
(
[Store],
[Week], [xCount]
)
VALUES
(102, 1, 96),
(101, 1, 138),
(105, 1, 37),
(109, 1, 59),
(101, 2, 282),
(102, 2, 212),
(105, 2, 78),
(109, 2, 97),
(105, 3, 60),
(102, 3, 123),
(101, 3, 220),
(109, 3, 87);
If your values are known, then you will hard-code the query:
select *
from
(
select store, week, xCount
from yt
) src
pivot
(
sum(xcount)
for week in ([1], [2], [3])
) piv;
See SQL Demo
Then if you need to generate the week number dynamically, your code will be:
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT ',' + QUOTENAME(Week)
from yt
group by Week
order by Week
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT store,' + #cols + ' from
(
select store, week, xCount
from yt
) x
pivot
(
sum(xCount)
for week in (' + #cols + ')
) p '
execute(#query);
See SQL Demo.
The dynamic version, generates the list of week numbers that should be converted to columns. Both give the same result:
| STORE | 1 | 2 | 3 |
---------------------------
| 101 | 138 | 282 | 220 |
| 102 | 96 | 212 | 123 |
| 105 | 37 | 78 | 60 |
| 109 | 59 | 97 | 87 |
This is for dynamic # of weeks.
Full example here:SQL Dynamic Pivot
DECLARE #DynamicPivotQuery AS NVARCHAR(MAX)
DECLARE #ColumnName AS NVARCHAR(MAX)
--Get distinct values of the PIVOT Column
SELECT #ColumnName= ISNULL(#ColumnName + ',','') + QUOTENAME(Week)
FROM (SELECT DISTINCT Week FROM #StoreSales) AS Weeks
--Prepare the PIVOT query using the dynamic
SET #DynamicPivotQuery =
N'SELECT Store, ' + #ColumnName + '
FROM #StoreSales
PIVOT(SUM(xCount)
FOR Week IN (' + #ColumnName + ')) AS PVTTable'
--Execute the Dynamic Pivot Query
EXEC sp_executesql #DynamicPivotQuery
I've achieved the same thing before by using subqueries. So if your original table was called StoreCountsByWeek, and you had a separate table that listed the Store IDs, then it would look like this:
SELECT StoreID,
Week1=(SELECT ISNULL(SUM(xCount),0) FROM StoreCountsByWeek WHERE StoreCountsByWeek.StoreID=Store.StoreID AND Week=1),
Week2=(SELECT ISNULL(SUM(xCount),0) FROM StoreCountsByWeek WHERE StoreCountsByWeek.StoreID=Store.StoreID AND Week=2),
Week3=(SELECT ISNULL(SUM(xCount),0) FROM StoreCountsByWeek WHERE StoreCountsByWeek.StoreID=Store.StoreID AND Week=3)
FROM Store
ORDER BY StoreID
One advantage to this method is that the syntax is more clear and it makes it easier to join to other tables to pull other fields into the results too.
My anecdotal results are that running this query over a couple of thousand rows completed in less than one second, and I actually had 7 subqueries. But as noted in the comments, it is more computationally expensive to do it this way, so be careful about using this method if you expect it to run on large amounts of data .
This is what you can do:
SELECT *
FROM yourTable
PIVOT (MAX(xCount)
FOR Week in ([1],[2],[3],[4],[5],[6],[7])) AS pvt
DEMO
I'm writing an sp that could be useful for this purpose, basically this sp pivot any table and return a new table pivoted or return just the set of data, this is the way to execute it:
Exec dbo.rs_pivot_table #schema=dbo,#table=table_name,#column=column_to_pivot,#agg='sum([column_to_agg]),avg([another_column_to_agg]),',
#sel_cols='column_to_select1,column_to_select2,column_to_select1',#new_table=returned_table_pivoted;
please note that in the parameter #agg the column names must be with '[' and the parameter must end with a comma ','
SP
Create Procedure [dbo].[rs_pivot_table]
#schema sysname=dbo,
#table sysname,
#column sysname,
#agg nvarchar(max),
#sel_cols varchar(max),
#new_table sysname,
#add_to_col_name sysname=null
As
--Exec dbo.rs_pivot_table dbo,##TEMPORAL1,tip_liq,'sum([val_liq]),sum([can_liq]),','cod_emp,cod_con,tip_liq',##TEMPORAL1PVT,'hola';
Begin
Declare #query varchar(max)='';
Declare #aggDet varchar(100);
Declare #opp_agg varchar(5);
Declare #col_agg varchar(100);
Declare #pivot_col sysname;
Declare #query_col_pvt varchar(max)='';
Declare #full_query_pivot varchar(max)='';
Declare #ind_tmpTbl int; --Indicador de tabla temporal 1=tabla temporal global 0=Tabla fisica
Create Table #pvt_column(
pivot_col varchar(100)
);
Declare #column_agg table(
opp_agg varchar(5),
col_agg varchar(100)
);
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(#table) AND type in (N'U'))
Set #ind_tmpTbl=0;
ELSE IF OBJECT_ID('tempdb..'+ltrim(rtrim(#table))) IS NOT NULL
Set #ind_tmpTbl=1;
IF EXISTS (SELECT * FROM sys.objects WHERE object_id = OBJECT_ID(#new_table) AND type in (N'U')) OR
OBJECT_ID('tempdb..'+ltrim(rtrim(#new_table))) IS NOT NULL
Begin
Set #query='DROP TABLE '+#new_table+'';
Exec (#query);
End;
Select #query='Select distinct '+#column+' From '+(case when #ind_tmpTbl=1 then 'tempdb.' else '' end)+#schema+'.'+#table+' where '+#column+' is not null;';
Print #query;
Insert into #pvt_column(pivot_col)
Exec (#query)
While charindex(',',#agg,1)>0
Begin
Select #aggDet=Substring(#agg,1,charindex(',',#agg,1)-1);
Insert Into #column_agg(opp_agg,col_agg)
Values(substring(#aggDet,1,charindex('(',#aggDet,1)-1),ltrim(rtrim(replace(substring(#aggDet,charindex('[',#aggDet,1),charindex(']',#aggDet,1)-4),')',''))));
Set #agg=Substring(#agg,charindex(',',#agg,1)+1,len(#agg))
End
Declare cur_agg cursor read_only forward_only local static for
Select
opp_agg,col_agg
from #column_agg;
Open cur_agg;
Fetch Next From cur_agg
Into #opp_agg,#col_agg;
While ##fetch_status=0
Begin
Declare cur_col cursor read_only forward_only local static for
Select
pivot_col
From #pvt_column;
Open cur_col;
Fetch Next From cur_col
Into #pivot_col;
While ##fetch_status=0
Begin
Select #query_col_pvt='isnull('+#opp_agg+'(case when '+#column+'='+quotename(#pivot_col,char(39))+' then '+#col_agg+
' else null end),0) as ['+lower(Replace(Replace(#opp_agg+'_'+convert(varchar(100),#pivot_col)+'_'+replace(replace(#col_agg,'[',''),']',''),' ',''),'&',''))+
(case when #add_to_col_name is null then space(0) else '_'+isnull(ltrim(rtrim(#add_to_col_name)),'') end)+']'
print #query_col_pvt
Select #full_query_pivot=#full_query_pivot+#query_col_pvt+', '
--print #full_query_pivot
Fetch Next From cur_col
Into #pivot_col;
End
Close cur_col;
Deallocate cur_col;
Fetch Next From cur_agg
Into #opp_agg,#col_agg;
End
Close cur_agg;
Deallocate cur_agg;
Select #full_query_pivot=substring(#full_query_pivot,1,len(#full_query_pivot)-1);
Select #query='Select '+#sel_cols+','+#full_query_pivot+' into '+#new_table+' From '+(case when #ind_tmpTbl=1 then 'tempdb.' else '' end)+
#schema+'.'+#table+' Group by '+#sel_cols+';';
print #query;
Exec (#query);
End;
GO
This is an example of execution:
Exec dbo.rs_pivot_table #schema=dbo,#table=##TEMPORAL1,#column=tip_liq,#agg='sum([val_liq]),avg([can_liq]),',#sel_cols='cod_emp,cod_con,tip_liq',#new_table=##TEMPORAL1PVT;
then Select * From ##TEMPORAL1PVT would return:
Here is a revision of #Tayrn answer above that might help you understand pivoting a little easier:
This may not be the best way to do this, but this is what helped me wrap my head around how to pivot tables.
ID = rows you want to pivot
MY_KEY = the column you are selecting from your original table that contains the column names you want to pivot.
VAL = the value you want returning under each column.
MAX(VAL) => Can be replaced with other aggregiate functions. SUM(VAL), MIN(VAL), ETC...
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT ',' + QUOTENAME(MY_KEY)
from yt
group by MY_KEY
order by MY_KEY ASC
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT ID,' + #cols + ' from
(
select ID, MY_KEY, VAL
from yt
) x
pivot
(
sum(VAL)
for MY_KEY in (' + #cols + ')
) p '
execute(#query);
select * from (select name, ID from Empoyee) Visits
pivot(sum(ID) for name
in ([Emp1],
[Emp2],
[Emp3]
) ) as pivottable;
Just give you some idea how other databases solve this problem. DolphinDB also has built-in support for pivoting and the sql looks much more intuitive and neat. It is as simple as specifying the key column (Store), pivoting column (Week), and the calculated metric (sum(xCount)).
//prepare a 10-million-row table
n=10000000
t=table(rand(100, n) + 1 as Store, rand(54, n) + 1 as Week, rand(100, n) + 1 as xCount)
//use pivot clause to generate a pivoted table pivot_t
pivot_t = select sum(xCount) from t pivot by Store, Week
DolphinDB is a columnar high performance database. The calculation in the demo costs as low as 546 ms on a dell xps laptop (i7 cpu). To get more details, please refer to online DolphinDB manual https://www.dolphindb.com/help/index.html?pivotby.html
Pivot is one of the SQL operator which is used to turn the unique data from one column into multiple column in the output. This is also mean by transforming the rows into columns (rotating table). Let us consider this table,
If I want to filter this data based on the types of product (Speaker, Glass, Headset) by each customer, then use Pivot operator.
Select CustmerName, Speaker, Glass, Headset
from TblCustomer
Pivot
(
Sum(Price) for Product in ([Speaker],[Glass],[Headset])
) as PivotTable
I have the following table:
CREATE TABLE dbo.Test
(
Name NVARCHAR(50)
,StartDate DATE
,EndDate DATE
)
INSERT INTO dbo.Test VALUES('ABC','28-Feb-14','03-Mar-14')
INSERT INTO dbo.Test VALUES('DEF','04-Mar-14','04-Mar-14')
Basically this contain start and end date of leave for a given user. I am expecting an output as shown below.
Expected output:
Name | WorkHour| LeaveHour | Remarks
-------------------------------------
ABC | 27 | 18 | 28-Feb, 03-Mar
DEF | 36 | 9 | 04-Mar
1 day of leave corresponds to 9 hours and a work week refers to Friday to the next Thursday. In this case that would be from 28-Feb to 06-Mar.
WorkHour refers to number of hours user has worked barring the leaves and not including the weekends.
LeaveHour refers to the number of hours user is on leave.
'Remarks' refers to distinct leaves for the user between StartDate and EndDate values that should appear as comma separated.
I am able to get the work hour (including weekends which is not desired), leave hour values but finding it difficult to have remarks and value excluding weekends
SELECT
RN.Name
,ISNULL(45 - ((DATEDIFF(DD, VT.StartDate, VT.EndDate) + 1) * 9 ), 0) AS 'WorkHours'
,ISNULL(((DATEDIFF(DD, VT.StartDate, VT.EndDate) + 1) * 9 ), 0) AS 'LeaveHours'
--distinct leave dates seperated by comma should be displayed as remarks
FROM
Test VT
LEFT JOIN
ResourceNames RN ON VT.UserId = RN.UserId
Can anyone help?
This should not be done in SQL, but here is a function that will do what you want:
create function CSVDates (#startDate datetime, #endDate datetime)
returns nvarchar(4000)
as
begin
declare #csv nvarchar(4000) = ''
declare #maxDays int = DATEDIFF(DD, #startDate, #endDate)
declare #count int = 0
declare #date datetime
while(#count <= #maxDays )
begin
if (DATENAME(dw, #date) = 'Saturday' OR DATENAME(dw, #date) = 'Sunday')
BEGIN
set #count = #count + 1
CONTINUE
END
set #date = DATEADD(d,#count, #startDate)
if (len(#csv) > 0) set #csv = #csv + ','
set #csv = #csv + DATENAME(day,#date) + '-' + DATENAME(month,#date)
set #count = #count + 1
end
return #csv
end
plug it into your select as CSVDates(vt.StartDate, vt.EndDate)
if you have a lot of dates in between, nvarchar(4000) may not be enough...
I figured out what was going wrong as finally I had time to work on this. Basically the block under weekend was not allowing date to get incremented which was resulting in the data not appearing.
Here is the working code for someone looking for similar ask
create function CSVDates (#startDate datetime, #endDate datetime)
returns nvarchar(4000)
as
begin
declare #csv nvarchar(4000) = ''
declare #maxDays int = DATEDIFF(DD, #startDate, #endDate)
declare #count int = 0
--assign start date
declare #date datetime = #startDate
while(#count <= #maxDays )
begin
if (DATENAME(dw, #date) = 'Saturday' OR DATENAME(dw, #date) = 'Sunday')
begin
--do nothing
set #count =#count
end
else
begin
if (len(#csv) > 0)
set #csv = #csv + ','
set #csv = #csv + DATENAME(day,#date) + '-' + SUBSTRING(DATENAME(month,#date),1,3)
end
set #count = #count + 1
set #date = DATEADD(d,#count, #startDate)
end
return #csv
end
I have a table with the following contents:
CategoryID
ParentID
Name
I would like to have a search functionality that would search the whole hierarchy, for exmple this is the breadcrumb of a category:
Motorcycles/Japan/Kawasaki/600cc to 800cc/1998-2004
If someone searches for "600cc Kawasaki" I would like the above category to be returned. So the categorypath which has the most matches should return.
At the moment I came up with this:
IF ISNULL(#searchTerm, '') = ''
SET #searchTerm = '""'
DECLARE #Result TABLE (CategoryId int)
DECLARE CategoryCursor CURSOR LOCAL FAST_FORWARD FOR
SELECT CategoryId, ParentId, Name
FROM Category
WHERE FREETEXT([Name], #searchTerm)
OPEN CategoryCursor
DECLARE #CategoryId int
DECLARE #ParentId int
DECLARE #Name nvarchar(100)
FETCH NEXT FROM CategoryCursor INTO #CategoryId, #ParentId, #Name
WHILE ##FETCH_STATUS = 0
BEGIN
DECLARE #FullPath nvarchar(1000)
SET #FullPath = #Name
WHILE #ParentId <> 0
BEGIN
SELECT #ParentId = ParentId, #Name = [Name]
FROM Category
WHERE CategoryId = #ParentId
SET #FullPath = #Name + '\' + #FullPath
END
-- Check if #FullPath contains all of the searchterms
DECLARE #found bit
DECLARE #searchWords NVARCHAR(100)
DECLARE #searchText NVARCHAR(255)
DECLARE #pos int
SET #found = 1
SET #searchWords = #searchTerm + ' '
SET #pos = CHARINDEX(' ', #searchWords)
WHILE #pos <> 0
BEGIN
SET #searchText = LEFT(#searchWords, #pos - 1)
SET #searchWords = STUFF(#searchWords, 1, #pos, '')
SET #pos = CHARINDEX(' ', #searchWords)
IF #searchText = '' CONTINUE
IF #FullPath NOT LIKE '%' + #searchText + '%'
BEGIN
SET #found = 0
BREAK
END
END
IF #found = 1
INSERT INTO #Result VALUES(#CategoryId)
FETCH NEXT FROM CategoryCursor INTO #CategoryId, #ParentId, #Name
END
CLOSE CategoryCursor
DEALLOCATE CategoryCursor
SELECT *
FROM Category
WHERE categoryID IN (SELECT categoryId FROM #Result)
This will first find all catagorynames which contain any of the searchwords. Problem is, I don't want "600cc" for other brands to return, only the one which is related to "Kawasaki".
So next I build the breadcrumb for the current category and see if it contains all of the searchwords.
It works but I think it is ineffective so i'm looking for a better method.
Perhaps storing the complete path as text in a new column and search on that?
I'd suggest using the hierarchyid which is in 2008. You would essentially set your hierarchy like this
/1/ - Root Node
/1/1/ - Motorcycles
/1/1/1/ - Japan
/1/1/1/1/ - Kawasaki
/1/1/1/2/ - Honda
/1/1/2/ - US
/1/1/2/1/ - Harley.
Then you can use the hierarchyid to get the entire tree from your 600cc 1984 kawasaki all the way up to motorcycles.
Here's a code sample from Programming Microsoft SQL Server 2008
CREATE FUNCTION dbo.fnGetFullDisplayPath(#EntityNodeId hierarchyid) RETURNS varchar(max) AS
BEGIN
DECLARE #EntityLevelDepth smallint
DECLARE #LevelCounter smallint
DECLARE #DisplayPath varchar(max)
DECLARE #ParentEmployeeName varchar(max)
-- Start with the specified node
SELECT #EntityLevelDepth = NodeId.GetLevel(),
#DisplayPath = EmployeeName
FROM Employee
WHERE NodeId = #EntityNodeId
-- Loop through all its ancestors
SET #LevelCounter = 0
WHILE #LevelCounter < #EntityLevelDepth
BEGIN
SET #LevelCounter = #LevelCounter + 1
SELECT #ParentEmployeeName = EmployeeName
FROM Employee WHERE NodeId = (SELECT NodeId.GetAncestor(#LevelCounter)
FROM Employee
WHERE NodeId = #EntityNodeId)
-- Prepend the ancestor name to the display path
SET #DisplayPath = #ParentEmployeeName + ' > ' + #DisplayPath
END
RETURN(#DisplayPath)
END
My /1/1/2 representation is the string representation. In the database you'd actually see the hex representation (e.g. 0x79).
There are a few key functions on the hierarchyid.
declare #motorcycleAncestor hieararchyid
select #motorcycleAncestor = nodeId.GetAncestor(1)
from parts
where Label = 'motorcycle'
select * from Parts
where Node.GetAncestor(1) = #motorcyleAncestor;
This query does a couple things. First, it gets the hierarchy id for the node that contains "Motorcycle" as the label. (I assume the hiearchy field is named 'nodeid' but you can obviously call it whatever.)
Next, it takes this node value and finds all the immediate children of motorcycles (who's ancestor, 1 level up, is the motorcycle node. You can actually specify any value, like GetAncestor(3) would be the ancestor 3 levels up). So in that case, it would find Japan, US, Germany etc.
There is another method, called IsDescendantOf(node). You can use it like this:
declare #motorcycleAncestor hieararchyid
select #motorcycleAncestor = nodeId.GetAncestor(1)
from parts
where Label = 'motorcycle'
select * from Parts
where Node.IsDescendantOf(#motorcycleAncestor) = 1
This would return all items that are children (of any level) underneath motorcycles. It would actually also include Motorcycles.
You can combine these in different ways. For example, we're using them in an org chart of sorts. We have the ability to show results for a single user, or for a user and his siblings (everyone at the exact same level) and a user and all his descendants.
So I could show your information, or I could show everyone in your department, or I could show everyone in your company.
I am working on a appliaction in which I have a following schema
Tasks Master
Task_ID
Task_Name
Task_Details
Task_ID
Task_Date
Task_Count (can be any number like 2 or 3 or 4 or 40)
the Input Form is like that which the staff will fill at the end of the day.
Date | Task Name | Task_Count
24/01/2010 | How many cheque books issued today | 12
24/01/2010 | How many ATM Issued today | 7
Now I want a matrix report showing all tasks suppose 28 tasks in vertical row and on given month it should show all the dates of the particular month horizontal direction like from 1 to 31 days or 30 or 28 as per month days with the task_count using PIVOT in query. i am failed to produce the result as i dont know to make it work. please help.
thanks
You'll need to use a dynamic query since the columns returned by the pivot changes each month.
With some time this could probably be made more elegant, but here's the basic idea:
declare #StartOfMonth datetime = '11/1/2010';
declare #counter datetime = #StartOfMonth;
declare #sql varchar(MAX) = '';
declare #columnnames varchar(MAX);
declare #columnfilter varchar(MAX);
declare #fieldname varchar(12);
--First, create a string of dynamic columns, one for each day of the month.
WHILE (MONTH(#counter) = MONTH(#StartOfMonth))
BEGIN
SET #fieldname = '[' + CONVERT(varchar(10), #counter, 101) + ']';
--Wrap the columns in ISNULL(#,0) to avoid having null values for days without tasks.
SET #columnnames = ISNULL(#columnnames + ',', '') + 'ISNULL(' + #fieldname + ',0) AS ' + #fieldname;
--Also create a dynamic list of the Task_Date values to include in the pivot.
SET #columnfilter = ISNULL(#columnfilter + ',', '') + #fieldname;
SET #counter = DATEADD(DAY,1,#counter);
END
--Put it all together into a pivot query.
set #sql = 'SELECT Task_Name, ' + #columnnames + ' FROM (';
set #sql = #sql + 'SELECT M.Task_Name, D.Task_Date, D.Task_Count '
set #sql = #sql + 'FROM Task_Detail D JOIN Task_Master M ON D.Task_ID = M.Task_ID) as SourceTable ';
set #sql = #sql + 'PIVOT (SUM(Task_Count) FOR Task_Date IN (' + #columnfilter + ')) AS PivotTable';
exec (#sql)