SQL Server: the ROLLBACK TRANSACTION request has no corresponding BEGIN TRANSACTION - sql-server

I have a trigger that works (it fires when it has to) but I still get an error.
I understand the error but I don't know how to resolve it.
I tried to put some BEGIN TRANSACTION with all the code who go with it but I think my grammar is wrong because I always get a timeout!
So my question is, where exactly do I have to put my BEGIN TRANSACTION statements in my code?
Also, do I need 3 BEGIN TRANSACTION statements since I have 3 ROLLBACK?
Thank you in advance!
My code:
ALTER TRIGGER [dbo].[Tr_CheckOverlap]
ON [dbo].[Tranche]
FOR INSERT
AS
BEGIN
SET NOCOUNT ON;
DECLARE #IdVol INT, #IdTranche INT,
#AgeMinInserted DATE, #AgeMaxInserted DATE
SELECT #AgeMinInserted = t.TRA_Age_Min
FROM Tranche t
JOIN inserted AS i ON t.TRA_Id = i.TRA_Id
SELECT #AgeMaxInserted = t.TRA_Age_Max
FROM Tranche t
JOIN inserted AS i ON t.TRA_Id = i.TRA_Id
DECLARE CR_TrancheVol CURSOR FOR
SELECT t.TRA_Vol_Id,t.TRA_Id
FROM Tranche t
JOIN inserted AS i ON t.TRA_Vol_Id = i.TRA_Vol_Id;
OPEN CR_TrancheVol
FETCH CR_TrancheVol INTO #IdVol, #IdTranche
WHILE( ##FETCH_STATUS = 0)
BEGIN
DECLARE #AgeMin DATE, #AgeMax DATE
SELECT #AgeMin = t.TRA_Age_Min
FROM Tranche t
WHERE t.TRA_Id = #IdTranche
SELECT #AgeMax = t.TRA_Age_Max
FROM Tranche t
WHERE t.TRA_Id = #IdTranche
IF #AgeMinInserted > #AgeMin AND #AgeMinInserted < #AgeMax
BEGIN
PRINT 'Trans1'
RAISERROR('Overlap: Date de naissance minimum déjà couverte', 1, 420)
ROLLBACK TRANSACTION
END
IF #AgeMaxInserted > #AgeMin AND #AgeMaxInserted < #AgeMax
BEGIN
PRINT 'Trans2'
RAISERROR('Overlap: Date de naissance maximum déjà couverte', 1, 421)
ROLLBACK TRANSACTION
END
IF #AgeMinInserted < #AgeMin AND #AgeMaxInserted > #AgeMax
BEGIN
PRINT 'Trans3'
RAISERROR('Overlap: Tranche déjà couverte complètement', 1, 422)
ROLLBACK TRANSACTION
END
FETCH CR_TrancheVol INTO #IdVol, #IdTranche
END
CLOSE CR_TrancheVol
DEALLOCATE CR_TrancheVol
END
EDIT:
Okay, so I tried your answer without cursor (I understand that my way was clearly not the best!) but for now it doesn't work.
My goal: I have a DB to book a flight. In this DB, i have a table "Tranche" who contains some dates and some prices (depending when the flight is).
I need to prevent and avoid any overlap of birthdate, for example:
1y-17y: 80€
18y-64y: 120€
So my trigger has to fire when I try to insert 17y-63y: xx € (because I already have a price for those ages).
Sorry if my English is not perfect btw!
Here's my table "Tranche":
https://i.stack.imgur.com/KuQH8.png
TRA_Vol_ID is a foreign key of another table "Vol" who contain the flights
Here's the code I have atm:
ALTER TRIGGER [dbo].[Tr_CheckOverlap]
ON [dbo].[Tranche]
FOR INSERT
AS
BEGIN
/*
Some SQL goes here to get the value of Minimum age.
I assuming that it doesn't vary by entry, however,
I don't really have enough information to go on to tell
*/
SET NOCOUNT ON;
DECLARE #MinAge DATE, #MaxAge DATE
SELECT #MinAge = t.TRA_Age_Min
FROM Tranche t
JOIN Vol AS v ON v.VOL_Id = t.TRA_Vol_Id
JOIN inserted AS i ON t.TRA_Id = i.TRA_Id
WHERE t.TRA_Id = i.TRA_Id
SELECT #MaxAge = t.TRA_Age_Max
FROM Tranche t
JOIN inserted AS i ON t.TRA_Id = i.TRA_Id
JOIN Vol AS v ON v.VOL_Id = t.TRA_Vol_Id
WHERE t.TRA_Id = i.TRA_Id
IF (SELECT COUNT(CASE WHEN i.TRA_Age_Min > #MinAge AND i.TRA_Age_Min < #MaxAge THEN 1 END) FROM inserted i) > 0
BEGIN
RAISERROR('Overlap: Birthday min reached',1,430);
ROLLBACK
END
ELSE IF (SELECT COUNT(CASE WHEN i.TRA_Age_Max > #MinAge AND i.TRA_Age_Max < #MaxAge THEN 1 END) FROM inserted i) > 0
BEGIN
RAISERROR('Overlap: Birthday max reached',1,430);
ROLLBACK
END
END

I don't really know what the OP's goals are here. However, I wanted to post a small example how to do a dataset approach, and how to check all the rows in one go.
At the moment, the trigger the OP has will only "work" if the user is inserting 1 row. Any more, and things aren't going to work properly. Then we also have the problem of the CURSOR. I note that the declaration of the cursors aren't referencing inserted at all, so I don't actually know what their goals are. It seems more like the OP is auditing the data already in the table when a INSERT occurs, not the data that is being inserted. This seems very odd.
Anyway, this isn't a solution for the OP, however, I don't have enough room in a comment to put all this. Maybe it'll push the OP in the right direction.
ALTER TRIGGER [dbo].[Tr_CheckOverlap]
ON [dbo].[Tranche]
FOR INSERT
AS
BEGIN
/*
Some SQL goes here to get the value of Minimum age.
I assuming that it doesn't vary by entry, however,
I don't really have enough information to go on to tell
*/
IF (SELECT COUNT(CASE WHEN i.Age < #MinAge THEN 1 END) FROM inserted i) > 0 BEGIN
RAISERROR('Age too low',1,430);
ROLLBACK
END
ELSE
IF (SELECT COUNT(CASE WHEN i.Age > #MaxAge THEN 1 END) FROM inserted i) > 0 BEGIN
RAISERROR('Age too high',1,430);
ROLLBACK
END
END
The question at hand seems to very much be an xy question; the problem isn't the CURSOR or the ROLLBACK, the problems with this trigger are much more fundamental. I'd suggest revising your question and actually explaining your goal of what you want to do with your Trigger. Provide DDL to CREATE your table and INSERT statements for any sample data. You might want to also provide some INSERT statements that will have different results for your trigger (make sure to include ones that have more than one row to be inserted at a time).
I realise this is more commenting, however, again, there is definitely not enough room in a comment for me to write all this. :)

Related

Trigger is not working properly in SQL Server

I understand that perhaps the problem is that I use a select on the same table that I update or insert a record, but this trigger throws an exception in most cases. Then what should I rewrite?
The purpose of the trigger is to block inserting or updating entries if the room is already occupied on a certain date, i.e. the dates overlap
CREATE TABLE [dbo].[settlements]
(
[id] [int] IDENTITY(1,1) NOT NULL,
[client_id] [int] NOT NULL,
[checkin_date] [date] NOT NULL,
[checkout_date] [date] NOT NULL,
[room_id] [int] NOT NULL,
[employee_id] [int] NULL
);
ALTER TRIGGER [dbo].[On_Hotel_Settlement]
ON [dbo].[settlements]
AFTER INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
DECLARE #room_id int
DECLARE #checkin_date Date, #checkout_date Date
DECLARE cursor_settlement CURSOR FOR
SELECT room_id, checkin_date, checkout_date
FROM inserted;
OPEN cursor_settlement;
FETCH NEXT FROM cursor_settlement INTO #room_id, #checkin_date, #checkout_date;
WHILE ##FETCH_STATUS = 0
BEGIN
IF EXISTS (SELECT 1
FROM settlements AS s
WHERE s.room_id = #room_id
AND ((s.checkin_date >= #checkin_date AND s.checkin_date <= #checkout_date)
OR (s.checkout_date >= #checkin_date AND s.checkout_date <= #checkout_date)))
BEGIN
RAISERROR ('Room is not free', 16, 1);
ROLLBACK;
END;
FETCH NEXT FROM cursor_settlement INTO #room_id, #checkin_date, #checkout_date;
END;
CLOSE cursor_settlement;
DEALLOCATE cursor_settlement;
RETURN
I tried to test the code by removing the condition with dates and leaving only the room _id, but the trigger does not work correctly in this case either.
I tried query like
IF EXISTS (SELECT 1
FROM settlements AS s
WHERE s.room_id = 9
AND ((s.checkin_date >= '2022-12-10' AND s.checkin_date <= '2022-12-30')
OR (s.checkout_date >= '2022-12-10' AND s.checkout_date <= '2022-12-30')))
BEGIN
RAISERROR ('Room is not free', 16, 1);
END;
and it worked correctly. Problem is that is not working in my trigger
As noted by comments on the original post above, the cursor loop is not needed and would best be eliminated to improve efficiency.
As for the date logic, consider a new record with a check-in date that is the same as the checkout date from a prior record. I believe that your logic will consider this an overlap and throw an error.
My suggestion is that you treat the check-in date as inclusive (that night is in use) and the checkout date as exclusive (that night is not in use).
A standard test for overlapping dates would then be Checkin1 < Checkout2 AND Checkin2 < Checkout1. (Note use of inequality.) It may not be obvious, but this test covers all overlapping date cases. (It might be more obvious if this condition is inverted and rewritten as NOT (Checkin1 >= Checkout2 OR Checkin2 >= Checkout1).)
Also, if you are inserting multiple records at once, I would suggest that you also check the inserted records for mutual conflicts.
Suggest something like:
ALTER TRIGGER [dbo].[On_Hotel_Settlement]
ON [dbo].[settlements]
AFTER INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON;
IF EXISTS(
SELECT *
FROM inserted i
JOIN settlements s
ON s.room_id = i.room_id
AND s.checkin_date < i.checkout_date
AND i.checkin_date < s.checkout_date
AND s.id <> i.id
)
BEGIN
RAISERROR ('Room is not free', 16, 1);
ROLLBACK;
END;
RETURN;
END
One more note: Be careful with an early rollback of a transaction. If your overall logic could potentially execute additional DML after the error is thrown, that would now execute outside the transaction and there would be no remaining transaction to roll back.

How to read updated data in a stored procedure called multiple times simultaneously

There are 2 tables:
Wallets;
Transactions.
There is a stored procedure that handles (I think with ACID operation):
updating on Wallet table
inserting one row into Transactions table
every time it is called.
The issue occurs when there are many calls to the SP at same time, infact the value of PreviousBalance is not correct (sequentially wrong), cause in the SP read old value, meantime another process of call is running.
To understand better look the following screenshot.
There are 3 Transaction with same DT (IDs 1289, 1288, 1287), in all of those PreviouseBalance is equal, but is not correct, because the value for :
Trx ID 1288 should be 180,78 as Balance of previous row;
Trx ID 1289 should be 168,07 = 180,78 - 12,08
I think that the issue is in the SET of #OLDBalance var; at same time those 3 thread read same value, so when the SP goes to INSERT loads same value of PreviousBalance.
How can I do in order to read #OLDBalance correct after commit of one operation?
I tried to set several type of Isolation Levet into SP, the result was the same and sometime went in error for deadlock.
I have the following stored procedure:
Stored Procedure
ALTER PROCEDURE [dbo].[upsMovimenta_internal]
#AccountID int,
#Amount money,
#TypeTransactionID int,
#ProductID int,
#notes nvarchar(max)
AS
BEGIN
SET NOCOUNT ON;
DECLARE #OLDBalance MONEY;
DECLARE #PreviousBalance MONEY;
DECLARE #CurrentBalance MONEY;
DECLARE #Molt float;
BEGIN TRANSACTION;
IF NOT EXISTS( SELECT * FROM Accounts WHERE AccountID = #AccountID)
BEGIN
RaisError(N'Account not found ', 10, 1, N'number', 3);
return (1)
END
SELECT #Molt = Moltiplicatore
FROM TypeTransactions
where TypeTransactionID = #TypeTransactionID
;
IF (#Molt is null )
BEGIN
RaisError(N'Error transaction', 10, 1, N'number', 3);
return (1)
END
SET #Amount = #Amount * #Molt;
--SELECT * FROM Wallets
SELECT TOP 1 #OLDBalance = TotalAmount
FROM Wallets
where AccountID = #AccountID
;
SET #CurrentBalance = #OLDBalance + #Amount;
IF (#ProductID = 1 )
BEGIN
UPDATE Wallets
SET TotalAmount+=#Amount,
Cash+=#Amount
FROM Wallets where AccountID = #AccountID
;
END
IF (#ProductID = 2 )
BEGIN
UPDATE Wallets
SET TotalAmount+=#Amount,
Fun+=#Amount
FROM Wallets where AccountID = #AccountID
;
END
INSERT INTO Transactions
( AccountID, ProductID, DT, TypeTransactionID, Amout, Balance, PreviousBalance, Notes )
VALUES
( #AccountID, #ProductID, GETDATE(), #TypeTransactionID, #Amount, #CurrentBalance, #OLDBalance, #notes)
;
COMMIT TRANSACTION;
return (0)
END
Thank you so much guys
Generally, one way of managing locks on records, is to apply a dummy update on the rows you want to work on, right after starting transaction.
In this case SQL Server guarantees that those rows will be locked and no other transactions can access the rows. So you can change your design to something like this:
begin tran
update myTable
set Field1 = Field1
where someKeyField = 212
-- do the same for other tables that you want to protect against change
-- at this moment all working rows will be locked, other procedure calls will be on hold
-- do your main operations here
commit tran
The issue with this will be the other proc calls will wait and this may degrade performance or even time-out if the traffic is high and your operation in this proc is lengthy
If you are working on high transaction environment, you need to change your design.
Update: Design Suggestion
I don't get why you have PreviousBalance and Balance in your transaction (it is against the design rules, however you can override rule in special case).
Probably you have that to speed up your calculations or make your queries simpler. But it is not good practice in OLTP database.
Rules say you keep the Amount column and calculate PreviousBalance and Balance somewhere else.
You should drop PreviousBalance but keep the Balance column, and every time you insert a transaction, you update (increase/decrease) the Balance column. Plus you need to initialize the Balance column at the first transaction.
This is what I can think of. If I knew your whole system, I would be able to have better ideas though.

Truncate some rows in table SQL Server Management Studio [duplicate]

Let's say we have table Sales with 30 columns and 500,000 rows. I would like to delete 400,000 in the table (those where "toDelete='1'").
But I have a few constraints :
the table is read / written "often" and I would not like a long "delete" to take a long time and lock the table for too long
I need to skip the transaction log (like with a TRUNCATE) but while doing a "DELETE ... WHERE..." (I need to put a condition), but haven't found any way to do this...
Any advice would be welcome to transform a
DELETE FROM Sales WHERE toDelete='1'
to something more partitioned & possibly transaction log free.
Calling DELETE FROM TableName will do the entire delete in one large transaction. This is expensive.
Here is another option which will delete rows in batches :
deleteMore:
DELETE TOP(10000) Sales WHERE toDelete='1'
IF ##ROWCOUNT != 0
goto deleteMore
I'll leave my answer here, since I was able to test different approaches for mass delete and update (I had to update and then delete 125+mio rows, server has 16GB of RAM, Xeon E5-2680 #2.7GHz, SQL Server 2012).
TL;DR: always update/delete by primary key, never by any other condition. If you can't use PK directly, create a temp table and fill it with PK values and update/delete your table using that table. Use indexes for this.
I started with solution from above (by #Kevin Aenmey), but this approach turned out to be inappropriate, since my database was live and it handles a couple of hundred transactions per second and there was some blocking involved (there was an index for all there fields from condition, using WITH(ROWLOCK) didn't change anything).
So, I added a WAITFOR statement, which allowed database to process other transactions.
deleteMore:
WAITFOR DELAY '00:00:01'
DELETE TOP(1000) FROM MyTable WHERE Column1 = #Criteria1 AND Column2 = #Criteria2 AND Column3 = #Criteria3
IF ##ROWCOUNT != 0
goto deleteMore
This approach was able to process ~1.6mio rows/hour for updating and ~0,2mio rows/hour for deleting.
Turning to temp tables changed things quite a lot.
deleteMore:
SELECT TOP 10000 Id /* Id is the PK */
INTO #Temp
FROM MyTable WHERE Column1 = #Criteria1 AND Column2 = #Criteria2 AND Column3 = #Criteria3
DELETE MT
FROM MyTable MT
JOIN #Temp T ON T.Id = MT.Id
/* you can use IN operator, it doesn't change anything
DELETE FROM MyTable WHERE Id IN (SELECT Id FROM #Temp)
*/
IF ##ROWCOUNT > 0 BEGIN
DROP TABLE #Temp
WAITFOR DELAY '00:00:01'
goto deleteMore
END ELSE BEGIN
DROP TABLE #Temp
PRINT 'This is the end, my friend'
END
This solution processed ~25mio rows/hour for updating (15x faster) and ~2.2mio rows/hour for deleting (11x faster).
What you want is batch processing.
While (select Count(*) from sales where toDelete =1) >0
BEGIN
Delete from sales where SalesID in
(select top 1000 salesId from sales where toDelete = 1)
END
Of course you can experiment which is the best value to use for the batch, I've used from 500 - 50000 depending on the table. If you use cascade delete, you will probably need a smaller number as you have those child records to delete.
One way I have had to do this in the past is to have a stored procedure or script that deletes n records. Repeat until done.
DELETE TOP 1000 FROM Sales WHERE toDelete='1'
You should try to give it a ROWLOCK hint so it will not lock the entire table. However, if you delete a lot of rows lock escalation will occur.
Also, make sure you have a non-clustered filtered index (only for 1 values) on the toDelete column. If possible make it a bit column, not varchar (or what it is now).
DELETE FROM Sales WITH(ROWLOCK) WHERE toDelete='1'
Ultimately, you can try to iterate over the table and delete in chunks.
Updated
Since while loops and chunk deletes are the new pink here, I'll throw in my version too (combined with my previous answer):
SET ROWCOUNT 100
DELETE FROM Sales WITH(ROWLOCK) WHERE toDelete='1'
WHILE ##rowcount > 0
BEGIN
SET ROWCOUNT 100
DELETE FROM Sales WITH(ROWLOCK) WHERE toDelete='1'
END
My own take on this functionality would be as follows.
This way there is no repeated code and you can manage your chunk size.
DECLARE #DeleteChunk INT = 10000
DECLARE #rowcount INT = 1
WHILE #rowcount > 0
BEGIN
DELETE TOP (#DeleteChunk) FROM Sales WITH(ROWLOCK)
SELECT #rowcount = ##RowCount
END
I have used the below to delete around 50 million records -
BEGIN TRANSACTION
DeleteOperation:
DELETE TOP (BatchSize)
FROM [database_name].[database_schema].[database_table]
IF ##ROWCOUNT > 0
GOTO DeleteOperation
COMMIT TRANSACTION
Please note that keeping the BatchSize < 5000 is less expensive on resources.
As I assume the best way to delete huge amount of records is to delete it by Primary Key. (What is Primary Key see here)
So you have to generate tsql script that contains the whole list of lines to delete and after this execute this script.
For example code below is gonna generate that file
GO
SET NOCOUNT ON
SELECT 'DELETE FROM DATA_ACTION WHERE ID = ' + CAST(ID AS VARCHAR(50)) + ';' + CHAR(13) + CHAR(10) + 'GO'
FROM DATA_ACTION
WHERE YEAR(AtTime) = 2014
The ouput file is gonna have records like
DELETE FROM DATA_ACTION WHERE ID = 123;
GO
DELETE FROM DATA_ACTION WHERE ID = 124;
GO
DELETE FROM DATA_ACTION WHERE ID = 125;
GO
And now you have to use SQLCMD utility in order to execute this script.
sqlcmd -S [Instance Name] -E -d [Database] -i [Script]
You can find this approach explaned here https://www.mssqltips.com/sqlservertip/3566/deleting-historical-data-from-a-large-highly-concurrent-sql-server-database-table/
Here's how I do it when I know approximately how many iterations:
delete from Activities with(rowlock) where Id in (select top 999 Id from Activities
(nolock) where description like 'financial data update date%' and len(description) = 87
and User_Id = 2);
waitfor delay '00:00:02'
GO 20
Edit: This worked better and faster for me than selecting top:
declare #counter int = 1
declare #msg varchar(max)
declare #batch int = 499
while ( #counter <= 37600)
begin
set #msg = ('Iteration count = ' + convert(varchar,#counter))
raiserror(#msg,0,1) with nowait
delete Activities with (rowlock) where Id in (select Id from Activities (nolock) where description like 'financial data update date%' and len(description) = 87 and User_Id = 2 order by Id asc offset 1 ROWS fetch next #batch rows only)
set #counter = #counter + 1
waitfor delay '00:00:02'
end
Declare #counter INT
Set #counter = 10 -- (you can always obtain the number of rows to be deleted and set the counter to that value)
While #Counter > 0
Begin
Delete TOP (4000) from <Tablename> where ID in (Select ID from <sametablename> with (NOLOCK) where DateField < '2021-01-04') -- or opt for GetDate() -1
Set #Counter = #Counter -1 -- or set #counter = #counter - 4000 if you know number of rows to be deleted.
End

Trigger with a RAISERROR and ELSE case issue

I am trying to make a bit of code that takes in 2 separate columns, a month and a year. From there I want it to see if those numbers entered have already passed or not. If they have passed, cause an error to pass and stop the transaction. Otherwise, I want it to continue on and insert new information into the table. I know I am close on getting this to work, but I cant seem to get the RAISERROR to fire. I am sure it has to do with the fact I am pretty new at this and I am missing some small detail.
Currently I am taking the two months in as variables and the making a third variable to use to turn the other two into a proper datetime format. Then I use the datediff function to try and see if it has passed that way. To no avail though. I keep getting the insert function going, even if the card date is old.
USE AdventureWorks2012
GO
CREATE TRIGGER BadCreditCardDate
ON Sales.CreditCard
INSTEAD OF INSERT
AS
Begin
DECLARE #ExpMonth tinyint,
#ExpYear smallint,
#ExpMonthYear datetime
SELECT #ExpMonth=ExpMonth,
#ExpYear=ExpYear,
#ExpMonthYear = #ExpYear + '-' + #ExpMonth + '-00'
FROM INSERTED
IF
DATEDIFF(MONTH,#ExpMonthYear,GETDATE()) < 0
BEGIN
RAISERROR ('The Credit Card you have entered has expired.' ,10,1)
ROLLBACK TRANSACTION
END
ELSE
Begin
INSERT INTO CreditCard (CardType, CardNumber, ExpMonth, ExpYear, ModifiedDate)
Select CardType, CardNumber, ExpMonth, ExpYear, ModifiedDate FROM inserted
END
End
I think there is a simpler way to check for expiration:
CREATE TRIGGER BadCreditCardDate
ON Sales.CreditCard
INSTEAD OF INSERT
AS
BEGIN
IF EXISTS (
SELECT 1
FROM inserted
WHERE (YEAR(GETDATE()) > ExpYear) OR (YEAR(GETDATE()) = ExpYear AND MONTH(GETDATE()) > ExpMonth)
)
BEGIN
RAISERROR ('The Credit Card you have entered has expired.' ,10,1)
ROLLBACK TRANSACTION
END
ELSE
BEGIN
INSERT INTO CreditCard (CardType, CardNumber, ExpMonth, ExpYear, ModifiedDate)
SELECT CardType, CardNumber, ExpMonth, ExpYear, ModifiedDate
FROM inserted
END
END
In this way you effectively check every record to be inserted in CreditCard.

TSQL Trigger Not Saving Variables and/or not Executing Properly

I've having trouble getting a TSQL trigger to even work correctly. I've run it through the debugger and it's not setting any of the variables according to SQL Server Management Studio. The damnedest thing is that the trigger itself is executing correctly and there are no errors when it is executed (just says 'execution successful').
The code is as follows (it's a work in progress.... just getting my self familiar):
USE TestDb
IF EXISTS (SELECT name FROM sysobjects
WHERE name = 'OfficeSalesQuotaUpdate' AND type = 'TR')
DROP TRIGGER OfficeSalesQuotaUpdate
GO
CREATE TRIGGER OfficeSalesQuotaUpdate
ON SalesReps
AFTER UPDATE, DELETE, INSERT
AS
DECLARE #sales_difference int, #quota_difference int
DECLARE #sales_original int, #quota_original int
DECLARE #sales_new int, #quota_new int
DECLARE #officeid int
DECLARE #salesrepid int
--UPDATE(Sales) returns true for INSERT and UPDATE.
--Not for DELETE though.
IF ((SELECT COUNT(*) FROM inserted) = 0)
SET #salesrepid = (SELECT SalesRep FROM deleted)
ELSE
SET #salesrepid = (SELECT SalesRep FROM inserted)
--If you address the #salesrepid variable, it does not work. Doesn't even
--print out the 'this should work line.
PRINT 'This should work...' --+ convert(char(30), #salesrepid)
IF (#salesrepid = NULL)
PRINT 'SalesRepId is null'
ELSE
PRINT 'SalesRepId is not null'
PRINT convert(char(50), #salesrepid)
SET #officeid = (SELECT RepOffice
FROM SalesReps
WHERE SalesRep = #salesrepid)
SELECT #sales_original = (SELECT Sales FROM deleted)
SELECT #sales_new = (SELECT Sales FROM inserted)
--Sales can not be null, so we'll remove this later.
--Use this as a template for quota though, since that can be null.
IF (#sales_new = null)
BEGIN
SET #sales_new = 0
END
IF (#sales_original = 0)
BEGIN
SET #sales_original = 0
END
SET #sales_difference = #sales_new - #sales_original
UPDATE Offices
SET Sales = Sales + #sales_difference
WHERE Offices.Office = #officeid
GO
So, any tips? I've completely stumped on this one. Thanks in advance.
Your main problem seems to be that there is a difference between #foo = NULL and #foo IS NULL:
declare #i int
set #i = null -- redundant, but explicit
if #i = null print 'equals'
if #i is null print 'is'
The 'This should work' PRINT statement doesn't work because concatenating a NULL with a string gives a NULL, and PRINT NULL doesn't print anything.
As for actually setting the value of #salerepid, it seems most likely that the inserted and/or deleted table is in fact empty. What statements are you using to test the trigger? And have you printed out the COUNT(*) value?
You should also consider (if you haven't already) what happens if someone changes more than one row at once. Your current code assumes that only one row is changed at a time, which may be a reasonable assumption in your environment, but it can easily break if someone bulk loads data or does other 'batch processing'.
Finally, you should always mention your MSSQL version and edition; it can be relevant for some syntax questions.
You should replace the body of the trigger with something like this:
;WITH Totals AS (
SELECT RepOffice,SUM(Sales) as Sales FROM inserted GROUP BY RepOffice
UNION ALL
SELECT RepOffice,-SUM(Sales) FROM deleted GROUP BY RepOffice
), SalesDelta AS (
SELECT RepOffice,SUM(Sales) as Delta FROM Totals GROUP BY RepOffice
)
UPDATE o
SET Sales = Sales + sd.Delta
FROM
Offices o
inner join
SalesDelta sd
on
o.Office = sd.RepOffice
This will adequately cope with multiple rows in inserted and deleted. I'm assuming SalesRep is the primary key of the SalesReps table.
Updated above, to cope with UPDATE changing the RepOffice of a particular Sales Rep (which the original doesn't, presumable, get correct either)
Just a suggestion...have you tried putting BEGIN and END to encapsulate the 'AS' part of your trigger?

Resources