How to Find Circular References in CTE between Parent/Child tables - sql-server

I have a CTE to show dependency tree over 2 tables (a parent and a child table).
There is a data problem resulting in a circular dependency, resulting in a Max Recursion level error being thrown.
i.e.
Table: Parent
Id
ItemId
Table: Child
Id
ParentId
ItemId
Example Circular Ref data
Table: Parent
Id ItemId
1 A
2 B
Table: Child
Id ParentId ItemId
1 1 B
2 2 A
There are thousands of rows in these tables. How can I write a query identify the offending reference? Or is there a way to set the Max Recursion level that will then just stop the CTE once hit instead of throw an error...then I could view the results and identify the problem child.
WITH Recursive_CTE AS
(
SELECT
ItemId,
CAST(ItemDescription AS varchar(100)) AS ItemDescription,
Qty,
CAST(ParentItemId AS SmallInt) AS ParentItemId,
CAST(ItemId AS varchar(100)) AS ParentGroupItemId,
CAST(' -' AS varchar(100)) AS LVL,
CAST(ItemId AS varchar(100)) AS HierarchyItem,
CAST(SKU AS varchar(100)) AS HierarchySKU,
CAST(ItemDescription AS varchar(100)) AS HierarchyName,
0 AS RecursionLevel
FROM dbo.vw_BOM AS child
WHERE (ParentItemId = 0)
--and ItemId = #BOMHeaderItemId
UNION ALL
SELECT
child.ItemId,
CAST(parent.LVL + child.ItemDescription AS varchar(100)) AS ItemDescription,
child.Qty,
CAST(child.ParentItemId AS SmallInt) AS ParentItemId,
parent.ParentGroupItemId,
CAST(' -' + parent.LVL AS varchar(100)) AS LVL,
CAST(parent.HierarchyItem + ':' + CAST(child.ItemId AS varchar(100)) AS varchar(100)) AS HierarchyItem,
CAST(parent.HierarchySKU + ':' + CAST(child.SKU AS varchar(100)) AS varchar(100)) AS HierarchySKU,
CAST(parent.HierarchyName + '/' + CAST(child.ItemDescription AS varchar(100)) AS varchar(100)) AS HierarchyName,
parent.RecursionLevel + 1 AS RecursionLevel
FROM Recursive_CTE AS parent INNER JOIN
dbo.vw_BOM AS child ON child.ParentItemId = parent.ItemId
)
SELECT
Recursive_CTE_1.RecursionLevel,
Recursive_CTE_1.ParentGroupItemId,
Recursive_CTE_1.ParentItemId,
Recursive_CTE_1.ItemId,
Recursive_CTE_1.Qty,
DATALENGTH(Recursive_CTE_1.LVL) AS LVLLength,
Recursive_CTE_1.ItemDescription,
item.SKU,
item.OnHandQty,
item.AllocQty,
item.AvailableQty,
item.ToBeReceivedQty,
item.AvailableWFutureQty,
Recursive_CTE_1.HierarchyItem,
Recursive_CTE_1.HierarchySKU,
Recursive_CTE_1.HierarchyName
FROM Recursive_CTE AS Recursive_CTE_1 INNER JOIN
dbo.vw_ItemInventorySummary AS item ON Recursive_CTE_1.ItemId = item.Id
ORDER BY Recursive_CTE_1.HierarchySKU
option (maxrecursion 200)
The View vw_BOM
SELECT dbo.BillOfMaterialHeader.Id AS Id, dbo.BillOfMaterialHeader.ItemId AS ItemId, 0 AS ParentItemId, FGItems.SKU AS SKU, FGItems.SKU + N': ' + FGItems.ShortDescription AS ItemDescription,
dbo.BillOfMaterialHeader.Quantity AS Qty
FROM dbo.BillOfMaterialHeader INNER JOIN
dbo.Items AS FGItems ON dbo.BillOfMaterialHeader.ItemId = FGItems.Id
UNION ALL
SELECT dbo.BillOfMaterialDetail.Id AS Id, dbo.BillOfMaterialDetail.ItemId AS ItemId, BOMHdr.ItemId AS ParentItemId, RMItems.SKU AS SKU, RMItems.SKU + N': ' + RMItems.ShortDescription AS ItemDescription,
dbo.BillOfMaterialDetail.Quantity AS Qty
FROM dbo.Items AS RMItems INNER JOIN
dbo.BillOfMaterialDetail ON RMItems.Id = dbo.BillOfMaterialDetail.ItemId INNER JOIN
dbo.BillOfMaterialHeader BOMHdr ON dbo.BillOfMaterialDetail.BillOfMaterialHeaderId = BOMHdr.Id
UPDATE
Tab's answer pointed me in the right direction. I used the flattened Parent Child table in vw_BOM and then joined it to itself per Tab's answer, which showed me where 6 Items had the same Item Id in the Parent Table and the Child Table.
Like so:
SELECT dbo.vw_BOM.SKU AS ParentSKU, vw_BOM_1.SKU AS ChildSKU
FROM dbo.vw_BOM INNER JOIN
dbo.vw_BOM AS vw_BOM_1 ON dbo.vw_BOM.ItemId = vw_BOM_1.ParentItemId AND dbo.vw_BOM.ParentItemId = vw_BOM_1.ItemId

