Stop Recursion Once Condition Satisfied - sql

I am using a CTE to explode out a Bill of Materials and need to locate all those materials that have recursive components.
What I was attempting, was to limit the number of cycles (levels) deep, by setting BOM_Level in the child node to a maximum bound:
exec pr_sys_drop_object '#BOMExploded'
;with BOM
as
(
select
Prod_Plant_CD
, Demand_Plant_CD
, material_cd
, component_cd
, component_quantity
, component_quantity AS Calculated_Component_Quantity
, BOM_Level
, Demand_Quantity
, CONVERT(float,1) AS Produced_Ratio
, Material_CD AS Demand_Material_CD
from #firstLevel a
UNION ALL
SELECT
b.Plant_CD as 'Prod_Plant_CD'
, a.Demand_Plant_CD
, b.Material_CD
, b.Component_CD
, b.component_quantity
, b.component_quantity
, a.BOM_Level + 1
, a.Demand_Quantity
, a.Produced_Ratio * a.Component_Quantity -- Produced Quantity for the current level = Produced Quantity (level -1) * Component_Quantity (level -1)
, a.Demand_Material_CD
FROM BOM a
inner join #BOM_ProdVersion_Priority b
on a.component_cd = b.material_cd
inner join #base_routes c
on a.Demand_Plant_CD = c.Recipient_Plant_CD
and b.Plant_CD = c.Source_Plant_CD
and c.Material_CD = b.Material_CD -- Need to have material_cd to link
where b.Material_CD != b.Component_CD
and b.Component_Quantity > 0
and BOM_Level < 5 -- Give the max number of levels deep we are allowed to cyncle to
)
select *
into #BOMExploded
from BOM
OPTION (MAXRECURSION 20)
Using this method however, would require a post-process to locate when the cycling on the recursive component level started, then back trace.
How can a CTE recursive query be stopped given a certain condition?
ie. when top-level material_cd = component_cd for a deeper BOM_Level

