How to create query in sql to pivot data? [duplicate] - sql

This question already has answers here:
Get ROWS as COLUMNS (SQL Server dynamic PIVOT query)
(2 answers)
Closed 9 years ago.
I have two tables named PRODUCT and DETAIL
TABLE: PRODUCT
slno product
1 x
2 y
3 z
TABLE: DETAIL
product detail
x good
y bad
z worse
x bad
I need to get output as
TABLE
X Y Z
good bad worse
bad

This data transformation is known as a PIVOT and starting in SQL Server 2005 there is a function to convert the data from rows to columns.
There are several ways that this can be done depending on whether or not you have a static number of values to transpose into columns. All of them involve adding a row_number() to the data so you can return the multiple rows of any of the products.
You can use an aggregate function with a CASE expression:
select
max(case when product = 'x' then detail end) x,
max(case when product = 'y' then detail end) y,
max(case when product = 'z' then detail end) z
from
(
select p.product, d.detail,
row_number() over(partition by p.product order by p.slno) rn
from product p
inner join detail d
on p.product = d.product
) src
group by rn
See SQL Fiddle with Demo
You can use the PIVOT function:
select x, y, z
from
(
select p.product, d.detail,
row_number() over(partition by p.product order by p.slno) rn
from product p
inner join detail d
on p.product = d.product
) src
pivot
(
max(detail)
for product in (x, y, z)
) piv
See SQL Fiddle with Demo.
If you have an unknown number of values (products in this case) to turn into columns, then you will want to use dynamic SQL:
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT distinct ',' + QUOTENAME(product)
from product
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT ' + #cols + ' from
(
select p.product, d.detail,
row_number() over(partition by p.product order by p.slno) rn
from product p
inner join detail d
on p.product = d.product
) x
pivot
(
max(detail)
for product in (' + #cols + ')
) p '
execute(#query)
See SQL Fiddle with Demo
The result of all of the queries is:
| X | Y | Z |
--------------------------
| good | bad | worse |
| bad | (null) | (null) |

This is your query.
select p.product, d.detail from product p
inner join detail d on p.product = d.product

Look into the SQL Join statements. This blogpost explains it nicely.

select d.product, d.detail from detail d
join product p on d.product = p.product

Related

SQL Server mulitple results to one row by ID (Version 2012)

Consider this query and result set:
Select udbA.userId, d.dbName
from user_db_access udbA
Inner join dbList d on d.dbid = udbA.dbid
Order By udbA.userId
1 az
1 nc_west
1 bsc_mo
1 NS_002
What I am looking for is a way to flatten this into one record. I know I can do it with a temp table and select into, but I was curious to see if a query could do it directly. A user could have up to 15 databases available to them.
Looking for results like below ( 2 columns -- userid and the database names ):
userid dbname
1 az nc_west bsc_mo NS_002
SQL Server Version: Microsoft SQL Server 2012 (SP3) (KB3072779) - 11.0.6020.0 (X64) Oct 20 2015 15:36:27 Copyright (c) Microsoft Corporation Enterprise Edition (64-bit) on Windows NT 6.3 (Build 9600: ) (Hypervisor)
Assuming you want a space-delimited list of database names:
DECLARE #Access table ( userId int, dbName varchar(50) );
INSERT INTO #Access VALUES
( 1, 'az' ), ( 1, 'nc_west' ), ( 1, 'bsc_mo' ), ( 1, 'NS_002' );
SELECT DISTINCT
ax.userId, db.list
FROM #Access AS ax
OUTER APPLY (
SELECT LTRIM ( (
SELECT ' ' + dbName AS "text()" FROM #Access AS x WHERE x.userId = ax.userId
FOR XML PATH ( '' )
) ) AS list
) AS db;
Returns
+--------+--------------------------+
| userId | list |
+--------+--------------------------+
| 1 | az nc_west bsc_mo NS_002 |
+--------+--------------------------+
For a comma-delimited list:
SELECT DISTINCT
ax.userId, db.list
FROM #Access AS ax
OUTER APPLY (
SELECT STUFF ( (
SELECT ',' + dbName AS "text()" FROM #Access AS x WHERE x.userId = ax.userId
FOR XML PATH ( '' )
), 1, 1, '' ) AS list
) AS db;
Returns
+--------+--------------------------+
| userId | list |
+--------+--------------------------+
| 1 | az,nc_west,bsc_mo,NS_002 |
+--------+--------------------------+
You need GROUP BY and row_number as follows:
select userId,
max(case when rn = 1 then dbName end) as val1,
max(case when rn = 2 then dbName end) as val2,
max(case when rn = 3 then dbName end) as val3,
max(case when rn = 4 then dbName end) as val4
from
(Select udbA.userId, d.dbName,
row_number() over (partition by udbA.userId order by d.dbName) as rn
from user_db_access udbA
Inner join dbList d on d.dbid = udbA.dbid ) t
group by userId
-- update
You just need two columns then use the STRING_AGG as follows:
Select udbA.userId,
string_agg(d.dbName, ' ') within group (order by d.dbName) as dbName
from user_db_access udbA
Inner join dbList d on d.dbid = udbA.dbid
group by udbA,userId
-- For SQL server 2012
Select distinct udbA.userId,
STUFF((SELECT distinct '' + d.dbName
from dbList d
where d.dbid = udbA.dbid
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,0,'') AS dbid
FROM user_db_access udbA
WHERE EXISTS (SELECT 1 FROM dbList d
where d.dbid = udbA.dbid)
Or use CTE as follows:
with CTE as
(Select udbA.userId, d.dbName
from user_db_access udbA
Inner join dbList d on d.dbid = udbA.dbid)
Select distinct c.userId,
STUFF((SELECT distinct '' + cc.dbName
from cte cc
where c.dbid = cc.dbid
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,0,'') AS dbid
FROM cte c
Depending on your SQL Server version you can use STRING_AGG
Like this:
Select udbA.userId, string_agg(d.dbName, ', ') as name
from user_db_access udbA
Inner join dbList d on d.dbid = udbA.dbid
group by udbA.userId
If you are using an older version you already have answers here: How to make a query with group_concat in sql server
Like this:
select
udbA.userId,
stuff((
select cast(',' as varchar(max)) + d.dbName
from dbList d
where d.dbid = udbA.dbid
order by d.dbName
for xml path('')), 1, 1, '') as dbList
from
user_db_access udbA
inner join dbList dl on dl.dbid = udbA.dbid
order by
udbA.userId;

SQL Query & unpivot Syntax

I have a permission system that defines users access to certain items.
I need a report page to show members and their access to the items.
The table needs to be like this:
+---------+--------+--------+------+--------+
| Members | Item 1 | Item 2 | .... | Item N |
+---------+--------+--------+------+--------+
| Member1 | x | + | .... | + |
+---------+--------+--------+------+--------+
| Member2 | + | x | .... | + |
+---------+--------+--------+------+--------+
I have a 3 table joined query for listing the members and the items they can access but I could not convert it into pivot syntax.
Select members.id memberID, members.name memberName, items.name as item
From members
Left Join member_permission On memberID = members.id
Left Join items On items.id = member_permission.itemID
This is the not working query I have:
Select memberName, itemName
From (
select members.id, members.name, item.name as itemName
from members
Left Join member_permission On memberID = members.id
Left Join item On item.id = member_permission.itemID
) p
Unpivot
(itemName For name IN (item.name)) as unp
and the error I receive:
The column name "name" specified in the UNPIVOT operator conflicts with the existing column name in the UNPIVOT argument.
I created an example to play with Sqlfiddle
You don't need to use UNPIVOT for this query. UNPIVOT is used to convert multiple columns into multiple rows. You only need to apply the PIVOT function to turn your items into columns.
I would first suggest using a windowing function like row_number() to create your new column headers, then apply the PIVOT function:
select id, name, Item1, Item2, Item3
from
(
select members.id, members.name, items.name as item,
'item'+
cast(row_number() over(partition by members.id
order by members.id) as varchar(10)) col
from members
Left Join member_permission
On memberID = members.id
Left Join items
On items.id = member_permission.itemID
) d
pivot
(
max(item)
for col in (Item1, Item2, Item3)
) piv;
See SQL Fiddle with Demo. Then if you have an unknown number of values your query would need to use dynamic SQL:
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT ',' + QUOTENAME('item'+cast(seq as varchar(10)))
from
(
select row_number() over(partition by memberid
order by memberid) seq
from member_permission
) d
group by seq
order by seq
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = N'SELECT id, name, ' + #cols + N'
from
(
select members.id, members.name, items.name as item,
''item''+
cast(row_number() over(partition by members.id
order by members.id) as varchar(10)) col
from members
Left Join member_permission
On memberID = members.id
Left Join items
On items.id = member_permission.itemID
) x
pivot
(
max(item)
for col in (' + #cols + N')
) p '
execute sp_executesql #query;
See SQL Fiddle with Demo

I need to know how to create a crosstab query

I need help creating the below results. I thought of a sql pivot but I don't know how to use it. Looked at a few examples and cannot come up with a solution. Any other ideas on how to accomplish this is also welcome. Status columns must be dynamically generated.
Have three tables, assets, assettypes, assetstatus
Table: assets
assetid int
assettag varchar(25)
assettype int
assetstatus int
Table: assettypes
id int
typename varchar(20) (ex: Desktop, Laptop, Server, etc.)
Table: assetstatus
id int
statusname varchar(20) (ex: Deployed, Inventory, Shipped, etc.)
Desired results:
AssetType Total Deployed Inventory Shipped ...
-----------------------------------------------------------
Desktop 100 75 20 5 ...
Laptop 75 56 19 1 ...
Server 60 50 10 0 ...
Some Data:
assets table:
1,hol1234,1,1
2,hol1233,1,2
3,hol3421,2,3
4,svr1234,3,1
assettypes table:
1,Desktop
2,Laptop
3,Server
assetstatus table:
1,Deployed
2,Inventory
3,Shipped
This type of transformation is called a pivot. You did not specify what database you are using so I will provide a answers for SQL Server and MySQL.
SQL Server: If you are using SQL Server 2005+ you can implement the PIVOT function.
If you have a known number of values that you want to convert to columns then you can hard-code the query.
select typename, total, Deployed, Inventory, shipped
from
(
select count(*) over(partition by t.typename) total,
s.statusname,
t.typename
from assets a
inner join assettypes t
on a.assettype = t.id
inner join assetstatus s
on a.assetstatus = s.id
) d
pivot
(
count(statusname)
for statusname in (Deployed, Inventory, shipped)
) piv;
See SQL Fiddle with Demo.
But if you have an unknown number of status values, then you will need to use dynamic sql to generate the list of columns at run-time.
DECLARE #cols AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX)
select #cols = STUFF((SELECT distinct ',' + QUOTENAME(statusname)
from assetstatus
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query = 'SELECT typename, total,' + #cols + ' from
(
select count(*) over(partition by t.typename) total,
s.statusname,
t.typename
from assets a
inner join assettypes t
on a.assettype = t.id
inner join assetstatus s
on a.assetstatus = s.id
) x
pivot
(
count(statusname)
for statusname in (' + #cols + ')
) p '
execute(#query)
See SQL Fiddle with Demo
This can also be written using an aggregate function with a case expression:
select typename,
total,
sum(case when statusname ='Deployed' then 1 else 0 end) Deployed,
sum(case when statusname ='Inventory' then 1 else 0 end) Inventory,
sum(case when statusname ='Shipped' then 1 else 0 end) Shipped
from
(
select count(*) over(partition by t.typename) total,
s.statusname,
t.typename
from assets a
inner join assettypes t
on a.assettype = t.id
inner join assetstatus s
on a.assetstatus = s.id
) d
group by typename, total
See SQL Fiddle with Demo
MySQL: This database does not have a pivot function so you will have to use the aggregate function and a CASE expression. It also does not have windowing functions, so you will have to alter the query slightly to the following:
select typename,
total,
sum(case when statusname ='Deployed' then 1 else 0 end) Deployed,
sum(case when statusname ='Inventory' then 1 else 0 end) Inventory,
sum(case when statusname ='Shipped' then 1 else 0 end) Shipped
from
(
select t.typename,
(select count(*)
from assets a1
where a1.assettype = t.id
group by a1.assettype) total,
s.statusname
from assets a
inner join assettypes t
on a.assettype = t.id
inner join assetstatus s
on a.assetstatus = s.id
) d
group by typename, total;
See SQL Fiddle with Demo
Then if you need a dynamic solution in MySQL, you will have to use a prepared statement to generate the sql string to execute:
SET #sql = NULL;
SELECT
GROUP_CONCAT(DISTINCT
CONCAT(
'sum(CASE WHEN statusname = ''',
statusname,
''' THEN 1 else 0 END) AS `',
statusname, '`'
)
) INTO #sql
FROM assetstatus;
SET #sql
= CONCAT('SELECT typename,
total, ', #sql, '
from
(
select t.typename,
(select count(*)
from assets a1
where a1.assettype = t.id
group by a1.assettype) total,
s.statusname
from assets a
inner join assettypes t
on a.assettype = t.id
inner join assetstatus s
on a.assetstatus = s.id
) d
group by typename, total');
PREPARE stmt FROM #sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
See SQL Fiddle with Demo.
The result is the same for all queries in both databases:
| TYPENAME | TOTAL | DEPLOYED | INVENTORY | SHIPPED |
-----------------------------------------------------
| Desktop | 2 | 1 | 1 | 0 |
| Laptop | 1 | 0 | 0 | 1 |
| Server | 1 | 1 | 0 | 0 |
Using a non pivot compliant DBMS (Absolute Database) I was more successful using this SQL cross-tab equivalent statement:
SELECT
sub.TypeName
, SUM(sub.[Count]) AS "Total"
, SUM(CASE WHEN AssetStatus='1' THEN sub.[Count] ELSE 0 END) AS "Deployed"
, SUM(CASE WHEN AssetStatus='2' THEN sub.[Count] ELSE 0 END) AS "Inventory"
, SUM(CASE WHEN AssetStatus='3' THEN sub.[Count] ELSE 0 END) AS "Shipped"
FROM
(
SELECT
t.TypeName
, AssetStatus
, COUNT(AssetID) AS "Count"
FROM
Assets
JOIN AssetTypes t ON t.ID = AssetType
JOIN AssetStatus s ON s.ID = AssetStatus
GROUP BY t.TypeName, AssetStatus, s.StatusName
) sub
GROUP BY sub.TypeName
;
As I realized this code (above) didn't work with MySQL I adapted my code as below executing equally well in MySQL as in my current Absolute Database. The reason is the specific NULL handling avoiding the pitfall of dBase, Paradox as well as Absolute Database generously accepting COUNT(NULL) = 0 not accepted in mainstream databases.
So believing this will execute well in most databases (handling CASE ..) this is my adapted code:
SELECT
sub.TypeName
, SUM(sub.AssetCase) AS "Total"
, SUM(CASE WHEN sub.StatusName = 'Deployed' THEN sub.AssetCase ELSE 0 END) AS "Deployed"
, SUM(CASE WHEN sub.StatusName = 'Inventory' THEN sub.AssetCase ELSE 0 END) AS "Inventory"
, SUM(CASE WHEN sub.StatusName = 'Shipped' THEN sub.AssetCase ELSE 0 END) AS "Shipped"
FROM
(
SELECT
c.TypeName
, c.StatusName
, CASE WHEN a.AssetID IS NULL THEN 0 ELSE 1 END AS "AssetCase"
FROM
(
SELECT
t.ID AS tID
, t.TypeName
, s.ID AS sID
, s.StatusName
FROM
AssetTypes t, AssetStatus s
) c
LEFT JOIN Assets a
ON a.AssetType = c.tID AND a.AssetStatus = c.sID
) sub
GROUP BY
sub.TypeName
;
Best Regards
Niels Knabe

sql comma delimited list of a column in analytical function

I am using SQL server 2008, and I need to make a common delimeted list of a column. I know how to do it, but I need this time while I use analytical function, I mean I don't want to use group by clause.
Since I will also select the records in outer query "where row_num=1"
Here is the query:
SELECT UserId
,ProductList
,Value
FROM
(
SELECT p.UserId
,p.Value
, ROW_NUMBER()OVER (PARTITION BY p.UserId ORDER BY p.RecordCreateDate asc) AS 'row_num'
--here I need a solution OVER (PARTITION BY p.UserId) AS 'ProductList'
FROM Products p
INNER JOIN
Users u
ON p.UserId = u.Id
) result
WHERE result.row_num = 1
Users data:
Id Name ....
1 John
2 Anton
3 Craig
Products data:
Id UserId Name RecordCreateDate Value
1 1 a 21.12.2012 10
2 1 b 11.12.2012 20
3 1 c 01.12.2012 30
4 2 e 05.12.2012 40
5 2 f 17.12.2012 50
6 3 d 21.12.2012 60
7 3 i 31.12.2012 70
I need a result such as:
UserId ProductList Value
1 a,b,c 30
2 e,f 40
3 d,i 60
Thanks for your help
Since you are going to filter on row_num = 1 you need to put your query in a CTE or the likes where you include the extra columns from Products. Then you can build your comma separated string in the outer query using the for XML path trick without using group by.
;WITH C as
(
SELECT p.UserId
, ROW_NUMBER()OVER (PARTITION BY p.UserId ORDER BY p.RecordCreateDate asc) AS 'row_num'
--, Some other fields from Products
FROM Products p
INNER JOIN
Users u
ON p.UserId = u.Id
)
SELECT UserId,
--, Some other fields from Products
--, Build the concatenated list here using for xml path()
FROM C
WHERE C.row_num = 1
Just for completeness. Remove the # symbols for your actual solution.
SET NOCOUNT ON;
CREATE TABLE #users
(
Id INT,
Name VARCHAR(32)
);
INSERT #users VALUES
(1,'John'),
(2,'Anton'),
(3,'Craig');
CREATE TABLE #products
(
Id INT,
UserId INT,
Name VARCHAR(32),
RecordCreateDate DATE,
Value INT
);
INSERT #products VALUES
(1,1,'a','2012-12-21',10),
(2,1,'b','2012-12-11',20),
(3,1,'c','2012-12-01',30),
(4,2,'e','2012-12-05',40),
(5,2,'f','2012-12-17',50),
(6,3,'d','2012-12-21',60),
(7,3,'i','2012-12-31',70);
The query:
;WITH x AS
(
SELECT UserId, Value,
row_num = ROW_NUMBER() OVER
(
PARTITION BY UserId
ORDER BY RecordCreateDate
)
FROM #products
)
SELECT
x.UserId,
u.Name,
ProductList = STUFF((
SELECT ',' + Name
FROM #Products AS p
WHERE p.UserId = x.UserId
FOR XML PATH(''),
TYPE).value(N'./text()[1]', N'varchar(max)'),1,1,''),
x.Value
FROM x
INNER JOIN #users AS u
ON x.UserId = u.Id
WHERE x.row_num = 1;
Then clean up:
DROP TABLE #users, #products;
Results:
UserId Name ProductList Value
1 John a,b,c 30
2 Anton e,f 40
3 Craig d,i 60
I'm not sure what you're asking in the beginning, but this will give you the requested output
SELECT UserId,
STUFF((SELECT ',' + ProductName from Products p WHERE p.UserID = u.UserID FOR XML PATH('')), 1, 1, '') as ProductList
FROM Users u

How to get table-like query result on SQL Server 2005/8?

I have 3 tables:
users (id, name)
currency (id, name)
accounts (id, user_id, currency_id, amount)
And I want to read the data from accounts and present it in table-like view:
owner currency1 currency2 currency3
1 0 0 0
2 10 20 30
3 0 5 10
Where owner is ID of accounts.owner, currency1,2,3 - (SELECT id FROM currency WHERE name = '1',etc)
I can get such result only for one specific ID:
SELECT
SELECT amount FROM accounts WHERE currency = (SELECT id FROM currency WHERE name = 'currency1') AND owner = #user) AS [currency1],
SELECT amount FROM accounts WHERE currency = (SELECT id FROM currency WHERE name = 'currency2') AND owner = #user) AS [currency2],
SELECT amount FROM accounts WHERE currency = (SELECT id FROM currency WHERE name = 'currency2') AND owner = #user) AS [currency2]
Is it possible to get the same result for every object in users table? Without using Reporing Service, etc.
Use a pivot table and dynamic SQL to retrieve the columns
DECLARE #columns VARCHAR(2000)
SELECT #columns = STUFF(( SELECT DISTINCT TOP 100 PERCENT
'],[' + c.name
FROM currency AS c
ORDER BY '],[' + c.name
FOR XML PATH('')
), 1, 2, '') + ']'
DECLARE #query NVARCHAR(4000)
SET #query = N'SELECT UserName, ' + #columns +
'FROM
(SELECT u.Name AS UserName, c.name AS CurrencyName, a.Amount
FROM Accounts AS a WITH(NOLOCK)
JOIN Users u WITH(NOLOCK) ON a.user_id = u.user_id
JOIN Currency c WITH(NOLOCK) ON a.currency_id = c.currency_id
) p
PIVOT
(
SUM (p.Amount)
FOR p.CurrencyName IN
( '+ #columns +')
) AS pvt
ORDER BY UserName'
EXECUTE(#query)
This was tested in SQL Server 2005
Sounds like you want a Pivot table. It will be difficult to do if you have a varying number of rows in currency, but could still be done by using dynamiclly written sql.
Here's a resource from MSDN that explains how to use the pivot table: http://msdn.microsoft.com/en-us/library/ms177410.aspx
SELECT u.name, [1] AS Currency1, [2] AS Currency2, [3] AS Currency3
FROM
(SELECT u.Name AS UserName, c.Currency_ID, a.Amount
FROM Accounts AS a WITH(NOLOCK)
JOIN Users u WITH(NOLOCK) ON a.user_id = u.user_id
) p
PIVOT
(
SUM (p.Amount)
FOR p.Currency_id IN
( [1], [2], [3] )
) AS pvt
ORDER BY pvt.UserName