Your CTE already has a hierarchy with the ItemID path concatenated. How about using that to determine if the item has already been seen?
Add a new column to the anchor portion of your CTE, HasCycle = Convert(bit, 0).
Then in the recursive portion of your CTE, add the column and a condition in the WHERE clause like so:
...
UNION ALL
SELECT
... other columns,
HasCycle = Convert(bit,
CASE
WHEN ':' + parent.HierarchyItem + ':' LIKE
'%:' + Convert(varchar(100), child.ItemID) + ':%'
THEN 1
ELSE 0
END)
FROM
...
WHERE
...
AND parent.HasCycle = 0 --terminate after cycle is found
;
Then you can select from the recursive CTE WHERE HasCycle = 1 and see all the rows that begin a cycle and their exact path upwards in the HierarchyItem.

Simple self-join should do it:
SELECT * FROM MyTable t1
INNER JOIN MyTable t2
ON t1.Parent=t2.Child
AND t1.Child=t2.Parent

I have seen these issues before and have resorted to adding items one level at a time, ignoring those seen before.

Related

How can I refer to a LAG() function column in SQL Server?

I have a query in which I use LAG function :
WITH Tr AS
(
SELECT
DocDtls.Warehouse, Transactions.Code, DocDtls.zDate,
Transactions.ID, Transactions.QtyIn, Transactions.QtyOut,
Transactions.BalanceAfter
FROM
DocDtls
INNER JOIN
Transactions ON DocDtls.[PrimDocNum] = Transactions.[DocNum]
)
SELECT
ID, Code, QtyIn, QtyOut, BalanceAfter,
LAG(BalanceAfter, 1, 0) OVER (PARTITION BY Warehouse, Code
ORDER BY Code, ID) Prev_BlncAfter
FROM
Tr;
It's working fine but when I try to add this column before FROM:
SUM(Prev_BlncAfter + QtyIn) - QtyOut AS NewBlncAfter
I get this error :
Msg 207, Level 16, State 1, Line 3
Invalid column name 'Prev_BlncAfter'
How can I fix this ? Thanks
You can create the LAG column inside the CTE instead of in the outer query. E.g.
declare #DocDtls table (Warehouse int, zDate date, [PrimDocNum] int);
declare #Transactions table (code int, id int, QtyIn int, QtyOut int, balanceafter int, [DocNum] int)
;with Tr As
(
SELECT
d.Warehouse
, t.Code
, d.zDate
, t.ID
, t.QtyIn
, t.QtyOut
, t.BalanceAfter
,LAG(BalanceAfter,1,0) Over (partition by Warehouse,Code order by Code,ID) Prev_BlncAfter
FROM #DocDtls d
INNER JOIN #Transactions t ON d.[PrimDocNum] = t.[DocNum]
)
select ID,Code,QtyIn,QtyOut,BalanceAfter
,SUM(Prev_BlncAfter + QtyIn)-QtyOut As NewBlncAfter
from Tr
group by ID,Code,QtyIn,QtyOut,BalanceAfter;
You can nest this query to refer the newly added column from the outer scope, or create another with like you've done before for referencing it afterwards:
with Tr As (
SELECT
DocDtls.Warehouse,
Transactions.Code,
DocDtls.zDate,
Transactions.ID,
Transactions.QtyIn,
Transactions.QtyOut,
Transactions.BalanceAfter
FROM
DocDtls
INNER JOIN Transactions ON DocDtls.[PrimDocNum] = Transactions.[DocNum]
),
formatted_tr as (
select
ID,
Code,
QtyIn,
QtyOut,
BalanceAfter,
LAG(BalanceAfter, 1, 0) Over (
partition by Warehouse,
Code
order by
Code,
ID
) Prev_BlncAfter
from
Tr
)
select
SUM(Prev_BlncAfter + QtyIn) - QtyOut As NewBlncAfter
from
formatted_tr
group by
ID, QtyOut
;
Based on comments , I combined the two answers to get what I need :
with Tr As (
SELECT
DocDtls.Warehouse,
Transactions.Code,
DocDtls.zDate,
Transactions.ID,
Transactions.QtyIn,
Transactions.QtyOut,
Transactions.BalanceAfter
FROM
DocDtls
INNER JOIN Transactions ON DocDtls.[PrimDocNum] = Transactions.[DocNum]
),
formatted_tr as (
select
ID,
Code,
QtyIn,
QtyOut,
BalanceAfter,
LAG(BalanceAfter, 1, 0) Over (
partition by Warehouse,
Code
order by
Code,zDate,ID
) Prev_BlncAfter
from
Tr
)
select ID,Code,QtyIn,QtyOut,BalanceAfter
,SUM(Prev_BlncAfter + QtyIn)-QtyOut As NewBlncAfter
from formatted_tr
group by ID,Code,QtyIn,QtyOut,BalanceAfter;
;

