Get more than 1 result set for recursive CTE? - sql-server

I have a simple table which has leafs and sub leafs info. ( like a forum questions)
A main message is defined where childId and ParentID are the same
So here we see 2 main questions and their answers.
I've also managed to calc the depth of each element :
In short this is the main query :
WITH CTE AS
(
SELECT childID
,parentID,
0 AS depth,name
FROM #myTable
WHERE childID = parentID AND childID=1 -- problem line
UNION ALL
SELECT TBL.childID
,TBL.parentID,
CTE.depth + 1 , TBL.name
FROM #myTable AS TBL
INNER JOIN CTE ON TBL.parentID = CTE.childID
WHERE TBL.childID<>TBL.parentID
)
SELECT childID,parentID,REPLICATE('----', depth) + name
But the problem is Line #8 (commented).
I currently ask "give me all the cluster for question id #1"
So where is the problem ?
I want to have multiple result set , for each question !
so here i need to have 2 result sets :
one for childId=parentId=1
and one for
one for childId=parentId=6
full working sql online
(and I dont want to use cursor)

You can build your queries dynamically.
DECLARE #SQL NVARCHAR(MAX) =
(SELECT '
WITH CTE AS
(
SELECT childID
,parentID,
0 AS depth,name
FROM myTable
WHERE childID = parentID AND childID = '+CAST(childID AS NVARCHAR(10))+'
UNION ALL
SELECT TBL.childID
,TBL.parentID,
CTE.depth + 1 , TBL.name
FROM myTable AS TBL
INNER JOIN CTE ON TBL.parentID = CTE.childID
WHERE TBL.childID<>TBL.parentID
)
SELECT childID,parentID,REPLICATE(''----'', depth) + name
FROM CTE
ORDER BY
childID;'
FROM myTable
WHERE childID = parentID
FOR XML PATH(''), TYPE).value('text()[1]', 'NVARCHAR(MAX)');
EXEC sp_executesql #SQL;
Update:
As suggested by Bogdan Sahlean we can minimize compilations by making the actual query parameterized.
DECLARE #SQL1 NVARCHAR(MAX) =
'WITH CTE AS
(
SELECT childID
,parentID,
0 AS depth,name
FROM myTable
WHERE childID = parentID AND childID = #childID
UNION ALL
SELECT TBL.childID
,TBL.parentID,
CTE.depth + 1 , TBL.name
FROM myTable AS TBL
INNER JOIN CTE ON TBL.parentID = CTE.childID
WHERE TBL.childID<>TBL.parentID
)
SELECT childID,parentID,REPLICATE(''----'', depth) + name
FROM CTE
ORDER BY
childID;'
DECLARE #SQL2 NVARCHAR(MAX) =
(SELECT 'exec sp_executesql #SQL, N''#childID int'', '+CAST(childID AS NVARCHAR(10))+';'
FROM myTable
WHERE childID = parentID
FOR XML PATH(''), TYPE).value('text()[1]', 'NVARCHAR(MAX)');
EXEC sp_executesql #SQL2, N'#SQL NVARCHAR(MAX)', #SQL1;

To present multiple result sets to your client, you're going to have to use a cursor or a while loop to perform independent SELECT operations. You can't do that from a CTE, since a CTE can only be used by exactly one subsequent query.
Now, the source of the problem has nothing to do with cursors really, but the fact that you're using an HTML repeater. Why do you need to use an HTML repeater for this? A simple DataReader can loop through all of the results from the CTE's single set, and make conditional formatting decisions based on the loop and determining when the root ID changes. So I suggest you look into solving the presentation problem a different way, rather than trying to coerce SQL Server to accommodate your presentation implementation.