If I understand you correctly, you don't need to stop at a certain depth/level, or rather you want to stop at a certain level, but you also need to stop in case you start cycling through materials repeatedly.
In the case of the following recursive path: mat_1->mat_2->mat_3->mat_1, you would want to stop before that last mat_1 starts cycling again to mat_2 and so on.
If that's correct, then your best bet is to add a Path field to your recursive query that tracks each term that it finds as it moves down the recursive path:
exec pr_sys_drop_object '#BOMExploded'
;with BOM
as
(
select
Prod_Plant_CD
, Demand_Plant_CD
, material_cd
, component_cd
, component_quantity
, component_quantity AS Calculated_Component_Quantity
, BOM_Level
, Demand_Quantity
, CONVERT(float,1) AS Produced_Ratio
, Material_CD AS Demand_Material_CD
, CAST(material_cd AS VARCHAR(100)) AS Path
from #firstLevel a
UNION ALL
SELECT
b.Plant_CD as 'Prod_Plant_CD'
, a.Demand_Plant_CD
, b.Material_CD
, b.Component_CD
, b.component_quantity
, b.component_quantity
, a.BOM_Level + 1
, a.Demand_Quantity
, a.Produced_Ratio * a.Component_Quantity -- Produced Quantity for the current level = Produced Quantity (level -1) * Component_Quantity (level -1)
, a.Demand_Material_CD
, a.Path + '|' + b.material_cd
FROM BOM a
inner join #BOM_ProdVersion_Priority b
on a.component_cd = b.material_cd
inner join #base_routes c
on a.Demand_Plant_CD = c.Recipient_Plant_CD
and b.Plant_CD = c.Source_Plant_CD
and c.Material_CD = b.Material_CD -- Need to have material_cd to link
where b.Material_CD != b.Component_CD
and b.Component_Quantity > 0
and BOM_Level < 5 -- Give the max number of levels deep we are allowed to cyncle to
and a.path NOT LIKE '%' + b.material_cd + '%'
)
select *
into #BOMExploded
from BOM
OPTION (MAXRECURSION 20)
Now you have a path that is pipe delimited and you can test your current material_cd to see if it's already in the path. If it is, then you end that leg of the recursion to prevent cycling.
Updating to include a version where we capture material_cd cycles and only report those at the end of the recursion:
exec pr_sys_drop_object '#BOMExploded'
;with BOM
as
(
select
Prod_Plant_CD
, Demand_Plant_CD
, material_cd
, component_cd
, component_quantity
, component_quantity AS Calculated_Component_Quantity
, BOM_Level
, Demand_Quantity
, CONVERT(float,1) AS Produced_Ratio
, Material_CD AS Demand_Material_CD
, CAST(material_cd AS VARCHAR(100)) AS Path
, CAST(NULL AS CHAR(5)) as Cycle_Flag
, 0 as Cycle_Depth
from #firstLevel a
UNION ALL
SELECT
b.Plant_CD as 'Prod_Plant_CD'
, a.Demand_Plant_CD
, b.Material_CD
, b.Component_CD
, b.component_quantity
, b.component_quantity
, a.BOM_Level + 1
, a.Demand_Quantity
, a.Produced_Ratio * a.Component_Quantity -- Produced Quantity for the current level = Produced Quantity (level -1) * Component_Quantity (level -1)
, a.Demand_Material_CD
, a.Path + '|' + b.material_cd
, CASE WHEN a.path NOT LIKE '%' + b.material_cd + '%' then 'Cycle' END AS Cycle_Flag,
, CASE WHEN a.path NOT LIKE '%' + b.material_cd + '%' then a.Cycle_Depth + 1 END as Cycle_Depth
FROM BOM a
inner join #BOM_ProdVersion_Priority b
on a.component_cd = b.material_cd
inner join #base_routes c
on a.Demand_Plant_CD = c.Recipient_Plant_CD
and b.Plant_CD = c.Source_Plant_CD
and c.Material_CD = b.Material_CD -- Need to have material_cd to link
where b.Material_CD != b.Component_CD
and b.Component_Quantity > 0
and a.cycle_depth < 2 --stop the query if we start cycling, but only after we capture at least one full cycle
)
select *
into #BOMExploded
from BOM
WHERE cycle_flag IS NOT NULL
OPTION (MAXRECURSION 20)
This will capture cycle_depth which is a counter that measures how deep into a cycle we get. We stop the recursion after we get to cycle_depth of 1 so the cycle can be captures in the final select.

Related

How to write below query in PostgreSQL?

I am migrating procedure from SQL Server to PostgreSQL. I can not write this query in PostgreSQL. How to use temp within WITH clause in PostgreSQL?
WITH TEMP AS
(
SELECT a.AssignNo as LowestAssignNo
, a.AssignNo
, a.AssignName
, HierarchyLevel
, UpperAssignNo
FROM TAssignInfo a
WHERE DeleteYesNo = 'N'
AND UseYesNo = 'Y'
UNION ALL
SELECT a.LowestAssignNo
, b.AssignNo
, b.AssignName
, b.HierarchyLevel
, b.UpperAssignNo
FROM TEMP a
INNER JOIN TAssignInfo b
ON a.UpperAssignNo = b.AssignNo
WHERE DeleteYesNo = 'N'
AND UseYesNo = 'Y'
)
SELECT DISTINCT LowestAssignNo
, STUFF(( SELECT '|' + AssignName
FROM TEMP A
WHERE a.LowestAssignNo = b.LowestAssignNo
AND HierarchyLevel > 1
ORDER BY HierarchyLevel
FOR XML PATH('')
), 1, 1, '') AS AssignNamePath
INTO #NewAssignInfo
FROM TEMP B
WHERE B.HierarchyLevel <> 0
ORDER BY LowestAssignNo DESC
enter image description here

Converting recursive CTE from Postgre SQL to SQL Server

