How to transform SQL Server row data to columns? - sql-server

I queried data like this
member_no cover_version product_id product_name product_type
--------- ------------- ---------- -------------------- ------------
11421 7 4 Excellent More E
11421 7 15 Comprehensive Data D
But I want to shape this data like this:
member_no cover_version product_e_id product_e_name product_d_id product_d_name
--------- ------------- ------------ -------------------- ------------ --------------------
11421 7 4 Excellent More 15 Comprehensive Data
I am using SQL Server 2008. What should be the best approach to shape the data as I want?

Assuming you've only got product types D and E as stated, a simple self-join will get you what you're after.
If you want something more generic, please expand your question.
DROP TABLE IF EXISTS #Demo
SELECT
*
INTO
#Demo
FROM
(VALUES
(11421, 7, 4, 'Excellent More', 'E')
,(11421, 7, 15, 'Comprehensive Data', 'D')) A
(member_no, cover_version, product_id, product_name, product_type)
SELECT
D.member_no
,D.cover_version
,E.product_id product_e_id
,E.product_name product_e_name
,D.product_id product_d_id
,D.product_name product_d_name
FROM
#Demo D
JOIN #Demo E ON D.member_no = E.member_no
AND D.product_type = 'D'
AND E.product_type = 'E';
member_no cover_version product_e_id product_e_name product_d_id product_d_name
----------- ------------- ------------ ------------------ ------------ ------------------
11421 7 4 Excellent More 15 Comprehensive Data