Optimizing query with huge amount of data

How can I optimize the query. I looked at the execution plan and created all the index. Every table has huge data. And this query execution time is very large. By looking at the query could you please suggest where can I optimize more.
If I give little background of the query the structure like:
There are many companies
Each company can have multiple managers
Data is in pagination format
Filter on #parent_manager so another temp table created parent_manager_filter just to use for the filtering purpose as #parent_manager has name in "," separated format
CREATE TABLE #parent_manager
(
cid NUMERIC(18) PRIMARY KEY,
name NVARCHAR(MAX),
code NVARCHAR(MAX)
);
CREATE INDEX cte_parent_manager ON #parent_manager(cid);
CREATE TABLE #parent_manager_filter
(
cid NUMERIC(18),
name NVARCHAR(1000),
code NVARCHAR(1000)
);
CREATE INDEX cte_parent_manager_filter_idx ON #parent_manager_filter(cid);
INSERT INTO #parent_manager
SELECT DISTINCT
mgrc.cid,
name = CAST (STUFF ((SELECT ', ' + CAST(c.company_name AS varchar(2000))
FROM manager_company mc
INNER JOIN company c ON (mc.mgr_cid = c.cid )
WHERE mc.cid = mgrc.cid
AND c.company_name IS NOT NULL
FOR XML PATH ('')), 1, 1, '') AS VARCHAR(2000)),
code = CAST (STUFF ((SELECT ', ' + CAST(c.code AS varchar(2000))
FROM manager_company mc
INNER JOIN company c ON (mc.mgr_cid = c.cid )
WHERE mc.cid = mgrc.cid
AND c.company_name IS NOT NULL
FOR XML PATH ('')), 1, 1, '') AS VARCHAR(2000))
FROM
manager_company mgrc
INNER JOIN
company c ON (mgrc.mgr_cid = c.cid )
JOIN
handler h ON (c.handlerId = h.handlerid )
WHERE
h.handlerid = 5800657002370
INSERT INTO #parent_manager_filter
SELECT DISTINCT
mc.cid,
c.company_name as name,
c.code as code
FROM
manager_company mc
INNER JOIN
company c ON (mc.mgr_cid = c.cid )
JOIN
handler h ON (h.handlerid = c.handlerid)
WHERE
h.handlerid = 5800657002370 ;
WITH company AS
(
SELECT DISTINCT
c.cid AS cid,
parentManager.name AS MANAGER_NAME,
parentManager.code AS code
FROM
company c
LEFT JOIN
#parent_manager parentManager ON (parentManager.cid = c.cid)
LEFT JOIN
# parent_manager_filter parentManagerFilter ON (parentManagerFilter.cid = c.cid)
WHERE
parentManagerFilter.name IN (:managerList)
),
total_rows AS
(
SELECT
COUNT(*) OVER () AS TOTALCOUNT,
ROW_NUMBER() OVER (ORDER BY company_name ASC) AS rnum,
grid.*
FROM
company grid
)
SELECT *
FROM total_rows rnum
WHERE rnum >= 1
AND rnum <= 10
DROP TABLE #parent_manager;
DROP TABLE #parent_manager_filter;
If you are building up temp tables then I would make sure you don't miss a clustered index, else your temp table is simply a heap. You don't have one covering the filter table.
INSERT INTO #parent_manager_filter ...
CREATE CLUSTERED INDEX cte_parent_manager_filter On #parent_manager_filter(cid);

