convert Rows to Columns in SQL with same ID - sql

Folks,
I have a table with data as below.
enter image description here
Please advise sql logic to be applied to achieve this output.

One method is to use conditional aggregation:
select travelid,
max(case when seqnum = 1 then earn end) as earn,
max(case when seqnum = 1 then burn end) as burn,
max(case when seqnum = 2 then earn end) as earnA,
max(case when seqnum = 2 then burn end) as burnB
from (select t.*, row_number() over (partition by travelid order by travelid) as seqnum
from t
) t
group by travelid

SQL Server Dynamic version :
--TEST DATA
CREATE TABLE #TestTable
([TravelID] int, [SitelD] int, [Flagi] int, [FIag2] int)
;
INSERT INTO #TestTable
([TravelID], [SitelD], [Flagi], [FIag2])
VALUES
(1001, 1, 1, 0),(1001, 1, 0, 1),(1001, 1, 3, 4),
(1002, 1, 1, 0),(1002, 2, 0, 1),(1002, 2, 3, 4)
;
--STEP 1 rank data
SELECT * into #rank_table from (
select *
,ROW_NUMBER() OVER (PARTITION BY [TravelID],[SitelD] order by [SitelD]) [rank]
from (
select * from #TestTable
) T100
)T;
--STEP 2 Group by row_count
SELECT * into #group_table from (
select [TravelID],[SitelD] ,count(1) [count]
from #TestTable T
group by [TravelID],[SitelD]
)T;
--Use Exec
DECLARE #select_sql AS NVARCHAR(MAX) = ' select T.[TravelID], T.[SitelD] ',
#join_sql AS NVARCHAR(MAX) = ' from #group_table T ',
#max_count INT = (SELECT max([count]) FROM #group_table),
#temp_string NVARCHAR(5),
#temp_string_addone NVARCHAR(5)
;
DECLARE #index int = 0 ;
WHILE #index < #max_count
BEGIN
sELECT #temp_string = Convert(nvarchar(10),#index);
sELECT #temp_string_addone = Convert(nvarchar(10),#index+1);
select #select_sql = #select_sql + ' , T'+#temp_string_addone+'.[Flagi] as Flag'+Convert(nvarchar(10),2*#index+1)+' '
+ ' , T'+#temp_string_addone+'.[FIag2] as Flag'+Convert(nvarchar(10),2*#index+2)+' ';
select #join_sql = #join_sql + 'left join #rank_table T'+#temp_string_addone+' on ' + ' T.[TravelID] = T'+#temp_string_addone+'.[TravelID] and '
+ ' T.[SitelD] = T'+#temp_string_addone+'.[SitelD] and '
+ 'T'+#temp_string_addone+'.[rank] = '+#temp_string_addone+' ';
SET #index = #index + 1;
END;
EXEC (#select_sql
+ #join_sql
+' order by [TravelID],[SitelD] ; ')
;
DEMO : convert Rows to Columns in SQL with same ID, Sql Server - rextester

with CTE as(
select
*
,ROW_NUMBER() OVER (PARTITION BY [TravelID],[SitelD] order by [SitelD]) [rk]
from [Table]
)
select distinct a.[TravelID], a.[SitelD], a.[Flagi] as Flag1, a.[FIag2] as Flag1, b.[Flagi] as Flag3, b.[FIag2] as Flag4
from (select * from CTE where [rk] = 1) a
left join (select * from CTE where [rk] = 2) b on a.[TravelID] = b.[TravelID] and a.[SitelD] = + b.[SitelD]

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 Transform a list of numbers into ranges in one column

If I have a list of ranges in a table e.g.
ID Number
1 4
1 5
1 6
1 7
1 9
Is there a way to put this into the format: '4-7,9' into one varchar column using SQL ?
Thanks.
You can use ROW_NUMBER and XML PATH:
DECLARE #Mock TABLE (Id INT, Number INT)
INSERT INTO #Mock
VALUES
(1, 4),
(1, 5),
(1, 6),
(1, 7),
(1, 9)
;WITH CTE
AS
(
SELECT ROW_NUMBER() OVER (ORDER BY Number) AS RowId,*
FROM #Mock
)
SELECT
STUFF(
(
SELECT
',' + CAST(MIN(C.Number) AS VARCHAR(10)) + CASE WHEN MIN(C.Number) = MAX(C.Number) THEN '' ELSE '-' + CAST(MAX(C.Number) AS VARCHAR(10)) END
FROM
CTE C
GROUP BY
C.Number - C.RowId
FOR XML PATH ('')
), 1, 1, '') Result
Output: 4-7,9
Considering you have another to find the order of ranges
;WITH cte
AS (SELECT *,
Sum(CASE
WHEN number = prev_lag + 1 THEN 0
ELSE 1
END)
OVER(
ORDER BY iden_col) AS grp
FROM (SELECT *,
Lag(number)
OVER(
partition BY [ID]
ORDER BY iden_col) AS prev_lag
FROM Yourtable)a),
intr
AS (SELECT id,
CASE
WHEN Min(number) = Max(number) THEN Cast(Min(number) AS VARCHAR(50))
ELSE Concat(Min(number), '-', Max(number))
END AS intr_res
FROM cte
GROUP BY id,
grp)
SELECT DISTINCT Id,
Stuff(concat_col, 1, 1, '')
FROM intr a
CROSS apply (SELECT ',' + intr_res
FROM intr b
WHERE a.ID = b.ID
FOR xml path('')) cs (concat_col)
Demo

