TSQL Cursor calls Stored Procedure 'View Nesting Level Exceeded' - sql

Being a bit of a SQL Newb, I tried to create a SQL script to execute a stored procedure on a number of rows using a cursor. I found the code to create the cursor and the stored procedure works as expected.
However, sometimes during execution I get the error
Maximum stored procedure, function, trigger, or view nesting level exceeded (limit 32)
Here is the SQL script in question. Any ideas why I get this error?
CREATE PROCEDURE p_MigrateRenewalOptions
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
DECLARE #OrderId VARCHAR(255) = NULL
DECLARE #RenewalId INT = 0
DECLARE #DiscountCode VARCHAR(255) = NULL
DECLARE #UpgradeCode VARCHAR(255) = NULL
DECLARE #ProductCode VARCHAR(255) = NULL
DECLARE rCursor CURSOR FOR
SELECT RenewalId FROM t_Renewals WHERE DiscountCode IS NOT NULL AND UpgradeCode IS NOT NULL
OPEN rCursor
FETCH NEXT FROM rCursor INTO #RenewalId
-- Iterate over t_Renewals with DiscountCode, UpgradeCode
WHILE ##FETCH_STATUS = 0
BEGIN
SELECT #OrderId = OrderId from t_Renewals where RenewalId = #RenewalId
SELECT #DiscountCode = DiscountCode from t_Renewals where RenewalId = #RenewalId
SELECT #UpgradeCode = UpgradeCode from t_Renewals where RenewalId = #RenewalId
SELECT #ProductCode = ProductCode from t_Order Where OrderId = #OrderId
-- Create renewal options for the t_Renewal entry
EXEC p_SelectOrCreateRenewalOptions #OrderId
-- Migrate the DiscountCode, UpgradeCode from the renewal record
UPDATE t_Renewal_Option
SET CouponCode = #DiscountCode
WHERE RenewalId = #RenewalId AND OptionType = 0 AND CouponCode IS NULL
UPDATE t_Renewal_Option
SET CouponCode = #UpgradeCode
WHERE RenewalId = #RenewalId AND OptionType = 1 AND CouponCode IS NULL
-- NULL the Renewal record DiscountCode, UpgradeCode
UPDATE t_Renewals
SET DiscountCode = NULL, UpgradeCode = NULL
WHERE RenewalId = #RenewalId
FETCH NEXT FROM rCursor INTO #RenewalId
END
CLOSE rCursor
DEALLOCATE rCursor
END
GO
One thing I noted, that the statement
-- Create renewal options for the t_Renewal entry
EXEC p_SelectOrCreateRenewalOptions #OrderId
returns a set, e.g. if you run this code outside of a stored procedure, inside SQL Server Management Studio, you get several sets of results in the output window. Is this the cause? Is it possible to dump the data as I don't need the returned rows from p_SelectOrCreateRenewalOptions
EDIT: As requested, here is the code for p_SelectOrCreateRenewalOptions
CREATE PROCEDURE p_SelectOrCreateRenewalOptions
(
#OrderId VARCHAR(255) = NULL
)
AS
BEGIN
SET NOCOUNT ON
DECLARE #ProductCode VARCHAR(255);
SELECT #ProductCode = ProductCode FROM t_Order WHERE OrderId = #OrderId;
DECLARE #StaticOptionCount INT;
SET #StaticOptionCount = dbo.f_QueryRenewalOptionCount(#ProductCode);
DECLARE #OptionCount INT;
SELECT #OptionCount = COUNT(*) FROM t_Renewal_Option ro INNER JOIN t_Renewals r ON r.RenewalId = ro.RenewalId WHERE r.OrderId = #OrderId
IF (#OptionCount != #StaticOptionCount)
BEGIN
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION
SELECT #OptionCount = COUNT(*) FROM t_Renewal_Option ro INNER JOIN t_Renewals r ON r.RenewalId = ro.RenewalId WHERE r.OrderId = #OrderId
IF (#OptionCount != #StaticOptionCount)
BEGIN
DECLARE #RenewalId INT;
SELECT #RenewalId = RenewalId FROM t_Renewals r WHERE r.OrderId = #OrderId;
DELETE FROM t_Renewal_Option WHERE RenewalId = #RenewalId;
INSERT INTO t_Renewal_Option (RenewalId, OptionProductCode, OptionType, LastUpdated)
SELECT #RenewalId, sro.OptionProductCode, sro.OptionTypeId, GETDATE()
FROM t_Static_Renewal_Option sro
WHERE sro.ProductCode = #ProductCode
END
COMMIT TRANSACTION
END
SELECT ro.* FROM t_Renewal_Option ro INNER JOIN t_Renewals r ON r.RenewalId = ro.RenewalId WHERE r.OrderId = #OrderId
END
GO
f_QueryRenewalOptionCount simply does a select:
-- f_QueryRenewalOptionCount
SELECT #Count = COUNT(*) FROM t_Static_Renewal_Option o WHERE o.ProductCode = #ProductCode
Triggers wise, I don't think we have any triggers. There is a trigger on the t_Order table on INSERT but apart from that, nothing else.
UPDATE 12-SEP-14:
This 'works' but I don't understand the problem fully. I wrapped the EXEC call into a Begin/End Transaction
Note this script is called once to migrate some old part of the schema to a new part. Its not performance intensive and just has to 'work once'

Do you have a trigger on any of your tables? If an update trigger updates its own table, it will trigger itself again, which will update the table again, etc.
Do you use view or function anywhere in your code? Views can call on other views or functions, which call other views / functions.

The cursor here, while makes my skin crawl, is likely not the cause of the error your seeing. Without being able to see what the nested stored procedure is doing, this is just an educated guess, but that's probably where the issue lies.
The error you're seeing happens whenever a recursive SQL operation basically gets stuck in an infinite loop. The two ways I have had it happen is with a poorly written recursive CTE (basically a table which infinitely unions to itself) or when, as is more likely in this case, a store procedure calls a stored procedure which calls a stored procedure... and so on down the line. For instance (and I haven't tested this) if p_selectOrCreateRenewalOptions called p_MigrateRenewalOptions, you'd probably see a similar error.

Related

sql stored procedureExecution Timeout Expired.

i have couple of stored procedures in our sql server 2008 that gets executed one after another, im facing a sql error (Execution Timeout Expired. The timeout period elapsed prior to completion of the operation or the server is not responding) at the last one which updates or inserts against a 1 million records table while it works with smaller files but when updating or inserting this number of records it stops and throws that error , how can i tune the stored procedure, i tried to clean the server with sp_updatestates but no luck, i'd appreciate any help, thank you, here is my stored procedure code :
USE [DBTest]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[SP_UpdateData]
#ItemsExt dbo.TT_ItemsExt READONLY
AS
BEGIN
SET NOCOUNT ON;
Declare
#ItemID nvarchar(36)
,#DescE nvarchar(50)
,etc
,etc
---------------------------------------
DECLARE Item_cursor CURSOR FOR
SELECT
ItemID
,DescE
,etc
,etc
FROM #ItemsExt
OPEN Item_cursor
FETCH NEXT FROM Item_cursor INTO
#ItemID
,#DescE
,etc
,etc
WHILE ##FETCH_STATUS = 0
BEGIN
If exists (select * from Items WHERE ItemID = #ItemID)
Begin
Update dbo.Items SET
DescEng = #DescE
,etc = #etc
WHERE ItemID = #ItemID
End
ElSE
Begin
Declare #ItemNumber nvarChar(36)
SET #ItemNumber = REPLACE((REPLACE(RIGHT(#ItemID, LEN(#ItemID) - 1))
insert into dbo.Items
(ItemID , etc)
VALUES (#ItemID, etc)
End
--ItemsReplacment Table
If exists (select * from dbo.ItemsReplacement WHERE ItemID = #ItemID)
Begin
Update dbo.ItemsReplacement SET
ItemAlter= #AlternativeItem
,etc
WHERE ItemID = #ItemID
End
ElSE
Begin
Insert Into dbo.ItemsReplacement Values (
#ItemID
,etc
,etc)
End
FETCH NEXT FROM Item_cursor INTO
#ItemID
,#DescE
,etc
,etc
END
CLOSE Item_cursor;
DEALLOCATE Item_cursor;
END
ps: the timeout of execution is set to 0 meaning infinite.
Here's a better aproach to those updates and inserts:
ALTER PROCEDURE [dbo].[SP_UpdateData]
#ItemsExt dbo.TT_ItemsExt READONLY
AS
BEGIN
UPDATE IT
SET
DescEng = ITE.DescE,
etc = ITE.etc
FROM
dbo.Items IT
INNER JOIN ItemsExt ITE ON (IT.ItemID = ITE.ItemID);
INSERT INTO
dbo.Items
(
ItemID,
etc
)
SELECT
ITE.ItemID,
ITE.etc
FROM
dbo.ItemsExt ITE;
WHERE
NOT EXISTS (SELECT 1 FROM dbo.Items WHERE ItemID = ITE.ItemID);
UPDATE ITR
SET
ItemAlt = 0, --AlternativeItem?
etc = ITE.etc
FROM
dbo.ItemsReplacement ITR
INNER JOIN ItemsExt ITE ON (ITR.ItemID = ITE.ItemID);
INSERT INTO
dbo.ItemsReplacement
(
ItemID,
etc
)
SELECT
ITE.ItemID,
ITE.etc
FROM
dbo.ItemsExt ITE;
WHERE
NOT EXISTS (SELECT 1 FROM dbo.ItemsReplacement WHERE ItemID = ITE.ItemID);
END
That way you avoid unecessary cursor-loops and let the Database do it's magic on planning the execution.
And by the way: Shouldn't it have any kind of "restriction" on those updates and inserts? Are you planning on run all those tables on all executions?
!! On your original code you were using #AlternativeItem that I couldn't find anywhere. Used 0 in my response.

UDF don't give the same result

when I exececute the UDF out of the trigger the result is not the same
the UDF return always true when executing in the trigger
but out of the trigger the result is true or false
ALTER FUNCTION [dbo].[MandatExist]
(
#Numero int,
#IdBranche int,
#Exercice int
)
RETURNS bit
AS
BEGIN
DECLARE #Result bit
DECLARE #Nbr int
DECLARE #Categ int
SELECT #Categ = CategorieNumero
FROM Branche
WHERE IdBranche = #IdBranche
SELECT #Nbr=COUNT(*)
FROM Mandat AS M INNER JOIN Branche AS B ON M.IdBranche=B.IdBranche
WHERE (Numero = #Numero) AND (B.CategorieNumero = #Categ) AND (Exercice = #Exercice)
IF #Nbr = 0
SET #Result = 0
ELSE
SET #Result = 1
RETURN #Result
END
the trigger call MandatExist to get if the number exist or not
ALTER TRIGGER [dbo].[ValidInsertUpdate_Mandat]
ON [dbo].[Mandat]
FOR INSERT,UPDATE
AS
BEGIN
SET NOCOUNT ON;
DECLARE #Cloturer AS bit
DECLARE #Exercice AS int
DECLARE #IdBranche AS int
DECLARE #Numero AS int
DECLARE #Message AS nvarchar(100)
SELECT #Cloturer=Cloturer, #Exercice=Exercice, #Numero=Numero, #IdBranche=IdBranche
FROM INSERTED
IF (dbo.MandatExist(#Numero, #IdBranche, #Exercice)=1)
BEGIN
SET #Message = 'Numero de mandat existant.'
RAISERROR(#Message, 16, 1)
ROLLBACK TRAN
END
INSERTED is a table, thus may contain more than one row which means that this code
SELECT #Cloturer=Cloturer, #Exercice=Exercice, #Numero=Numero, #IdBranche=IdBranche
FROM INSERTED
is essentially incorrect.
UDFs are not the best choice for set-based programming and may cause performance degradation. Specifically this UDF makes absolutely no sense, has no reason for encapsulating this code into separate module. This is a trivial EXISTS code.
Entire function can and must be replaced by EXISTS statement, the whole code of the trigger might look like:
IF EXISTS(
SELECT 1
FROM INSERTED
INNER JOIN ...
WHERE ...
)
BEGIN
RAISERROR(...)
END
I'm not sure what's the meaning of your tables and columns, but I assume you are trying to check some uniqueness. So, assuming that you don't want another Mandat record for the same CategorieNumero, the final EXISTS might look like:
IF EXISTS(
SELECT 1
FROM INSERTED i
INNER JOIN Branch b on b.IdBranche = i.IdBranch
-- other branches with the same CategorieNumero; why isn't CategorieNumero unique?
INNER JOIN Branch b_dup on b_dup.CategorieNumero = b.CategorieNumero
-- existing Mandat rows for the same CategorieNumero with any IdBranch
INNER JOIN Mandat m_dup on m_dup = b_dup.IdBranch
-- ensure you're not comparing inserted/updated Mandat row to itself
WHERE i.ID != m_dup.ID
)
...
But your intent is unclear to me and I think that after clarification most of your needs will be easily satisfied by unique constraints.
If you don't want more that one row for each set of (Exercice, Numero, IdBranch) - just add a unique constraint to Mandat table and drop both trigger and function!
Don't overuse triggers. And UDFs.
i have used the solution of Ivan
IF EXISTS(
SELECT 1
FROM INSERTED I INNER JOIN Branche b ON b.IdBranche = i.IdBranche
INNER JOIN Branche b_dup ON b_dup.IdBranche = b.IdBranche
INNER JOIN Mandat m_dup on (m_dup.Exercice = i.Exercice) AND (m_dup.Numero = i.Numero) AND (b_dup.IdBranche=i.IdBranche)
WHERE i.IdMandat != m_dup.IdMandat
)
BEGIN
RAISERROR('error', 16, 1)
ROLLBACK TRAN
END

trigger to allow multiple updates

I have this trigger I want to make it allow multiple row updates, currently it handling only single row update.when i update record, it says sub query returning more then 1 value..
GO
ALTER TRIGGER [dbo].[OnReceiptUpdate]
ON [dbo].[paymentReceipt]
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON;
Declare #correctdate VARCHAR(19);
Declare #receiptNo VARCHAR(50);
DECLARE #customerID NCHAR(50)
SET #customerID= (SELECT customerID FROM inserted)
set #correctdate = (SELECT CONVERT(VARCHAR(19),paymentDate,103) FROM inserted)
set #receiptNo = (SELECT receiptNo FROM inserted)
BEGIN
UPDATE Paymentreceipt
SET paymentDate = #correctdate
WHERE customerID = #customerID and receiptNo=#receiptNo
END
END
Update p
Set p.paymentDate = CONVERT(VARCHAR(19),i.paymentDate,103)
From Paymentreceipt p
inner join inserted i
On p.customerID = i.customerID and p.receiptNo = i.receiptNo
should do it I think.
PS why is p.paymentdate a string? That's asking for it.
The easiest way is to use an update statement such as the one below in the Trigger.
UPDATE Paymentreceipt
SET paymentDate = CONVERT(VARCHAR(19),paymentDate,103)
FROM inserted
WHERE Inserted.receiptNo = Paymentreceipt.receiptNo
AND Inserted.customerID = Paymentreceipt.customerID
Note I don't have SQL server in front of me so the syntax might not be 100% correct but that gives you the general idea.
In general I try and avoid triggers but if your really need the trigger then use it but it may be possible to address this issue through the use of a stored procedure.

How to execute a Stored Procedure on the results of a SQL Server CTE

I have a CTE which returns DISTINCT ID's. I want to execute a scalar function on each of the Id's returned.
WITH cte (reqID) as
(SELECT DISTINCT pol.ReqID FROM
LOG_PackingListItems pli
JOIN
v_PO_LN pol on pol.PO_ID = pli.PoId
WHERE
pli.PackingListHeaderID = 1)
EXEC dbo.spUpdateLOG_ReqCompleteCheck reqID -- Error "Incorrect Syntax near EXEC"
The EXEC line is what I want to make work but I get a syntax error. Not sure if what I want to do is possible or if I do in fact have a syntax error. Any ideas?
EDIT:
I'm adding the code for the Stored Procedure since I am now using a Table-Valued Parameter as suggested by realnumber3012
EDIT:
I have changed my CTE code so it populates a Table-Type as realnumber has suggested. I now get an error when executing spUpdateLOG_ReqCompleteCheck "Subquery returns more than one value."
DECLARE #ReqIdTVP as ReqIdType;
DELETE FROM #ReqIDTVP;
with cte (reqID) as
(select distinct pol.ReqID from
LOG_PackingListItems pli
join
v_PO_LN pol on pol.PO_ID = pli.PoId
where
pli.PackingListHeaderID = #PackingListHeaderID)
INSERT INTO #ReqIdTVP
SELECT * FROM cte
EXEC dbo.spUpdateLOG_ReqCompleteCheck #ReqIdTVP
Sproc code :
Alter PROCEDURE spUpdateLOG_ReqCompleteCheck
(#ReqIdTVP ReqIdType READONLY )
AS
BEGIN
DECLARE #TotalOrd int
DECLARE #TotalRx int
DECLARE #ReqID char(8)
SET #ReqID = (SELECT ReqID FROM #ReqIdTVP)
SET #TotalOrd = (SELECT ISNULL(SUM(ORD_QTY),0)
FROM dbo.v_PoLnNonFreight l
WHERE l.ReqID = #reqID)
SET #TotalRx = (SELECT ISNULL(SUM(TotalRxSite),0)
FROM dbo.v_PoLnNonFreight l
WHERE l.ReqID = #reqID)
IF #TotalRx >= #TotalOrd
BEGIN
DECLARE #curDate datetime
SET #CurDate = ISNULL(#CurDate,GetDate())
SET NOCOUNT ON;
UPDATE LOG_ReqHeader
SET
ReqCompleteDate = #curDate,
ReqStatus = 'Complete'
WHERE ReqID = #ReqID
END
END
Seems that the only thing your stored proc does is to update a logging table: (it only changes state via this statement and doesn't return anything????
UPDATE LOG_ReqHeader
SET
ReqCompleteDate = #curDate,
ReqStatus = 'Complete'
WHERE ReqID = #ReqID
How about splitting the logic out and write a function (inline if possible that will evaluate the condition you are looking for (didn't really understand what you are doind there) -- run the function on the results of the CTE (wrapping it in another CTE if you want) with the CROSS APPLY OPERATOR.
You'd end up with a result set that looks like [ReqId], [UpdateLog] (where updateLog is a BIT)
Then simply do a set based upadete JOINING to the results:
UPDATE l SET
ReqCompleteDate = #curDate,
ReqStatus = 'Complete'
FROM
LOG_ReqHeader AS l
JOIN <CTE> AS c ON c.[ReqID] = l.[ReqID]
WHERE
c.[UpdateLog] = 0x1
Does this make any sense?

Modify SQL trigger to work when inserted table contains more than one row

I've got a SQL trigger written for a table in SQL Server 2008. It works well when there is only one row in the 'inserted' table. How can I modify this trigger to work correctly when there are multiple rows? Performance is key here, so I'd like to stay away from cursors, temp tables, etc. (if possible).
Essentially the trigger checks to see if either the 'ClientID' or 'TemplateID' fields were changed. If they were, and the OriginalClientID or OriginalTemplateID fields are null, it populates them (thus setting the OriginalXXX fields once and only once so I can always see what the first values were).
CREATE TRIGGER [dbo].[trigSetOriginalValues]
ON [dbo].[Review]
FOR INSERT, UPDATE
AS
BEGIN
IF (NOT UPDATE(TemplateID) AND NOT UPDATE(ClientID)) return
DECLARE #TemplateID int
DECLARE #OriginalTemplateID int
DECLARE #ClientID int
DECLARE #OriginalClientID int
DECLARE #ReviewID int
SET #ReviewID = (SELECT ReviewID FROM inserted)
SET #ClientID = (SELECT ClientID FROM inserted)
SET #TemplateID = (SELECT TemplateID FROM inserted)
SET #OriginalTemplateID = (SELECT OriginalTemplateID FROM inserted);
SET #OriginalClientID = (SELECT OriginalClientID FROM inserted);
IF (#OriginalTemplateID IS NULL AND #TemplateID IS NOT NULL)
BEGIN
UPDATE [dbo].[Review] SET OriginalTemplateID = #TemplateID WHERE ReviewID=#ReviewID
END
IF (#OriginalClientID IS NULL AND #ClientID IS NOT NULL)
BEGIN
UPDATE [dbo].[Review] SET OriginalClientID = #ClientID WHERE ReviewID=#ReviewID
END
END
This should be your trigger:
UPDATE A
SET A.OriginalTemplateID = B.TemplateID
FROM [dbo].[Review] A
INNER JOIN INSERTED B
ON A.ReviewID = B.ReviewID
WHERE A.OriginalTemplateID IS NULL AND B.TemplateID IS NOT NULL
UPDATE A
SET A.OriginalClientID = B.ClientID
FROM [dbo].[Review] A
INNER JOIN INSERTED B
ON A.ReviewID = B.ReviewID
WHERE A.OriginalClientID IS NULL AND B.ClientID IS NOT NULL
Though you could still do this on a single UPDATE, but with a more complicated filter.