Recursive Function for MS SQL - sql

I need to write a recursive function to get the references from table B and combine in a field in table A.
Table A:
ID   Name      Reference
1    Item A
2    Item B
3    Item C
Table B:
ID   Parent_ID   Reference
1    1           ABC
2    1           DEF
3    2           GHI
Expected result:
ID   Name      Reference
1    Item A     ABCDEF
2    Item B     GHI
3    Item C

I don't see any recursion here:
DECLARE #a TABLE
(
ID INT ,
Name VARCHAR(10) ,
Reference VARCHAR(100)
)
DECLARE #b TABLE
(
ID INT ,
ParentID INT ,
Reference VARCHAR(3)
)
INSERT INTO #a
VALUES ( 1, 'Item A', NULL ),
( 2, 'Item B', NULL ),
( 3, 'Item C', NULL )
INSERT INTO #b
VALUES ( 1, 1, 'ABC' ),
( 2, 1, 'DEF' ),
( 3, 2, 'GHI' )
UPDATE a
SET Reference = ca.data
FROM #a a
CROSS APPLY ( SELECT
( SELECT b.Reference
FROM #b b
WHERE a.ID = b.ParentID
ORDER BY ID
FOR XML PATH('') ,
TYPE
).value('.', 'varchar(max)') AS DATA
) ca
SELECT *
FROM #a
Output:
ID Name Reference
1 Item A ABCDEF
2 Item B GHI
3 Item C NULL

Related

TSQL - How to avoid UNION ALL

Sample Data:
DECLARE #Parent TABLE
(
[Id] INT
, [Misc_Val] VARCHAR(5)
) ;
DECLARE #Children TABLE
(
[Id] INT
, [P_ID] INT
) ;
INSERT INTO #Parent
VALUES
( 1, 'One' )
, ( 2, 'Two' )
, ( 3, 'Three' )
, ( 5, 'Four' ) ;
INSERT INTO #Children
VALUES
( 10, 1 )
, ( 11, 1 )
, ( 21, 2 )
, ( 23, 2 )
, ( 30, 3 )
, ( 40, 4 ) ;
Goal:
To efficiently output three fields ( [Id] and [IsChild], [Misc_Val] ). Output all records from #Parent table with [IsChild] = 0 and output all MATCHING records from #Child table (#Parent.Id = #Children.P_Id) with [IsChild] = 1.
Expected Output
Id IsChild Misc_Val
1 0 One
2 0 Two
3 0 Three
5 0 Four
10 1 One
11 1 One
21 1 Two
23 1 Two
30 1 Three
My try:
SELECT [P].[Id]
, 0 AS [IsChild]
, [P].[Misc_Val]
FROM #Parent AS [P]
UNION ALL
SELECT [C].[Id]
, 1
, [P].[Misc_Val]
FROM #Parent AS [P]
JOIN #Children AS [C]
ON [C].[P_ID] = [P].[Id] ;
Is there a better way to do this than using UNION ALL? #Parent and #Children tables are quite big and so am trying to avoid querying the #Parent table twice.
UPDATE: The below answer made me realized something I missed out when creating the post with mocked data. We do need some additional data from #Parent table regardless in the final output.
You can use CROSS APPLY to add the child table to the parent table.
This may or may not be faster, it can depend on indexing and so forth. You need to check the query plan.
SELECT v.Id
, v.IsChild
, P.Misc_Val
FROM #Parent AS P
CROSS APPLY (
SELECT
P.Id,
0 AS IsChild
UNION ALL
SELECT
C.Id,
1
FROM #Children AS C
WHERE C.P_ID = P.Id
) v;
Note that the first SELECT in the apply has no FROM and therefore does not do any table access.

How to get anchor details when using CTE?