Unable concate NULL value in SQL using CONCAT, COALESCE and ISNULL

I have a query with multiple joins where I want to combine records from two columns into one. If one column is empty then I want to show one column value as result. I tried with CONCAT, COALEASE and ISNULL but no luck. What am I missing here?
My objective is, create one column which has combination of s.Script AS Original and FromAnotherTable from query. Below query runs but throws Invalid column name 'Original' and Invalid column name 'FromAnotherTable'. when I try to use CONCAT, COALEASE or ISNULL .
SQL Query:
SELECT DISTINCT
c.Name AS CallCenter,
LTRIM(RTRIM(s.Name)) Name,
d.DNIS,
s.ScriptId,
s.Script AS Original,
(
SELECT TOP 5 CCSL.Line+'; '
FROM CallCenterScriptLine CCSL
WHERE CCSL.ScriptId = s.ScriptId
ORDER BY ScriptLineId FOR XML PATH('')
) AS FromAnotherTable,
--CONCAT(s.Script, SELECT TOP 5 CCSL.Line+'; ' FROM dbo.CallCenterScriptLine ccsl WHERE ccsl.ScriptId = s.ScriptId ORDER BY ccsl.ScriptLineId xml path(''))
--CONCAT(Original, FromAnotherTable) AS Option1,
--COALESCE(Original, '') + FromAnotherTable AS Option2,
--ISNULL(Original, '') + FromAnotherTable AS Option3,,
r.UnitName AS Store,
r.UnitNumber
FROM CallCenterScript s WITH (NOLOCK)
INNER JOIN CallCenterDNIS d WITH (NOLOCK) ON d.ScriptId = s.ScriptId
INNER JOIN CallCenter c WITH (NOLOCK) ON c.Id = s.CallCenterId
INNER JOIN CallCenterDNISRestaurant ccd WITH (NOLOCK) ON ccd.CallCenterDNISId = d.CallCenterDNISId
INNER JOIN dbo.Restaurant r WITH (NOLOCK) ON r.RestaurantID = ccd.CallCenterRestaurantId
WHERE c.Id = 5
AND (1 = 1)
AND (s.IsDeleted = 0 OR s.IsDeleted IS NULL)
ORDER BY DNIS ASC;
Output:
This works:
DECLARE #Column1 VARCHAR(50) = 'Foo',
#Column2 VARCHAR(50) = NULL;
SELECT CONCAT(#Column1,#Column2);
SELECT COALESCE(#Column2, '') + #Column1
SELECT ISNULL(#Column2, '') + #Column1
So I am not sure what I am missing in my original query.
Look at row 3 in the results you are getting. In your concatenated columns (Option1, 2, 3) you are getting the first script column twice. Not the first one + the second one like you expect.
The reason is because you've aliased your subquery "script" which is the same name as another column in your query, which makes it ambiguous.
Change the alias of the subquery and the problem should go away. I'm frankly surprised your query didn't raise an error.
EDIT: You can't use a column alias in another column's definition in the same level of the query. In other words, you can't do this:
SELECT
SomeColumn AS A
, (Subquery that returns a column) AS B
, A + B --this is not allowed
FROM ...
You can either create a CTE that returns the aliased columns and then concatenate them in the main query that selects from the CTE, or you have to use the original sources of the aliases, like so:
SELECT
SomeColumn AS A
, (Subquery that returns a column) AS B
, SomeColumn + (Subquery that returns a column) --this is fine
FROM ...
I took another approach where instead on creating separate column, I used ISNULL in my subQuery which returns my desired result.
Query:
SELECT DISTINCT
c.Name AS CallCenter,
LTRIM(RTRIM(s.Name)) Name,
d.DNIS,
s.ScriptId,
s.Script AS Original,
(
SELECT TOP 5 ISNULL(CCSL.Line, '')+'; ' + ISNULL(s.Script, '')
FROM CallCenterScriptLine CCSL
WHERE CCSL.ScriptId = s.ScriptId
ORDER BY ScriptLineId FOR XML PATH('')
) AS FromAnotherTable,
r.UnitName AS Store,
r.UnitNumber
FROM CallCenterScript s WITH (NOLOCK)
INNER JOIN CallCenterDNIS d WITH (NOLOCK) ON d.ScriptId = s.ScriptId
INNER JOIN CallCenter c WITH (NOLOCK) ON c.Id = s.CallCenterId
INNER JOIN CallCenterDNISRestaurant ccd WITH (NOLOCK) ON ccd.CallCenterDNISId = d.CallCenterDNISId
INNER JOIN dbo.Restaurant r WITH (NOLOCK) ON r.RestaurantID = ccd.CallCenterRestaurantId
WHERE c.Id = 5
AND (1 = 1)
AND (s.IsDeleted = 0 OR s.IsDeleted IS NULL)
ORDER BY DNIS ASC;
Here's a simplified example using table variables.
Instead of using a subquery for a field, it uses a CROSS APPLY.
And CONCAT in combination with STUFF is used to glue the strings together.
declare #Foo table (fooID int identity(1,1) primary key, Script varchar(30));
declare #Bar table (barID int identity(1,1) primary key, fooID int, Line varchar(30));
insert into #Foo (Script) values
('Test1'),('Test2'),(NULL);
insert into #Bar (fooID, Line) values
(1,'X'),(1,'Y'),(2,NULL),(3,'X'),(3,'Y');
select
f.fooID,
f.Script,
x.Lines,
CONCAT(Script+'; ', STUFF(x.Lines,1,2,'')) as NewScript
from #Foo f
cross apply (
select '; '+b.Line
from #Bar b
where b.fooID = f.fooID
FOR XML PATH('')
) x(Lines)
Result:
fooID Script Lines NewScript
----- ------- ------- -----------
1 Test1 ; X; Y Test1; X; Y
2 Test2 NULL Test2;
3 NULL ; X; Y X; Y

how to write nested query

I have a table named Options. have Three fields Caption, OptionID, ParentOptionID.
it contains some records like :
OptiondID Caption ParentOptionID
1 Entry 0
2 Sale 1
3 Sale Invoice 2
----------------------------------------------
I want the result as :
OptiondID Caption ParentOptionID
1 Entry 0
2 Entry - Sale 1
3 Entry - Sale - Sale Invoice 2
-----------------------------------------------
Option Caption of its parent option - added in current Options Caption, and it should be nested.
This is the query that I have tried:
;with MyRelation as (
-- Anchor member definition
select OID, Cast(Caption as Varchar(1000)) as Caption, POID, iid
from #tmpOptions as e
UNION ALL
-- Recursive member definition
select e.OID, Cast(e.Caption + '-' + r.Caption as Varchar(1000)) as Caption, e.POID, e.iid
from #tmpOptions as e join MyRelation R on e.POID = R.OID
)
-- Statement that executes the CTE
select OID, Caption, POID, iid
from MyRelation
Could you tried with below query
;WITH MyRelation AS (
SELECT OptiondID, convert(varchar(max), Caption) AS Caption, ParentOptionID
FROM Options
WHERE ParentOptionID = 0
UNION ALL
SELECT Options.OptiondID, MyRelation.Caption + ' - ' + Options.Caption, Options.ParentOptionID
FROM Options
INNER JOIN MyRelation ON Options.ParentOptionID = MyRelation.OptiondID
WHERE Options.ParentOptionID <> 0
)
SELECT * FROM MyRelation

TSQL - While loop within select?

In SQL server
Ok, so I'm working with a database table in which rows can have parent rows, which can then have parent rows of their own. I need to select the root 'row'. I don't know the best way to do this.
There is a field called ParentId, which links the row to the row with that ID. When the ParentId = 0, it is the root row.
This is my query now:
SELECT Releases.Name,WorkLog.WorkLogId
FROM WorkLog,Releases
WHERE
Releases.ReleaseId = WorkLog.ReleaseId
and WorkLogDateTime >= #StartDate
and WorkLogDateTime <= #end
I don't really need the Release Name of the child releases, I want only the root Release Name, so I want to select the result of a While loop like this:
WHILE (ParentReleaseId != 0)
BEGIN
#ReleaseId = ParentReleaseId
END
Select Release.Name
where Release.RealeaseId = #ReleaseId
I know that syntax is horrible, but hopefully I'm giving you an idea of what I'm trying to acheive.
Here is an example, which could be usefull:
This query is getting a lower element of a tree, and searching up to the parent of parents.
Like I have 4 level in my table -> category 7->5, 5->3, 3-> 1. If i give it to the 5 it will find the 1, because this is the top level of the three.
(Changing the last select you can have all of the parents up on the way.)
DECLARE #ID int
SET #ID = 5;
WITH CTE_Table_1
(
ID,
Name,
ParentID
)
AS(
SELECT
ID,
Name,
ParentID
FROM Table_1
WHERE ID = #ID
UNION ALL
SELECT
T.ID,
T.Name,
T.ParentID
FROM Table_1 T
INNER JOIN CTE_Table_1 ON CTE_Table_1.ParentID = T.ID
)
SELECT * FROM CTE_Table_1 WHERE ParentID = 0
something like this
with cte as
(
select id,parent_id from t where t.id=#myStartingValue
union all
select t.id,t.parent_id
from cte
join t on cte.parent_id = t.id where cte.parent_id<>0
)
select *
from cte
join t on cte.id=t.id where cte.parent_id = 0
and with fiddle : http://sqlfiddle.com/#!3/a5fa1/1/0
Using Andras approach, I edited the final select to directly give me the ID of the root release
WITH cte_Releases
(
ReleaseId,
ParentReleaseID
)
AS(
SELECT
ReleaseId,
ParentReleaseID
FROM Releases
Where ReleaseId = 905
UNION ALL
SELECT
R.ReleaseId,
R.ParentReleaseID
FROM Releases R
INNER JOIN cte_Releases ON cte_Releases.ParentReleaseID = R.ReleaseId
)
SELECT max(ReleaseId) as ReleaseId, min(ReleaseId) as RootReleaseId FROM cte_Releases
My problem now is I want to run through all #IDs (905 in that code) and join each record to a result

Resources