Related
I have two tables, let's call them Users and Fruit.
Users
ID Name Fruit
-------------------
1 Bob 1,3
2 Jack 3
Fruit
ID Name
-------------
1 Apple
2 Orange
3 Grape
How does one join those two tables to fill a datatable with the users choice of fruit names?
Would I need to write a stored procedure with a loop?
I am rather new to SQL Server and would be glad for any help or to be pointed in the right direction.
Created Physical tables with sample data
CREATE TABLE TempUsers
( ID INT,
Name VARCHAR(100),
Fruit VARCHAR(100)
)
INSERT INTO TempUsers
SELECT 1,'Bob' ,'1,3' UNION ALL
SELECT 2,'Jack','3'
CREATE TABLE TempFruit
( ID INT,
Name VARCHAR(100))
INSERT INTO TempFruit
SELECT 1,'Apple' UNION ALL
SELECT 2,'Orange'UNION ALL
SELECT 3,'Grape'
Create A Table-valued-Function to retrive the fruit names as comma separated
CREATE FUNCTION [dbo].[udf_GetFruitNames]
(
#vc_String nvarchar(max)
)
RETURNS #OutTable TABLE
(
Reqdata nvarchar(max)
)
AS
BEGIN
DECLARE #Temp AS TABLE
(
DATA nvarchar(max)
)
INSERT INTO #Temp
SELECT #vc_String;
DECLARE #Temp1 AS TABLE
(
DATA nvarchar(max)
)
INSERT INTO #Temp1
SELECT
STUFF((SELECT DISTINCT ','+ Name FROM
(
SELECT ID,
Name
FROm TempFruit
WHERE ID IN ( SELECT
CAST(Split.a.value('.', 'nvarchar(1000)') AS INT) AS FruitId
FROM
( SELECT
CAST( '<S>'+ REPLACE(DATA,',','</S><S>')+'</S>' AS XML) AS FruitId
FROM #Temp f
)AS A
CROSS APPLY FruitId.nodes('S') AS Split(a))
) As dt FOR XML PATH ('')),1,1,'') As FruitName
INSERT INTO #OutTable
SELECT * FROM #Temp1
RETURN
END
Sql query
SELECT ID
,Name
,uf.Reqdata AS FruitNames
FROM TempUsers u
CROSS APPLY [dbo].[udf_GetFruitNames](u.Fruit) AS uf
Or
SELECT ID
,Name
,(SELECT Reqdata FROM [dbo].[udf_GetFruitNames](u.Fruit) ) AS FruitNames
FROM TempUsers u
Result
ID Name FruitNames
---------------------
1 Bob Apple,Grape
2 Jack Grape
First of all, you need to redesign your tables. There is need for junction table, which will hold which user is connected to what fruit. It is a N:N raletionship.
So, you should create such table:
FruitUser
UserId FruitId
1 1
1 3
2 3
UserId is FK to Users table, FruitId is FK to Fruits table and both of these columns form a compoiste primary key. That's a standard approach.
Then you can use simple join to get results:
select * from users u
join FruitUser fu on u.id = fu.userid
join Fruit f on f.id = fu.fruitId
Sample Data
DECLARE #Users AS TABLE(ID INt, Name VARCHAR(100),fruit VARCHAR(100))
INSERT INTO #Users
SELECT 1,'Bob' ,'1,3' UNION ALL
SELECT 2,'Jack','3'
DECLARE #Fruit AS TABLE(ID INt, Name VARCHAR(100))
INSERT INTO #Fruit
SELECT 1,'Apple' UNION ALL
SELECT 2,'Orange'UNION ALL
SELECT 3,'Grape'
Sql Script
;WITH CTE
AS
(
SELECT UserId,
UserName ,
CAST(Split.a.value('.', 'nvarchar(1000)') AS INT) AS FruitId
FROM
( SELECT u.ID AS UserId,
u.Name AS UserName ,
CAST( '<S>'+ REPLACE(fruit,',','</S><S>')+'</S>' AS XML) AS FruitId
FROM #Fruit f
INNER JOIN #Users u
ON u.ID=f.ID
)AS A
CROSS APPLY FruitId.nodes('S') AS Split(a)
)
SELECT Userid,
UserName,
FruitId,
ft.name AS FruitName
FROM CTE c
LEFT JOIN (SELECT * FROM #Fruit) AS Ft
ON ft.ID=c.FruitId
Result
Userid UserName FruitId FruitName
------------------------------------------
1 Bob 1 Apple
1 Bob 3 Grape
2 Jack 3 Grape
For SQL Server 2014 where you can't use STRING_SPLIT , you can split the varchar using XML like following.
;WITH cte
AS (SELECT id,
name,
fruitid
FROM (SELECT *,
Cast('<X>' + Replace(F.fruit, ',', '</X><X>')
+ '</X>' AS XML) AS xmlfilter
FROM users F)F1
CROSS apply (SELECT fdata.d.value('.', 'varchar(50)') AS FruitId
FROM f1.xmlfilter.nodes('X') AS fdata(d)) O)
SELECT *
FROM cte C
INNER JOIN fruit F
ON F.id = Cast(C.fruitid AS INT)
DEMO
You can achieve the desire result without changing anything with this query
SELECT u.Id, u.Name, f.name
FROM Users u
inner join Fruit f on f.ID IN (SELECT cast(value as int)FROM STRING_SPLIT(u.fruit, ','));
Since you have sql server 2014 you have various options like CLR, XML and number functions. Best one is CLR but it's complex. So you can use this XML code.
select * from
(SELECT ID, [name],LTRIM(RTRIM(m.n.value('.[1]','varchar(8000)'))) AS fruitid
FROM
(SELECT ID,[name],CAST('<XMLRoot><RowData>' + REPLACE(fruit,',','</RowData><RowData>') + '</RowData></XMLRoot>' AS XML) AS x FROM {User table})t
CROSS APPLY x.nodes('/XMLRoot/RowData')m(n)) u
inner join {fruit table} f on f.id = u.fruitid
My table is:
SBType|SBName|Qty
===================
SMDB SB01 1
SMDB SB01 4
SMDB SB02 2
SMDB SB02 5
SMDB SB03 3
SMDB SB03 6
My desired output is:
SB01 | SB02 | SB03
==================
1 2 3
4 5 6
This is what my code looks like:
SELECT *
FROM (
SELECT
SM.SBName,ISNULL(ES.Qty,0)Qty
FROM RE_ES_SwitchBoard_Mast SM
left outer join RE_ES_Estimations ES on SM.PrCode=ES.PrCode and
Sm.SBType=ES.SBType and SM.SBName=ES.SBName
Where SM.PrCode='PR004' and SM.SBType='SMDB'
) as s
PIVOT
(
Max(Qty)
FOR [SBName] IN (SB01, SB02, SB03)
)AS pvthere
and the result of my attempt looks like:
SB01 SB02 SB03
1 2 3
I have tried with MAX(Qty) but it is not working.
Thanks in advance.
You are almost there.
By adding ROW_NUMBER() OVER (PARTITION BY SBName ORDER BY Qty) rn to the source of PIVOT clause you get multiple rows for different SBName instead of one grouped row. Your query should look like:
SELECT SB01, SB02, SB03
FROM (
SELECT
ROW_NUMBER() OVER (PARTITION BY SB.SBName ORDER BY Qty) rn,
SB.SBName,ISNULL(ES.Qty,0) Qty
FROM RE_ES_SwitchBoard_Mast SM
left outer join RE_ES_Estimations ES on SM.PrCode=ES.PrCode and
Sm.SBType=ES.SBType and SM.SBName=ES.SBName
Where SM.PrCode='PR004' and SM.SBType='SMDB'
) as s
PIVOT
(
Max(Qty)
FOR [SBName] IN (SB01, SB02, SB03)
)AS pvthere
A verifiable example here:
CREATE TABLE #sample
(
SBType varchar(MAX),
SBName varchar(MAX),
Qty int
)
INSERT INTO #sample VALUES ('SMDB','SB01',1)
INSERT INTO #sample VALUES ('SMDB','SB01',4)
INSERT INTO #sample VALUES ('SMDB','SB02',2)
INSERT INTO #sample VALUES ('SMDB','SB02',5)
INSERT INTO #sample VALUES ('SMDB','SB03',3)
INSERT INTO #sample VALUES ('SMDB','SB03',6)
SELECT SB01, SB02, SB03
FROM (
SELECT
ROW_NUMBER() OVER (PARTITION BY SBName ORDER BY Qty) rn, SBName,ISNULL(Qty,0) Qty
FROM #sample
) as s
PIVOT
(
Max(Qty)
FOR [SBName] IN (SB01, SB02, SB03)
) AS pvthere
DROP TABLE #sample
Dynamic query is the only way to use varchar columns in pivot. Have a look at below code to get idea.
First step is to generate comma separated list of items for column you need to use in pivot.
Then you can use this generated list in dynamic query for pivot columns.
Note: For example purpose I have used temp table. Replace it with your actual table.
CREATE TABLE #temptable
(
SBType VARCHAR(20),
SBName VARCHAR(20),
Qty INT
)
INSERT INTO #temptable SELECT 'SMDB','SB01',1
INSERT INTO #temptable SELECT 'SMDB','SB01',4
INSERT INTO #temptable SELECT 'SMDB','SB02',2
INSERT INTO #temptable SELECT 'SMDB','SB02',5
INSERT INTO #temptable SELECT 'SMDB','SB03',3
INSERT INTO #temptable SELECT 'SMDB','SB03',6
SELECT * FROM #temptable
DECLARE #cols AS NVARCHAR(MAX)
SELECT #cols = STUFF((SELECT ',' + QUOTENAME(SBName)
from #temptable
group by SBName
order by SBName
FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)')
,1,1,'')
SELECT #cols
DECLARE #query NVARCHAR(MAX)
SET #query = '
SELECT *
FROM
(
SELECT SBType,SBName,Qty,
row_number() over (partition by SBName order by Qty) as rn
FROM #temptable
) src
PIVOT
(
MIN(Qty)
FOR SBName IN (' + #cols + ')
) piv;'
EXEC(#query)
DROP TABLE #temptable
We have these tables
CREATE TABLE tbl01
(
[id] int NOT NULL PRIMARY KEY,
[name] nvarchar(50) NOT NULL
)
CREATE TABLE tbl02
(
[subId] int NOT NULL PRIMARY KEY ,
[id] int NOT NULL REFERENCES tbl01(id),
[val] nvarchar(50) NULL,
[code] int NULL
)
If we run this query:
SELECT
tbl01.id, tbl01.name, tbl02.val, tbl02.code
FROM
tbl01
INNER JOIN
tbl02 ON tbl01.id = tbl02.id
we get these results:
-------------------------------
id | name | val | code
-------------------------------
1 | one | FirstVal | 1
1 | one | SecondVal | 2
2 | two | YourVal | 1
2 | two | OurVal | 2
3 | three | NotVal | 1
3 | three | ThisVal | 2
-------------------------------
You can see that each two rows are related to same "id"
The question is: we need for each id to retrieve one record with all val, each val will return in column according to the value of column code
if(code = 1) then val as val-1
else if (code = 2) then val as val-2
Like this:
-------------------------------
id | name | val-1 | val-2
-------------------------------
1 | one | FirstVal | SecondVal
2 | two | YourVal | OurVal
3 | three | NotVal | ThisVal
-------------------------------
Any advice?
Use can use MAX and Group By to achieve this
SELECT id,
name,
MAX([val1]) [val-1],
MAX([val2]) [val-2]
FROM ( SELECT tbl01.id, tbl01.name,
CASE code
WHEN 1 THEN tbl02.val
ELSE ''
END [val1],
CASE code
WHEN 2 THEN tbl02.val
ELSE ''
END [val2]
FROM tbl01
INNER JOIN tbl02 ON tbl01.id = tbl02.id
) Tbl
GROUP BY id, name
Is it the PIVOT operator (http://technet.microsoft.com/en-us/library/ms177410(v=sql.105).aspx) that you are looking for?
You've already got a few answers, but heres one using PIVOT as an alternative. The good thing is this approach is easy to scale if there are additional columns required later
-- SETUP TABLES
DECLARE #t1 TABLE (
[id] int NOT NULL PRIMARY KEY,
[name] nvarchar(50) NOT NULL
)
DECLARE #t2 TABLE(
[subId] int NOT NULL PRIMARY KEY ,
[id] int NOT NULL,
[val] nvarchar(50) NULL,
[code] int NULL
)
-- SAMPLE DATA
INSERT #t1 ( id, name )
VALUES ( 1, 'one'), (2, 'two'), (3, 'three')
INSERT #t2
( subId, id, val, code )
VALUES ( 1,1,'FirstVal', 1), ( 2,1,'SecondVal', 2)
,( 3,2,'YourVal', 1), ( 4,2,'OurVal', 2)
,( 5,3,'NotVal', 1), ( 6,3,'ThisVal', 2)
-- SELECT (using PIVOT)
SELECT id, name, [1] AS 'val-1', [2] AS 'val-2'
FROM
(
SELECT t2.id, t1.name, t2.val, t2.code
FROM #t1 AS t1 JOIN #t2 AS t2 ON t2.id = t1.id
) AS src
PIVOT
(
MIN(val)
FOR code IN ([1], [2])
) AS pvt
results:
id name val-1 val-2
---------------------------------
1 one FirstVal SecondVal
2 two YourVal OurVal
3 three NotVal ThisVal
If there are always only two values, you could join them or even easier, group them:
SELECT tbl01.id as id, Min(tbl01.name) as name, MIN(tbl02.val) as val-1, MAX(tbl02.val) as val-2
FROM tbl01
INNER JOIN tbl02 ON tbl01.id = tbl02.id
GROUP BY tbl02.id
note: this query will always put the lowest value in the first column and highest in the second, if this is not wanted: use the join query:
Join query
If you always want code 1 in the first column and code 2 in the second:
SELECT tbl01.id as id, tbl01.name as name, tbl02.val as val-1, tbl03.val as val-2
FROM tbl01
INNER JOIN tbl02 ON tbl01.id = tbl02.id
ON tbl02.code = 1
INNER JOIN tbl03 ON tbl01.id = tbl03.id
ON tbl03.code = 2
Variable amount of columns
You cannot get an variable amount of columns, only when you do this by building your query in code or t-sql stored procedures.
My advice:
If its always to values: join them in query, if not, let your server-side code transform the data. (or even better, find a way which makes it not nessecery to transform data)
Try this - it uses a pivot function but it also creates creates the dynamic columns dependent on code
DECLARE #ColumnString varchar(200)
DECLARE #sql varchar(1000)
CREATE TABLE #ColumnValue
(
Value varchar(500)
)
INSERT INTO #ColumnValue (Value)
SELECT DISTINCT '[' + 'value' + Convert(Varchar(20),ROW_NUMBER() Over(Partition by id Order by id )) + ']'
FROM Test
SELECT #ColumnString = COALESCE(#ColumnString + ',', '') + Value
FROM #ColumnValue
Drop table #ColumnValue
SET #sql =
'
SELECT *
FROM
(
SELECT
id,name,val,''value'' + Convert(Varchar(20),ROW_NUMBER() Over(Partition by id Order by id ))as [values]
FROM Test
) AS P
PIVOT
(
MAX(val) FOR [values] IN ('+#ColumnString+')
) AS pv
'
--print #sql
EXEC (#sql)
I have a data in one table. I need to copy it to another table. One of the column is text delimited string. So what I'm thinking to select all columns insert get indentity value and with subquery to split based on delimiter and insert it to another table.
Here is the data example
ID Name City Items
1 Michael Miami item|item2|item3|item4|item5
2 Jorge Hallandale item|item2|item3|item4|item5
copy Name, City to one table get identity
and split and copy Items to another table with Identity Column Value
So output should be
Users table
UserID Name City
1 Michael Miami
2 Jorge Hallandale
...
Items table
ItemID UserID Name
1 1 Item
2 1 Item2
3 1 Item3
4 1 Item4
5 2 Item
6 2 Item2
7 2 Item3
8 2 Item4
Not really sure how to do it with T-SQL. Answers with examples would be appreciated
You may create you custom function to split the string in T-Sql. You could then use the Split function as part of a JOIN with your base table to generate the final results for your INSERT statement. Have a look at this post. Hope this help.
You can do this using xml and cross apply.
See the following:
DECLARE #t table (ID int, Name varchar(20), City varchar(20), Items varchar(max));
INSERT #t
SELECT 1,'Michael','Miami' ,'item|item2|item3|item4|item5' UNION
SELECT 2,'Jorge' ,'Hallandale','item|item2|item3|item4|item5'
DECLARE #u table (UserID int identity(1,1), Name varchar(20), City varchar(20));
INSERT #u (Name, City)
SELECT DISTINCT Name, City FROM #t
DECLARE #i table (ItemID int identity(1,1), UserID int, Name varchar(20));
WITH cte_Items (Name, Items) as (
SELECT
Name
,CAST(REPLACE('<r><i>' + Items + '</i></r>','|','</i><i>') as xml) as Items
FROM
#t
)
INSERT #i (UserID, Name)
SELECT
u.UserID
,s.Name as Name
FROM
cte_Items t
CROSS APPLY (SELECT i.value('.','varchar(20)') as Name FROM t.Items.nodes('//r/i') as x(i) ) s
INNER JOIN #u u ON t.Name = u.Name
SELECT * FROM #i
See more here:
http://www.kodyaz.com/articles/t-sql-convert-split-delimeted-string-as-rows-using-xml.aspx
Can you accomplish this with recursion? My T-SQL is rusty but this may help send you in the right direction:
WITH CteList AS (
SELECT 0 AS ItemId
, 0 AS DelimPos
, 0 AS Item_Num
, CAST('' AS VARCHAR(100)) AS Item
, Items AS Remainder
FROM Table1
UNION ALL
SELECT Row_Number() OVER(ORDER BY UserID) AS ItemId
, UserID
, CASE WHEN CHARINDEX('|', Remainder) > 0
THEN CHARINDXEX('|', Remainder)
ELSE LEN(Remainder)
END AS dpos
, Item_num + 1 as Item_Num
, REPLACE(Remainder, '|', '') AS Element
, right(Remainder, dpos+1) AS Remainder
FROM CteList
WHERE dpos > 0
AND ItemNum < 20 /* Force a MAX depth for recursion */
)
SELECT ItemId
, Item
FROM CteList
WHERE item_num > 0
ORDER BY ItemID, Item_Num
If I had this structure with the columns:
Primary_Key, Name, Parent_Primary_ID, DISPLAY_ORDER
1 Event NULL 1
2 News NULL 2
3 Event_List 1 1
4 Event_Detail 1 2
5 News_List 2 1
6 News_Details 2 2
how would you return data like:
1 Event
3 Event_List
4 Event_Detail
2 News
5 News_List
6 News_Detail
Thanks
Rob
If SQL Server 2005+
DECLARE #YourTable TABLE
(Primary_Key INT PRIMARY KEY,
Name VARCHAR(100),
Parent_Primary_ID INT NULL,
DISPLAY_ORDER INT)
INSERT INTO #YourTable
SELECT 1,'Event',NULL,1 UNION ALL
SELECT 2,'News',NULL,2 UNION ALL
SELECT 3,'Event_List',1,1 UNION ALL
SELECT 4,'Event_Detail',1,2 UNION ALL
SELECT 5,'News_List',2,1 UNION ALL
SELECT 6,'News_Details',2,2;
WITH Hierarchy
AS (SELECT *,
path = CAST(DISPLAY_ORDER AS VARCHAR(100))
FROM #YourTable
WHERE Parent_Primary_ID IS NULL
UNION ALL
SELECT y.Primary_Key,
y.Name,
y.Parent_Primary_ID,
y.DISPLAY_ORDER,
CAST(path + '.' + CAST(y.DISPLAY_ORDER AS VARCHAR) AS VARCHAR(100))
FROM #YourTable y
JOIN Hierarchy h
ON h.Primary_Key = y.Parent_Primary_ID)
SELECT Primary_Key,
Name
FROM Hierarchy
ORDER BY path
Try (asumming standardish sql is supported)
DECLARE #YourTable TABLE
(Primary_Key INT PRIMARY KEY,
Name VARCHAR(100),
Parent_Primary_ID INT NULL,
DISPLAY_ORDER INT)
INSERT INTO #YourTable
SELECT 1,'Event',NULL,1 UNION ALL
SELECT 2,'News',NULL,2 UNION ALL
SELECT 3,'Event_List',1,1 UNION ALL
SELECT 4,'Event_Detail',1,2 UNION ALL
SELECT 5,'News_List',2,1 UNION ALL
SELECT 6,'News_Details',2,2;
select
primary_key = t1.primary_key,
name = t1.name
from
#YourTable t1
left join #YourTable t2 on t1.parent_primary_id = t2.Primary_Key
order by
coalesce(t2.DISPLAY_ORDER,t1.DISPLAY_ORDER,0),
case
when t2.Primary_Key is null then 0
else t1.DISPLAY_ORDER
end
I don't see any grouping in your results. Unless you are trying to do something you aren't telling us I would use the query below:
SELECT Primary_Key, Name FROM YourTable
I don't see how you are ordering those results so I didn't try to order them.