SQL dynamic pivot after CTE - sql

I hope this is more specific? sorry if I am unclear, kind of new to this. Thank you for the help!!
I'm trying to get a dynamic pivot to work on a CTE. I have looked around a bit and I have a couple of problems. For what I fount, it seems that something like the following post is pretty standard for a dynamic sql:
Pivot Table and Concatenate Columns
I have the following columns in my table with trades:
Date | product | time | price | volume |
I want to get the average price for each quarter of the day, so I want to pivot the time column after rounding it down to the nearest quarter time. and taking the Weighted average price per product and date.
so I use one CTE to create the pivot list:
DECLARE #pivot_list as varchar(max)
;with startquarter(starttradequarter)
AS
(
SELECT cast(DATEadd(mi,(datediff(mi,0,Time))/15*15,0)as varchar)
from [table]
where date > '2014-04-15'
),
PIVOT_CODES(PIVOT_CODE)
AS
(
SELECT DISTINCT starttradequarter AS PIVOT_CODE
from startquarter
)
SELECT #pivot_list = COALESCE(#pivot_list + ',[' + PIVOT_CODE + ']','[' + PIVOT_CODE + ']')
FROM PIVOT_CODES
then I want to use this variable in a pivot of the table:
;With productselector(Date,startquarter,product,volume,price)
as
(
SELECT [Date]
,cast(DATEadd(mi,(datediff(mi,0,Time))/15*15,0)as varchar) as startquarter
,[product]
,[Volume]
,[Price]
FROM [table]
where DelDate = '2014-01-06' and product = 'x'
),
WAPricequarter(startquarter,date,sumvolume,WAPq,product)
AS
(
SELECT startquarter
,Date
,sum(volume) as sumvolume
,round(sum(volume*price)/sum(volume),2) as WAPq
,product
from productselector
group by date, startquarter, product
)
SELECT date, product, + #pivot_list
from WAPricequarter
PIVOT (
SUM([sumvolume])
FOR startquarter IN (#pivot_list)
) AS pvt
So I see in all dynamic pivots the second statement first put in a variable and then executed, is this necessary?
If not how do I get the pivot to work on the columns in the #pivot_list, it now gives an incorrect syntax error that I can't get to work.
If it is necessary to put it in a variable and then execute, how can I then still filter for product or date inside that variable since I have to use '' around it.

Related

Order by with variable Coalesce

I have the following sql command
DECLARE #cols NVARCHAR (MAX)
SELECT #cols = COALESCE (#cols + ',['+ cast(Month as nvarchar(2))+']' , '['+ Cast(Month as nvarchar(2))+']')
FROM (select distinct Month from Employee WHERE Year*100+Month BETWEEN 201704 and 201712 ) as e
PRINT #cols
The result was
[9],[12],[6],[7],[10],[4],[5],[11],[8]
But I really want it to result in sort order
[4],[5],[6],[7],[8],[9],[10],[11],[12]
Variable coalescing is documented as being non-deterministic, and may cause incorrect results, in particular in the presence of ORDER BY. You can also not place ORDER BY in a derived table or view, for obvious reasons: the final ordering is determined only by the outer query.
You should instead just use STRING_AGG to aggregate. You can use WITHIN GROUP (ORDER BY to get the ordering.
Note also:
Always use QUOTENAME to get brackets round your column names, instead of doing it yourself, as escaping can be complex.
It's better to make exact comparisons on columns, rather than doing calculations on them and then comparing, as then you can hit indexes (sarge-ability).
It's probably better to store dates in actual date columns, rather than messing around with multiple columns, but I will leave database redesign to you
DECLARE #cols NVARCHAR(MAX) = (
SELECT STRING_AGG(QUOTENAME(Month), N',') WITHIN GROUP (ORDER BY Month)
FROM (
select distinct
Month
from Employee
WHERE Year = 2017
AND Month BETWEEN 4 AND 12
) as e
);
For SQL Server 2016 and earlier, you can use the old FOR XML method:
DECLARE #cols NVARCHAR(MAX) = STUFF((
SELECT N',' + QUOTENAME(Month)
FROM (
select distinct
Month
from Employee
WHERE Year = 2017
AND Month BETWEEN 4 AND 12
) as e
ORDER BY Month
FOR XML PATH(''), TYPE
).value('text()[1]','nvarchar(max)')
, 1, LEN(N','), N'');

SQL Server - Dynamic Pivot with 2 Group Variables and 2 Aggregate Calculations

