Convert rows to columns with values based on ranking - sql

I have a table of customers who have several kinds of "accounts", each with a different integer balance, such as
CUSTID AcctType Balance
12345 Checking 1000
12345 Savings 5000
12345 Investment 3000
98765 Savings 2000
98765 Checking 8000
98765 Investment 1000
98765 Retirement 2500
I do not know how many accounts a customer may have (it could be anywhere from 1 to 6). I have to create a result showing the Accounts and Balances as columns, in order from highest to lowest, like this:
CUSTID AcctType1 Balance1 AcctType2 Balance2 AcctType3 Balance3 AcctType4 Balance 4
12345 Savings 5000 Investment 3000 Checking 1000
98765 Checking 8000 Retirement 2500 Savings 2000 Investment 1000
Any ideas how to create this in SQL Server? (Ideally as a View)

You can unpivot the columns first into rows, then pivot the rows back into column with row number, something like this:
WITH CTE
AS
(
SELECT
CAST(CUSTID AS NVARCHAR(50)) AS CUSTID
,CAST(AcctType AS NVARCHAR(50)) AS AcctType
,CAST(Balance AS NVARCHAR(50)) AS Balance
,ROW_NUMBER() OVER(PARTITION BY [CUSTID] ORDER BY Balance DESC) AS RN
FROM Data
), unpivoted
AS
(
SELECT CUSTID, val, col + ' ' + CAST(RN AS NVARCHAR(50)) AS col
FROM CTE
UNPIVOT
(
val
FOR col IN(AcctType, Balance)
) AS u
)
SELECT *
FROM unpivoted AS u
PIVOT
(
MAX(val)
FOR col IN([AcctType 1], [Balance 1],
[AcctType 2], [Balance 2],
[AcctType 3], [Balance 3],
[AcctType 4], [Balance 4])
) AS p;
SQL Fiddle Demo
Update:
If you want to make this dynamic for any number of customers, you have to do it dynamically like this:
DECLARE #cols AS NVARCHAR(MAX);
DECLARE #query AS NVARCHAR(MAX);
WITH CTE
AS
(
SELECT
CAST(CUSTID AS NVARCHAR(50)) AS CUSTID
,CAST(AcctType AS NVARCHAR(50)) AS AcctType
,CAST(Balance AS NVARCHAR(50)) AS Balance
,ROW_NUMBER() OVER(PARTITION BY [CUSTID] ORDER BY Balance DESC) AS RN
FROM Data
), Data
AS
(
SELECT col, MAX(RN) AS RN
FROM
(
SELECT RN, col + CAST(RN AS NVARCHAR(50)) AS col
FROM CTE
UNPIVOT
(
val
FOR col IN(AcctType, Balance)
) AS u
) AS t
GROUP BY col
)
select #cols = STUFF((SELECT ',' +
QUOTENAME(col)
FROM Data
ORDER BY RN
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
, 1, 1, '');
SELECT #query = 'WITH CTE
AS
(
SELECT
CAST(CUSTID AS NVARCHAR(50)) AS CUSTID
,CAST(AcctType AS NVARCHAR(50)) AS AcctType
,CAST(Balance AS NVARCHAR(50)) AS Balance
,ROW_NUMBER() OVER(PARTITION BY [CUSTID] ORDER BY Balance DESC) AS RN
FROM Data
), unpivoted
AS
(
SELECT CUSTID, val, col + CAST(RN AS NVARCHAR(50)) AS col
FROM CTE
UNPIVOT
(
val
FOR col IN(AcctType, Balance)
) AS u
)
SELECT *
FROM unpivoted AS u
PIVOT
(
MAX(val)
FOR col IN('+ #cols + ')
) AS p;';
EXECUTE(#query);
Dynamic Demo

If it's a limited amount of columns(as you said 6 per CUSTID) then you can use conditional aggregation with ROW_NUMBER():
SELECT t.custID,
MAX(CASE WHEN t.rnk = 1 THEN t.accttype END) as accttype1,
MAX(CASE WHEN t.rnk = 1 THEN t.balance END) as balance1,
MAX(CASE WHEN t.rnk = 2 THEN t.accttype END) as accttype2,
MAX(CASE WHEN t.rnk = 2 THEN t.balance END) as balance2,
MAX(CASE WHEN t.rnk = 3 THEN t.accttype END) as accttype3,
MAX(CASE WHEN t.rnk = 3 THEN t.balance END) as balance3,
MAX(CASE WHEN t.rnk = 4 THEN t.accttype END) as accttype4,
MAX(CASE WHEN t.rnk = 4 THEN t.balance END) as balance4,
MAX(CASE WHEN t.rnk = 5 THEN t.accttype END) as accttype5,
MAX(CASE WHEN t.rnk = 5 THEN t.balance END) as balance5,
MAX(CASE WHEN t.rnk = 6 THEN t.accttype END) as accttype6,
MAX(CASE WHEN t.rnk = 6 THEN t.balance END) as balance6
FROM (SELECT s.*,
ROW_NUMBER() OVER(PARTITION BY s.custID ORDER BY s.balance DESC) as rnk
FROM YourTable s) t
GROUP BY t.custID

Related

split columns (more than one) into multiple columns with dynamic sql [duplicate]

This question already has answers here:
SQL Server dynamic PIVOT query?
(9 answers)
Closed 4 years ago.
I have table with data as below
I have got result to convert one column to multiple columns. But I need output to convert multiple columns
The result is expected as below
Output needed as
Name Q1 G1 Q2 G2 Q3 G3
Antony HSE A Degree C NULL NULL
Bob HSE B Degree B Masters A
Marc HSE D Degree C Masters B
If those Qualifications have fixed values, then you can get that result via conditional aggregation.
SELECT
Name,
MAX(CASE WHEN Qualification = 'HSE' THEN Qualification END) AS Q1,
MAX(CASE WHEN Qualification = 'HSE' THEN Grade END) AS G1,
MAX(CASE WHEN Qualification = 'Degree' THEN Qualification END) AS Q2,
MAX(CASE WHEN Qualification = 'Degree' THEN Grade END) AS G2,
MAX(CASE WHEN Qualification = 'Masters' THEN Qualification END) AS Q3,
MAX(CASE WHEN Qualification = 'Masters' THEN Grade END) AS G3
FROM YourTable
GROUP BY Name
ORDER BY Name
If the qualification names aren't fixed, then you could generate a row_number and use that.
Then you can add as many Qn & Gn as a Name can have qualifications.
To test that: select top 1 [Name], count(*) Total from #YourTable group by [Name] order by Total desc
SELECT
Name,
MAX(CASE WHEN RN = 1 THEN Qualification END) AS Q1,
MAX(CASE WHEN RN = 1 THEN Grade END) AS G1,
MAX(CASE WHEN RN = 2 THEN Qualification END) AS Q2,
MAX(CASE WHEN RN = 2 THEN Grade END) AS G2,
MAX(CASE WHEN RN = 3 THEN Qualification END) AS Q3,
MAX(CASE WHEN RN = 3 THEN Grade END) AS G3
FROM
(
SELECT Name, Qualification, Grade,
ROW_NUMBER() OVER (PARTITION BY Name ORDER BY Qualification) AS RN
FROM YourTable
) q
GROUP BY Name
ORDER BY Name
Or doing it dynamic
declare #MaxTotalQualifications int = (select top 1 count(*) from YourTable group by [Name] order by count(*) desc);
declare #cols varchar(max);
WITH DIGITS(n) AS (
SELECT n FROM (VALUES (0),(1),(2),(3),(4),(5),(6),(7),(8),(9)) v(n)
)
, NUMBERS(n) AS
(
SELECT ones.n + 10*tens.n + 100*hundreds.n + 1000*thousands.n
FROM DIGITS AS ones
CROSS JOIN DIGITS as tens
CROSS JOIN DIGITS as hundreds
CROSS JOIN DIGITS as thousands
)
select #cols = concat(#cols+CHAR(13)+CHAR(10)+', ', 'MAX(CASE WHEN RN = ', n ,' THEN Qualification END) AS [Q', n ,'], MAX(CASE WHEN RN = ', n ,' THEN Grade END) AS [G', n,']')
from NUMBERS
WHERE n BETWEEN 1 AND #MaxTotalQualifications;
-- select #MaxTotalQualifications as MaxTotalQualifications, #cols as cols;
declare #DynSql nvarchar(max);
set #DynSql = N'SELECT Name, '+ #cols + N'
FROM
(
SELECT Name, Qualification, Grade,
ROW_NUMBER() OVER (PARTITION BY Name ORDER BY Qualification) AS RN
FROM YourTable
) q
GROUP BY Name
ORDER BY Name';
-- select #DynSql as DynSql;
exec(#DynSql);
A test on db<>fiddle here

How to split an SQL Table into half and send the other half of the rows to new columns with SQL Query?

Country Percentage
India 12%
USA 20%
Australia 15%
Qatar 10%
Output :
Country1 Percentage1 Country2 Percentage2
India 12% Australia 15%
USA 20% Qatar 10%
For example there is a table Country which has percentages, I need to divide the table in Half and show the remaining half (i.e. the remaining rows) in the new columns. I've also provided the table structure in text.
First, this type of operation should be done at the application layer and not in the database. That said, it can be an interesting exercise to see how to do this in the database.
I would use conditional aggregation or pivot. Note that SQL tables are inherently unordered. Your base table has no apparent ordering, so the values could come out in any order.
select max(case when seqnum % 2 = 0 then country end) as country_1,
max(case when seqnum % 2 = 0 then percentage end) as percentage_1,
max(case when seqnum % 2 = 1 then country end) as country_2,
max(case when seqnum % 2 = 1 then percentage end) as percentage_2
from (select c.*,
(row_number() over (order by (select null)) - 1) as seqnum
from country c
) c
group by seqnum / 2;
Try this
declare #t table
(
Country VARCHAR(20),
percentage INT
)
declare #cnt int
INSERT INTO #T
VALUES('India',12),('USA',20),('Australia',15),('Quatar',12)
select #cnt = count(1)+1 from #t
;with cte
as
(
select
SeqNo = row_number() over(order by Country),
Country,
percentage
from #t
)
select
*
from cte c1
left join cte c2
on c1.seqno = (c2.SeqNo-#cnt/2)
and c2.SeqNo >= (#cnt/2)
where c1.SeqNo <= (#cnt/2)
My variant
SELECT 'A' Country,1 Percentage INTO #Country
UNION ALL SELECT 'B' Country,2 Percentage
UNION ALL SELECT 'C' Country,3 Percentage
UNION ALL SELECT 'D' Country,4 Percentage
UNION ALL SELECT 'E' Country,5 Percentage
;WITH numCTE AS(
SELECT
*,
ROW_NUMBER()OVER(ORDER BY Country) RowNum,
COUNT(*)OVER() CountOfCountry
FROM #Country
),
set1CTE AS(
SELECT Country,Percentage,ROW_NUMBER()OVER(ORDER BY Country) RowNum
FROM numCTE
WHERE RowNum<=CEILING(CountOfCountry/2.)
),
set2CTE AS(
SELECT Country,Percentage,ROW_NUMBER()OVER(ORDER BY Country) RowNum
FROM numCTE
WHERE RowNum>CEILING(CountOfCountry/2.)
)
SELECT
s1.Country,s1.Percentage,
s2.Country,s2.Percentage
FROM set1CTE s1
LEFT JOIN set2CTE s2 ON s1.RowNum=s2.RowNum
DROP TABLE #Country
I just wanted to try something. I have used the function OFFSET. It does the requirement i think for your sample data, but dont know if its bulletproof all the way:
SQL Code
declare #myt table (country nvarchar(50),percentage int)
insert into #myt
values
('India' ,12),
('USA' ,20),
('Australia' ,15),
('Qatar' ,10),
('Denmark',10)
DECLARE #TotalRows int
SET #TotalRows = (select CEILING(count(*) / 2.) from #myt);
WITH dataset1 AS (
SELECT *,ROW_NUMBER() over(order by country ) as rn from (
SELECT Country,percentage from #myt a
ORDER BY country OFFSET 0 rows FETCH FIRST #TotalRows ROWS ONLY
) z
)
,dataset2 AS (
SELECT *,ROW_NUMBER() over(order by country ) as rn from (
SELECT Country,percentage from #myt a
ORDER BY country OFFSET #TotalRows rows FETCH NEXT #TotalRows ROWS ONLY
) z
)
SELECT * FROM dataset1 a LEFT JOIN dataset2 b ON a.rn = b.rn
Result
Assuming you want descending alphabetic country names, but the left column is determined by where India is located in the result:
with CoutryCTE as (
select c.*
, row_number() over (order by country)-1 as rn
from country c
)
, Col as (
select rn % 2 as num from CoutryCTE
where Country = 'India'
)
select max(case when rn % 2 = Col.num then country end) as country_1
, max(case when rn % 2 = Col.num then percentage end) as percentage_1
, max(case when rn % 2 <> Col.num then country end) as country_2
, max(case when rn % 2 <> Col.num then percentage end) as percentage_2
from CoutryCTE
cross join Col
group by rn / 2
;
SQLFiddle Demo
| country_1 | percentage_1 | country_2 | percentage_2 |
|-----------|--------------|-----------|--------------|
| India | 12% | Australia | 15% |
| USA | 20% | Qatar | 10% |
nb: this is extremely similar to an earlier answer by Gordon Linoff

Pivot table with more then one record

I have data that is structured as follow:
LocationId, GroupId, DayOfWeek, Count, DatetimeValue15Min
2 9 4 5 2014-01-02 08:15:00.000
2 9 4 5 2014-01-02 09:15:00.000
I want to calculate the mode for each day, the data above already contains the count to know the mode. I have written a query with a pivot.
SELECT
pvt.LocationId, pvt.GroupId, [1], [2], [3], [4],[5]
FROM
#TempResult
PIVOT
(min ([DatetimeValue15Min])
FOR DayOfWeek IN ( [1], [2], [3], [4],[5])) AS pvt
In this case I have two modes but i want to show them both. My query returns in this case just the mode with the minimum value. I know that I can make a second query with the max value but what if i have more than two modes?
The output should be like:
LocationId GroupId 1 2 3 4 5
2 9 08:15, 09:15
I am using SQL Server 2005.
You are almost there. You just need to build the comma-separated list. A little xml type abuse works really well for that.
;WITH
t1 AS ( --Add a grouping id for quick reference
SELECT
[LocationId],[GroupId],[DayOfWeek],[DatetimeValue15Min],
DENSE_RANK() OVER(ORDER BY [LocationId],[GroupId],[DayOfWeek]) [i]
FROM #TempResult
),
t2 AS ( --Build a comma-separated list of all [DatetimeValue15Min] with same grouping id
SELECT [LocationId],[GroupId],[DayOfWeek],
CAST(REPLACE((SELECT CONVERT(time, [DatetimeValue15Min]) AS a FROM t1 WHERE [i] = t.[i] FOR xml PATH('')),'</a><a>',',') AS xml).value('a[1]','varchar(max)') [dtv_list]
FROM t1 t
)
SELECT pvt.LocationId, pvt.GroupId, [1], [2], [3], [4],[5]
FROM t2
PIVOT
(
min ([dtv_list])
FOR DayOfWeek IN ( [1], [2], [3], [4],[5])
) AS pvt
The xml trick works like this:
SELECT [DatetimeValue15Min] FOR XML -> <a>08:15</a><a>09:15</a>
replace '</a><a>' with ',' -> <a>08:15,09:15</a>
extract first node from xml -> '08:15,09:15'
SELECT LocationId, GroupId
, STUFF (
MAX (CASE WHEN DayOfWeek = 1 AND num = 1 THEN Value ELSE '' END)
+ MAX (CASE WHEN DayOfWeek = 1 AND num = 2 THEN Value ELSE '' END), 1, 2, '') AS [1]
, STUFF (
MAX (CASE WHEN DayOfWeek = 2 AND num = 1 THEN Value ELSE '' END)
+ MAX (CASE WHEN DayOfWeek = 2 AND num = 2 THEN Value ELSE '' END), 1, 2, '') AS [2]
, STUFF (
MAX (CASE WHEN DayOfWeek = 3 AND num = 1 THEN Value ELSE '' END)
+ MAX (CASE WHEN DayOfWeek = 3 AND num = 2 THEN Value ELSE '' END), 1, 2, '') AS [3]
, STUFF (
MAX (CASE WHEN DayOfWeek = 4 AND num = 1 THEN Value ELSE '' END)
+ MAX (CASE WHEN DayOfWeek = 4 AND num = 2 THEN Value ELSE '' END), 1, 2, '') AS [4]
, STUFF (
MAX (CASE WHEN DayOfWeek = 5 AND num = 1 THEN Value ELSE '' END)
+ MAX (CASE WHEN DayOfWeek = 5 AND num = 2 THEN Value ELSE '' END), 1, 2, '') AS [5]
FROM (
SELECT LocationId, GroupId, DayOfWeek, ', ' + CONVERT (VARCHAR(5), DatetimeValue15Min, 8) AS Value
, ROW_NUMBER() OVER(PARTITION BY LocationId, GroupId, DayOfWeek ORDER BY DatetimeValue15Min) AS num
FROM #TempResult
) T
GROUP BY LocationId, GroupId
UPD. Solution for unlimited number of values per day.
WITH cte AS (
SELECT LocationId, GroupId, DayOfWeek, CONVERT (VARCHAR(5), DatetimeValue15Min, 8) AS Value
, ROW_NUMBER() OVER(PARTITION BY LocationId, GroupId, DayOfWeek ORDER BY DatetimeValue15Min) AS num
FROM #TempResult
)
SELECT LocationId, GroupId
, MIN (CASE DayOfWeek WHEN 1 THEN Value END) AS [1]
, MIN (CASE DayOfWeek WHEN 2 THEN Value END) AS [2]
, MIN (CASE DayOfWeek WHEN 3 THEN Value END) AS [3]
, MIN (CASE DayOfWeek WHEN 4 THEN Value END) AS [4]
, MIN (CASE DayOfWeek WHEN 5 THEN Value END) AS [5]
FROM (
SELECT DISTINCT c1.LocationId, c1.GroupId, c1.DayOfWeek
, STUFF ( (
SELECT ', ' + c2.Value
FROM cte c2
WHERE c1.LocationId = c2.LocationId AND c1.GroupId = c2.GroupId AND c1.DayOfWeek = c2.DayOfWeek
FOR XML PATH ('')
), 1, 2, '') AS Value
FROM cte c1
) t
GROUP BY LocationId, GroupId

SQL Server 2008: Converting rows into columns

I have two tables:
CREATE TABLE #A (id int, cond_id int)
INSERT INTO #A (id, cond_id)
VALUES (101,20),
(101,22),
(101,24),
(102,23),
(102,22)
Now, each id can have max of 4 cond_ids. I want to populate table #B so that there is one id and all cond_ids will be populated in the columns as one row according to cond_id ascending.
like for id 102, cond_id 22 goes in cond_id and 23 goes in cond_id2.
create table #B (id int, cond_id1 int, cond_id2 int, cond_id3 int, cond_id4 int)
Desired result:
Table #B
id cond_id1 cond_id2 cond_id3 cond_id4
101 20 22 24 null
102 22 23 null null
Thanks in advance!
Because you know the maximum number of columns, one option is to use row_number, max and case:
with cte as (
select row_number() over (partition by id order by cond_id) rn, id, cond_id
from a)
select id,
max(case when rn = 1 then cond_id end) cond_id1,
max(case when rn = 2 then cond_id end) cond_id2,
max(case when rn = 3 then cond_id end) cond_id3,
max(case when rn = 4 then cond_id end) cond_id4
from cte
group by id
SQL Fiddle Demo
Or you could look at Pivot:
select id, [1] cond_id1, [2] cond_id2, [3] cond_id3, [4] cond_id4
from
(select row_number() over (partition by id order by cond_id) rn, id, cond_id
from a) t
pivot
(
max(cond_id)
for rn in ([1], [2], [3], [4])
) p
More Fiddle

rows into columns [duplicate]

This question already has answers here:
SQL turning values returned in 11 rows into 89 total columns
(2 answers)
Closed 9 years ago.
this is my query
select * from dbo.tblHRIS_ChildDetails where intSID=463
output:
intCHID intsid nvrchildname nvrgender dttchildDOB Occupation
3 463 SK Female 2001-12-11 00:00:00.000 Studying
4 463 SM Male 2007-10-08 00:00:00.000 Student
i need the output like this this is query is dynamic it may return n number of rows based on the intSID
chidname1 gender DOB childoccupation1 chidname2 gender DOB childoccupation2
SK female 2001-12-11 00:00:00.000 studying SM Male 2007-10-08 00:00:00.000 Student
For this type of data, you will need to implement both the UNPIVOT and then the PIVOT functions of SQL Server. The UNPIVOT takes your data from the multiple columns and place it into two columns and then you apply the PIVOT to transform the data back into columns.
If you know all of the values that you want to transform, then you can hard-code it, similar to this:
select *
from
(
select value, col+'_'+cast(rn as varchar(10)) col
from
(
select nvrchildname,
nvrgender,
convert(varchar(10), dttchildDOB, 120) dttchildDOB,
occupation,
row_number() over(partition by intsid order by intCHID) rn
from tblHRIS_ChildDetails
where intsid = 463
) src
unpivot
(
value
for col in (nvrchildname, nvrgender, dttchildDOB, occupation)
) unpiv
) src1
pivot
(
max(value)
for col in ([nvrchildname_1], [nvrgender_1],
[dttchildDOB_1], [occupation_1],
[nvrchildname_2], [nvrgender_2],
[dttchildDOB_2], [occupation_2])
) piv
See SQL Fiddle with Demo
Now, if you have an unknown number of values to transform, then you can use dynamic SQL for this:
DECLARE #colsUnpivot AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX),
#colsPivot as NVARCHAR(MAX)
select #colsUnpivot = stuff((select ','+quotename(C.name)
from sys.columns as C
where C.object_id = object_id('tblHRIS_ChildDetails') and
C.name not in ('intCHID', 'intsid')
for xml path('')), 1, 1, '')
select #colsPivot = STUFF((SELECT ','
+ quotename(c.name
+'_'+ cast(t.rn as varchar(10)))
from
(
select row_number() over(partition by intsid order by intCHID) rn
from tblHRIS_ChildDetails
) t
cross apply sys.columns as C
where C.object_id = object_id('tblHRIS_ChildDetails') and
C.name not in ('intCHID', 'intsid')
group by c.name, t.rn
order by t.rn
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query
= 'select *
from
(
select col+''_''+cast(rn as varchar(10)) col, value
from
(
select nvrchildname,
nvrgender,
convert(varchar(10), dttchildDOB, 120) dttchildDOB,
occupation,
row_number() over(partition by intsid order by intCHID) rn
from tblHRIS_ChildDetails
where intsid = 463
) x
unpivot
(
value
for col in ('+ #colsunpivot +')
) u
) x1
pivot
(
max(value)
for col in ('+ #colspivot +')
) p'
exec(#query)
See SQL Fiddle with Demo
The result of both queries is:
| NVRCHILDNAME_1 | NVRGENDER_1 | DTTCHILDDOB_1 | OCCUPATION_1 | NVRCHILDNAME_2 | NVRGENDER_2 | DTTCHILDDOB_2 | OCCUPATION_2 |
-----------------------------------------------------------------------------------------------------------------------------
| SK | Female | 2001-12-11 | Studying | SM | Male | 2007-10-08 | Student |
You have to provide distinct column names but besides that, it's simple. But as others stated, this is not commonly done and looks like if you want print some report with two columns.
select
max(case when intCHID=1 then nvrchildname end) as chidname1,
max(case when intCHID=1 then gender end) as gender1,
max(case when intCHID=1 then dttchildDOB end) as DOB1,
max(case when intCHID=1 then Occupation end) as cildOccupation1,
max(case when intCHID=2 then nvrchildname end) as chidname2,
max(case when intCHID=2 then gender end) as gender2,
max(case when intCHID=2 then dttchildDOB end) as DOB2,
max(case when intCHID=2 then Occupation end) as cildOccupation2
from
dbo.tblHRIS_ChildDetails
where
intSID=463