I am having trouble adapting some recursive CTE code from PostgreSQL to SQL Server.
Here is my table:
CREATE TABLE flight (
src CHAR(3)
, dest CHAR(3)
, stt DATETIME
, endt DATETIME);
INSERT INTO flight VALUES
('MSP', 'SLC', '2022-10-02 11:45:00', '2022-10-02 14:10:00'),
('SLC', 'LAX', '2022-10-02 15:20:00', '2022-10-02 17:45:00'),
('MSP', 'LAX', '2022-10-02 12:15:00', '2022-10-02 15:05:00')
and what I am trying to adapt:
WITH RECURSIVE flight_paths (src, flights, path, dest, stt, endt) AS (
SELECT
src
, ARRAY[src || '-' || dest]
, ARRAY[src]
, dest
, stt
, endt
FROM flight
UNION ALL
SELECT
fp.src
, fp.flights || (f.src || '-' || f.dest)
, fp.path || f.src
, f.dest
, fp.stt
, f.endt
FROM flight f
JOIN flight_paths fp ON f.src = fp.dest
WHERE NOT f.src = ANY(fp.path)
AND NOT 'LAX' = ANY(fp.path)
AND f.stt > fp.endt
)
SELECT flights, stt, endt, path[2:] stopovers
FROM flight_paths
WHERE src = 'MSP' AND dest = 'LAX'
I have been having issues adapting the use of ARRAYs. Any pointers would be really helpful!
Much more quicker will be :
WITH flight_paths (src, flights, path, dest, stt, endt) AS (SELECT
src
, CAST('{"' + src + '-' + dest +'"}' AS nvarchar(max))
, CAST('{"' + src + '"}' AS nvarchar(max))
, dest
, stt
, endt
FROM flight
WHERE src = 'MSP'
UNION ALL
SELECT
fp.src
, fp.flights + '{"' +f.src+'"}' + '{"' +f.dest+'"}'
, fp.path + '{"' +f.src+'"}'
, f.dest
, fp.stt
, f.endt
FROM flight f
JOIN flight_paths fp ON f.src = fp.dest AND f.dest <> fp.src
AND fp.dest <> 'LAX'
WHERE fp.path NOT LIKE '%' + '{"' +f.src+'"}' + '%'
AND f.stt > fp.endt
)
SELECT flights, stt, endt, path stopovers
FROM flight_paths
WHERE dest = 'LAX';
Tested with 100003 rows, the results are :
Charlieface Query 1 : 5' 05"
Charlieface Query 2 : 4' 58"
My own one : 0' 01"
Running on a Windows 2019 Developper / PC with 256 Go RAM and 24 cores x 2 intel Xeon CPU E5-1650 v4
Conclusion : JSON or XML are very costly in recursive queries...
Unfortunately, SQL Server does not support arrays. But you could use a JSON array instead.
A pretty straight-up copy of your original code results in this
WITH flight_paths (src, flights, path, dest, stt, endt) AS (
SELECT
src
, CAST('["' + src + '-' + dest +'"]' AS nvarchar(max))
, CAST('["' + src + '"]' AS nvarchar(max))
, dest
, stt
, endt
FROM flight
UNION ALL
SELECT
fp.src
, JSON_MODIFY(fp.flights, 'append $', f.src + '-' + f.dest)
, JSON_MODIFY(fp.path, 'append $', f.src)
, f.dest
, fp.stt
, f.endt
FROM flight f
JOIN flight_paths fp ON f.src = fp.dest AND f.dest <> fp.src
WHERE EXISTS (SELECT 1
FROM OPENJSON(fp.path) arr
WHERE arr.value NOT IN(f.src, 'LAX')
)
AND f.stt > fp.endt
)
SELECT flights, stt, endt, path stopovers
FROM flight_paths
WHERE src = 'MSP'
AND dest = 'LAX'
Some minor changes in the logic were necessary:
It's difficult to delete an element from a JSON array. So instead the path only builds the intermediate nodes, and a separate check is made to exclude the start point.
I note that it's probably faster to check explicitly that the destination has not been reached, and also to push the start point into the anchor part of the CTE:
WITH flight_paths (src, flights, path, dest, stt, endt) AS (
SELECT
src
, CAST('["' + src + '-' + dest +'"]' AS nvarchar(max))
, CAST('["' + src + '"]' AS nvarchar(max))
, dest
, stt
, endt
FROM flight
WHERE src = 'MSP'
UNION ALL
SELECT
fp.src
, JSON_MODIFY(fp.flights, 'append $', f.src + '-' + f.dest)
, JSON_MODIFY(fp.path, 'append $', f.src)
, f.dest
, fp.stt
, f.endt
FROM flight f
JOIN flight_paths fp ON f.src = fp.dest AND f.dest <> fp.src
AND fp.dest <> 'LAX'
WHERE EXISTS (SELECT 1
FROM OPENJSON(fp.path) arr
WHERE arr.value <> f.src
)
AND f.stt > fp.endt
)
SELECT flights, stt, endt, path stopovers
FROM flight_paths
WHERE dest = 'LAX';
db<>fiddle

