SQL combine multiple rows into 1 row - sql-server

I am trying to take rows with the same ID and return them on the same row. My data looks like the follow:
ID Fruit
1 Banana
1 Apple
1 Grapefruit
2 Cherry
2 Blueberry
3 Lime
3 Pear
And I would like it to look like this:
ID Fruit Fruit1 Fruit2
1 Banana Apple Grapefruit
2 Cherry Blueberry NULL
I have tried this as a query, but I don't seem to be having much luck:
SELECT a.[ID],a.[Fruit],b.[Fruit]
FROM [test].[dbo].[Fruit] a
JOIN [test].[dbo].[Fruit] b
ON a.ID = b.ID
WHERE a.FRUIT <> b.FRUIT
Can anybody help with this?
Thanks!

If the fruit count is not fixed, you can you use dynamic script:
IF OBJECT_ID('tempdb..#t') IS NOT NULL DROP TABLE #t
CREATE TABLE #t(ID INT,Fruit VARCHAR(100))
INSERT INTO #t(ID,Fruit)
SELECT 1,'Banana' UNION
SELECT 1,'Apple' UNION
SELECT 1,'Grapefruit' UNION
SELECT 2,'Cherry' UNION
SELECT 2,'Blueberry' UNION
SELECT 3,'Lime' UNION
SELECT 3,'Pear'
DECLARE #sql NVARCHAR(max),#cols VARCHAR(max)
SELECT #cols=ISNULL(#cols+',','')+t.col FROM (
SELECT *,'Fruit'+LTRIM(ROW_NUMBER()OVER(PARTITION BY ID ORDER BY(SELECT 1) )) AS col FROM #t AS t
) AS t GROUP BY t.col
SET #sql='
SELECT * FROM (
SELECT *,''Fruit''+LTRIM(ROW_NUMBER()OVER(PARTITION BY ID ORDER BY(SELECT 1) )) AS col FROM #t AS t
) AS t PIVOT(MAX(Fruit) FOR col in ('+#cols+')) p
'
PRINT #sql
EXEC(#sql)
IF OBJECT_ID('tempdb..#t') IS NOT NULL DROP TABLE #t
ID Fruit1 Fruit2 Fruit3
----------- ---------- ---------- ----------
1 Apple Banana Grapefruit
2 Blueberry Cherry NULL
3 Lime Pear NULL

You can use combination of a windowing function like row_number and then some conditional aggregation using a CASE expression with MAX() to get the result that you want:
select
Id,
Fruit = max(case when rn = 1 then Fruit end),
Fruit1 = max(case when rn = 2 then Fruit end),
Fruit2 = max(case when rn = 3 then Fruit end)
from
(
select
Id,
Fruit,
rn = row_number() over(partition by Id order by Id)
from [test].[dbo].[Fruit]
) d
group by Id;
See a Demo. The row_number() function creates a unique number for each id, then using this number along with CASE and MAX you will convert your rows of data into columns.

you can use pivot to do this as below:
Select Id, [0] as Fruit, [1] as [Fruit1], [2] as [Fruit2] from (
Select *, RowN = Row_Number() over (partition by Id order by Fruit) - 1 from yourtable )
pivot ( max(Fruit) for RowN in ([0], [1],[2]) ) p

Related

Variable within SQL query

I have this:
SELECT NEWID() as id,
'OwnerReassign' as name,
1 as TypeId,
'MyOrganisation' as OrgName,
'07DA8E53-74BD-459C-AF94-A037897A51E3' as SystemUserId,
0 as StatusId,
GETDATE() as CreatedAt,
'{"EntityName":"account","Ids":["'+CAST(AccountId as varchar(50))+'"],"OwnerId":"0C01C994-1205-E511-988E-26EE4189191B"}' as [Parameters]
FROM Account
WHERE OwnerIdName IN ('John Smith') AND New_AccountType = 1
Within the parameter field is an id (0C01C994-1205-E511-988E-26EE4189191B). Is it possible it could sequentially assign a different id from a list for each row? There are 5 id's in total.
What i'm trying to get to is this result set equally split between the 5 different id's.
Thanks
You can add one more NEWID() in the sub query and handle in the SELECT as below:
SELECT id, [name], TypeId, OrgName, SystemUserId, StatusId, CreatedAt,
'{"EntityName":"account","Ids":["' + AccountId +'"],"OwnerId":"' + ParamId + '"}' as [Parameters]
FROM (
SELECT NEWID() as id,
'OwnerReassign' as name,
1 as TypeId,
'MyOrganisation' as OrgName,
'07DA8E53-74BD-459C-AF94-A037897A51E3' as SystemUserId,
0 as StatusId,
GETDATE() as CreatedAt,
CAST(NEWID() AS VARCHAR (36)) as ParamId,
CAST(AccountId as varchar(50)) as AccountId
FROM Account
WHERE OwnerIdName IN ('John Smith') AND New_AccountType = 1
) A
You can use something like the following. Basically, use a row number for both your IDs and your data table to update, then do a MOD (%) operation with the amount of ID's you want to assign, so your data table to update is split into N groups. Then use that group ID to assign each ID.
IF OBJECT_ID('tempdb..#IDsToAssign') IS NOT NULL
DROP TABLE #IDsToAssign
CREATE TABLE #IDsToAssign (
IDToAssign VARCHAR(100))
-- 3 IDs example
INSERT INTO #IDsToAssign (
IDToAssign)
SELECT IDToAssign = NEWID()
UNION ALL
SELECT IDToAssign = NEWID()
UNION ALL
SELECT IDToAssign = NEWID()
DECLARE #AmountIDsToAssign INT = (SELECT COUNT(1) FROM #IDsToAssign)
IF OBJECT_ID('tempdb..#Account') IS NOT NULL
DROP TABLE #Account
CREATE TABLE #Account (
PrimaryKey INT PRIMARY KEY,
AssignedID VARCHAR(100))
-- 10 Rows example
INSERT INTO #Account (
PrimaryKey)
VALUES
(100),
(200),
(351),
(154),
(194),
(345),
(788),
(127),
(124),
(14)
;WITH DataRowNumber AS
(
SELECT
A.*,
RowNumber = ROW_NUMBER() OVER (ORDER BY (SELECT NULL))
FROM
#Account AS A
),
IDsRowNumbers AS
(
SELECT
D.IDToAssign,
RowNumber = ROW_NUMBER() OVER (ORDER BY D.IDToAssign)
FROM
#IDsToAssign AS D
),
NewIDAssignation AS
(
SELECT
R.*,
IDRowNumberAssignation = (R.RowNumber % #AmountIDsToAssign) + 1
FROM
DataRowNumber AS R
)
UPDATE A SET
AssignedID = R.IDToAssign
FROM
NewIDAssignation AS N
INNER JOIN IDsRowNumbers AS R ON N.IDRowNumberAssignation = R.RowNumber
INNER JOIN #Account AS A ON N.PrimaryKey = A.PrimaryKey
SELECT
*
FROM
#Account AS A
ORDER BY
A.AssignedID
/* Results:
PrimaryKey AssignedID
----------- ------------------------------------
124 1CC7F0F1-7EDE-4F7F-B0A3-739D74A62390
194 1CC7F0F1-7EDE-4F7F-B0A3-739D74A62390
351 1CC7F0F1-7EDE-4F7F-B0A3-739D74A62390
788 2A58A573-EDCB-428E-A87A-6BFCED265A9C
200 2A58A573-EDCB-428E-A87A-6BFCED265A9C
127 2A58A573-EDCB-428E-A87A-6BFCED265A9C
14 2A58A573-EDCB-428E-A87A-6BFCED265A9C
100 FD8036DA-0E15-453E-8A59-FA3C2BDB8FB1
154 FD8036DA-0E15-453E-8A59-FA3C2BDB8FB1
345 FD8036DA-0E15-453E-8A59-FA3C2BDB8FB1
*/
The ordering of the ROW_NUMBER() function will determine how ID's are assigned.
You could potentially do this by using the ROW_NUMBER() field in a subquery; for example:
SELECT NEWID() as id, 'OwnerReassign' as name, 1 as TypeId,
'MyOrganisation' as OrgName,
'07DA8E53-74BD-459C-AF94-A037897A51E3' as SystemUserId,
0 as StatusId, GETDATE() as CreatedAt,
case B / ##ROWCOUNT
when 0 then '0C01C994-1205-E511-988E-26EE4189191B'
when 1 then '12345677-1205-E511-988E-26EE4189191B'
when 2 then '66666666-1205-E511-988E-26EE4189191B'
etc...
end
FROM
(
SELECT ROW_NUMBER() OVER (ORDER BY A.Id)
FROM Account A
WHERE OwnerIdName IN ('John Smith') AND New_AccountType = 1
) AS B
If you want the system to pick those values then you could put then in their own temporary table, too.

Select Distinct on one column and eliminate nulls in Select Distinct?

follow this question
I have...
ID SKU PRODUCT
=======================
1 FOO-23 Orange
2 BAR-23 Orange
3 FOO-24 Apple
4 FOO-25 Orange
5 FOO-25 null
6 FOO-25 null
expected result:
1 FOO-23 Orange
3 FOO-24 Apple
5 FOO-25 null
6 FOO-25 null
This query isn't getting me there. How can I SELECT DISTINCT on just one column and eliminate null in SELECT DISTINCT?
SELECT *
FROM (SELECT ID, SKU, Product,
ROW_NUMBER() OVER (PARTITION BY PRODUCT ORDER BY ID) AS RowNumber
FROM MyTable
WHERE SKU LIKE 'FOO%') AS a
WHERE a.RowNumber = 1
Perhaps one approach is using the WITH TIES in concert with a conditional PARTITION
Example
Declare #YourTable Table ([ID] int,[SKU] varchar(50),[PRODUCT] varchar(50))
Insert Into #YourTable Values
(1,'FOO-23','Orange')
,(2,'BAR-23','Orange')
,(3,'FOO-24','Apple')
,(4,'FOO-25','Orange')
,(5,'FOO-25',NULL)
,(6,'FOO-25',NULL)
Select top 1 with ties *
From #YourTable
Where SKU Like 'FOO%'
Order By Row_Number() over (Partition By IsNull(Product,NewID()) Order By ID)
Returns
ID SKU PRODUCT
6 FOO-25 NULL
5 FOO-25 NULL
3 FOO-24 Apple
1 FOO-23 Orange
Using John Cappelletti's sample data here is another approach. All you really needed was to add the OR predicate to your where clause.
Declare #YourTable Table ([ID] int,[SKU] varchar(50),[PRODUCT] varchar(50))
Insert Into #YourTable Values
(1,'FOO-23','Orange')
,(2,'BAR-23','Orange')
,(3,'FOO-24','Apple')
,(4,'FOO-25','Orange')
,(5,'FOO-25',NULL)
,(6,'FOO-25',NULL)
SELECT *
FROM
(
SELECT ID
, SKU
, Product
, ROW_NUMBER() OVER (PARTITION BY PRODUCT ORDER BY ID) AS RowNumber
FROM #YourTable
WHERE SKU LIKE 'FOO%'
) AS a
WHERE a.RowNumber = 1
OR a.PRODUCT IS NULL --This was the only part you were missing
I changed your row_number to dense rank:
Declare #YourTable Table ([ID] int,[SKU] varchar(50),[PRODUCT] varchar(50))
Insert Into #YourTable Values
(1,'FOO-23','Orange')
,(2,'BAR-23','Orange')
,(3,'FOO-24','Apple')
,(4,'FOO-25','Orange')
,(5,'FOO-25',NULL)
,(6,'FOO-25',NULL)
SELECT *
FROM (SELECT ID, SKU, Product,
Dense_RANK() OVER (PARTITION BY SKU ORDER BY Product) AS RowNumber
FROM #YourTable
WHERE left(SKU,3) = 'FOO') AS a
WHERE a.RowNumber = 1
Results:
ID SKU Product RowNumber
1 FOO-23 Orange 1
3 FOO-24 Apple 1
5 FOO-25 NULL 1
6 FOO-25 NULL 1

How to select the values of multiple columns in sorted order in mssql?

In a table there are three columns, each containing numeric values (in my case representing length, width and height, but could be anything).
How can I select them, ordered by their value?
For example, given the values:
id | length | width | height
1 | 100 | 30 | 50
2 | 6 | 12 | 9
Expected output would be:
id | min | mid | max
1 | 30 | 50 | 100
2 | 6 | 9 | 12
Use the Table Value Constructor with ORDER BY to get the relevent values in sorted order.
Using OFFSET X ROWS and FETCH NEXT 1 ROWS ONLY, you can access a specific position inside the sorted column values.
Use it repeatedly (increasing OFFSET by 1 in each step) to access the sorted column values at each position.
SELECT
length,
width,
height,
(
SELECT dimensions
FROM ( VALUES (length),(width),(height) ) AS compare(dimensions)
ORDER BY dimensions ASC
OFFSET 0 ROWS
FETCH NEXT 1 ROWS ONLY
) AS minDimension,
(
SELECT dimensions
FROM ( VALUES (length),(width),(height) ) AS compare(dimensions)
ORDER BY dimensions ASC
OFFSET 1 ROWS
FETCH NEXT 1 ROWS ONLY
) AS midDimension,
(
SELECT dimensions
FROM ( VALUES (length),(width),(height) ) AS compare(dimensions)
ORDER BY dimensions ASC
OFFSET 2 ROWS
FETCH NEXT 1 ROWS ONLY
) AS maxDimension
FROM sometable
Try this
DECLARE #T TABLE
(
Id INT,
[Length] INT,
Width INT,
Height INT
)
INSERT INTO #T
VALUES(1,100,30,50),(2,6,9,12)
;WITH CTE
AS
(
SELECT
Id,
Val = [Length]
FROM #T
UNION ALL
SELECT
Id,
Val = Width
FROM #T
UNION ALL
SELECT
Id,
Val = Height
FROM #T
),C2
AS
(
SELECT
SeqNo = ROW_NUMBER() OVER(PARTITION BY ID ORDER BY Val),
Id,
Val
FROM CTE
)
SELECT
Id,
[1] "min",
[2] "mid",
[3] "max"
FROM C2
PIVOT
(
MAX(VAL)
FOR
SeqNo IN
(
[1],[2],[3]
)
)q
One alternate solution, using PERCENTILE_CONT to obtain the Median:
WITH Measurement AS(
SELECT *
FROM (VALUES (1,100,30,50),
(2,6,12,9)) V(ID, [Length], Width, Height)),
Pvt AS(
SELECT ID,
D.Measurement AS MeasurementType,
CASE WHEN D.Measurement = 'Length' THEN M.[Length]
WHEN D.Measurement = 'Width' THEN M.Width
WHEN D.Measurement = 'Height' THEN M.Height
END AS Measurement
FROM Measurement M
CROSS APPLY (VALUES ('Length'),('Width'),('Height')) D(Measurement)),
Median AS(
SELECT ID,
PERCENTILE_CONT(0.5) WITHIN GROUP (ORDER BY Measurement) OVER (PARTITION BY ID) AS Median,
MeasurementType, Measurement
FROM Pvt)
SELECT ID,
MIN(Measurement) AS [Min],
Median,
MAX(Measurement) AS [Max]
FROM Median
GROUP BY ID, Median
ORDER BY ID;
First, make the values of columns length, width and height to one column using union. And then use this result-set as a sub-query and give a row number based on group by id and descending order of the newly added column. Then by using a CASE expression take the row having row number 1 as max, 2 as mid and 3 as min.
Query
;with cte as(
select [rn] = row_number() over(
partition by t.[id]
order by t.[new_col] desc
), *
from (
select [id], [length] as [new_col] from [your_table_name]
union all
select [id], [width] from [your_table_name]
union all
select [id], [height] from [your_table_name]
) t
)
select [id],
max(case [rn] when 1 then [new_col] end) as [max],
max(case [rn] when 2 then [new_col] end) as [mid],
max(case [rn] when 3 then [new_col] end) as [min]
from cte
group by [id];
Fiddle demo
This seems to work:
Query
declare #table table (id integer, length integer, width integer, height integer)
insert into #table select 1,100,30,50
insert into #table select 2,6,12,9
select id, MIN(a) min
,(select top 1 max(c.a) from
(
select id,length a from #table
union all
select id,width a from #table
union all
select id,height a from #table
)c
where c.id = b.id and c.a <> max(b.a) and c.a <> min(b.a) group by c.id) mid, max(a) max
from
(
select id,length a from #table
union all
select id,width a from #table
union all
select id,height a from #table
)b
group by id

I need a more reliable sort for CTE hierarchy query

Example table:
CREATE TABLE Fruit (
ID int identity(1,1) NOT NULL,
ParentID int NULL,
Name varchar(255)
);
I want to sort parent and child records from the same table in alphabetical order (more than one level deep):
Apples
--Green
----Just Sour
----Really Sour
--Red
----Big
----Small
Bananas
--etc.
I attempted this:
;WITH CTE(ID, ParentID, Name, Sort) AS
(
SELECT
ID
,ParentID
,Name
,cast('\' + Name as nvarchar(255)) AS Sort
FROM Fruit
WHERE ParentID IS NULL
UNION ALL
SELECT
a.ID
,a.ParentID
,a.Name
,cast(b.Sort + '\' + a.Name as nvarchar(255)) AS Sort
FROM Fruit a
INNER JOIN CTE b ON a.ParentID = b.ID
)
SELECT * FROM CTE Order by Sort
This produces results for the sort like:
\Apples
\Apples\Green
\Apples\Green\Just Sour
\etc.
Just when I thought things were good, it isn't reliable. For example, if an item has more than one word. Like:
\Apples
\Apples A <-- culprit
\Apples\Green
If I can expand my question while I'm at it, I'd like to show actual hyphens or something in the results:
Parent
- Child
--Grandchild
The cruddy way I quickly did this was by adding a prefix column in the table with the value of - for all records. Then I could do this:
;WITH CTE(ID, ParentID, Name, Sort, Prefix) AS
(
SELECT
ID
,ParentID
,Name
,cast('\' + Name as nvarchar(255)) AS Sort
,Prefix
FROM Fruit
WHERE ParentID IS NULL
UNION ALL
SELECT
a.ID
,a.ParentID
,a.Name
,cast(b.Sort + '\' + a.Name as nvarchar(255)) AS Sort
,cast(b.Prefix + a.Prefix as nvarchar(10)) AS Prefix
FROM Fruit a
INNER JOIN CTE b ON a.ParentID = b.ID
)
SELECT * FROM CTE Order by Sort
But that seems incorrect or not optimal.
These hierarchical queries still give me a headache, so perhaps I'm just not seeing the obvious.
I tend to use row_number() ordered by Name in this case
Example
Declare #YourTable table (id int,ParentId int,Name varchar(50))
Insert into #YourTable values
( 1, NULL,'Apples')
,( 2, 1 ,'Green')
,( 3, 2 ,'Just Sour')
,( 4, 2 ,'Really Sour')
,( 5, 1 ,'Red')
,( 6, 5 ,'Big')
,( 7, 5 ,'Small')
,( 8, NULL,'Bananas')
Declare #Top int = null --<< Sets top of Hier Try 5
Declare #Nest varchar(25) = '|-----' --<< Optional: Added for readability
;with cteP as (
Select Seq = cast(1000+Row_Number() over (Order by Name) as varchar(500))
,ID
,ParentId
,Lvl=1
,Name
From #YourTable
Where IsNull(#Top,-1) = case when #Top is null then isnull(ParentId ,-1) else ID end
Union All
Select Seq = cast(concat(p.Seq,'.',1000+Row_Number() over (Order by r.Name)) as varchar(500))
,r.ID
,r.ParentId
,p.Lvl+1
,r.Name
From #YourTable r
Join cteP p on r.ParentId = p.ID)
Select A.ID
,A.ParentId
,A.Lvl
,Name = Replicate(#Nest,A.Lvl-1) + A.Name
,Seq --<< Can be removed
From cteP A
Order By Seq
Returns
ID ParentId Lvl Name Seq
1 NULL 1 Apples 1001
2 1 2 |-----Green 1001.1001
3 2 3 |-----|-----Just Sour 1001.1001.1001
4 2 3 |-----|-----Really Sour 1001.1001.1002
5 1 2 |-----Red 1001.1002
6 5 3 |-----|-----Big 1001.1002.1001
7 5 3 |-----|-----Small 1001.1002.1002
8 NULL 1 Bananas 1002
I'm going to guess you want this result
\Apples
\Apples\Green
\Apples A
Maybe try something like this:
SELECT *
FROM CTE
ORDER BY replace(Sort, ' ', '~')
'~' is ascii 126, You can also check using excel sorting.

Trying to pivot event dates in t-sql without using a cursor

I have the following table:
What I want is to get to this:
EventTypeId 1 and 3 are valid start events and EventTypeId of 2 is the only valid end event.
I have tried to do a pivot, but I don't believe a pivot will get me the multiple events for a person in the result set.
SELECT PersonId, [1],[3],[2]
FROM
(
SELECT PersonId, EventTypeId, EventDate
from #PersonEvent
) as SourceTable
PIVOT
(
count(EventDate) FOR EventTypeId
IN ([1],[3],[2])
) as PivotTable
Select PersonID,
Min(Case WHEN EventTypeId IN (1,3) THEN EventDate END) as StartDate,
Min(Case WHEN EventTypeId IN (2) THEN EventDate END) as EndDate
FROM #PersonEvent
group by personid
I can do a cursor, but my original table is over 90,000 rows, and this is to be for a report, so I don't think I can use that option. Any other thoughts that I might be missing?
Assuming the table is called [dbo].[PersonEventRecords] this will work...
With StartEvents As
(
Select *
From [dbo].[PersonEventRecords]
Where EventTypeId In (1,3)
), EndEvents As
(
Select *
From [dbo].[PersonEventRecords]
Where EventTypeId In (2)
)
Select IsNull(se.PersonId,ee.PersonId) As PersonId,
se.EventTypeId As StartEventTypeId,
se.EventDate As StartEventDate,
ee.EventTypeId As EndEventTypeId,
ee.EventDate As EndEventDate
From StartEvents se
Full Outer Join EndEvents ee
On se.PersonId = ee.PersonId
And se.EventSequence = ee.EventSequence - 1
Order By IsNull(se.PersonId,ee.PersonId),
IsNull(se.EventDate,ee.EventDate);
/**** TEST DATA ****/
If Object_ID('[dbo].[PersonEventRecords]') Is Not Null
Drop Table [dbo].[PersonEventRecords];
Create Table [dbo].[PersonEventRecords]
(
PersonId Int,
EventTypeId Int,
EventDate Date,
EventSequence Int
);
Insert [dbo].[PersonEventRecords]
Select 1,1,'2012-10-13',1
Union All
Select 1,2,'2012-10-20',2
Union All
Select 1,1,'2012-11-01',3
Union All
Select 1,2,'2012-11-13',4
Union All
Select 2,1,'2012-05-07',1
Union All
Select 2,2,'2012-06-01',2
Union All
Select 2,3,'2012-07-01',3
Union All
Select 2,2,'2012-08-30',4
Union All
Select 3,2,'2012-04-05',1
Union All
Select 3,1,'2012-05-04',2
Union All
Select 3,2,'2012-05-24',3
Union All
Select 4,1,'2013-01-03',1
Union All
Select 4,1,'2013-02-20',2
Union All
Select 4,2,'2013-03-20',3;
Try this
SELECT E1.PersonId, E1.EventTypeId, E1.EventDate, E2.EventTypeId, E2.EventDate
FROM PersonEvent AS E1
OUTER APPLY(
SELECT TOP 1 PersonEvent.EventTypeId, PersonEvent.EventDate
FROM PersonEvent
WHERE PersonEvent.PersonId = E1.PersonId
AND PersonEvent.EventSequence = E1.EventSequence + 1
AND PersonEvent.EventTypeId = 2
) AS E2
WHERE E1.EventTypeId = 1 OR E1.EventTypeId = 3
UNION
SELECT E3.PersonId, NULL, NULL, E3.EventTypeId, E3.EventDate
FROM PersonEvent E3
WHERE E3.EventTypeId = 2
AND NOT EXISTS(
SELECT *
FROM PersonEvent
WHERE PersonEvent.PersonId = E3.PersonId
AND PersonEvent.EventSequence = E3.EventSequence - 1)
It is not completely clear how do you want the result to be ordered – add order as needed.

Resources