I am not sure what you mean by "returning two result sets". Could you just have one result set, with the root question being assigned into another column? The following tweak on your query does this:
WITH CTE AS (
SELECT ChildId as WhichQuestion, childID, parentID, 0 AS depth, name
FROM #myTable
WHERE childID = parentID
UNION ALL
SELECT cte.WhichQuestion, TBL.childID, TBL.parentID, CTE.depth + 1 , TBL.name
FROM #myTable AS TBL
INNER JOIN CTE ON TBL.parentID = CTE.childID
WHERE TBL.childID <> TBL.parentID
)
SELECT WhichQuestion, childID, parentID, REPLICATE('----', depth) + name
FROM CTE
ORDER BY WhichQuestion, childID;

You can save in anchor part childId of root question and then access all the branch by needed Id:
WITH CTE AS (
SELECT childID as RootId
, childID
, parentID
, 0 AS depth,name
FROM #myTable
WHERE childID = parentID
UNION ALL
SELECT CTE.RootId
, TBL.childID
, TBL.parentID
, CTE.depth + 1 , TBL.name
FROM #myTable AS TBL
INNER JOIN CTE ON TBL.parentID = CTE.childID
WHERE TBL.childID<>TBL.parentID
)

Related

How to find all objects of type Item with thier parent type Family