Oracle get UNIQUE constraint violation error too late

What should I check why Oracle server takes more then 20 sec to return UNIQUE constraint violation error for specific data?
One of our processes is processing over 30000 data one day with multi process and some time gets UNIQUE constraint violation error in 1 sec
but it takes more then 20 sec to return UNIQUE constraint violation error for specific data.
Query is same as below. (Modified only table name)
MERGE
INTO TableA S
USING (
SELECT NVL(:sccm_cd , ' ') SCCM_CD
, NVL(:oder_dt , ' ') ODER_DT
, NVL(:mrkt_dstn_cd, ' ') MRKT_DSTN_CD
, NVL(:oder_no , ' ') ODER_NO
, NVL(:cncd_unpr , 0) CNCD_UNPR
, B.SLBY_FEE_GRD_CD
, B.ACCT_MNGR_EMPL_NO
, C.AO_FEE_GRD_CD
FROM DUAL A
, TableB B
, TableC C
WHERE 1 = 1
AND B.SCCM_CD = :sccm_cd
AND B.ACNO = :acno
AND C.SCCM_CD(+) = B.SCCM_CD
AND C.EMPL_NO(+) = B.ACCT_MNGR_EMPL_NO
) T
ON ( S.sccm_cd = T.sccm_cd
AND S.oder_dt = T.oder_dt
AND S.mrkt_dstn_cd = T.mrkt_dstn_cd
AND S.oder_no = T.oder_no
AND S.cncd_unpr = T.cncd_unpr
)
WHEN MATCHED THEN
UPDATE
SET S.cncd_qty = S.cncd_qty + NVL(:cncd_qty ,0)
, S.slby_fee = S.slby_fee + NVL(:slby_fee ,0)
, S.slby_fee_srtx = S.slby_fee_srtx + NVL(:slby_fee_srtx,0)
, S.idx_fee_amt = S.idx_fee_amt + NVL(:idx_fee_amt ,0)
, S.cltr_fee = S.cltr_fee + NVL(:cltr_fee ,0)
, S.trtx = S.trtx + NVL(:trtx ,0)
, S.otc_fee = S.otc_fee + NVL(:otc_fee ,0)
, S.wht_fee = S.wht_fee + NVL(:wht_fee ,0)
WHEN NOT MATCHED THEN
INSERT (
sccm_cd
, oder_dt
, mrkt_dstn_cd
, oder_no
, cncd_unpr
, acno
, item_cd
, slby_dstn_cd
, md_dstn_cd
, cncd_qty
, stlm_dt
, trtx_txtn_dstn_cd
, proc_cmpl_dstn_cd
, item_dstn_cd
, slby_fee_grd_cd
, slby_fee
, slby_fee_srtx
, idx_fee_amt
, cltr_fee
, trtx
, wht_fee
, otc_fee
, acct_mngr_empl_no
, ao_fee_grd_cd
)
VALUES
( T.sccm_cd
, T.oder_dt
, T.mrkt_dstn_cd
, T.oder_no
, T.cncd_unpr
, :acno
, :item_cd
, :slby_dstn_cd
, :md_dstn_cd
, NVL(:cncd_qty ,0)
, DECODE(:mrkt_dstn_cd, 'TN', T.oder_dt, :stlm_dt)
, :trtx_txtn_dstn_cd
, '0'
, :item_dstn_cd
, NVL(:slby_fee_grd_cd, T.SLBY_FEE_GRD_CD)
, NVL(:slby_fee ,0)
, NVL(:slby_fee_srtx ,0)
, NVL(:idx_fee_amt ,0)
, NVL(:cltr_fee ,0)
, NVL(:trtx ,0)
, NVL(:wht_fee , 0)
, NVL(:otc_fee , 0)
, T.acct_mngr_empl_no
, T.ao_fee_grd_cd
)
There could be multiple reasons for it. I will list here some of the possible causes for this behavior.
Concurrency issue
Your insert might be waiting for other operations, like other inserts or updated or deletions.
Network issues
It is possible that for some reason your network is overwhelmed with requests or, if the server is remote, this could be an internet speed issue as well.
Server load
The server might be overwhelmed with lots of jobs to do.
Slow query
It's also possible that the select you use in your insert command is very slow. It would make sense to test its speed. Also, it would make sense to test insert speed as well.

