Merging rows to columns - sql

I have the following situation (heavily abstracted, please ignore bad design):
CREATE TABLE dbo.PersonTest (Id INT, name VARCHAR(255))
INSERT INTO dbo.PersonTest
(Id, name )
VALUES (1, 'Pete')
, (1, 'Marie')
, (2, 'Sam')
, (2, 'Daisy')
I am looking for the following result:
Id Name1 Name2
1 Marie Pete
2 Daisy Sam
So, for each Id, the rows should be merged.
Getting this result I used the following query:
WITH PersonRN AS
(
SELECT *
, ROW_NUMBER() OVER(PARTITION BY Id ORDER BY name) RN
FROM dbo.PersonTest
)
SELECT PT1.Id
, PT1.name Name1
, PT2.name Name2
FROM PersonRN AS PT1
LEFT JOIN PersonRN AS PT2 -- Left join in case there's only 1 name
ON PT2.Id = PT1.Id
AND PT2.RN = 2
WHERE PT1.RN = 1
Which works perfectly fine.
My question is: Is this the best way (best in terms of performance and resilience)? If, for example, one of these Id's has a third name, this third name is ignored by my query. I'm thinking the best way to deal with that would be dynamic SQL, which would be fine, but if it can be done without dynamic, I would prefer that.

Aside from dynamic PIVOT, you can do this using Dynamic Crosstab, which I prefer for readability.
SQL Fiddle
DECLARE #sql1 VARCHAR(1000) = '',
#sql2 VARCHAR(1000) = '',
#sql3 VARCHAR(1000) = ''
DECLARE #max INT
SELECT TOP 1 #max = COUNT(*) FROM PersonTest GROUP BY ID ORDER BY COUNT(*) DESC
SELECT #sql1 =
'SELECT
ID' + CHAR(10)
SELECT #sql2 = #sql2 +
' , MAX(CASE WHEN RN =' + CONVERT(VARCHAR(5), RN)
+ ' THEN name END) AS ' + QUOTENAME('Name' + CONVERT(VARCHAR(5), RN)) + CHAR(10)
FROM(
SELECT TOP(#max)
ROW_NUMBER() OVER(ORDER BY (SELECT NULL)) AS RN
FROM sys.columns
)t
ORDER BY RN
SELECT #sql3 =
'FROM(
SELECT *,
RN = ROW_NUMBER() OVER(PARTITION BY ID ORDER BY name)
FROM PersonTest
)t
GROUP BY ID
ORDER BY ID'
PRINT (#sql1 + #sql2 + #sql3)
EXEC (#sql1 + #sql2 + #sql3)

Related

Multiple Rows into One Row multiple columns

We need to list all numbers as a flat data set, how can we do that?
Table Name: Telephone
ID TYPE NUMBER
==================================
123 MN 042153939
123 HN 2242116
123 MN 1234567890
123 HN 12345678
Create Table Telephone
(
ID Integer,
Type char(3),
Number Varchar(20)
);
insert into Telephone values
(123, 'MN', '042153939'),
(123, 'HN', '2242116'),
(123, 'MN', '1234567890'),
(123, 'HN', '12345678');
I want SQL to return data in this format
ID MN#1 Mn#2 HN#1 HN#2
================================================
123 042153939 1234567890 2242116 12345678
Dynamic approach
Init
DROP TABLE IF EXISTS #Telephone;
CREATE TABLE #Telephone(ID INT,Type CHAR(3),Number VARCHAR(20));
INSERT INTO #Telephone (ID,Type,Number) VALUES
(123, 'MN', '042153939'),
(123, 'HN', '2242116'),
(123, 'MN', '1234567890'),
(123, 'HN', '12345678');
The code
DECLARE #ColumnList NVARCHAR(MAX);
SELECT #ColumnList = STUFF((SELECT ',[' + RTRIM(t.[Type]) + '#'
+ CONVERT(NVARCHAR(255),ROW_NUMBER()OVER(PARTITION BY t.[Type] ORDER BY t.ID)) + ']'
FROM #Telephone t FOR XML PATH(''),TYPE).value('(./text())[1]','NVARCHAR(MAX)'),1,1,'')
;
DECLARE #sql NVARCHAR(MAX) = '';
SET #sql = N'
SELECT ID,' + #ColumnList + N'
FROM (
SELECT t.ID,t.Number, RTRIM(t.[Type]) + ''#'' + CONVERT(NVARCHAR(255),ROW_NUMBER()OVER(PARTITION BY t.[Type] ORDER BY t.ID)) AS [Type]
FROM #Telephone t
) a
PIVOT(MAX(a.Number) FOR a.Type IN (' + #ColumnList + N')) p
'
;
--PRINT #sql
IF #sql IS NOT NULL EXEC(#sql);
try pivoting like below :
SELECT first_column AS <first_column_alias>,
[pivot_value1], [pivot_value2], ... [pivot_value_n]
FROM
(<source_table>) AS <source_table_alias>
PIVOT
(
aggregate_function(<aggregate_column>)
FOR <pivot_column> IN ([pivot_value1], [pivot_value2], ... [pivot_value_n])
) AS <pivot_table_alias>;
We can try using a pivot query with the help of ROW_NUMBER():
WITH cte AS (
SELECT *, ROW_NUMBER() OVER (ORDER BY TYPE DESC, NUMBER) rn
FROM Telephone
)
SELECT
ID,
MAX(CASE WHEN rn = 1 THEN NUMBER END) AS [MN#1],
MAX(CASE WHEN rn = 2 THEN NUMBER END) AS [MN#2],
MAX(CASE WHEN rn = 3 THEN NUMBER END) AS [HN#3],
MAX(CASE WHEN rn = 4 THEN NUMBER END) AS [HN#4]
FROM cte
GROUP BY ID;
You may try this. with row_number() and pivot.
For more info about pivot you may find this link PIVOT.
; with cte as (
select row_number() over (partition by type order by id ) as Slno, * from Telephone
)
, ct as (
select id, type + '#' + cast(slno as varchar(5)) as Type, values from cte
)
select * from (
select * from ct
) as d
pivot
( max(values) for type in ( [MN#1],[Mn#2],[HN#1],[HN#2] )
) as p

SQL Server 2016: Turn multiple lines into columns with the different iterations

I have a table that has many InsuranceNo's for unique MemberIDs. If there are more than one InsuranceNo, I want the InsuranceNo's to shift to a column, so in the end there is one line per MemberID, with all the iterations of that ID's InsuranceNo's as a Column.
MemberID InsuranceNo
--------------------------
123456 dser
124571 jklh
123456 abcd
I want it to look like this:
MemberID InsuranceNo1 InsuranceNo2
-----------------------------------------------------
123456 dser abcd
124571 jklh
Thank you!
Yet another option... Just change "YourTable" to your actual table name.
Example
Declare #SQL varchar(max) = '
Select *
From (
Select MemberID
,Item = concat(''InsuranceNo'',row_number() over (Partition By MemberID Order By (Select NULL)))
,Value = InsuranceNo
From YourTable
) A
Pivot (max([Value]) For [Item] in (' + Stuff((Select ','+QuoteName(concat('InsuranceNo',ColNr))
From (Select Distinct ColNr=row_number() over (Partition By MemberID Order By (Select NULL)) from YourTable ) A
For XML Path('')),1,1,'') + ') ) p'
--Print #SQL
Exec(#SQL);
Returns
MemberID InsuranceNo1 InsuranceNo2
123456 dser abcd
124571 jklh NULL
If it helps wrap your head around PIVOT, the SQL Generated looks like this:
Select *
From (
Select MemberID
,Item = concat('InsuranceNo',row_number() over (Partition By MemberID Order By (Select NULL)))
,Value = InsuranceNo
From YourTable
) A
Pivot (max([Value]) For [Item] in ([InsuranceNo1],[InsuranceNo2]) ) p
I prefer a dynamic cross tab to the dynamic pivot. I find the syntax far less obtuse and it is super easy if you need to add additional columns. Here is I would go about tackling this. Of course in your case you don't need a temp table because you have an actual table to use.
if OBJECT_ID('tempdb..#Something') is not null
drop table #Something
create table #Something
(
MemberID int
, InsuranceNo varchar(10)
)
insert #Something values
(123456, 'dser')
, (124571, 'jklh')
, (123456, 'abcd')
declare #StaticPortion nvarchar(2000) =
'with OrderedResults as
(
select *, ROW_NUMBER() over(partition by MemberID order by InsuranceNo) as RowNum
from #Something
)
select MemberID';
declare #DynamicPortion nvarchar(max) = '';
declare #FinalStaticPortion nvarchar(2000) = ' from OrderedResults Group by MemberID order by MemberID';
with E1(N) AS (select 1 from (values (1),(1),(1),(1),(1),(1),(1),(1),(1),(1))dt(n)),
E2(N) AS (SELECT 1 FROM E1 a, E1 b), --10E+2 or 100 rows
cteTally(N) AS
(
SELECT ROW_NUMBER() OVER (ORDER BY (SELECT NULL)) FROM E2
)
select #DynamicPortion = #DynamicPortion +
', MAX(Case when RowNum = ' + CAST(N as varchar(6)) + ' then InsuranceNo end) as InsuranceNo' + CAST(N as varchar(6)) + CHAR(10)
from cteTally t
where t.N <=
(
select top 1 Count(*)
from #Something
group by MemberID
order by COUNT(*) desc
)
select #StaticPortion + #DynamicPortion + #FinalStaticPortion
declare #SqlToExecute nvarchar(max) = #StaticPortion + #DynamicPortion + #FinalStaticPortion;
exec sp_executesql #SqlToExecute

form a query as per column values

I am using sql server 2012 and I have a table like this:
FieldName FieldValue
DivisionId 1
DivisionId 2
DivisionId 3
CompanyId 2
CompanyId 3
LocationId 1
What i want is concatenate columns and form a where clause query like this
(DivisionId=1 OR DivisionId=2 OR DivisionId=3) AND
(CompanyId=2 OR CompanyId=3) AND
(LocationId = 1)
What I was able to figure out is, I need to concatenate columns values like this
DECLARE #Query VARCHAR(MAX)
SELECT #Query =
ISNULL(#Query,'') + IIF(#Query IS NOT NULL, ' AND ', '') + CONCAT(DF.FieldName,'=',DA.FieldValue)
FROM TABLE
SELECT #Query;
But this code will not handle OR condition.
Try following solution:
DECLARE #eav TABLE (
FieldName NVARCHAR(128) NOT NULL,
FieldValue VARCHAR(50) NOT NULL
)
INSERT #eav (FieldName, FieldValue)
SELECT 'DivisionId', 1 UNION ALL
SELECT 'DivisionId', 2 UNION ALL
SELECT 'DivisionId', 3 UNION ALL
SELECT 'CompanyId ', 2 UNION ALL
SELECT 'CompanyId ', 3 UNION ALL
SELECT 'LocationId', 1
DECLARE #Predicate NVARCHAR(MAX) = N''
SELECT #Predicate = #Predicate
+ CASE WHEN rn_asc = 1 THEN ' AND ' + FieldName + ' IN (' + LTRIM(FieldValue) ELSE '' END
+ CASE WHEN rn_asc > 1 THEN ', ' + LTRIM(FieldValue) ELSE '' END
+ CASE WHEN rn_desc = 1 THEN ') ' ELSE '' END
FROM (
SELECT *,
rn_asc = ROW_NUMBER() OVER(PARTITION BY x.FieldName ORDER BY x.FieldValue),
rn_desc= ROW_NUMBER() OVER(PARTITION BY x.FieldName ORDER BY x.FieldValue DESC)
FROM #eav x
) y
ORDER BY FieldName, FieldValue
SELECT #Predicate = STUFF(#Predicate, 1, 5, '')
SELECT #Predicate
-- Results: CompanyId IN (2, 3) AND DivisionId IN (1, 2, 3) AND LocationId IN (1)
Then you could use #Predicate to create a dynamic SQL SELECT statement (for example)
DECLARE #SqlStatement NVARCHAR(MAX)
SET #SqlStatement = 'SELECT ... FROM dbo.Table1 WHERE ' + #Predicate
EXEC sp_executesql #SqlStatement

Iterating a SELECT statement in SQL Server 2008 using a WHILE LOOP

I have a database that gives an employeeID, a job, an effectiveDate, and a dept. If an employee has worked more than one job they will have an additional row of data. My goal is to compress the rows corresponding to each employee into one. Basically I need a query to that pulls from a db that looks like this:
EmpID Job EffDate Dept
001 QB 01-01-2001 OFF
001 LB 01-01-2010 DEF
001 K 01-01-2005 SPEC
002 HC 01-01-2007 STAFF
003 P 01-01-2001 SPEC
003 CB 01-01-2002 DEF
To output like this:
EmpID Job1 EffDate1 Dept1 Job2 EffDate2 Dept2 Job3 EffDate3 Dept3
001 QB 01-01-2001 OFF K 01-01-2005 SPEC LB 01-01-2010 DEF
002 HC 01-01-2007 STAFF
003 P 01-01-2001 SPEC CB 01-01-2002 DEF
So far I have done this:
SELECT
EmpNo
, Job
, EffDate
, Dept
, ROW_NUMBER() OVER (PARTITION BY EmpNo ORDER BY EffDate) AS RowNum
INTO #temp1
FROM JobHist
ORDER BY EffDate DESC
SELECT
JobHist.EmpNo
, JobHist.Job AS Job1
, JobHist.EjhJobDesc AS JobDesc1
, JobHist.EffDate AS EffDate1
, JobHist.Dept AS Dept1
, temp2.Job AS Job2
, temp2.EffDate AS EffDate2
, temp2.Dept AS Dept2
FROM #temp1 AS JobHist LEFT JOIN #temp1 AS temp2 ON JobHist.EmpNo = temp2.EmpNo AND temp2.RowNum = 2
WHERE JobHist.RowNum = 1
And that works just fine. The problem is that I need to make many columns, and I do not want to write all that code 20 times. So I want to iterate through using a WHILE command. Here is what I tried in that second SELECT statement:
DECLARE #Flag INT
DECLARE #FlagPlus INT
SET #Flag = 1
SET #FlagPlus = (#Flag + 1)
WHILE(#Flag < 20)
BEGIN
SELECT
temp#Flag.EmpNo
, temp#Flag.Job AS Job#Flag
, temp#Flag.EjhJobDesc AS JobDesc#Flag
, temp#Flag.EffDate AS EffDate#Flag
, temp#Flag.Dept AS Dept#Flag
FROM #temp1 AS temp#Flag
LEFT JOIN #temp#Flag AS temp#FlagPlus
ON temp#Flag.EmpNo = temp#FlagPlus.EmpNo AND temp#FlagPlus.RowNum = #FlagPlus
WHERE JobHist.RowNum = 1
SET #Flag = (#Flag + 1)
SET #FlagPlus = (#FlagPlus + 1)
END
I knew this probably wouldn't work because SQL will not understand the naming conventions I am trying to call each table and field. Is there a way using a cast or a concat command that I can automate the process so it just increments the numbers where I am asking it to?
First, let me maske clear that this is not directly an answer to the question. However, due to the large code block it is not suitable for a comment either, and I feel that it does add value to the question. So here it goes...
Having a dynamic number of columns is rarely a good solution. I'd opt for a different solution if using XML is an option:
SELECT
e.EmpNo,
(SELECT
h.Job,
h.EffDate,
h.Dept
FROM JobHist h
WHERE e.EmpNo = h.EmpNo
ORDER BY EffDate DESC
FOR XML PATH('job'), ROOT('jobs'), TYPE
) Jobs
FROM (SELECT DISTINCT EmpNo FROM JobHist) e
You can do an UNPIVOT and then a PIVOT of the data. this can be done either statically or dynamically:
Static Version:
select *
from
(
select empid, col + cast(rn as varchar(10)) colname, value
from
(
select Top 20 empid,
job,
convert(varchar(10), effdate, 101) effdate,
dept,
row_number() over(partition by empid order by effdate) rn
from yourtable
order by empid
) x
unpivot
(
value
for col in (Job, Effdate, Dept)
) u
) x1
pivot
(
min(value)
for colname in([Job1], [EffDate1], [Dept1],
[Job2], [EffDate2], [Dept2],
[Job3], [EffDate3], [Dept3])
)p
see SQL Fiddle with Demo
Dynamic Version:
DECLARE #colsUnpivot AS NVARCHAR(MAX),
#query AS NVARCHAR(MAX),
#colsPivot as NVARCHAR(MAX),
#colsPivotName as NVARCHAR(MAX)
select #colsUnpivot = stuff((select ','+ quotename(C.name)
from sys.columns as C
where C.object_id = object_id('yourtable') and
C.name not in ('empid')
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 empid order by effdate) rn
from yourtable
) t
cross apply sys.columns as C
where C.object_id = object_id('yourtable') and
C.name not in ('empid')
group by c.name, t.rn
order by t.rn, c.name desc
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
set #query
= 'select *
from
(
select empid, col + cast(rn as varchar(10)) colname, value
from
(
select Top 20 empid,
job,
convert(varchar(10), effdate, 101) effdate,
dept,
row_number() over(partition by empid order by effdate) rn
from yourtable
order by empid
) x
unpivot
(
value
for col in ('+ #colsunpivot +')
) u
) x1
pivot
(
min(value)
for colname in ('+ #colspivot +')
) p'
exec(#query)
see SQL Fiddle with Demo
Here is the solution.No matter how many job changes for the Emp it will pivot all of them
If you want to Pivot only 20 then set #MAXCol =20
edit: forget parentheses around #SQL in last line
SELECT
EmpNo
, Job
, EffDate
, Dept
, ROW_NUMBER() OVER (PARTITION BY EmpNo ORDER BY EffDate) AS RowNum
INTO #temp1
FROM JobHist
ORDER BY EffDate DESC
DECLARE #MAXCol INT = (SELECT MAX(RowNum)FROM #temp1)
,#index INT =1
,#ColNames varchar(4000)=''
,#SQL VARCHAR(MAX)=''
WHILE (#index<=#MAXCol)
BEGIN
SET #ColNames =#ColNames +'MAX(CASE WHEN RowNum = '+LTRIM(STR(#index))+' THEN Job END) as Job'+LTRIM(STR(#index))+','
+'MAX(CASE WHEN RowNum = '+LTRIM(STR(#index))+' THEN EffDate END) as EffDate'+LTRIM(STR(#index))+','
+'MAX(CASE WHEN RowNum = '+LTRIM(STR(#index))+' THEN Dept END) as Dept'+LTRIM(STR(#index))+','
SET #Index=#Index +1
END
SET #ColNames = LEFT(#ColNames,LEN(#ColNames)-1) -- Remove Last Comma
SET #SQL = 'SELECT EmpNo ,'+#ColNames+' FROM #temp1 GROUP BY EmpNo'
EXECUTE (#SQL)
Here is SQL Fiddle Demo working
http://sqlfiddle.com/#!3/99cea/1
Here's one way using a series of dynamically-created MAX/CASE expressions. You could also do this with PIVOT but this is quicker for me:
DECLARE #sql NVARCHAR(MAX) = N'SELECT EmpID';
SELECT TOP (20) #sql += N',
Job' + rn + ' = MAX(CASE WHEN rn = ' + rn + ' THEN Job END),
EffDate' + rn + ' = MAX(CASE WHEN rn = ' + rn + ' THEN EffDate END),
Dept' + rn + ' = MAX(CASE WHEN rn = ' + rn + ' THEN Dept END)'
FROM
(
SELECT rn = RTRIM(ROW_NUMBER() OVER (ORDER BY name))
FROM sys.all_objects
) AS x;
SET #sql += ' FROM (SELECT *, rn = ROW_NUMBER() OVER
(PARTITION BY EmpID ORDER BY EffDate) FROM dbo.your_table) AS y
GROUP BY EmpID;';
EXEC sp_executesql #sql;
You can probably tune this so that it determines the maximum number of job changes for any employee, rather than just defaulting to 20. You might also consider ordering the opposite way - surely an employee's last 20 job changes are more relevant than their first 20, if they've had more than 20.

How to declare the columns dynamically in a Select query using PIVOT

I am writing a query to get the address for PersonID. Following query is working for me but it only returns with two Addresses. I want to handle the 'n' number of address with a single query. Is there any way to do this?
Many thanks
SELECT
PersonID, PersonName
[Address1], [Address2]
FROM
(
SELECT
P.PersonID,
P.PersonName,
(ROW_NUMBER() OVER(PARTITION BY P.PersonID ORDER BY A.AddressID)) RowID
FROM tblPerson
INNER JOIN tblAddress AS A ON A.PersonID = P.PersonID
) AS AddressTable
PIVOT
(
MAX(AddressID)
FOR RowID IN ([Address1], [Address2])
) AS PivotTable;
Assuming the following tables and sample data:
USE tempdb;
GO
CREATE TABLE dbo.tblPerson(PersonID INT, PersonName VARCHAR(255));
INSERT dbo.tblPerson SELECT 1, 'Bob'
UNION ALL SELECT 2, 'Charlie'
UNION ALL SELECT 3, 'Frank'
UNION ALL SELECT 4, 'Amore';
CREATE TABLE dbo.tblAddress(AddressID INT, PersonID INT, [Address] VARCHAR(255));
INSERT dbo.tblAddress SELECT 1,1,'255 1st Street'
UNION ALL SELECT 2,2,'99 Elm Street'
UNION ALL SELECT 3,2,'67 Poplar Street'
UNION ALL SELECT 4,2,'222 Oak Ave.'
UNION ALL SELECT 5,1,'36 Main Street, Suite 22'
UNION ALL SELECT 6,4,'77 Sicamore Ct.';
The following query gets the results you want, and shows how it handles 0, 1 or n addresses. In this case the highest number is 3 but you can play with more addresses if you like by adjusting the sample data slightly.
DECLARE #col NVARCHAR(MAX) = N'',
#sel NVARCHAR(MAX) = N'',
#from NVARCHAR(MAX) = N'',
#query NVARCHAR(MAX) = N'';
;WITH m(c) AS
(
SELECT TOP 1 c = COUNT(*)
FROM dbo.tblAddress
GROUP BY PersonID
ORDER BY c DESC
)
SELECT #col = #col + ',[Address' + RTRIM(n.n) + ']',
#sel = #sel + ',' + CHAR(13) + CHAR(10) + '[Address' + RTRIM(n.n) + '] = x'
+ RTRIM(n.n) + '.Address',
#from = #from + CHAR(13) + CHAR(10) + ' LEFT OUTER JOIN xMaster AS x'
+ RTRIM(n.n) + ' ON x' + RTRIM(n.n) + '.PersonID = p.PersonID AND x'
+ RTRIM(n.n) + '.rn = ' + RTRIM(n.n)
FROM m CROSS JOIN (SELECT n = ROW_NUMBER() OVER (ORDER BY [object_id])
FROM sys.all_columns) AS n WHERE n.n <= m.c;
SET #query = N';WITH xMaster AS
(
SELECT PersonID, Address,
rn = ROW_NUMBER() OVER (PARTITION BY PersonID ORDER BY Address)
FROM dbo.tblAddress
)
SELECT PersonID, PersonName' + #col
+ ' FROM
(
SELECT p.PersonID, p.PersonName, ' + STUFF(#sel, 1, 1, '')
+ CHAR(13) + CHAR(10) + ' FROM dbo.tblPerson AS p ' + #from + '
) AS Addresses;';
PRINT #query;
--EXEC sp_executesql #query;
If you print the SQL you will see this result:
;WITH xMaster AS
(
SELECT PersonID, Address,
rn = ROW_NUMBER() OVER (PARTITION BY PersonID ORDER BY Address)
FROM dbo.tblAddress
)
SELECT PersonID, PersonName,[Address1],[Address2],[Address3] FROM
(
SELECT p.PersonID, p.PersonName,
[Address1] = x1.Address,
[Address2] = x2.Address,
[Address3] = x3.Address
FROM dbo.tblPerson AS p
LEFT OUTER JOIN xMaster AS x1 ON x1.PersonID = p.PersonID AND x1.rn = 1
LEFT OUTER JOIN xMaster AS x2 ON x2.PersonID = p.PersonID AND x2.rn = 2
LEFT OUTER JOIN xMaster AS x3 ON x3.PersonID = p.PersonID AND x3.rn = 3
) AS Addresses;
If you execute it, you will see this:
I know the query to get here is an ugly mess, but your requirement dictates it. It would be easier to return a comma-separated list as I suggested in my comment, or to have the presentation tier deal with the pivoting.