Here is my Objects table -
I am trying to write a query that can fetch all objects of type C with their parent of type A. So the query should return like
I am trying to do it using recursion but not getting the desired result. Any help would be appreciated. Thank you.
This gets you what you are after, but if the logic is right for your requirements is a different question:
DECLARE #ObjectID int = 3;
DECLARE #EndType char(1) = 'A';
WITH VTE AS(
SELECT *
FROM (VALUES(1,'A',NULL),
(2,'B',1),
(3,'C',2))V(ObjectID, ObjectType, ParentID)),
rCTE AS(
SELECT V.ObjectID,
V.ObjectType,
V.ParentID,
V.ObjectID AS StartID,
V.ObjectType AS StartType
FROM VTE V
WHERE v.ObjectID = #ObjectID
UNION ALL
SELECT V.ObjectID,
V.ObjectType,
V.ParentID,
r.StartID,
r.StartType
FROM rCTE r
JOIN VTE V ON V.ObjectID = r.ParentID)
SELECT r.StartID AS ObjectID,
r.StartType AS ObjectType,
r.ObjectID AS ParentObjectID,
r.ObjectType AS PArentObjectType
FROM rCTe r
WHERE r.ObjectType = #EndType;
Sample data
DECLARE #Temp AS TABLE (ObjectId INT,ObjectType VARCHAR(2),ParentObjectId INT)
INSERT INTO #Temp
SELECT 1,'A',NUll UNION ALL
SELECT 2,'B',1 UNION ALL
SELECT 3,'C',NULL UNION ALL
SELECT 4,'D',3 UNION ALL
SELECT 5,'E',4 UNION ALL
SELECT 6,'B',3
Sql server Script
;WITH CTE
AS
(
SELECT ObjectId,
ObjectType,
ParentObjectId,
CAST('\'+ CAST(ObjectId AS VARCHAR(MAX))AS VARCHAR(MAX)) AS [ObjectIdHierarchy] ,
CAST('\'+ ObjectType AS VARCHAR(MAX)) AS [Hierarchy]
FROM #Temp
WHERE ParentObjectId IS NULL
UNION ALL
SELECT t.ObjectId,
t.ObjectType,
t.ParentObjectId,
[ObjectIdHierarchy]+'\'+ CAST(t.ObjectId AS VARCHAR(MAX)) AS [ObjectIdHierarchy],
[Hierarchy]+'\'+ t.ObjectType AS [Hierarchy]
FROM CTE c
INNER JOIN #Temp t
ON t.ParentObjectId = c.ObjectId
)
SELECT ObjectId,
ObjectType,
LEFT(RIGHT([ObjectIdHierarchy],LEN([ObjectIdHierarchy])-1),1) AS ParentObjectId,
LEFT(RIGHT([Hierarchy],LEN([Hierarchy])-1),1) AS ParentChildHierarchy
FROM CTE
WHERE ObjectId =1
Result
ObjectId ObjectType ParentObjectId ParentChildHierarchy
------------------------------------------------------------
1 A 1 A

T-SQL Merge from multiple source records

I've seen several similar questions but haven't found one that answers my question.
I have a source table with many individual notes for a company
CompanyName, Notes3
Company1, "spoke with someone"
Company2, "Email no longer works"
Company1, "Moved address"
I have a destination table (vchCompanyName is unique here)
vchCompanyName, vchNotes
Company1, "started business in 2005"
Company2, null
I want to end up with
vchCompanyName, vchNotes
Company1, "started business in 2005
spoke with someone
Moved address"
Company2, "Email no longer works"
I have tried this code
WITH CTE
AS (SELECT ROW_NUMBER() OVER (PARTITION BY CompanyName, Notes3 ORDER BY CompanyName) RowNum, *
FROM CompanyContact
)
merge dCompany as target
using CTE as source
on target.vchcompanyname = source.companyname
when matched and len(source.notes3)>0 and source.RowNum = 1
then
update set target.vchnote = vchnote + CHAR(13) + source.Notes3
But get the error
The MERGE statement attempted to UPDATE or DELETE the same row more than once. This happens when a target row matches more than one source row. A MERGE statement cannot UPDATE/DELETE the same row of the target table multiple times. Refine the ON clause to ensure a target row matches at most one source row, or use the GROUP BY clause to group the source rows.
Which is accurate.
I have also tried STRING_AGG but get an undefined UDF error.
How do I change my code to run iteratively?
--EDIT--
I had tried the following update code
WITH CTE
AS (SELECT ROW_NUMBER() OVER (PARTITION BY CompanyName, Notes3 ORDER BY CompanyName) RowNum, *
FROM CompanyContact
)
UPDATE dCompany SET vchNote = vchNote +
(select CHAR(13) + cc.Notes3 from CompanyContact cc
inner JOIN dCompany dc ON dc.vchCompanyName COLLATE database_default = LEFT(cc.CompanyName,50) COLLATE database_default
inner join CTE on dc.vchCompanyName COLLATE database_default = LEFT(CTE.CompanyName,50) COLLATE database_default
WHERE LEN(cc.Notes3)>0
and RowNum = 1
);
But get the error
Subquery returned more than 1 value. This is not permitted when the subquery follows =, !=, <, <= , >, >= or when the subquery is used as an expression.
#Chris Crawshaw, I will approach this by doing a 'union all' on the source and destination table to pick up all the notes for each company. Then using the STUFF function, it is easy to concatenate all the notes into one cell, while grouping by the induvidual company names. See the mockup below:
DECLARE #Source TABLE (CompanyName VARCHAR(20), Notes3 VARCHAR(50))
INSERT INTO #Source
SELECT 'Company1', 'spoke with someone' UNION ALL
SELECT 'Company2', 'Email no longer works' UNION ALL
SELECT 'Company1', 'Moved address'
DECLARE #Destination TABLE (vchCompanyName VARCHAR(20), vchNotes VARCHAR(500))
INSERT INTO #Destination
SELECT 'Company1', 'started business in 2005' UNION ALL
SELECT 'Company2', NULL
;WITH Temp AS (
SELECT *
FROM
(
SELECT *
FROM
#Destination D
WHERE D.vchNotes is not null
UNION ALL
SELECT *
FROM
#Source S
)h
)
update D
SET D.vchNotes=U.vchNotes
FROM #Destination D
LEFT JOIN(
SELECT t2.vchCompanyName, vchNotes=STUFF((
SELECT ',' + vchNotes
FROM Temp t1 where t1.vchCompanyName=t2.vchCompanyName
FOR XML PATH(''), TYPE).value('.', 'NVARCHAR(MAX)'), 1, 1, '')
FROM
#Destination t2
GROUP BY
t2.vchCompanyName
)U ON
U.vchCompanyName=D.vchCompanyName
--TEST--
SELECT *
FROM
#Destination

Stored Procedure Syntax with CTE

This is probably trivial but I am just learning about CTE (thanks to help here).
I have a procedure that is used to determine totals.
The first part is the totals are the sum of a position at their level an below. So I needed a way to retrieve records that (1) determined the level of the record (hierarchy) and (2) returned all records at and below. That was asked and answered here.
Now want to take the CTE table from the answer above and use it in the second part of my procedure (get the totals)
CREATE PROCEDURE [dbo].[GetProgramTotals]
#programId nvarchar(10) = null,
#owner int = null,
#totalAmount money OUT,
#usedAmount money OUT,
#remainingAmount money OUT
AS
BEGIN
WITH rCTE AS
(
SELECT
*, 0 AS Level
FROM Forecasting.dbo.Addressbook
WHERE Addressbook = #owner
UNION ALL
SELECT
t.*, r.Level + 1 AS Level
FROM Addressbook t
INNER JOIN rCTE r ON t.ParentAddressbook = r.Addressbook
)
Select #totalAmount = (Select Sum(Amount) from dbo.Budget where
(#programId IS NULL or (ProgramId = #programId)) and (#owner IS NULL or (BudgetOwner in (SELECT Addressbook from rCTE))))
Select #usedAmount = (Select Sum(SubTotal) from dbo.OrderLine where
(#programId IS NULL or (ProgramId = #programId) and (#owner IS NULL) or (Budget in (SELECT Addressbook from rCTE))))
if (#totalAmount is null)
set #totalAmount = 0
if (#usedAmount is null)
set #usedAmount = 0
Set #remainingAmount = (#totalAmount - #usedAmount)
END
The idea of this procedure is the dynamically calculate an individual (or all) programs based an a users position in a hierarchy.
So a regional managers totals would be the sum of all districts and district reps.
UPDATE: I updated this based on squillman (thank you) comment below.
Now I have a different problem. When I execute the proc - I get 'Invalid object name rCTE'.
You can't use SET in the middle of a query like that. Change it to a SELECT and it should remedy your syntax error.
CREATE PROCEDURE [dbo].[GetProgramTotals]
#programId nvarchar(10) = null,
#owner int = null,
#totalAmount money OUT,
#usedAmount money OUT,
#remainingAmount money OUT
AS
BEGIN
WITH rCTE AS(
SELECT *, 0 AS Level FROM Forecasting.dbo.Addressbook WHERE Addressbook = #owner
UNION ALL
SELECT t.*, r.Level + 1 AS Level
FROM Addressbook t
INNER JOIN rCTE r ON t.ParentAddressbook = r.Addressbook)
SELECT #totalAmount = (Select Sum(Amount) from dbo.Budget where
(#programId IS NULL or (ProgramId = #programId)) and (#owner IS NULL or (BudgetOwner in (SELECT Addressbook from rCTE))))
, #usedAmount = (Select Sum(SubTotal) from dbo.OrderLine where
(#programId IS NULL or (ProgramId = #programId) and (#owner IS NULL) or (Budget in (SELECT Addressbook from rCTE))))
if (#totalAmount is null)
set #totalAmount = 0
if (#usedAmount is null)
set #usedAmount = 0
Set #remainingAmount = (#totalAmount - #usedAmount)
END
CTE's can be a bit confusing at first, but they are really quite simple once they make sense. For me it clicked when I began thinking of them as just another temp table syntax (pro-tip: they're not in reality, just conceptually). So basically:
Create one or more "temp tables". These are your CTE expressions, and there can be more than one.
Perform a standard operation using one or more of the CTE expressions in the statement immediately following your CTE(s).
As Martin mentioned in comments below, the CTE(s) are only scoped for the next immediate statement and fall out of scope after that.
So,
;WITH cte1 AS
(
SELECT Col1 FROM Table1
),
cte2 AS
(
SELECT Col1 FROM Table2
)
SELECT Col1 FROM cte1 //In scope here
UNION
SELECT Col1 FROM cte1; //Still in scope since we're still in the first statement
SELECT Col1 FROM cte1; //Fails. cte1 is now out of scope (as is cte2)
In your case you're using the recursive CTE to form a parent/child hierarchy and then setting variables based on the results. Your CTE syntax is pretty close after the edit, you just need the comma to bring things back together into one statement.
//Variable assignment example
;WITH cte1 AS
(
SELECT Col1 FROM Table1
),
cte2 AS
(
SELECT Col1 FROM Table2
)
SELECT #var1 = (SELECT TOP 1 Col1 FROM cte1)
,#var2 = (SELECT TOP 1 Col1 FROM cte2) //You're missing the comma at the start of this line
Change Select #usedAmount=... to , #usedAmount=...

Tsql query Case statment

I'm trying to write a query (Microsoft SQL SERVER) the looks like :
select productid from T1
union
select productid from T2
union
select ProductID from T3
The issue is that I want that one of that union will be depend of a value(lets say #x)
So, I write this query :
select case when #x=1 then productid end
from T1
union
select productid from T2
union
select ProductID from T3
But its still scan all T1.
I know that "Case" scan the table even if its not match the condition:
select case when 1=2 then productid end from T1
How can I write a query that base on a value will now if execute or not ??
Something like : case #x=1 then select productid from T1 end....
* I need to find a way without dynamic sql
Thanks
use this:
Declare #SQL nvarchar(max)
Set #SQL = N' select productid From T1 '
Set #SQl = #SQL + N' UNION select productid From T2'
if(#x=1)
Set #SQl = #SQL + N' UNION select productid From T3'
exec (#SQL)
Thank you very much for your help,
I manage to do it as Pieter suggest, with a where clause . The query look like
select productid from t1 union select productid from t2
union select productid from t3 inner join t4 on t3.id=t4.t3id where t3.value=#x
Although I didn't want to use t4 table (because #x is one per all t3)
Thank you all.

Reversing the sort order of a cte

In Microsoft SQL Server, the following works, but produces:
,Son,Dad,Granddad,Great Granddad
whereas I need it to say:
Great Granddad,Granddad,Dad,Son
declare #Family Table(
ID Int Identity(100,1) Primary Key
,Person varchar(128)
,ParentID Int default 0
)
insert into #Family(Person,ParentID) values('Great Granddad',0)
insert into #Family(Person,ParentID) values('Granddad',100)
insert into #Family(Person,ParentID) values('Dad',101)
insert into #Family(Person,ParentID) values('Son',102)
DECLARE #ID Int = 103
;with cte1 as (
select
#ID AS cteID
,ID
,ParentID
,Person as ctePerson
from #Family
where ID = #ID -- this is the starting point you want in your recursion
UNION ALL
select #ID, F.ID, F.ParentID, F.Person
from #Family F
join cte1 P on P.ParentID = F.ID -- this is the recursion
)
-- cte2 can reverse the sort order based on something built in (OVER?)
-- ROW_NUMBER() OVER(ORDER BY ? DESC) AS Row
,cte3 AS(
select ID as cte3ID,(
SELECT ',' + ctePerson
FROM cte1
WHERE cteID = F.ID
FOR XML PATH ('')
) as People
from #Family F
where ID=#ID
)
SELECT * FROM CTE3
I would not order the result of a recursive CTE by using another CTE, as the results of CTEs are semantically tables, and therfore the order is not guaranteed. Instead order when selecting from a CTE, just als like with normal tables.
I would suggest to insert a field representing the level or relationship and order by that:
;with cte1 as (
select
#ID AS cteID
,ID
,ParentID
,Person as ctePerson
,0 lvl -- starting level with 0
from #Family
where ID = #ID -- this is the starting point you want in your recursion
UNION ALL
select #ID, F.ID, F.ParentID, F.Person
, lvl + 1 -- increase level by 1
from #Family F
join cte1 P on P.ParentID = F.ID -- this is the recursion
)
,cte3 AS(
select ID as cte3ID,STUFF(( -- stuff removes the first ','
SELECT ',' + ctePerson
FROM cte1
WHERE cteID = F.ID
ORDER by lvl DESC -- order by level DESC to start with latest ancestor
FOR XML PATH ('')
), 1, 1, '') as People
from #Family F
where ID=#ID
)
SELECT * FROM CTE3

Resources