Having Trouble with FOR XML Path - No Concatenating

I have a situation where an order can contain multiple license purchases - and if the order does contain multiple licenses, I want to display the license descriptions in a single cell with the values separated by commas. If we were on SQL 2017, I could use STRING_AGG but we are on SQL 2016 so I am trying the tried and true STUFF / FOR XML Path method.
From the screenshot below, Customer 4341073 had two license purchases on Order ID 18519173:
When I add the STUFF / FOR XML Path to the T-SQL, I am not able to achieve the desired result of showing the license description in the same record - each license still has it's own row.
SELECT x.CustomerID ,
x.ATOLicenseTypeID ,
x.ATOLicense ,
x.AuthorizationBeginDate ,
x.AuthorizationEndDate ,
x.OrderID ,
x.OrderDate ,
STUFF ( (
SELECT ',' + lt.description
FROM dbo.LicenseTypes AS lt
--INNER JOIN #XMLPATH ON lt.id = x.OrderLicenseTypeID
WHERE lt.id = x.OrderLicenseTypeID
--GROUP BY ',' + lt.description
FOR XML PATH ( '' )
) , 1 , 1 , '' ) AS Licenses
FROM #XMLPATH AS x
--GROUP BY x.CustomerID ,
-- x.ATOLicenseTypeID ,
-- x.ATOLicense ,
-- x.AuthorizationBeginDate ,
-- x.AuthorizationEndDate ,
-- x.OrderID ,
-- x.OrderDate ,
-- x.OrderLicenseTypeID;
I've tried different ways to join the sub-query to the outer query and added and removed GROUP BY to achieve the desired result but nothing is working for me.
Any suggestions on where I am going wrong with this query?
Sample dataset:
DROP TABLE IF EXISTS #XMLPATH;
CREATE TABLE #XMLPATH
(
CustomerID INT ,
ATOLicenseTypeID INT ,
ATOLicense VARCHAR (500) ,
AuthorizationBeginDate DATE ,
AuthorizationEndDate DATE ,
OrderID INT ,
OrderDate DATETIME ,
OrderLicenseTypeID INT
);
INSERT INTO #XMLPATH
VALUES ( 4341073, 52, 'Temporary Resident Fishing', N'2019-01-07T00:00:00', N'2019-01-07T00:00:00', 18519136, N'2019-01-07T12:01:55.317', 2141 ) ,
( 4341073, 52, 'Temporary Resident Fishing', N'2019-01-07T00:00:00', N'2019-01-07T00:00:00', 18519173, N'2019-01-07T12:34:13.107', 204 ) ,
( 4341073, 52, 'Temporary Resident Fishing', N'2019-01-07T00:00:00', N'2019-01-07T00:00:00', 18519173, N'2019-01-07T12:34:13.107', 2141 );
SELECT * FROM #XMLPATH;
SELECT x.CustomerID ,
x.ATOLicenseTypeID ,
x.ATOLicense ,
x.AuthorizationBeginDate ,
x.AuthorizationEndDate ,
x.OrderID ,
x.OrderDate ,
STUFF ( (
SELECT ',' + lt.description
FROM dbo.LicenseTypes AS lt
--INNER JOIN #XMLPATH ON lt.id = x.OrderLicenseTypeID
WHERE lt.id = x.OrderLicenseTypeID
--GROUP BY ',' + lt.description
FOR XML PATH ( '' )
) , 1 , 1 , '' ) AS Licenses
FROM #XMLPATH AS x
GROUP BY x.CustomerID ,
x.ATOLicenseTypeID ,
x.ATOLicense ,
x.AuthorizationBeginDate ,
x.AuthorizationEndDate ,
x.OrderID ,
x.OrderDate ,
x.OrderLicenseTypeID;
In order to get all rows of one OrderID as one result-row, you must not include the separating information (the OrderLicenseTypeID) into the GROUP BY. But then you have the issue you've encountered: You cannot use this ID within your FOR XML construct.
The trick is (as your out-commented trials show), to add the source table to the sub-select and filter there with a grouped column. But you have to use different aliases to deal with them as two different sets. Try this:
(I had to add one more temp table to test this...)
SELECT x.CustomerID ,
x.ATOLicenseTypeID ,
x.ATOLicense ,
x.AuthorizationBeginDate ,
x.AuthorizationEndDate ,
x.OrderID ,
x.OrderDate ,
STUFF ( (
SELECT ',' + lt.description
FROM #XMLPATH x2
INNER JOIN #LicenseTypes AS lt ON lt.id=x2.OrderLicenseTypeID
WHERE x2.OrderID = x.OrderID --you might need to add more columns here....
--in most cases we want to add an ORDER BY
FOR XML PATH ( '' )
) , 1 , 1 , '' ) AS Licenses
FROM #XMLPATH AS x
GROUP BY x.CustomerID ,
x.ATOLicenseTypeID ,
x.ATOLicense ,
x.AuthorizationBeginDate ,
x.AuthorizationEndDate ,
x.OrderID ,
x.OrderDate;
Btw: Starting with v2017 there is STRING_AGG(), which makes this much easier...