I have a dataset that is shaped like this:
I am trying to convert the data to this format:
As you can see, I'd like to sum the accounts and revenue (for each month) by State and Account Type. It is important to note that I seek a dynamic solution as these ARE NOT the only values (hard-coding is not an option!).
What SQL query can I write to accomplish this task, dynamically? (as these values are not the only ones present in the complete dataset).
Thanks!
I'm assuming you want to keep the columns in order by date, thus the top 100 percent ... order by in the section where we generate the columns
Example
Declare #SQL varchar(max) = '
Select *
From (
Select [State]
,[AccountType]
,B.*
From YourTable A
Cross Apply ( values (concat(''Accounts_'',format([Date],''MM/dd/yyyy'')),Accounts)
,(concat(''Revenue_'' ,format([Date],''MM/dd/yyyy'')),Revenue)
) B (Item,Value)
) A
Pivot (sum([Value]) For [Item] in (' + Stuff((Select ','+QuoteName('Accounts_'+format([Date],'MM/dd/yyyy'))
+','+QuoteName('Revenue_' +format([Date],'MM/dd/yyyy'))
From (Select top 100 percent [Date] from YourTable Group By [Date] Order by [Date] ) A
For XML Path('')),1,1,'') + ') ) p'
--Print #SQL
Exec(#SQL)
Returns
If it helps, the generated SQL looks like this:
Select *
From (
Select [State]
,[AccountType]
,B.*
From YourTable A
Cross Apply ( values (concat('Accounts_',format([Date],'MM/dd/yyyy')),Accounts)
,(concat('Revenue_' ,format([Date],'MM/dd/yyyy')),Revenue)
) B (Item,Value)
) A
Pivot (sum([Value]) For [Item] in ([Accounts_12/31/2017],[Revenue_12/31/2017],[Accounts_01/31/2018],[Revenue_01/31/2018]) ) p

SQL Multi Pivot

I am trying to do a pivot table to show order data by dayofyear. My first problem is my pivot doesn't appear to be showing the correct data. My second problem is I don't really want to type out a day for all 365 day columns. Is there an easier way?
Columns would be 1 - 365
Rows would be Year, #orders, #Tags
SELECT Yr, [01],[02],[03],[04],[05]....
FROM (
select TOP 100 PERCENT
YEAR(tagdata.shipdate) AS Yr,
DATEPART(dy,tagdata.shipdate) AS Day,
tagdata.#Orders,
tagdata.#Tags,
from tagData
GROUP BY tagData.ShipDate, tagdata.#Orders, tagdata.#Tags
) AS sourcetable
PIVOT
(
Max(#Orders) FOR Day IN ([01],[02],[03],[04],[05],.......),
Max(#Tags) FOR Day IN ([01],[02],[03],[04],[05],.......)
)
as pivottable
ORDER BY Yr
You have to do two pivots for each set of fields you are looking at and then join them on the year. Something like this:
SELECT isnull(pivottable1.Yr, pivottable2.Yr) Yr,
[1ORD],[2ORD],[3ORD],[4ORD],[5ORD],
[1TAG],[2TAG],[3TAG],[4TAG],[5TAG]
FROM (
select TOP 100 PERCENT
YEAR(shipdate) AS Yr,
Convert(varchar(3),DATEPART(dy,shipdate)) + 'ORD' AS Day,
#Orders
from #tagData
) AS sourcetable1
PIVOT
(
Max(#Orders) FOR Day IN ([1ORD],[2ORD],[3ORD],[4ORD],[5ORD])
) as pivottable1
full join (
select TOP 100 PERCENT
YEAR(shipdate) AS Yr,
Convert(varchar(3),DATEPART(dy,shipdate)) + 'TAG' AS Day,
#Tags
from #tagData
) AS sourcetable2
PIVOT
(
Max(#Tags) FOR Day IN ([1TAG],[2TAG],[3TAG],[4TAG],[5TAG])
) as pivottable2
on pivottable1.Yr = pivottable2.Yr
ORDER BY isnull(pivottable1.Yr, pivottable2.Yr)
As for all that typing... a simple script can cover you:
declare #number as int
declare #numbers as varchar(max)
set #number=1
while #number <= 365
begin
set #numbers = isnull(#numbers+',','') + '[' + convert(varchar(3),#number) + ']'
set #number=#number+1
end
print #numbers

Selecting data for columns based on a range of dates

I have a table that has a week_id and net_sales for that week (as well as a lot of other columns).
style_number, week_id, net_sales
ABCD, 1, 100.00
ABCD, 2, 125.00
EFGH, 1, 50.00
EFGH, 2, 75.00
I am trying to write a statement that will list the
style_number, net_sales
for the
MAX(week_id), net_sales for the MAX(week_id)-1 .... , MAX(week_id) - n
So that the results look like:
ABCD, 125.00, 100.00
EFGH, 75.00, 50.00
What is the best way to approach this, especially when n can be rather large (i.e. looking back 52 weeks)?
I hope this makes sense! I am using SQL Server 2008 R2. Thanks a lot in advance!
You can use PIVOT and dynamic SQL to deal with your large number of weeks
DECLARE #cols NVARCHAR(MAX), #sql NVARCHAR(MAX)
SET #cols = STUFF((SELECT DISTINCT ',' + QUOTENAME(week_id)
FROM sales
ORDER BY 1 DESC
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)'),1,1,'')
SET #sql = 'SELECT style_number, ' + #cols +
' FROM
(
SELECT style_number, week_id, net_sales
FROM sales
) x
PIVOT
(
MAX(net_sales) FOR week_id IN (' + #cols + ')
) p
ORDER BY style_number'
EXECUTE(#sql)
Here is SQLFiddle demo.
If you know the number of weeks, since you are using SQL Server 2008, you can use the PIVOT command, or you can use MAX with CASE:
Here's an example using MAX with CASE:
select
style_number,
max(case when week_id = 2 then net_sales end) week2sales,
max(case when week_id = 1 then net_sales end) week1sales
from yourtable
group by style_number
SQL Fiddle Demo
If you do not know the number of weeks, you'll need to look into using dynamic SQL. Just do a search, lots of posts on SO on it.
You might consider using the PIVOT command: http://msdn.microsoft.com/en-us/library/ms177410(v=sql.105).aspx
OR
If you are okay with the result being a comma separated list, you could use the STUFF and FOR XML commands like so.
SELECT DISTINCT
style_name,
STUFF(
(SELECT ',' + CAST(net_sales AS VARCHAR(20))
FROM MyTable AS SubTable
WHERE SubTableUser.style = MyTable.style_name
ORDER BY week_id DESC --DESC will get your max ID to be first
FOR XML PATH('')), 1, 1, '') AS net_sales_list
FROM MyTable
ORDER BY style_name
This will provide you with:
style_name | net_sales_list
---------------------------
ABCD | 100.00,125.00
EFGH | 75.00,50.00

How can I pivot these key+values rows into a table of complete entries?

Maybe I demand too much from SQL but I feel like this should be possible. I start with a list of key-value pairs, like this:
'0:First, 1:Second, 2:Third, 3:Fourth'
etc. I can split this up pretty easily with a two-step parse that gets me a table like:
EntryNumber PairNumber Item
0 0 0
1 0 First
2 1 1
3 1 Second
etc.
Now, in the simple case of splitting the pairs into a pair of columns, it's fairly easy. I'm interested in the more advanced case where I might have multiple values per entry, like:
'0:First:Fishing, 1:Second:Camping, 2:Third:Hiking'
and such.
In that generic case, I'd like to find a way to take my 3-column result table and somehow pivot it to have one row per entry and one column per value-part.
So I want to turn this:
EntryNumber PairNumber Item
0 0 0
1 0 First
2 0 Fishing
3 1 1
4 1 Second
5 1 Camping
Into this:
Entry [1] [2] [3]
0 0 First Fishing
1 1 Second Camping
Is that just too much for SQL to handle, or is there a way? Pivots (even tricky dynamic pivots) seem like an answer, but I can't figure how to get that to work.
No, in SQL you can't infer columns dynamically based on the data found during the same query.
Even using the PIVOT feature in Microsoft SQL Server, you must know the columns when you write the query, and you have to hard-code them.
You have to do a lot of work to avoid storing the data in a relational normal form.
Alright, I found a way to accomplish what I was after. Strap in, this is going to get bumpy.
So the basic problem is to take a string with two kinds of delimiters: entries and values. Each entry represents a set of values, and I wanted to turn the string into a table with one column for each value per entry. I tried to make this a UDF, but the necessity for a temporary table and dynamic SQL meant it had to be a stored procedure.
CREATE PROCEDURE [dbo].[ParseValueList]
(
#parseString varchar(8000),
#itemDelimiter CHAR(1),
#valueDelimiter CHAR(1)
)
AS
BEGIN
SET NOCOUNT ON;
IF object_id('tempdb..#ParsedValues') IS NOT NULL
BEGIN
DROP TABLE #ParsedValues
END
CREATE TABLE #ParsedValues
(
EntryID int,
[Rank] int,
Pair varchar(200)
)
So that's just basic set up, establishing the temp table to hold my intermediate results.
;WITH
E1(N) AS (SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL
SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1 UNION ALL SELECT 1),--Brute forces 10 rows
E2(N) AS (SELECT 1 FROM E1 a, E1 b), --Uses a cross join to generate 100 rows (10 * 10)
E4(N) AS (SELECT 1 FROM E2 a, E2 b), --Uses a cross join to generate 10,000 rows (100 * 100)
cteTally(N) AS (SELECT ROW_NUMBER() OVER (ORDER BY N) FROM E4)
That beautiful piece of SQL comes from SQL Server Central's Forums and is credited to "a guru." It's a great little 10,000 line tally table perfect for string splitting.
INSERT INTO #ParsedValues
SELECT ItemNumber AS EntryID, ROW_NUMBER() OVER (PARTITION BY ItemNumber ORDER BY ItemNumber) AS [Rank],
SUBSTRING(Items.Item, T1.N, CHARINDEX(#valueDelimiter, Items.Item + #valueDelimiter, T1.N) - T1.N) AS [Value]
FROM(
SELECT ROW_NUMBER() OVER (ORDER BY T2.N) AS ItemNumber,
SUBSTRING(#parseString, T2.N, CHARINDEX(#itemDelimiter, #parseString + #itemDelimiter, T2.N) - T2.N) AS Item
FROM cteTally T2
WHERE T2.N < LEN(#parseString) + 2 --Ensures we cut out once the entire string is done
AND SUBSTRING(#itemDelimiter + #parseString, T2.N, 1) = #itemDelimiter
) AS Items, cteTally T1
WHERE T1.N < LEN(#parseString) + 2 --Ensures we cut out once the entire string is done
AND SUBSTRING(#valueDelimiter + Items.Item, T1.N, 1) = #valueDelimiter
Ok, this is the first really dense meaty part. The inner select is breaking up my string along the item delimiter (the comma), using the guru's string splitting method. Then that table is passed up to the outer select which does the same thing, but this time using the value delimiter (the colon) to each row. The inner RowNumber (EntryID) and the outer RowNumber over Partition (Rank) are key to the pivot. EntryID show which Item the values belong to, and Rank shows the ordinal of the values.
DECLARE #columns varchar(200)
DECLARE #columnNames varchar(2000)
DECLARE #query varchar(8000)
SELECT #columns = COALESCE(#columns + ',[' + CAST([Rank] AS varchar) + ']', '[' + CAST([Rank] AS varchar)+ ']'),
#columnNames = COALESCE(#columnNames + ',[' + CAST([Rank] AS varchar) + '] AS Value' + CAST([Rank] AS varchar)
, '[' + CAST([Rank] AS varchar)+ '] AS Value' + CAST([Rank] AS varchar))
FROM (SELECT DISTINCT [Rank] FROM #ParsedValues) AS Ranks
SET #query = '
SELECT '+ #columnNames +'
FROM #ParsedValues
PIVOT
(
MAX([Value]) FOR [Rank]
IN (' + #columns + ')
) AS pvt'
EXECUTE(#query)
DROP TABLE #ParsedValues
END
And at last, the dynamic sql that makes it possible. By getting a list of Distinct Ranks, we set up our column list. This is then written into the dynamic pivot which tilts the values over and slots each value into the proper column, each with a generic "Value#" heading.
Thus by calling EXEC ParseValueList with a properly formatted string of values, we can break it up into a table to feed into our purposes! It works (but is probably overkill) for simple key:value pairs, and scales up to a fair number of columns (About 50 at most, I think, but that'd be really silly.)
Anyway, hope that helps anyone having a similar issue.
(Yeah, it probably could have been done in something like SQLCLR as well, but I find a great joy in solving problems with pure SQL.)
Though probably not optimal, here's a more condensed solution.
DECLARE #DATA varchar(max);
SET #DATA = '0:First:Fishing, 1:Second:Camping, 2:Third:Hiking';
SELECT
DENSE_RANK() OVER (ORDER BY [Data].[row]) AS [Entry]
, [Data].[row].value('(./B/text())[1]', 'int') as "[1]"
, [Data].[row].value('(./B/text())[2]', 'varchar(64)') as "[2]"
, [Data].[row].value('(./B/text())[3]', 'varchar(64)') as "[3]"
FROM
(
SELECT
CONVERT(XML, '<A><B>' + REPLACE(REPLACE(#DATA , ',', '</B></A><A><B>'), ':', '</B><B>') + '</B></A>').query('.')
) AS [T]([c])
CROSS APPLY [T].[c].nodes('/A') AS [Data]([row]);
Hope is not too late.
You can use the function RANK to know the position of each Item per PairNumber. And then use Pivot
SELECT PairNumber, [1] ,[2] ,[3]
FROM
(
SELECT PairNumber, Item, RANK() OVER (PARTITION BY PairNumber order by EntryNumber) as RANKing
from tabla) T
PIVOT
(MAX(Item)
FOR RANKing in ([1],[2],[3])
)as PVT