I have a table with 2 columns (id, childId) with the following data:
1, 2
2, 4
3, 5
4, 6
5, null
6, null
I have the following CTE that gets the records and it works fine.
DECLARE #id TABLE (id int, childId int);
INSERT INTO #id SELECT 1, 2;
INSERT INTO #id SELECT 2, 4;
INSERT INTO #id SELECT 3, 5;
INSERT INTO #id SELECT 4, 6;
INSERT INTO #id SELECT 5, null;
INSERT INTO #id SELECT 6, null;
WITH cte AS
(
SELECT id, childId
FROM #id
WHERE id = 1
UNION ALL
SELECT b.id, b.childId
FROM #id b
INNER JOIN cte
ON b.id = cte.childId
)
SELECT * FROM cte
However, I would like to add the anchor details so that the results will look like:
1, 2, null
2, 4, 1
4, 6, 1
6, null, 1
So that that 3rd column is the main anchor record.
Is this possible?
Just add a thrid column in CTE.
declare #mytable table ( id int, childId int );
insert #mytable
( id, childId )
values ( 1, 2 ),
( 2, 4 ),
( 3, 5 ),
( 4, 6 ),
( 5, null ),
( 6, null );
declare #id table ( id int );
insert into #id
select 1;
insert into #id
select 3;
with cte
as ( select id ,
childId ,
id root
from #mytable
where id in ( select id
from #id )
union all
select b.id ,
b.childId ,
cte.root
from #mytable b
inner join cte on b.id = cte.childId
)
select *
from cte;
WITH cte AS
(
SELECT id, childId, id AS anchorId
FROM mytable
WHERE
id IN (SELECT id FROM #id)
UNION ALL
SELECT b.id, b.childId, cte.anchorId
FROM mytable b
INNER JOIN cte
ON b.id = cte.childId
)
SELECT
id
, childId
, case
WHEN id = anchorId THEN NULL
ELSE anchorId
END as anchorId
FROM cte
With test code provided:
DECLARE #id TABLE (id int, childId int);
INSERT INTO #id SELECT 1, 2;
INSERT INTO #id SELECT 2, 4;
INSERT INTO #id SELECT 3, 5;
INSERT INTO #id SELECT 4, 6;
INSERT INTO #id SELECT 5, null;
INSERT INTO #id SELECT 6, null;
WITH cte AS
(
SELECT id, childId, id AS anchorId
FROM #id
WHERE id IN (1,3)
UNION ALL
SELECT b.id, b.childId, cte.anchorId
FROM #id b
INNER JOIN cte
ON b.id = cte.childId
)
SELECT
id
, childId
, CASE
WHEN id = anchorId THEN NULL
ELSE anchorId
END AS anchorId
FROM cte

Rows to single cell

I would like to get the desired output marked in green
the data points for each id get put into a single cell
Basically take all the events that have happened with A and attach it in the same order
Use Stuff Function:
DECLARE #tblTest AS Table(
ID INT,
EVENT VARCHAR(5)
)
INSERT INTO #tblTest VALUES
(1,'A'),
(1,'A'),
(1,'C'),
(2,'A'),
(2,'B'),
(2,'C')
SELECT DISTINCT
T1.ID,
STUFF
(
(SELECT '' + convert(varchar(10), T2.EVENT, 120)
FROM #tblTest T2
where T1.ID = T2.ID
FOR XML PATH (''))
, 1, 0, '') AS EVENT
FROM #tblTest T1
You can use FOR XML:
SELECT DISTINCT
ID,
(SELECT [EVENT] +''
FROM YourTable
WHERE ID = y.ID
FOR XML PATH('')
) as [EVENT]
FROM YourTable y
Output:
ID EVENT
1 AABCD
2 AABBCC
You can use UDF to do so as follows:
CREATE TABLE t(
id INT,
col CHAR(1)
);
INSERT INTO t VALUES (1,'a');
INSERT INTO t VALUES (1,'b');
INSERT INTO t VALUES (1,'c');
INSERT INTO t VALUES (1,'d');
INSERT INTO t VALUES (2,'e');
INSERT INTO t VALUES (2,'f');
INSERT INTO t VALUES (3,'g');
INSERT INTO t VALUES (4,'h');
The UDF (User defined function) -
USE [t]
GO
CREATE FUNCTION dbo.ConcatenateCols(#Id INT)
RETURNS VARCHAR(MAX)
AS
BEGIN
DECLARE #RtnStr VARCHAR(MAX)
SELECT #RtnStr = COALESCE(#RtnStr + '','') + col
FROM dbo.t
WHERE id = #Id AND col > ''
RETURN #RtnStr
END
GO
Finally the query and result:
SELECT id, dbo.ConcatenateCols(id) AS Cols -- UDF - ConcatenateCols(id)
FROM t GROUP BY Id
CREATE TABLE #temp(Id INt,Event Nvarchar(25))
INSERT INTO #temp
SELECT 1,
'A'
UNION ALL
SELECT 1,
'A'
UNION ALL
SELECT 1,
'B'
UNION ALL
SELECT 1,
'C'
UNION ALL
SELECT 1,
'D'
UNION ALL
SELECT 2,
'A'
UNION ALL
SELECT 2,
'A'
UNION ALL
SELECT 2,
'B'
UNION ALL
SELECT 2,
'B'
UNION ALL
SELECT 2,
'C'
UNION ALL
SELECT 2,
'C'
SELECT DISTINCT ID,
(SELECT [EVENT] +''
FROM #temp
WHERE ID = y.ID
FOR XML PATH('') ) AS [EVENT]
FROM #temp y

Insert Data with Foreign Keys

I have two tables connected with a foreign key constraint. Each table has a primary Key (ProductID ,BaseProductID ) that is an autoincrementing identity column.
Every BaseProduct has only 1 Product during the import.
BaseProduct
- BaseProductID
- BaseProductName
Product
- ProductID
- BaseProductID
- ProductName
I try to insert rows into both tables from a different Database that has the same Tables, but different seed Values. I dont want to keep the ids from the source database.
Is there a way to do this in one statement?
EDIT
The Select would be
Select ProductName FROM #SourceProduct
WHERE Not ProductName in (
select ProductName FROM #TargetBaseProduct
)
I tried the solution by Giorgi which is close, but the matching by
tgt.BaseProductID = src.BaseProductID
leads to result where source rows with BaseProductID= 1 are not inserted if the target already has an entry with BaseProductID = 1.
DECLARE #SourceBaseProduct TABLE
(
BaseProductID INT ,
BaseProductName NVARCHAR(MAX)
)
DECLARE #SourceProduct TABLE
(
ProductID INT ,
BaseProductID INT ,
ProductName NVARCHAR(MAX)
)
DECLARE #TargetBaseProduct TABLE
(
BaseProductID INT IDENTITY(1, 1) ,
BaseProductName NVARCHAR(MAX)
)
DECLARE #TargetProduct TABLE
(
ProductID INT IDENTITY(1, 1) ,
BaseProductID INT ,
ProductName NVARCHAR(MAX)
)
INSERT INTO #SourceBaseProduct
VALUES ( 1, 'BaseProduct1' ),
( 2, 'BaseProduct2' ),
( 4, 'BaseProduct3' )
INSERT INTO #SourceProduct
VALUES ( 3, 1, 'Product1' ),
( 9, 2, 'Product2' ),
( 27, 4, 'Product3' )
INSERT INTO #TargetBaseProduct
(
BaseProductName
)
VALUES ( 'Existing Product Base' )
INSERT INTO #TargetProduct(
BaseProductID ,
ProductName)
VALUES ( ##IDENTITY, 'Existing Product' )
MERGE INTO #TargetBaseProduct tgt
USING
( SELECT sbp.BaseProductID ,
sbp.BaseProductName ,
sp.ProductName
FROM #SourceBaseProduct sbp
JOIN #SourceProduct sp ON sp.BaseProductID = sbp.BaseProductID
) AS src
ON tgt.BaseProductID = src.BaseProductID
WHEN NOT MATCHED BY TARGET THEN
INSERT VALUES ( src.BaseProductName )
OUTPUT
Inserted.BaseProductID ,
src.ProductName
INTO #TargetProduct(BaseProductID, ProductName);
SELECT * FROM #TargetBaseProduct
SELECT * FROM #TargetProduct
Output
BaseProductID BaseProductName
1 Existing Product Base
2 BaseProduct2
3 BaseProduct3
ProductID BaseProductID ProductName
1 1 Existing Product
2 2 Product2
3 3 Product3
Expected result
BaseProductID BaseProductName
1 Existing Product Base
2 BaseProduct1
3 BaseProduct2
4 BaseProduct3
ProductID BaseProductID ProductName
1 1 Existing Product
2 2 Product1
3 3 Product2
4 4 Product3
EDIT
using
ON 1 = 0
like Giorgi suggested gave the right result
If I get you right, there is way with MERGE:
DECLARE #SourceBaseProduct TABLE
(
BaseProductID INT ,
BaseProductName NVARCHAR(MAX)
)
DECLARE #SourceProduct TABLE
(
ProductID INT ,
BaseProductID INT ,
ProductName NVARCHAR(MAX)
)
DECLARE #TargetBaseProduct TABLE
(
BaseProductID INT IDENTITY(1, 1) ,
BaseProductName NVARCHAR(MAX)
)
DECLARE #TargetProduct TABLE
(
ProductID INT IDENTITY(1, 1) ,
BaseProductID INT ,
ProductName NVARCHAR(MAX)
)
INSERT INTO #SourceBaseProduct
VALUES ( 1, 'BaseProduct1' ),
( 2, 'BaseProduct2' ),
( 4, 'BaseProduct3' )
INSERT INTO #SourceProduct
VALUES ( 3, 1, 'Product1' ),
( 9, 2, 'Product2' ),
( 27, 4, 'Product3' )
MERGE INTO #TargetBaseProduct tgt
USING
( SELECT sbp.BaseProductID ,
sbp.BaseProductName ,
sp.ProductName
FROM #SourceBaseProduct sbp
JOIN #SourceProduct sp ON sp.BaseProductID = sbp.BaseProductID
) AS src
ON tgt.BaseProductID = src.BaseProductID
WHEN NOT MATCHED BY TARGET THEN
INSERT VALUES ( src.BaseProductName )
OUTPUT
Inserted.BaseProductID ,
src.ProductName
INTO #TargetProduct(BaseProductID, ProductName);
SELECT * FROM #TargetBaseProduct
SELECT * FROM #TargetProduct
Output:
BaseProductID BaseProductName
1 BaseProduct1
2 BaseProduct2
3 BaseProduct3
ProductID BaseProductID ProductName
1 1 Product1
2 2 Product2
3 3 Product3
EDIT: if you want to insert existing rows too, then change
ON tgt.BaseProductID = src.BaseProductID
to
ON 1 = 0

How to apply 3 values for 1 row to 3 rows with each value?

I have a number of ROWS that have 1 to 6 values. For example:
Param1: A|B|C|D
Param2: B|Y
Param3: A
I need to transform it like this:
Param1: A
Param1: B
Param1: C
Param1: D
Param2: B
Param2: Y
Param3: A
Well, I guess a pivot-unpivot might work, but there are a lot of conditions and fields I need to get. Also, I have a View that divides all values and counts them. In the top example it will return dataset like this:
A 2
B 2
C 1
D 1
Y 1
Here is my own example, which is working alright on a few records and works very badly with more than 100000 rows.
Initial story is about this. I have some objects(obj), each has its params(prm), which have its values (val). So, as you see, each object is like a tree, which I need to expand.
Here is a simulation:
DECLARE #x TABLE
(
prm INT ,
iin VARCHAR(20) ,
oout VARCHAR(20)
)
INSERT INTO #x
VALUES ( 1, 'A/B/C', 'A' )
INSERT INTO #x
VALUES ( 1, 'A/B/C', 'B' )
INSERT INTO #x
VALUES ( 1, 'A/B/C', 'C' )
INSERT INTO #x
VALUES ( 3, 'D', 'D' )
INSERT INTO #x
VALUES ( 2, 'R/G', 'R' )
INSERT INTO #x
VALUES ( 2, 'R/G', 'G' )
DECLARE #y TABLE
(
obj INT ,
prm INT ,
val VARCHAR(20)
)
INSERT INTO #y
VALUES ( 10, 1, 'A/B/C' )
INSERT INTO #y
VALUES ( 10, 2, 'R/G' )
INSERT INTO #y
VALUES ( 10, 3, 'D' )
INSERT INTO #y
VALUES ( 20, 2, 'R/G' )
INSERT INTO #y
VALUES ( 20, 3, 'D' )
DECLARE #z TABLE
(
id INT ,
obj INT ,
prm INT ,
val VARCHAR(20)
)
INSERT INTO #z
VALUES ( 1, 10, 1, NULL )
INSERT INTO #z
VALUES ( 2, 10, 1, NULL )
INSERT INTO #z
VALUES ( 3, 10, 1, NULL )
INSERT INTO #z
VALUES ( 4, 10, 2, NULL )
INSERT INTO #z
VALUES ( 5, 10, 2, NULL )
INSERT INTO #z
VALUES ( 6, 10, 3, NULL )
INSERT INTO #z
VALUES ( 7, 20, 2, NULL )
INSERT INTO #z
VALUES ( 8, 20, 2, NULL )
INSERT INTO #z
VALUES ( 9, 20, 3, NULL )
And decision:
;
WITH a AS ( SELECT ROW_NUMBER() OVER ( PARTITION BY prm ORDER BY prm ) n ,
*
FROM #x
),
b AS ( SELECT ROW_NUMBER() OVER ( PARTITION BY obj, prm ORDER BY obj, prm ) n ,
*
FROM #z
)
UPDATE b
SET b.val = a.oout
FROM b
INNER JOIN #y y ON y.obj = b.obj
AND y.prm = b.prm
INNER JOIN a ON a.n = b.n
AND a.prm = b.prm
AND y.val = a.iin
SELECT *
FROM #z
#y table - is a table with arguments like the first example, where Param1,Param2 is 1,2 ets on column prm, concerning some object in obj
#z table - is simulation with val set to null, which represents, what params should be filled with values
#x table - is a simulation of dividing of values, that should be applied to #y table, replacing the null values of the #z table with actual ranked values.
Is there a better way to do this?
Well I'll not give you a full solution, but if I need split data like this, I'd try to use sqlxml (you have to try it on large number of rows to check if performance ok for you):
declare #x table (prm int,iin varchar(20))
insert into #x values(1, 'A/B/C')
insert into #x values(3, 'D')
insert into #x values(2, 'R/G')
select
x.prm, x.iin, T.C.value('.', 'nvarchar(max)') as oout
from #x as x
outer apply (
select cast('<d>' + replace(x.iin, '/', '</d><d>') + '</d>' as xml) as Data
) as D
outer apply D.Data.nodes('d') as T(C)
see sql fiddle demo to try it.