If you want to do this dynamically, based on unknown product types, you can use this.
DECLARE #SQL NVARCHAR(MAX),
#Columns NVARCHAR(MAX),
#CaseExpressions NVARCHAR(MAX) = 'MAX(CASE WHEN product_type = ''<<productType>>'' THEN product_id END) AS [product_<<productType>>_id],
MAX(CASE WHEN product_type = ''<<productType>>'' THEN product_name END) AS [product_<<productType>>_name]'
-- build concatenated string with 2 columns for each product_type in your table.
-- group by product_type to get distinct results.
-- replaces <<productType>> with the actual product_type value
SELECT #Columns = COALESCE(#Columns + ',', '') + REPLACE(#CaseExpressions, '<<productType>>', product_type)
FROM myTable
GROUP BY product_type
-- build select query
SET #SQL = 'SELECT member_no, cover_version,' + #Columns + ' FROM myTable GROUP BY member_no, cover_version'
-- to see what the dynamic sql looks like
PRINT #SQL
-- to execute the dynamic sql and see result
EXEC (#SQL)

Related

Pivot a table on two value columns

I am trying to pivot a table on two columns (we did some normalization on our table but need to create a view for backwards compatibility). My new data structure is this:
CREATE TABLE #Products (
product VARCHAR(30),
market_year INT,
value INT,
description varchar(20));
INSERT INTO #Products VALUES('Corn', 2003, 100, 'bad');
INSERT INTO #Products VALUES('Beer', 2003, 200, 'not so bad');
INSERT INTO #Products VALUES('Beef', 2003, 150, 'good');
INSERT INTO #Products VALUES('Corn', 2004, 10, 'doo');
INSERT INTO #Products VALUES('Beer', 2004, 20, 'foo');
INSERT INTO #Products VALUES('Beef', 2004, 10, 'bar');
which looks Like:
SELECT * FROM #Products p;
product market_year value description
Corn 2003 100 bad
Beer 2003 200 not so bad
Beef 2003 150 good
Corn 2004 10 doo
Beer 2004 20 foo
Beef 2004 10 bar
The result I need should look like the following:
market_year |corn_value |corn_description | beer_value | beer_description |beef_value | beef_description
2003 | 100 |bad | 150 | NOT so bad |150 | good
2003 | 10 |doo | 15 | foo |15 | bar
I know I could do this with two separate statements with one pivot each on market_year and join them by market_year but that would require this table to be read twice. Is there a more elegant way to solve this issue?
(my table has approximately 7 million records split by 70 columns that need to be reverted to 140 columns)
You can try to use dynamic pivot
declare two var #CONTENT to represnt pivot sql query CASE WHEN with MAX
then use #sql to connecte your query sql and use execute execute this sql dynamically.
DECLARE #SQL VARCHAR(MAX),#CONTENT VARCHAR(MAX)
SELECT #CONTENT = STUFF((
SELECT ', '+'MAX(CASE WHEN product = '''+ product + ''' THEN [VALUE] END) AS '''+product+'_value'',
MAX(CASE WHEN product = '''+ product + ''' THEN [description] END) AS '''+product+'_value'''
FROM #Products
FOR XML PATH(''),TYPE).value('(./text())[1]','VARCHAR(MAX)')
,1,2,'')
SELECT #SQL = 'SELECT market_year,' + #CONTENT + ' FROM #Products GROUP BY market_year';
execute(#SQL)
sqlfiddle
select *
from
(
select market_year,product, value
from #products p
) src
pivot
(
sum(value)
for product in (Beer, Com, Beef)
) piv;
try this it will help you to get some idea

How to get dynamically different type of datatype value in SQL server pivot table

I have this following table with header information:
InformationID Name DispalyName VAlueType
1 Occupation Occupation Text
3 Surgery Surgery Header
4 SurgeryType SurgeryType Text
5 SurgeryDate SurgeryDate Datetime
8 Rehabilitation Rehabilitation Header
9 RehabilitationPT Rehabilitation Physiotherapist Text
10 RehabilitationDate Rehabilitation Date DateTime
11 DischargeSession Discharge Session Number int
12 Height Height decimal
13 Weight Weight decimal
I have another table has all these rows values stored like this:
InfomationID ClientID TextValue NumberValue DateValue
1 XXXX Programmer null null
4 XXXX Left knee surgery null null
5 XXXX null null 2016-08-01 01:00:00
11 XXXX null 6 null
12 XXXX null 164.592 null
Now I need to get those in a sub-query with where ClientID = XXXX
Part of result would be something following:
CFname| CLname| Occupation| SurgeryType | SurgeryDate| DischargeSession
-------------------------------------------------------------------------------
CFName| CLname| Programmer| Left knee surgery| 2016-08-01 | 6
::::
::::
More records
Regards
you would just need to convert the number and date values to varchar.. you can use coalesce to get the first non null value.
your pivot would looks something like this
SELECT *
FROM (
SELECT c.CFName,
c.CLName,
fh.Name,
COALESCE(fv.TextValue,
CAST(fv.NumberValue AS VARCHAR(50)), -- convert numbers to text
CONVERT(VARCHAR(10), fv.DateValue, 101) AS DynamicValue -- convert date to text
-- increase varchar length for time
-- or 101 for different format
FROM ClientInfo c
JOIN FieldValues fv ON c.ClientID = fv.ClientID
JOIN FieldHeaders fh ON fh.InformationID = fv.InformationID
) t
PIVOT (
MAX(DynamicValue)
FOR fh.Name IN ([Occupation], [SurgeryType], [SurgeryDate], [DischargeSession])
) p
to do this dynamically you would need to build your sql and inject the pivot columns into the sql by selecting the values into a nvarchar using STUFF(FOR XML)
DECLARE #Sql NVARCHAR(MAX),
#Columns NVARCHAR(MAX)
SELECT #Columns = STUFF((
SELECT ',' + QUOTENAME(Name)
FROM FieldHeaders
FOR XML PATH('')
), 1, 1, 0)
SET #Sql = N'
SELECT *
FROM (
SELECT c.CFName,
c.CLName,
fh.Name,
COALESCE(fv.TextValue,
CAST(fv.NumberValue AS VARCHAR(50)), -- convert numbers to text
CONVERT(VARCHAR(10), fv.DateValue, 101) AS DynamicValue -- convert date to text
-- increase varchar length for time
-- or 101 for different format
FROM ClientInfo c
JOIN FieldValues fv ON c.ClientID = fv.ClientID
JOIN FieldHeaders fh ON fh.InformationID = fv.InformationID
) t
PIVOT (
MAX(DynamicValue)
FOR fh.Name IN (' + #Columns + ')
) p
'
EXEC(#Sql)

Converting rows to columns

I have a query with the columns 'Name', 'Amount', and 'ReasonId'. I want to sum the amount and put the reasons on one row to keep every name to a single line. There are about 50 distinct ReasonId's so I do not want to name the column the name of the ReasonId's. Instead, I would like to name the columns 'Reason1', 'Reason2', 'Reason3', and 'Reason4'. One single name can have up to 4 different reasons.
I have this:
Name Amount ReasonId
-------------------------
Bob $5 7
Bob $8 6
John $2 8
John $5 9
John $3 9
John $8 4
I want to produce the following:
Name Amount Reason1 Reason2 Reason3 Reason4
-----------------------------------------------------
Bob $13 7 6 NULL NULL
John $18 8 9 4 NULL
One way to do this is to use the dense_rank window function to number the rows, and then use conditional aggregation to put the reason in the correct columns.
I can't see anything that would give the specific order of the reason columns though, maybe there is some column missing that provides the order?
with cte as (
select
name,
reasonid,
amount,
dense_rank() over (partition by name order by reasonid) rn
from your_table
)
select
name,
sum(amount) amount,
max(case when rn = 1 then reasonid end) reason1,
max(case when rn = 2 then reasonid end) reason2,
max(case when rn = 3 then reasonid end) reason3,
max(case when rn = 4 then reasonid end) reason4
from cte
group by name
If you have some column that gives the order you want then change the order by clause used in the dense_rank function.
Sample SQL Fiddle (using PG as MSSQL seems to be offline).
The output from the query above would be:
| name | amount | reason1 | reason2 | reason3 | reason4 |
|------|--------|---------|---------|---------|---------|
| Bob | 13 | 6 | 7 | (null) | (null) |
| John | 18 | 4 | 8 | 9 | (null) |
You could also use a pivot to achieve this; if you know the columns you can enter them in the script, but if not, you can use dynamic sql (there are reasons why you might want to avoid the dynamic solution).
The advantage of this route is that you can enter the column list in a table and then changes to that table will result in changes to your output with change to the script involved. The disadvantages are all those associated with dynamic SQL.
In the interests of variation, here is a dynamic SQL solution using temp tables to hold your data, since a different possibility has been provided:
-- set up your data
CREATE TABLE #MyTab (Name VARCHAR(4), Amount INT, ReasonId INT)
CREATE TABLE #AllPossibleReasons (Id INT,Label VARCHAR(10))
INSERT #AllPossibleReasons
VALUES
(1,'Reason1')
,(2,'Reason2')
,(3,'Reason3')
,(4,'Reason4')
,(5,'Reason5')
,(6,'Reason6')
,(7,'Reason7')
,(8,'Reason8')
,(9,'Reason9')
INSERT #MyTab
VALUES
('Bob',7,7)
,('Bob',8,6)
,('John',2,8)
,('John',5,9)
,('John',3,9)
,('John',8,4)
-----------------------------------------------------------------------------
-- The actual query
DECLARE #ReasonList VARCHAR(MAX) = ''
DECLARE #SQL VARCHAR(MAX)
SELECT #ReasonList = #ReasonList + ',' + QUOTENAME(Label)
FROM #AllPossibleReasons
SET #ReasonList = SUBSTRING(#ReasonList,2,LEN(#ReasonList))
SET #SQL =
'SELECT Name,Value,' + #ReasonList + ' FROM
(SELECT
M.Name,SUM(Amount) AS This, Label, SUM(Total.Value) AS Value
FROM
#MyTab AS M
INNER JOIN #AllPossibleReasons AS Reason ON M.ReasonId = Reason.Id
INNER JOIN(SELECT T.Name, SUM(Amount)Value
FROM #MyTab T GROUP BY T.Name) AS Total ON M.Name = Total.Name
GROUP BY M.Name, Reason.Label) AS Up
PIVOT (SUM(THis) FOR Label IN (' + #ReasonList + ')) AS Pvt'
EXEC (#SQL)
DROP TABLE #AllPossibleReasons
DROP TABLE #MyTab
Working from the information in ListAGG in SQLSERVER, I came up with this somewhat ugly example:
with tbl1 as (
-- Set up initial data set
select 'Bob' name, 5 amount, 7 ReasonId
union all select 'Bob' , 3, 4
union all select 'Bob', 2, 1
union all select 'Brian', 8, 2
union all select 'Bob', 6, 4
union all select 'Brian', 1, 3
union all select 'Tim', 2, 2)
, TBL2 AS ( -- Add a blank to separate the concatenation
SELECT NAME
, AMOUNT
, CAST(ReasonId as varchar) + ' ' ReasonId from tbl1
)
select ta.name
, Total
, ReasonIds from (
(select distinct name, stuff((select distinct '' + t2.ReasonId from tbl2 t2
where t1.name = t2.name
for xml path(''), type).value('.','NVARCHAR(MAX)'),1,0,' ') ReasonIds from tbl2 t1) ta
inner join ( select name, sum(amount) Total from tbl1 group by name) tb on ta.name = tb.name) ;
This converts TBL1 to the following:
name Total ReasonIds
Bob 16 1 4 7
Brian 9 2 3
Tim 2 2

Group SQL Server Select Results into different Result tables

Is it possible in SQL Server to "group" a result from a single query based on data in a specific column as if I ran multiple select queries?
I'm trying to find a lazy way out to extract data such as the below:
StoreId | ClientId
1 | 4
1 | 5
2 | 5
2 | 6
2 | 7
3 | 8
whereby every store ID result is grouped into its own table.
Whilst I can create a select statement for every store id to have it grouped, the list is too long to do so.
I can't imagine that this is really helpful but you can use dynamic sql to do something like this. I can't say I would recommend this approach for generating excel documents but whatever.
create table #Something
(
StoreID int
, ClientID int
)
insert #Something
select 1, 4 union all
select 1, 5 union all
select 2, 5 union all
select 2, 6 union all
select 2, 7
declare #sql nvarchar(max) = ''
select #sql = #sql + 'select StoreID, ClientID from #Something where StoreID = ' + CAST(StoreID as varchar(4)) + ';'
from #Something
group by StoreID
select #sql
exec sp_executesql #sql
drop table #Something

SQL Server Pivot Table with multiple column with dates

I have a PIVOT situation.
Source table columns:
Title Description Datetime RecordsCount
A California 2015-07-08 10:44:39.040 5
A California 2015-07-08 12:44:39.040 6
A California 2015-05-08 15:44:39.040 3
B Florida 2015-07-08 16:44:39.040 2
B Florida 2015-05-08 19:44:39.040 4
Now I need this pivoted as
2015-07-08 2015-05-08
Title Description
A California 11 3
B Florida 2 4
if we have two record counts on same dates (no matter of time) then sum them, else display in different column.
Trying to write something like this, but it throws errors.
Select * from #DataQualTest
PIVOT (SUM(RecordCount) FOR DateTime IN (Select Datetime from #DataQualTest) )
AS Pivot_Table
Please help me out with this.
Thanks
Not exactly the word for word solution but this should give you a direction.
create table #tmp
(
country varchar(max)
, date1 datetime
, record int
)
insert into #tmp values ('California', '2010-01-01', 2)
insert into #tmp values ('California', '2010-01-01', 5)
insert into #tmp values ('California', '2012-01-01', 1)
insert into #tmp values ('Florida', '2010-01-01', 3)
insert into #tmp values ('Florida', '2010-01-01', 5)
select * from #tmp
pivot (sum(record) for date1 in ([2010-01-01], [2012-01-01])) as avg
output
country 2010-01-01 2012-01-01
California 7 1
Florida 8 NULL
If you want to be more flexible, you need some pre-processing to get from full timestamps to days (in order for later on the PIVOT's grouping to actually have the anticipated effect):
CREATE VIEW DataQualTestView AS
SELECT
title
, description
, DATEFROMPARTS (DATEPART(yyyy, date_time),
DATEPART(mm, date_time),
DATEPART(dd, date_time)) AS day_from_date_time
, recordsCount
FROM DataQualTest
;
From there you could continue:
DECLARE #query AS NVARCHAR(MAX)
DECLARE #columns AS NVARCHAR(MAX)
SELECT #columns = ISNULL(#columns + ',' , '')
+ QUOTENAME(day_from_date_time)
FROM (SELECT DISTINCT
day_from_date_time
FROM DataQualTestView) AS TheDays
SET #query =
N'SELECT
title
, description
, ' + #columns + '
FROM DataQualTestView
PIVOT(SUM(recordsCount)
FOR day_from_date_time IN (' + #columns + ')) AS Pivoted'
EXEC SP_EXECUTESQL #query
GO
... and would get:
| title | description | 2015-05-08 | 2015-07-08 |
|-------|-------------|------------|------------|
| A | California | 3 | 11 |
| B | Florida | 4 | 2 |
See it in action: SQL Fiddle.
Please comment, if and as this requires adjustment / further detail.

Resources