T-sql; adding positive and negative values with SUM()

I havenĀ“t done any T-SQL before and were wondering how could I change the script below to differentiate between positive and negative values. At the moment it seems to just to add the two together and giving out wrong values.
I would like to add an IF clause that s.summa would follow rules:
e.g
-1 + -1 = -2
1 + -1 = 0
1 + 1 = 2
If I am correct now it is being dealt with "etumerkki", so that if etumerkki variable is 0 s.sum is converted to negative and if etumerkki variable has not been set it remains normal.
SET #stmt = CONCAT( 'INSERT INTO tbl_1 (rivi,rivi_id,isanta_rivi,taso,lihavointi,tili_rivi,otsikko,selite,summa)
SELECT t.rivi , t.rivi_id , t.isanta_rivi , t.taso , t.lihavointi , t.rivi_tyyppi , t.otsikko , t.selite ,
CASE etumerkki
WHEN 0 THEN -1 * IFNULL(SUM(s.summa),0.00)
ELSE IFNULL(SUM(s.summa),0.00)
END AS summa
FROM tase_mem t
LEFT JOIN kuutio_paakirja s ON s.tili = t.tili
WHERE ' , nyk_summa_ehto , '
AND s.kpaikka_id = ' , kpaikka_id , ' AND s.projekti_id = ' , projekti_id , ' ' , isanta_ehto , '
GROUP BY t.rivi , t.rivi_id , t.taso , t.lihavointi , t.rivi_tyyppi , t.etumerkki , t.otsikko , t.selite;');
PREPARE stmt FROM #stmt;
EXECUTE stmt;
Thanks in advance
Not sure if i follow...
SUM()
does indeed evaluate signed numbers correctly. Eg
sum(1, -1) = 0, sum(-1, -1)=-2 and sum(1,1) = 2
See:
select sum(a)
from (
select 1 as a
union all select -1
) i
select sum(a)
from (
select -1 as a
union all select -1
) i
select sum(a)
from (
select 1 as a
union all select 1
) i
However, in your query, you write IFNULL(SUM(s.summa),0.00):
This means that your query first sums all s.summa, and AFTER that evaluates IFNULL.
I suppose you want to evalue whether s.summa is null before you add them together. changing the query to SUM(IFNULL(s.summa),0.00) should give you the proper result.