How to split data of 1 column in 3 columns?

Data is in given format-
Id Date Location
a123 6/6/2016 mmp
a123 6/7/2016 jpr
a123 6/8/2016 hjl
a123 6/9/2016 jhag
a678 6/10/2016 hjlwe
a678 6/11/2016 mkass
a980 6/7/2016 asdadf
a980 6/7/2016 lasdj
a980 6/7/2016 xswd
I want the same in given format-:
Id Date 1 Location1 Date 2 Location 2 Date 3 Location 3
a123 6/6/2016 mmp 6/7/2016 jpr 6/8/2016 hjl
a678 6/10/2016 hjlwe 6/11/2016 mkass
a980 6/7/2016 asdadf 6/7/2016 lasdj 6/7/2016
How to do that in SQL?
You can use ROW_NUMBER() with conditional aggregation :
SELECT s.id,
MAX(CASE WHEN s.rnk = 1 THEN s.date END) as date_1,
MAX(CASE WHEN s.rnk = 1 THEN s.location END) as location_1,
MAX(CASE WHEN s.rnk = 2 THEN s.date END) as date_2,
MAX(CASE WHEN s.rnk = 2 THEN s.location END) as location_2,
MAX(CASE WHEN s.rnk = 3 THEN s.date END) as date_3,
MAX(CASE WHEN s.rnk = 3 THEN s.location END) as location_3
FROM(
SELECT t.*,
ROW_NUMBER() OVER(PARTITION BY t.id ORDER BY t.Date) as rnk
FROM YourTable t) s
GROUP BY s.id
This can also be solved with PIVOT but I prefer to use conditional aggregation as long as the amount of the columns is limited .
If you need to add more levels, just follow the logic and replace 3 with 4 and so on..
Also you can make it with PIVOT (if number of columns can change dynamically you must use dynamic SQL):
;WITH cte AS (
SELECT *
FROM (VALUES
('a123', '6/6/2016', 'mmp'),
('a123', '6/7/2016', 'jpr'),
('a123', '6/8/2016', 'hjl'),
('a123', '6/9/2016', 'jhag'),
('a678', '6/10/2016', 'hjlwe'),
('a678', '6/11/2016', 'mkass'),
('a980', '6/7/2016', 'asdadf'),
('a980', '6/7/2016', 'lasdj'),
('a980', '6/7/2016', 'xswd')
) as t(Id, [Date], [Location])
)
SELECT p1.Id,
p1.[Date1],
p2.[Location1],
p1.[Date2],
p2.[Location2],
p1.[Date3],
p2.[Location3],
p1.[Date4],
p2.[Location4]
FROM
(SELECT *
FROM (
SELECT Id,
[Date],
'Date' + CAST(ROW_NUMBER() OVER (PARTITION BY Id ORDER BY [Date]) AS NVARCHAR(10)) as rn
fROM cte
) AS D
PIVOT (
MAX([Date]) for RN in ([Date1],[Date2],[Date3],[Date4])
) as pvt
) as p1
LEFT JOIN
(SELECT *
FROM (
SELECT Id,
[Location],
'Location' + CAST(ROW_NUMBER() OVER (PARTITION BY Id ORDER BY [Date]) AS NVARCHAR(10)) as rn
fROM cte
) AS D
PIVOT (
MAX([Location]) for RN in ([Location1],[Location2],[Location3],[Location4])
) as pvt
) as p2
ON p1.Id = p2.Id
Output:
Id Date1 Location1 Date2 Location2 Date3 Location3 Date4 Location4
---- --------- --------- --------- --------- --------- --------- --------- ---------
a123 6/6/2016 mmp 6/7/2016 jpr 6/8/2016 hjl 6/9/2016 jhag
a678 6/10/2016 hjlwe 6/11/2016 mkass NULL NULL NULL NULL
a980 6/7/2016 lasdj 6/7/2016 xswd 6/7/2016 asdadf NULL NULL
EDIT
With dynamic SQL (same output):
CREATE TABLE #temp (
Id nvarchar(10),
[Date] date,
[Location] nvarchar(10)
)
INSERT INTO #temp VALUES
('a123', '6/6/2016', 'mmp'),
('a123', '6/7/2016', 'jpr'),
('a123', '6/8/2016', 'hjl'),
('a123', '6/9/2016', 'jhag'),
('a678', '6/10/2016', 'hjlwe'),
('a678', '6/11/2016', 'mkass'),
('a980', '6/7/2016', 'asdadf'),
('a980', '6/7/2016', 'lasdj'),
('a980', '6/7/2016', 'xswd')
DECLARE #locs nvarchar(max),
#dates nvarchar(max),
#cols nvarchar(max),
#sql nvarchar(max)
SELECT #locs = STUFF((
SELECT DISTINCT ',' + QUOTENAME('Location' + CAST(ROW_NUMBER() OVER (PARTITION BY Id ORDER BY [Date]) AS NVARCHAR(10)))
FROM #temp
FOR XML PATH('')
),1,1,'')
SELECT #dates = STUFF((
SELECT DISTINCT ',' + QUOTENAME('Date' + CAST(ROW_NUMBER() OVER (PARTITION BY Id ORDER BY [Date]) AS NVARCHAR(10)))
FROM #temp
FOR XML PATH('')
),1,1,'')
SELECT #cols = STUFF((
SELECT DISTINCT ',' + QUOTENAME('Date' + CAST(ROW_NUMBER() OVER (PARTITION BY Id ORDER BY [Date]) AS NVARCHAR(10))) +
',' + QUOTENAME('Location' + CAST(ROW_NUMBER() OVER (PARTITION BY Id ORDER BY [Date]) AS NVARCHAR(10)))
FROM #temp
FOR XML PATH('')
),1,1,'')
SELECT #sql ='
SELECT p1.Id,
'+#cols+'
FROM
(SELECT *
FROM (
SELECT Id,
[Date],
''Date'' + CAST(ROW_NUMBER() OVER (PARTITION BY Id ORDER BY [Date]) AS NVARCHAR(10)) as rn
fROM #temp
) AS D
PIVOT (
MAX([Date]) for RN in ('+#dates+')
) as pvt
) as p1
LEFT JOIN
(SELECT *
FROM (
SELECT Id,
[Location],
''Location'' + CAST(ROW_NUMBER() OVER (PARTITION BY Id ORDER BY [Date]) AS NVARCHAR(10)) as rn
fROM #temp
) AS D
PIVOT (
MAX([Location]) for RN in ('+#locs+')
) as pvt
) as p2
ON p1.Id = p2.Id'
EXECUTE sp_executesql #sql
DROP TABLE #temp
If you don't know how many columns you have, you can use a dynamic version of sagi's answer. Here is one using a dynamic crosstab:
DECLARE #sql NVARCHAR(MAX) = N'';
SELECT #sql =
'SELECT
t.id' + CHAR(10);
SELECT #sql = #sql +
(SELECT
' , MAX(CASE WHEN rn = ' + CAST(rn AS VARCHAR(10)) + ' THEN t.Date END) AS ' + QUOTENAME('Date' + CAST(rn AS VARCHAR(10))) + CHAR(10) +
' , MAX(CASE WHEN rn = ' + CAST(rn AS VARCHAR(10)) + ' THEN t.Location END) AS ' + QUOTENAME('Location' + CAST(rn AS VARCHAR(10))) + CHAR(10)
FROM (
SELECT DISTINCT
ROW_NUMBER() OVER (PARTITION BY Id ORDER BY(SELECT NULL)) AS rn
FROM tbl
) t
ORDER BY rn
FOR XML PATH(''));
SELECT #sql = #sql +
'FROM (
SELECT *,
ROW_NUMBER() OVER(PARTITION BY Id ORDER BY Date) AS rn
FROM tbl
) t
GROUP BY t.Id;';
PRINT (#sql);
EXEC (#sql);
ONLINE DEMO

Merging rows to columns

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)

Comma delimiting results of one column to show next to another?

I have a table:
lease_id, suite_id
Lease is a key and you can have multiple suite’s to one lease. I’m trying to create a query that shows me all of the suite’s that a lease is associated with, essentially an output like the following:
lease_id: 1 suite_list: A1, A2, B1
Unfortunately I’m unsure how to approach this (or even how to begin) as this is a new kind of problem for me... Any help would be greatly appreciated!
You can use FOR XML.
The code will be something like this:
-- Sample data tables
select *
into #leases
from (
select '1' as lease_id
union
select '2' as lease_id
) a
select *
into #leaseSuites
from (
select '1' as lease_id,
'A1' as suite_id
union
select '1' as lease_id,
'A2' as suite_id
union
select '1' as lease_id,
'B1' as suite_id
union
select '2' as lease_id,
'C2' as suite_id
union
select '2' as lease_id,
'B3' as suite_id
) a
-- Creates comma delimited with child table.
select left(suite_list, LEN(suite_list) - 1) as suite_list
from (
SELECT 'lease_id: ' + lease_id + ' ' +
'suite_list: ' + (
SELECT s.suite_id + ','
FROM #leaseSuites s
WHERE l.lease_id = s.lease_id
ORDER BY s.suite_id
FOR XML PATH('')
) AS suite_list
FROM #leases l ) a
Click here to see an article with an example.
I'll assume your table is called LeasedSuites.
We'll need a function:
create function dbo.AllSuite (#l int) returns varchar(100)
as begin
declare #v varchar(2);
declare #r varchar(100);
DECLARE sc CURSOR FOR select suite_id from LeasedSuites where lease_id = #l
OPEN sc
FETCH NEXT FROM sc INTO #v
WHILE ##FETCH_STATUS = 0 BEGIN
select #r = #r + ',' + #v;
FETCH NEXT FROM sc INTO #v
END
CLOSE sc
DEALLOCATE sc
return substring(#r, 2, len(#r) - 1);
end
And a query:
declare #l int;
create table #out (lease_id int, suite_str varchar(100) null)
insert #out (lease_id) select distinct lease_id from LeasedSuites
while (select count(*) from #out where suite_str is null) > 0 begin
select #l = min(lease_id) from #out where suite_str is null;
update #out set suite_str = dbo.AllSuite(#l) where lease_id = #l;
end
select 'Lease ID: ' + cast(lease_id as varchar(3)) + ' Suites: ' + suite_str from #out order by l;
Hope this helps. Regards JB
If this represents an answer please mark as answer.
I've ended up solving this in two ways. My first method, which was unfortunately quite slow was:
declare #period_id integer =
(
select period_id
from property.period
where getdate() between period_start and period_end
)
;with cte_data as
(
select lp.*
from property.lease_period lp
where period_id = #period_id
)
, cte_suites as
(
select d.lease_id
, (
select stuff
(
( select ', ' + a.suite_id
from
( select a.suite_id
from cte_data a
where a.lease_id = d.lease_id
) a
for xml path(''))
, 1, 2, ''
) as suite_list
) suite_list
from cte_data d
group by d.lease_id
) ,
cte_count as
(
select lease_id ,
count(suite_id) as 'suites'
from property.lease_period
where period_id = #period_id
and lease_id <> 'No Lease'
group by lease_id
)
select d.period_id ,
d.building_id ,
d.lease_id ,
s.suite_list
from cte_data d
left outer join cte_suites s
on d.lease_id = s.lease_id
inner join cte_count c
on d.lease_id = c.lease_id
where period_id = 270
and d.lease_id <> 'No Lease'
and c.suites > 1
group by
d.period_id ,
d.building_id ,
d.lease_id ,
s.suite_list
I then stripped this back and re-approached it with a new direction, resulting in the following (much, much quicker):
declare #period_id integer =
(
select period_id
from property.period
where getdate() between period_start and period_end
)
;with CteLeaseInMultSuites as
(
select period_id,
building_id,
lease_id
from property.lease_period
where period_id = #period_id
and lease_id <> 'No Lease'
group by
period_id,
building_id,
lease_id
having count(*) > 1
)
select period_id,
building_id,
lease_id,
left(x.suite_list, len(x.suite_list) - 1) as suite_list
from CteLeaseInMultSuites lm
cross apply
(
select suite_id + ', '
from CteLeaseInMultSuites lmx
inner join property.lease_period lp
on lp.period_id = lmx.period_id
and lp.building_id = lmx.building_id
and lp.lease_id = lmx.lease_id
where lmx.period_id = lm.period_id
and lmx.building_id = lm.building_id
and lmx.lease_id = lm.lease_id
for xml path('')
) x (suite_list)