SQL Server : recursive update statement - sql

I'm somewhat new to SQL and I am trying to figure out the best way of doing this without hardcoding update statements in SQL Server 2012.
Basically I have a hierarchical table of companies (think of a supply chain) with columns (CompanyID, ParentID, ParentPriceAdj, CompanyPriceAdj). Each company gets assigned a price adjustment by their parent that modifies a list price in the PartMaster table and final price gets calculated by cascading the adjustments from parent to child.
If a parents price adjustment gets updated, I want that to reflect on all of his child companies and so forth
aka:
When updating a CompanyPriceAdj for a given updatedcompanyID, I want to recursively find the child CompanyID's (ParentId = updatedCompanyID) and update their ParentPriceAdj to ParentCompany's (parentPriceAdj * (1 + CompanyPriceAdj))
CompanyId ParentID ParentPriceAdj CompanyPriceAdj
5 6 0.96 .10
6 8 1 .20
7 6 0.96 .15
8 11 1 0
10 6 0.96 0
11 12 1 0
I was thinking of using a stored procedure that updates then repeatedly calls itself for every child that was just updated and then subsequently updates his children.... until the company has no children
I've tried looking around couldn't find any examples like this
This is what I have right now
ALTER PROCEDURE [dbo].[UpdatePricing]
#updatedCompanyID int, #PriceAdj decimal
AS
BEGIN
SET NOCOUNT ON;
WHILE (Select CompanyID From CompanyInfo Where ParentID = #updatedCompanyID) IS NOT NULL
UPDATE CompanyInfo
SET ParentPriceAdj = #PriceAdj * (1+CompanyPriceAdj),
#updatedCompanyId = CompanyID,
#PriceAdj = CompanyPriceAdj
WHERE ParentID = #updatedCompanyID
--- some method to call itself again for each (#updatedCompanyID, #PriceAdj)
END

Recursive CTE can be used to walk hierarchy, something like:
ALTER PROCEDURE [dbo].[UpdatePricing]
(
#companyID int,
#PriceAdj decimal
)
as
begin
set nocount on
update CompanyInfo
set CompanyPriceAdj = #PriceAdj
where CompanyID = #companyID
;with Hierarchy(CompanyID, ParentID, InPriceAdj, OutPriceAdj)
as (
select D.CompanyID, D.ParentID, cast(D.ParentPriceAdj as float),
cast(D.ParentPriceAdj as float) * cast(1 + D.CompanyPriceAdj as float)
from CompanyInfo D
where CompanyID = #companyID
union all
select D.CompanyID, D.ParentID,
H.OutPriceAdj, H.OutPriceAdj * (1 + D.CompanyPriceAdj)
from Hierarchy H
join CompanyInfo D on D.ParentID = H.CompanyID
)
update D
set D.ParentPriceAdj = H.InPriceAdj
from CompanyInfo D
join Hierarchy H on H.CompanyID = D.CompanyID
where
D.CompanyID != #companyID
end

You can use WITH expression in t-sql to get all parent records for given child record. And can update each record in record set accordingly with your logic.
Here are links for WITH expression --
http://msdn.microsoft.com/en-us/library/ms175972.aspx
http://msdn.microsoft.com/en-us/library/ms186243(v=sql.105).aspx

Have you looked at using a CURSOR? It's a bit of overhead, but it may give you what you need. In case you're not familiar, below is a basic outline. It allows you pull each company ID one at a time, make your changes, and move on. You can nest them, include them in loops or include loops in them, whatever you need to go through a list performing actions until there are no actions left needing attention.
declare #updatedCompanyID Varchar(5)
DECLARE D_cursor CURSOR FOR Select updatedCompanyID from
OPEN D_cursor
FETCH NEXT FROM D_cursor into #updatedCompanyID
WHILE ##FETCH_STATUS =0
BEGIN
--Run Your Code
WHERE parentID = #updatedCompanyID
FETCH NEXT FROM D_cursor into #updatedCompanyID
END
Close D_cursor
DEALLOCATE D_cursor
Hope this is helpful. I apologize if I've misunderstood.

Related

SQL: Delete Rows from Dynamic list of tables where ID is null

I'm a SQL novice, and usually figure things out via Google and SO, but I can't wrap my head around the SQL required for this.
My question is similar to Delete sql rows where IDs do not have a match from another table, but in my case I have a middle table that I have to query, so here's the scenario:
We have this INSTANCES table that basically lists all the occurrences of files sent to the database, but have to join with CROSS_REF so our reporting application knows which table to query for the report, and we just have orphaned INSTANCES rows I want to clean out. Each DETAIL table contains different fields from the other ones.
I want to delete all single records from INSTANCES if there are no records for that Instance ID in any DETAIL table. The DETAIL table got regularly cleaned of old files, but the Instance record wasn't cleaned up, so we have a lot of INSTANCE records that don't have any associated DETAIL data. The thing is, I have to select the Table Name from CROSS_REF to know which DETAIL_X table to look up the Instance ID.
In the below example then, since DETAIL_1 doesn't have a record with Instance ID = 1001, I want to delete the 1001 record from INSTANCES.
INSTANCES
Instance ID
Detail ID
1000
123
1001
123
1002
234
CROSS_REF
Detail ID
Table Name
123
DETAIL_1
124
DETAIL_2
125
DETAIL_3
DETAIL_1
Instance ID
1000
1000
2999
Storing table names or column names in a database is almost always a sign for a bad database design. You may want to change this and thus get rid of this problem.
However, when knowing the possible table names, the task is not too difficult.
delete from instances i
where not exists
(
select null
from cross_ref cr
left join detail_1 d1 on d1.instance_id = i.instance_id and cr.table_name = 'DETAIL_1'
left join detail_2 d2 on d2.instance_id = i.instance_id and cr.table_name = 'DETAIL_2'
left join detail_3 d3 on d3.instance_id = i.instance_id and cr.table_name = 'DETAIL_3'
where cr.detail_id = i.detail_id
and
(
d1.instance_id is not null or
d2.instance_id is not null or
d3.instance_id is not null
)
);
(You can replace is not null by = i.instance_id, if you find that more readable. In that case you could even remove these criteria from the ON clauses.)
Much thanks to #DougCoats, this is what I ended up with.
So here's what I ended up with (#Doug, if you want to update your answer, I'll mark yours correct).
DECLARE #Count INT, #Sql VARCHAR(MAX), #Max INT;
SET #Count = (SELECT MIN(DetailID) FROM CROSS_REF)
SET #Max = (SELECT MAX(DetailID) FROM CROSS_REF)
WHILE #Count <= #Max
BEGIN
IF (select count(*) from CROSS_REF where file_id = #count) <> 0
BEGIN
SET #sql ='DELETE i
FROM Instances i
WHERE NOT EXISTS
(
SELECT InstanceID
FROM '+(SELECT TableName FROM Cross_Ref WHERE DetailID=#Count)+' d
WHERE d.InstanceId=i.InstanceID
AND i.detailID ='+ cast(#Count as varchar) +'
)
AND i.detailID ='+ cast(#Count as varchar)
EXEC(#sql);
SET #Count=#Count+1
END
END
this answer assumes you have sequential data in the CROSS_REF table. If you do not, you'll need to alter this to account it (as it will bomb due to missing object reference).
However, this should give you an idea. It also could probably be written to do a more set based approach, but my answer is to demonstrate dynamic sql use. Be careful when using dynamic SQL though.
DECLARE #Count INT, #Sql VARCHAR(MAX), #Max INT;
SET #Count = (SELECT MIN(DetailID) FROM CROSS_REF)
SET #Max = (SELECT MAX(DetailID) FROM CROSS_REF)
WHILE #Count <= #Max
BEGIN
IF (select count(*) from CROSS_REF where file_id = #count) <> 0
BEGIN
SET #sql ='DELETE i
FROM Instances i
WHERE NOT EXISTS
(
SELECT InstanceID
FROM '+(SELECT TableName FROM Cross_Ref WHERE DetailID=#Count)+' d
WHERE d.InstanceId=i.InstanceID
AND i.detailID ='+ cast(#Count as varchar) +'
)
AND i.detailID ='+ cast(#Count as varchar)
EXEC(#sql);
SET #Count=#Count+1
END
END

Loop and update without cursor

I have a table where item balances are stored.
CREATE TABLE itembalance (
ItemID VARCHAR(15),
RemainingQty INT,
Cost Money,
Id INT
)
I need to make sure that whenever an item is being sent out, the proper balances are deducted from the itembalance table. I do it this way:
DECLARE crsr CURSOR LOCAL FAST_FORWARD FOR
SELECT
itembalance.Cost,
itembalance.RemainingQty
itembalance.Id
FROM dbo.itembalance
WHERE itembalance.ItemID = #v_item_to_be_updated AND RemainingQty > 0
OPEN crsr
FETCH crsr
INTO
#cost,
#qty,
#id
WHILE ##FETCH_STATUS = 0
BEGIN
IF #qty >= #qty_to_be_deducted
BEGIN
UPDATE itembalance SET RemainingQty = RemainingQty - #qty_to_be_deducted WHERE Id = #id
/*do something with cost*/ BREAK
END
ELSE
BEGIN
UPDATE itembalance SET RemainingQty = 0 WHERE Id = #id
/*do something with cost*/ SET #qty_to_be_deducted = #qty_to_be_deducted - #qty
END
FETCH crsr
INTO
#cost,
#qty,
#id
END
CLOSE crsr
DEALLOCATE crsr
The table may contain same item code but with different cost. This code is okay for few items being updated at a time but whenever a lot of items/quantities are being sent out, the process becomes really slow. Is there a way to optimize this code? I am guessing the cursor is making it slow so I want to explore a different code for this process.
This looks like you just need a simple CASE expression:
UPDATE dbo.itembalance
SET Qty = CASE WHEN Qty >= #qty_to_be_deducted THEN Qty - #qty_to_be_deducted ELSE 0 END
WHERE ItemID = #v_item_to_be_updated
--What is the difference between Qty and RemainingQty?
--Why are you checking one and updating the other?
AND RemainingQty > 0;
You code is not very clear as to how and why the mechanism is required and works.
However assuming that you must have multiple records with an outstanding balance, and that you must consider multiple records sequentially as part of this mechanism, then you have two options to solve that within SQL (handling in client code is another option):
1) Use a cursor as you have done
2) Use a temp table or table variable and iterate over it - pretty similar to a cursor but might be faster - you'd have to try and see e.g.
declare #TableVariable table (Cost money, RemainingQty int, Id int, OrderBy int, Done bit default(0))
declare #Id int, #Cost money, #RemainingQty int
insert into #TableVariable (Cost, RemainingQty, Id, OrderBy)
SELECT
itembalance.Cost
, itembalance.RemainingQty
, itembalance.Id
, 1 /* Some order by condition */
FROM dbo.itembalance
WHERE itembalance.ItemID = #v_item_to_be_updated AND RemainingQty > 0
while exists (select 1 from #TableVariable where Done = 0) begin
select top 1 #Id = id, #Cost = Cost, #RemainingQty
from #TableVariable
where Done = 0
order by OrderBy
-- Do stuff here
update #TableVariable set Done = 1 where id = #Id
end
However the code you have shown doesn't appear that it should be slow - so it may be that you are lacking the appropriate indexes, and that a single ItemId update is locking too many rows in the ItemBalance table which is then affecting other ItemId updates.

Update Trigger For Multiple Rows

I am trying to Insert data in a table named "Candidate_Post_Info_Table_ChangeLogs" whenever a record is updated in another table named "Candidate_Personal_Info_Table". my code works fine whenever a single record is updated but when i try to updated multiple rows it gives error:
"Sub query returned more then 1 value".
Following is my code :
ALTER TRIGGER [dbo].[Candidate_PostInfo_UPDATE]
ON [dbo].[Candidate_Post_Info_Table]
AFTER UPDATE
AS
BEGIN
IF ##ROWCOUNT = 0
RETURN
DECLARE #Candidate_Post_ID int
DECLARE #Candidate_ID varchar(50)
DECLARE #Action VARCHAR(50)
DECLARE #OldValue VARCHAR(MAX)
DECLARE #NewValue VARCHAR(MAX)
DECLARE #Admin_id int
IF UPDATE(Verified)
BEGIN
SET #Action = 'Changed Verification Status'
SET #Candidate_Post_ID = (Select ID From inserted)
SET #Candidate_ID = (Select Identity_Number from inserted)
SET #NewValue = (Select Verified From inserted)
SET #OldValue = (Select Verified From deleted)
IF(#NewValue != #OldValue)
BEGIN
INSERT INTO Candidate_Post_Info_Table_ChangeLogs(Candidate_Post_ID, Candidate_ID, Change_DateTime, action, NewValue, OldValue, Admin_ID)
VALUES(#Candidate_Post_ID, #Candidate_ID, GETDATE(), #Action, #NewValue, #OldValue, '1')
END
END
END
i have searched stack overflow for this issue but couldn't get any related answer specific to this scenario.
When you insert/update multiple rows into a table, the Inserted temporary table used by the system holds all of the values from all of the rows that were inserted or updated.
Therefore, if you do an update to 6 rows, the Inserted table will also have 6 rows, and doing something like this:
SET #Candidate_Post_ID = (Select ID From inserted)
Will return an error, just the same as doing this:
SET #Candidate_Post_ID = (SELECT 1 UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5 UNION ALL SELECT 6)
From the looks of things, you tried to do this with an iterative approach. Set-based is better. Maybe consider doing it like this in the body of your TRIGGER (without all of the parameters...):
IF UPDATE(Verified)
BEGIN
INSERT INTO Candidate_Post_Info_Table_ChangeLogs
(
Candidate_Post_ID
,Candidate_ID
,Change_DateTime
,action
,NewValue
,OldValue
,Admin_ID
)
SELECT
I.ID
,I.Identity_Number
,GETDATE()
,'Changed Verification Status'
,I.Verified
,O.Verified
,'1'
FROM Inserted I
INNER JOIN Deleted O
ON I.ID = O.ID -- Check this condition to make sure it's a unique join per row
WHERE I.Verified <> O.Verified
END
A similar case was solved in the following thread using cursors.... please check it
SQL Server A trigger to work on multiple row inserts
Also the below thread gives the solution based on set based approach
SQL Server - Rewrite trigger to avoid cursor based approach
*Both the above threads are from stack overflow...

Update From a Loop (Table Locking Issues + CLR Memory Issues)

I have a table with a geography column. I want to update this value, for a subset of rows (~1,000), based on the results of a query.
I have created a view that will return the geography column I want + the id of the row in the table to be updated.
If I run the query
UPDATE A
SET A.GeogCol = B.GeogCol
FROM TableToUpdate A
INNER JOIN UpdatedFrom_View B
ON A.ID = B.ID
I will receive a system out of memory error. This occurs because it is 32 bit sql server and the VAS reservation runs out of space because of the CLR functions I call to create the geography column. I am not the sever administrator, so I cannot temporarily allocate more space to the VAS reservation.
If I reduce this to WHERE A.ID BETWEEN 0 AND 100 and attempt to manually itterate, I still have this problem sometimes. I have found a few of the troublesome rows (larger geography objects than usual), and I can update those if I say WHERE A.ID = #ID.
My thought was then to update based on a loop.
DECLARE #ID INT
DECLARE #g GEOGRAPHY
DECLARE IDCursor CURSOR FOR SELECT ID FROM TableToUpdate
OPEN IDCursor
FETCH NEXT FROM IDCursor INTO #ID
WHILE ##FETCH_STATUS = 0
BEGIN
SET #g = (SELECT GeogCol FROM UpdatedFrom_View WHERE ID = #ID)
UPDATE TableToUpdate SET GeogCol = #g WHERE ID = #ID
FETCH NEXT FROM IDCursor INTO #ID
END
CLOSE IDCursor
DEALLOCATE IDCursor
However this appears to have issues with table locking (I think). It has now run for just under 2 days and has updated less than 150 records.
For reference, when done manually, UPDATE TableToUpdate SET GeogCol = #g WHERE ID = #ID takes less than 10 seconds.
Is there a better way to do this?
--EDIT
So I decided to test something.
I wrote this query which takes approximately 1 second.
UPDATE A
SET A.GeogCol = B.GeogCol
FROM TableToUpdate A
INNER JOIN UpdatedFrom_View B
ON A.ID = B.ID
WHERE A.ID BETWEEN 1 AND 1
Then I wrote a stored procedure that does the EXACT same thing.
CREATE PROCEDURE TestUpdate #IDLow INT, #IDHigh INT
AS
BEGIN
UPDATE A
SET A.GeogCol = B.GeogCol
FROM TableToUpdate A
INNER JOIN UpdatedFrom_View B
ON A.ID = B.ID
WHERE A.ID BETWEEN #IDLow AND #IDHigh
END
GO
EXEC TestUpdate #IDLow = 1, #IDHigh = 1
This query takes over half an hour. What is happening?

How to update a column fetched by a cursor in TSQL

Before I go any further: Yes, I know that cursors perform poorly compared with set-based operations. In this particular case I'm running a cursor on a temporary table of 100 or so records, and that temporary table will always be fairly small, so performance is less crucial than flexibility.
My difficulty is that I'm having trouble finding an example of how to update a column fetched by a cursor. Previously when I've used cursors I've retrieved values into variables, then run an update query at each step based upon these values. On this occasion I want to update a field in the temporary table, yet I can't figure out how to do it.
In the example below, I'm trying to update the field CurrentPOs in temporary table #t1, based upon a query that uses #t1.Product_ID to look up the required value. You will see in the code that I have attempted to use the notation curPO.Product_ID to reference this, but it doesn't work. I have also attempted to use an update statement against curPO, also unsuccessfully.
I can make the code work by fetching to variables, but I'd like to know how to update the field directly.
I think I'm probably missing something obvious, but can anyone help?
declare curPO cursor
for select Product_ID, CurrentPOs from #t1
for update of CurrentPOs
open curPO
fetch next from curPO
while ##fetch_status = 0
begin
select OrderQuantity = <calculation>,
ReceiveQuantity = <calculation>
into #POs
from PurchaseOrderLine POL
inner join SupplierAddress SA ON POL.Supplier_ID = SA.Supplier_ID
inner join PurchaseOrderHeader POH ON POH.PurchaseOrder_ID = POL.PurchaseOrder_ID
where Product_ID = curPO.Product_ID
and SA.AddressType = '1801'
update curPO set CurrentPOs = (select sum(OrderQuantity) - sum(ReceiveQuantity) from #POs)
drop table #POs
fetch next from curPO
end
close curPO
deallocate curPO
After doing a bit more googling, I found a partial solution. The update code is as follows:
UPDATE #T1
SET CURRENTPOS = (SELECT SUM(ORDERQUANTITY) - SUM(RECEIVEQUANTITY)
FROM #POS)
WHERE CURRENT OF CURPO
I still had to use FETCH INTO, however, to retrieve #t1.Product_ID and run the query that produces #POs, so I'd still like to know if it's possible to use FETCH on it's own.
Is this what you want?
declare curPO cursor
for select Product_ID, CurrentPOs from #t1
for update of CurrentPOs
open curPO
fetch next from curPO
while ##fetch_status = 0
begin
update curPO set CurrentPOs =
(select sum(<OrderQuantityCalculation>)
from PurchaseOrderLine POL
inner join SupplierAddress SA ON POL.Supplier_ID = SA.Supplier_ID
inner join PurchaseOrderHeader POH ON POH.PurchaseOrder_ID = POL.PurchaseOrder_ID
where Product_ID = curPO.Product_ID
and SA.AddressType = '1801') -
(select sum(<ReceiveQuantityCalculation>)
from PurchaseOrderLine POL
inner join SupplierAddress SA ON POL.Supplier_ID = SA.Supplier_ID
inner join PurchaseOrderHeader POH ON POH.PurchaseOrder_ID = POL.PurchaseOrder_ID
where Product_ID = curPO.Product_ID
and SA.AddressType = '1801')
fetch next from curPO
end
close curPO
deallocate curPO
Maybe you need something like that:
update DataBaseName..TableName
set ColumnName = value
where current of your_cursor_name;
Here's an example to calculate one column based upon values from two others (note, this could be done during the original table select). This example can be copy / pasted into an SSMS query window to be run without the need for any editing.
DECLARE #cust_id INT = 2, #dynamic_val NVARCHAR(40), #val_a INT, #val_b INT
DECLARE #tbl_invoice table(Cust_ID INT, Cust_Fees INT, Cust_Tax INT)
INSERT #tbl_invoice ( Cust_ID, Cust_Fees, Cust_Tax ) SELECT 1, 111, 11
INSERT #tbl_invoice ( Cust_ID, Cust_Fees, Cust_Tax ) SELECT 2, 222, 22
INSERT #tbl_invoice ( Cust_ID, Cust_Fees, Cust_Tax ) SELECT 3, 333, 33
DECLARE #TblCust TABLE
(
Rec_ID INT
, Val_A INT
, Val_B INT
, Dynamic_Val NVARCHAR(40)
, PRIMARY KEY NONCLUSTERED (Rec_ID)
)
INSERT #TblCust(Rec_ID, Val_A, Val_B, Dynamic_Val)
SELECT Rec_ID = Cust_ID, Val_A = Cust_Fees, Val_B = Cust_Tax, NULL
FROM #tbl_invoice
DECLARE cursor_cust CURSOR FOR
SELECT Rec_ID, Val_A, Val_B, Dynamic_Val
FROM #TblCust
WHERE Rec_ID <> #cust_id
FOR UPDATE OF Dynamic_Val;
OPEN cursor_cust;
FETCH NEXT FROM cursor_cust INTO #cust_id, #val_a, #val_b, #dynamic_val;
WHILE ##FETCH_STATUS = 0
BEGIN
UPDATE #TblCust
SET Dynamic_Val = N'#c = "' + LTRIM(STR((#val_a + #val_b), 40)) + N'"'
WHERE CURRENT OF cursor_cust
FETCH NEXT FROM cursor_cust INTO #cust_id, #val_a, #val_b, #dynamic_val;
END
CLOSE cursor_cust
DEALLOCATE cursor_cust
SELECT * FROM #TblCust