UDF don't give the same result - sql

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

Related

After Update Trigger puzzlement

I am trying to get my head round an AFTER UPDATE trigger.
Currently in our DB there is a Trigger that contains a cursor. From my understanding cursors in triggers are generally bad performing, so I'm trying to get rid of the cursor.
Currently the trigger looks like this:
ALTER TRIGGER [dbo].[trg_TaskMovement_Zone] ON [dbo].[Tasks_Movement]
AFTER INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON
DECLARE #rowcheck int
DECLARE #MovementID INT
DECLARE #SiteFromID INT
DECLARE #SiteToID INT
DECLARE #SiteResponsibleID INT
DECLARE #FromAddress_Postcode Varchar(20)
DECLARE #ToAddress_Postcode Varchar(20)
DECLARE zcursor CURSOR FOR SELECT ID, SiteFromID, SiteToID, SiteResponsibleID
, FromAddress_Postcode, ToAddress_Postcode FROM inserted
OPEN zcursor
SELECT #rowcheck=1
WHILE #rowcheck=1
BEGIN
FETCH NEXT FROM zcursor INTO #MovementID, #SiteFromID, #SiteToID, #SiteResponsibleID, #FromAddress_Postcode, #ToAddress_Postcode
IF (##FETCH_STATUS = 0)
BEGIN
UPDATE Tasks_Movement
SET ZoneFromID = dbo.fn_GetZoneFromPostcode(#FromAddress_Postcode),
ZoneToID = dbo.fn_GetZoneFromPostcode(#ToAddress_Postcode)
WHERE Tasks_Movement.ID = #MovementID
UPDATE Tasks_Movement
SET SiteResponsibleID = [dbo].[fn_GetDefaultDepotResponsibleForSite](#SiteFromID)
WHERE Tasks_Movement.ID = #MovementID
AND (#SiteResponsibleID Is NULL OR #SiteResponsibleID=0)
AND (#SiteFromID Is NOT NULL AND #SiteFromID>0)
UPDATE Tasks_Movement
SET SiteResponsibleID = [dbo].[fn_GetDefaultDepotResponsibleForSite](#SiteToID)
WHERE Tasks_Movement.ID = #MovementID
AND (#SiteResponsibleID Is NULL OR #SiteResponsibleID=0)
AND (#SiteToID Is NOT NULL AND #SiteToID>0)
END
ELSE
SELECT #rowcheck=0
END
CLOSE zcursor
DEALLOCATE zcursor
END
From what I can tell the cursor in this is completely unnecessary(?)
Would I be right in thinking that the following would work better:
ALTER TRIGGER [dbo].[trg_TaskMovement_Zone] ON [dbo].[Tasks_Movement]
AFTER INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON
UPDATE Tasks_Movement
SET ZoneFromID = dbo.fn_GetZoneFromPostcode(inserted.FromAddress_Postcode),
ZoneToID = dbo.fn_GetZoneFromPostcode(inserted.ToAddress_Postcode)
FROM inserted
WHERE Tasks_Movement.ID IN (SELECT id FROM inserted)
UPDATE Tasks_Movement
SET SiteResponsibleID = [dbo].[fn_GetDefaultDepotResponsibleForSite](inserted.SiteFromID)
FROM inserted
WHERE Tasks_Movement.ID IN (SELECT id FROM inserted
WHERE (inserted.SiteResponsibleID Is NULL OR inserted.SiteResponsibleID=0)
AND (inserted.SiteFromID Is NOT NULL AND inserted.SiteFromID>0))
UPDATE Tasks_Movement
SET SiteResponsibleID = [dbo].[fn_GetDefaultDepotResponsibleForSite](#SiteToID)
FROM inserted
WHERE Tasks_Movement.ID IN (SELECT id FROM inserted
WHERE (inserted.SiteResponsibleID Is NULL OR inserted.SiteResponsibleID=0)
AND (inserted.SiteToID Is NOT NULL AND inserted.SiteToID>0))
END
I think your trigger should be something like this:
ALTER TRIGGER [dbo].[trg_TaskMovement_Zone] ON [dbo].[Tasks_Movement]
AFTER INSERT, UPDATE
AS
BEGIN
SET NOCOUNT ON
UPDATE tm
SET ZoneFromID = dbo.fn_GetZoneFromPostcode(i.FromAddress_Postcode),
ZoneToID = dbo.fn_GetZoneFromPostcode(i.ToAddress_Postcode)
FROM Tasks_Movement tm
INNER JOIN inserted i
ON i.ID = tm.ID;
UPDATE tm
SET SiteResponsibleID = [dbo].[fn_GetDefaultDepotResponsibleForSite](i.SiteFromID)
FROM Tasks_Movement tm
INNER JOIN inserted i
ON i.ID = tm.ID
WHERE (i.SiteResponsibleID IS NULL OR i.SiteResponsibleID = 0)
AND i.SiteFromID > 0
UPDATE tm
SET SiteResponsibleID = [dbo].[fn_GetDefaultDepotResponsibleForSite](i.SiteToID)
FROM Tasks_Movement tm
INNER JOIN inserted i
ON i.ID = tm.ID
WHERE (i.SiteResponsibleID IS NULL OR i.SiteResponsibleID = 0)
AND i.SiteToID > 0
END
I've changed it to use SQl Server's UPDATE .. FROM syntax, and also removed the redundant null check when you are checking if a site ID > 0. NULL is not greater than or less than 0, so if SiteID is null SiteID > 0 can never evaluate to true, so it is a redundant additional check.
Finally, I would also recommend removing the user defined functions, although I can't see under the hood of these, based on the name they look very much like they are simple loukup functions that could be achived much more efficiently with joins.
EDIT
Rather than using the UPDATE(column) function I would add an additional join to the update to filter for updated rows, e.g.:
UPDATE tm
SET SiteResponsibleID = [dbo].[fn_GetDefaultDepotResponsibleForSite](i.SiteToID)
FROM Tasks_Movement tm
INNER JOIN inserted i
ON i.ID = tm.ID
LEFT JOIN deleted d
ON d.ID = i.ID
WHERE (i.SiteResponsibleID IS NULL OR i.SiteResponsibleID = 0)
AND i.SiteToID > 0
AND AND ISNULL(i.SiteToID, 0) != ISNULL(d.SiteToID);
I'd do it this way because UPDATE(siteToID) will return true if any row has an updated value, so if you update 1,000,000 rows and one has a change it will perform the update on all of them, not just the ones that have changed, by joining to deleted you can limit the update to relevant rows.

Efficient SQL Server stored procedure

I am using SQL Server 2008 and running the following stored procedure that needs to "clean" a 70 mill table from about 50 mill rows to another table, the id_col is integer (primary identity key)
According to the last running I made it is working good but it is expected to last for about 200 days:
SET NOCOUNT ON
-- define the last ID handled
DECLARE #LastID integer
SET #LastID = 0
declare #tempDate datetime
set #tempDate = dateadd(dd,-20,getdate())
-- define the ID to be handled now
DECLARE #IDToHandle integer
DECLARE #iCounter integer
DECLARE #watch1 nvarchar(50)
DECLARE #watch2 nvarchar(50)
set #iCounter = 0
-- select the next to handle
SELECT TOP 1 #IDToHandle = id_col
FROM MAIN_TABLE
WHERE id_col> #LastID and DATEDIFF(DD,someDateCol,otherDateCol) < 1
and datediff(dd,someDateCol,#tempDate) > 0 and (some_other_int_col = 1745 or some_other_int_col = 1548 or some_other_int_col = 4785)
ORDER BY id_col
-- as long as we have s......
WHILE #IDToHandle IS NOT NULL
BEGIN
IF ((select count(1) from SOME_OTHER_TABLE_THAT_CONTAINS_20k_ROWS where some_int_col = #IDToHandle) = 0 and (select count(1) from A_70k_rows_table where some_int_col =#IDToHandle )=0)
BEGIN
INSERT INTO SECONDERY_TABLE
SELECT col1,col2,col3.....
FROM MAIN_TABLE WHERE id_col = #IDToHandle
EXEC [dbo].[DeleteByID] #ID = #IDToHandle --deletes the row from 2 other tables that is related to the MAIN_TABLE and than from the MAIN_TABLE
set #iCounter = #iCounter +1
END
IF (#iCounter % 1000 = 0)
begin
set #watch1 = 'iCounter - ' + CAST(#iCounter AS VARCHAR)
set #watch2 = 'IDToHandle - '+ CAST(#IDToHandle AS VARCHAR)
raiserror ( #watch1, 10,1) with nowait
raiserror (#watch2, 10,1) with nowait
end
-- set the last handled to the one we just handled
SET #LastID = #IDToHandle
SET #IDToHandle = NULL
-- select the next to handle
SELECT TOP 1 #IDToHandle = id_col
FROM MAIN_TABLE
WHERE id_col> #LastID and DATEDIFF(DD,someDateCol,otherDateCol) < 1
and datediff(dd,someDateCol,#tempDate) > 0 and (some_other_int_col = 1745 or some_other_int_col = 1548 or some_other_int_col = 4785)
ORDER BY id_col
END
Any ideas or directions to improve this procedure run-time will be welcomed
Yes, try this:
Declare #Ids Table (id int Primary Key not Null)
Insert #Ids(id)
Select id_col
From MAIN_TABLE m
Where someDateCol >= otherDateCol
And someDateCol < #tempDate -- If there are times in these datetime fields,
-- then you may need to modify this condition.
And some_other_int_col In (1745, 1548, 4785)
And Not exists (Select * from SOME_OTHER_TABLE_THAT_CONTAINS_20k_ROWS
Where some_int_col = m.id_col)
And Not Exists (Select * From A_70k_rows_table
Where some_int_col = m.id_col)
Select id from #Ids -- this to confirm above code generates the correct list of Ids
return -- this line to stop (Not do insert/deletes) until you have verified #Ids is correct
-- Once you have verified that above #Ids is correctly populated,
-- then delete or comment out the select and return lines above so insert runs.
Begin Transaction
Delete OT -- eliminate row-by-row call to second stored proc
From OtherTable ot
Join MAIN_TABLE m On m.id_col = ot.FKCol
Join #Ids i On i.Id = m.id_col
Insert SECONDERY_TABLE(col1, col2, etc.)
Select col1,col2,col3.....
FROM MAIN_TABLE m Join #Ids i On i.Id = m.id_col
Delete m -- eliminate row-by-row call to second stored proc
FROM MAIN_TABLE m
Join #Ids i On i.Id = m.id_col
Commit Transaction
Explaanation.
You had numerous filtering conditions that were not SARGable, i.e., they would force a complete table scan for every iteration of your loop, instead of being able to use any existing index. Always try to avoid filter conditions that apply processing logic to a table column value before comparing it to some other value. This eliminates the opportunity for the query optimizer to use an index.
You were executing the inserts one at a time... Way better to generate a list of PK Ids that need to be processed (all at once) and then do all the inserts at once, in one statement.

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.

Update multiple rows in table from table variable

I'm writing a stored procedure to update multiple records based on a table variable parameter.
The existing table is: Tb_Project_Image with relevant columns:
id PK (identity 1,1)
cat_ord decimal(4,2)
The procedure will receive a temporary table variable (shown in the code below) containing the id as PI_ID, and the new value for cat_ord as newCatOrd. idx is a simple identity for each row containing 1...n where n is the rowcount of #tempTable.
For each row in #tempTable, I want to update Tb_Project_Image where id = PI_ID to the corresponding value.
DECLARE #tempTable table (
idx smallint Primary Key IDENTITY(1,1),
PI_ID bigint,
newCatOrd decimal(4, 2) not null )
INSERT INTO #tempTable values (3, 7.01)
INSERT INTO #tempTable values (4, 7.02)
INSERT INTO #tempTable values (5, 7.03)
--etc...
DECLARE #error int
DECLARE #update int
DECLARE #iter int
SET #iter = 1
BEGIN TRAN
WHILE #iter <= (select COUNT(*) from #tempTable)
BEGIN
UPDATE Tb_Project_Image
SET cat_ord = (SELECT newCatOrd FROM #tempTable
WHERE idx = #iter)
WHERE id = (SELECT PI_ID FROM #tempTable
WHERE idx = #iter)
--error checking
set #error = ##ERROR
set #update = ##ROWCOUNT
IF ((#error = 0) AND (#update = 1))
BEGIN
SET #iter = #iter + 1
CONTINUE
END
ELSE
BREAK
END
IF ((#error = 0) AND (#update = 1))
COMMIT TRAN
ELSE
ROLLBACK TRAN
GO
Now, the error checking is because, to ensure integrity, EACH row in the temporary table MUST make 1 update. (explanation omitted to save space) If a single iteration of the while loop threw an error, or didn't effect exactly 1 row, I want to break the loop and rollback the transaction
THE PROBLEM I'm having is that this error checking is not working. I'm currently running it with 14 rows in #tempTable and the 11th uses a PI_ID not found in the Project_Image table. Therefore, #update = 0... but it continues the loop and commits the data.
I'd be doubly glad if someone had a method of doing this that only used a single update statement.
You cannot do it this way, because even SET resets the state of ##ERROR and ##ROWNUMBER variables. In this case ##ROWCOUNT is set to 1 after set #error = ##ERROR. If you do not assign the values to local variables, your code will work:
IF ((##error = 0) AND (##rowcount = 1))
But you might rather try try...catch error handling and test ##rowcount separately after update.
UPDATE: doing it in single update:
UPDATE t
SET cat_ord = tt.newCatOrd
FROM Tb_Project_Image t
INNER JOIN #tempTable tt
ON t.id = tt.PI_ID
-- If there was PI_ID not found in Tb_Project_Image
-- But I think that this should have been dealt with
-- During the initial loading of temporary table
IF ##ROWCOUNT <> (select count (*) from #tempTable)
BEGIN
-- Error reporting here
ROLLBACK TRANSACTION
END
Instead of updating and then rolling back, you could also use a CTE to determine if any records should be updated prior to performing the update. Something like this should work:
WITH NON_SINGLETON AS (
-- Find any records in #tempTable that don't match
-- exactly one record in Tb_Project_Image
SELECT t.PI_ID, COUNT(pi.id) C
FROM #tempTable t
LEFT JOIN Tb_Project_Image pi ON t.PI_ID = pi.id
GROUP BY t.PI_ID
HAVING COUNT(pi.id) != 1
)
UPDATE Tb_Project_Image
SET cat_ord = t.newCatOrd
FROM Tb_Project_Image pi
JOIN #tempTable t ON pi.id = t.PI_ID
-- If any invalid records were found in the CTE,
-- then this condition will fail for all rows
-- and nothing will be updated
WHERE NOT EXISTS(SELECT 1 FROM NON_SINGLETON)
If it's possible for #tempTable to have duplicate entries for the same PI_ID, then this will handle those scenarios as well. And since it's a single statement, you don't have to explicitly managing the transaction in the proc (if it's the only thing that needs to be included in the transaction).

SQL: Query timeout expired

I have a simple query for update table (30 columns and about 150 000 rows).
For example:
UPDATE tblSomeTable set F3 = #F3 where F1 = #F1
This query will affected about 2500 rows.
The tblSomeTable has a trigger:
ALTER TRIGGER [dbo].[trg_tblSomeTable]
ON [dbo].[tblSomeTable]
AFTER INSERT,DELETE,UPDATE
AS
BEGIN
declare #operationType nvarchar(1)
declare #createDate datetime
declare #UpdatedColumnsMask varbinary(500) = COLUMNS_UPDATED()
-- detect operation type
if not exists(select top 1 * from inserted)
begin
-- delete
SET #operationType = 'D'
SELECT #createDate = dbo.uf_DateWithCompTimeZone(CompanyId) FROM deleted
end
else if not exists(select top 1 * from deleted)
begin
-- insert
SET #operationType = 'I'
SELECT #createDate = dbo..uf_DateWithCompTimeZone(CompanyId) FROM inserted
end
else
begin
-- update
SET #operationType = 'U'
SELECT #createDate = dbo..uf_DateWithCompTimeZone(CompanyId) FROM inserted
end
-- log data to tmp table
INSERT INTO tbl1
SELECT
#createDate,
#operationType,
#status,
#updatedColumnsMask,
d.F1,
i.F1,
d.F2,
i.F2,
d.F3,
i.F3,
d.F4,
i.F4,
d.F5,
i.F5,
...
FROM (Select 1 as temp) t
LEFT JOIN inserted i on 1=1
LEFT JOIN deleted d on 1=1
END
And if I execute the update query I have a timeout.
How can I optimize a logic to avoid timeout?
Thank you.
This query:
SELECT *
FROM (
SELECT 1 AS temp
) t
LEFT JOIN
INSERTED i
ON 1 = 1
LEFT JOIN
DELETED d
ON 1 = 1
will yield 2500 ^ 2 = 6250000 records from a cartesian product of INSERTED and DELETED (that is all possible combinations of all records in both tables), which will be inserted into tbl1.
Is that what you wanted to do?
Most probably, you want to join the tables on their PRIMARY KEY:
INSERT
INTO tbl1
SELECT #createDate,
#operationType,
#status,
#updatedColumnsMask,
d.F1,
i.F1,
d.F2,
i.F2,
d.F3,
i.F3,
d.F4,
i.F4,
d.F5,
i.F5,
...
FROM INSERTED i
FULL JOIN
DELETED d
ON i.id = d.id
This will treat update to the PK as deleting a record and inserting another, with a new PK.
Thanks Quassnoi, It's a good idea with "FULL JOIN". It is helped me.
Also I try to update table in portions (1000 items in one time) to make my code works faster because for some companyId I need to update more than 160 000 rows.
Instead of old code:
UPDATE tblSomeTable set someVal = #someVal where companyId = #companyId
I use below one:
declare #rc integer = 0
declare #parts integer = 0
declare #index integer = 0
declare #portionSize int = 1000
-- select Ids for update
declare #tempIds table (id int)
insert into #tempIds
select id from tblSomeTable where companyId = #companyId
-- calculate amount of iterations
set #rc=##rowcount
set #parts = #rc / #portionSize + 1
-- update table in portions
WHILE (#parts > #index)
begin
UPDATE TOP (#portionSize) t
SET someVal = #someVal
FROM tblSomeTable t
JOIN #tempIds t1 on t1.id = t.id
WHERE companyId = #companyId
delete top (#portionSize) from #tempIds
set #index += 1
end
What do you think about this? Does it make sense? If yes, how to choose correct portion size?
Or simple update also good solution? I just want to avoid locks in the future.
Thanks