What comparison method is better? - sql-server

I have a trigger in a table with a good number of columns (perhaps around 100) and quite a lot of updates (for some definition of "a lot of").
If any of some fields have changed, the trigger inserts some data in another table.
For obvious reasons, I want this trigger to run as fast as possible. What's the best method to do the comparison?
For now I have those:
IF NOT EXISTS (SELECT * FROM Inserted i, Deleted d WHERE
i.Fld1 = d.Fld1 AND i.Fld2 = d.Fld2 AND
i.Fld3 = d.Fld3 AND i.Fld4 = d.Fld4 AND
i.Fld5 = d.Fld5 AND i.Fld6 = d.Fld6 AND
i.Fld7 = d.Fld7)
THEN ...
IF ((SELECT Fld1 FROM Inserted) <> (SELECT Fld1 FROM Deleted) OR
(SELECT Fld2 FROM Inserted) <> (SELECT Fld2 FROM Deleted) OR
(SELECT Fld3 FROM Inserted) <> (SELECT Fld3 FROM Deleted) OR
(SELECT Fld4 FROM Inserted) <> (SELECT Fld4 FROM Deleted) OR
(SELECT Fld5 FROM Inserted) <> (SELECT Fld5 FROM Deleted) OR
(SELECT Fld6 FROM Inserted) <> (SELECT Fld6 FROM Deleted) OR
(SELECT Fld7 FROM Inserted) <> (SELECT Fld7 FROM Deleted))
THEN...
I would usually prefer the first method, as it's more compact and seems more idiomatic. However, when speed is an issue, how should I do it?

The second version is completely broken for multi-row UPDATES, so for that reason alone, I'd do a variant of the first:
INSERT INTO ANotherTable (Column1, COlumn2, /* Etc */)
SELECT i.Column1,d.Column1, /* Other COlumns */
FROM
inserted i
inner join
deleted d
on
i.Fld1 = d.Fld1 and /* For each column in PK */
i.Fld2 <> d.Fld2 /* For each non-PK column */
Assuming the PK is stable and unchanging

Why don't you test your changing columns by using IF UPDATE(Column1,Column2,...) That will let you know whether any of the columns have changed that you're interested in. See http://msdn.microsoft.com/en-us/library/ms187326.aspx for details on the UPDATE() function.
You'll also be able to use it if the PK has been modified, whereas comparing between the inserted and deleted the way you're trying to go about it will miss out on changes to the PK.
The solution involving testing the inequality of all of the fields will fail unless you SET ANSI_NULLS OFF before comparing.
For example:
create table table1 ( a varchar(4), b varchar(4) null)
create table table2 ( a varchar(4), b varchar(4) null)
go
insert into table1 ( a, b ) select 'asdf', null
insert into table2 ( a, b ) select 'asdf', 'zzzz'
--Expect no results
select *
from table1
inner join table2
on a.a = b.a
where a.b <> b.b
set ansi_nulls off
--Expect 1 result
select *
from table1
inner join table2
on a.a = b.a
where a.b <> b.b
Of course, you could put in additional tests rather than going with the ansi_nulls option, but that gets particularly crazy if you're doing this with many fields. Also: you must set ansi_nulls off prior to creating the trigger - you can't turn it on and off inside the trigger, so the whole thing must have the same ansi_nulls setting.

Related

Insert Into T2 all rows from T1 that are currently not in T2

I am trying to insert all records from T1 into T2 that are not currently in T2
I have tried in a loop as I am generating a code from a stored proc as the identifier of T2
declare #Part VARCHAR(255),
#GenValue VARCHAR(255),
#x INT
set #x = (select count(*) from T1)
WHILE #x >=0
BEGIN
EXEC [dbo].[usp_GenInd] #GenValue OUT,#GencCode = 'TKM', #GencIncrement = 1
set #Part = #GencValue
INSERT INTO dbo.T2
SELECT #Part AS [part],
[Prod_Code] + Column_Header AS [identifier],
[part_rev] = NULL,
'!' AS [u_version],
a.[Descr] AS [descr],
GETDATE() AS [last_updated],
'ME' AS [last_upd_user],
'EA' AS [basic_unit],
[source] = NULL,
'MAIN' AS [level_1],
'GROUP' AS [level_2],
'ME' AS [user_created],
'20' AS [status],
[Prod_Code] AS [master_part],
[drawing_no] = NULL
FROM [dbo].T1 a
LEFT JOIN dbo.T2 b
ON a.Prod_Code + a.Column_Header = b.part
WHERE b.part is null
END
I keep getting error saying primary key violation on T2 which is the #part variable I am generating from the stored proc.
really slow as well, I thought an insert on left join on null was quicker than a cursor.
only have 67 rows in T1
Thanks for helping in advance
Nope - go back to the cursor if you must continue to use this stored procedure to generate primary key values. The logic error you added to this script is the insert statement. It does not select a specific row from T1 - it selects all rows in T1 that do not exist in T2 (assuming that logic is correct - I'm not going to evaluate it). Presumably you must call the procedure usp_GenInd to generate a PK value for each row in T1. In addition, you never decrement #x - so you have an endless loop.
And notice the wording - "not exists". Generally I find it easier to understand undocumented logic when the query matches (as close as possible) the intent of the code. Your left join logic is the same as not exists - just more difficult to figure out. And you also have a potential problem with your concatenation logic to check for existence. 'AA' + 'B' = 'A' + 'AB' - but the columns contain different values. Be careful about assumptions.
I would try something like:
;WITH cte AS (
SELECT your needed data
FROM [dbo].T1
EXCEPT
SELECT already existing data
FROM [dbo].T2
)
INSERT INTO dbo.T2
SELECT *
FROM cte
Your JOIN logic is flawed.
In your INSERT you have this:
INSERT INTO dbo.T2
SELECT #Part AS [part],
[Prod_Code] + Column_Header AS [identifier],
Inserting #Part into [part]
But when you do your JOIN to rule out existing rows, you have this:
LEFT JOIN dbo.T2 b
ON a.Prod_Code + a.Column_Header = b.part
To rule out existing rows, you should be joining on #part=b.part.

TSQL Where clause based on temp table data

I have a straight forward SQL query that I am working with and trying to figure out the best way to approach the where clause.
Essentially, there are two temp tables created and if there is data in the XML string passed to the stored procedure, those tables are populated.
My where clause needs to check these temp tables for data, and if there is no data, it ignores them like they are not there and fetches all data.
-- Create temp tables to hold our XML filter criteria
DECLARE #users AS TABLE (QID VARCHAR(10))
DECLARE #dls AS TABLE (dlName VARCHAR(50))
-- Insert our XML filters
IF #xml.exist('/root/data/users') > 0
BEGIN
INSERT INTO #users( QID )
SELECT ParamValues.x1.value('QID[1]', 'varchar(10)')
FROM #xml.nodes('/root/data/users/user') AS ParamValues(x1)
END
IF #xml.exist('/root/data/dls') > 0
BEGIN
INSERT INTO #dls( dlName )
SELECT ParamValues.x1.value('dlName[1]', 'varchar(50)')
FROM #xml.nodes('/root/data/dld/dl') AS ParamValues(x1)
END
-- Fetch our document details based on the XML provided
SELECT d.documentID ,
d.sopID ,
d.documentName ,
d.folderLocation ,
d.userGroup ,
d.notes
FROM dbo.Documents AS d
LEFT JOIN dbo.DocumentContacts AS dc
ON dc.documentID = d.documentID
LEFT JOIN dbo.DocumentContactsDLs AS dl
ON dl.documentID = d.documentID
-- How can I make these two logic checks work only if there is data, otherwise, include everything.
WHERE dc.QID IN (SELECT QID FROM #users)
AND dl.DL IN (SELECT dlName FROM #dls)
FOR XML PATH ('data'), ELEMENTS, TYPE, ROOT('root');
In the query above, I am trying to used the data in the temp tables only if there is data in them, otherwise, it needs to act like that where statement isn't there for that specific value and include records regardless.
Example: If only #users had data, it would ignore AND dl.DL IN (SELECT dlName FROM #dls) and get everything, regardless of what was in the DL column on those joined records.
Use NOT EXISTS to check the existence of any record in variable table. Here is one way
WHERE ( dc.QID IN (SELECT QID FROM #users)
OR NOT EXISTS (SELECT 1 FROM #users) )
AND ( dl.DL IN (SELECT dlName FROM #dls)
OR NOT EXISTS (SELECT 1 FROM #dls) )
Try this. But please note that I did not get a chance to test it properly and I believe that you want to check the values in #users first and if there is no record existing in that table, then you want to check with the entries in #dls. Also if there are no entries in both of these tables, then you want to skip both the tables.
DECLARE #fl bit = 0
SELECT #fl = CASE WHEN EXISTS (SELECT 1 FROM #users) THEN
1
WHEN EXISTS (SELECT 1 FROM #dls) THEN
2
ELSE
0
END
WHERE ( (dc.QID IN (SELECT QID FROM #users) AND #fl = 1)
OR
(dl.DL IN (SELECT dlName FROM #dls) AND #fl = 2)
OR (1=1 AND #fl = 0)
)

Creating a single trigger for Insert/Update

I am using SQL Server 2008.
Assuming I have Table A which is a transaction table. And Table B which is a history table.
Whenever a row is inserted or updated in Table A, a new row should be inserted in Table B.
The Status column of Table B should change to INSERTED or UPDATED respectively.
How to handle this from a single trigger?
What you're asking for is quite simple:
CREATE TRIGGER TR_TableA_IU ON dbo.TableA FOR INSERT, UPDATE
AS
SET NOCOUNT ON;
INSERT dbo.TableB (Column1, Column2, Status)
SELECT
I.Column1,
I.Column2,
CASE WHEN EXISTS (SELECT * FROM Deleted) THEN 'UPDATED' ELSE 'INSERTED' END
FROM Inserted I;
If you also wanted to handle deletions, that can be done in a single statement, too:
CREATE TRIGGER TR_TableA_IUD ON dbo.TableA FOR INSERT, UPDATE, DELETE
AS
SET NOCOUNT ON;
INSERT dbo.TableB (Column1, Column2, Status)
SELECT
I.Column1,
I.Column2,
CASE WHEN EXISTS (SELECT * FROM Deleted) THEN 'UPDATED' ELSE 'INSERTED' END
FROM
Inserted I
UNION ALL
SELECT
D.Column1,
D.Column2,
'DELETED'
FROM Deleted D
WHERE NOT EXISTS (
SELECT * FROM Inserted
);
Wow, there are a lot of outright-wrong and semi-wrong (at least in being overcomplicated) answers given so far.
Assuming that both tables:
has an "Id" column as primary key.
has the same schema, except that history table has an extra "Status" column at the end.
You can create a trigger like this:
CREATE TRIGGER dbo.TableA_InsUpd
ON dbo.TableA
AFTER INSERT, UPDATE, DELETE
AS
BEGIN
Insert Into TableB
Select i.*, 'INSERTED'
From inserted i
Where not exists ( Select * From deleted d Where d.Id = i.Id )
Update B
Set [Status] = 'UPDATED',
Field1 = i.Field1,
Field2 = i.Field2
From TableB B
Inner Join inserted i On i.Id = B.Id
Where exists ( Select * From deleted d Where d.Id = i.Id )
Update B
Set [Status] = 'DELETED'
From TableB B
Inner Join deleted d On d.Id = B.Id
Where not exists ( Select * From inserted i Where i.Id = d.Id )
END
Here is a SqlFiddle with the complete code
(Note this will fail if a record Id is deleted and then inserted again)
Try this code
CREATE TRIGGER YouTriggerName
ON TableA
AFTER INSERT,DELETE,UPDATE
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
-- Insert statements for trigger here
DECLARE #type NVarChar(15)=
CASE when not exists(SELECT * FROM inserted)
THEN 'Deleted'
WHEN exists(SELECT * FROM deleted)
THEN 'Updated'
ELSE
'Inserted'
END
/*
TableB should contains all the columns of TableA
OR tweak it to suit your need
*/
IF #type = 'Deleted' BEGIN
INSERT INTO TableB
SELECT *, #type Stat FROM deleted
END
ELSE BEGIN
INSERT INTO TableB
SELECT *, #type Stat FROM inserted
END
END
NOTE
You will get this error if TableB has identity on since we use (select *)
An explicit value for the identity column in table 'TableB' can only be specified when a column list is used and IDENTITY_INSERT is ON.
this is tested.here employee and employee have same table structure.0 means updated and 1=inserted,2=deleted
Alter trigger trgTest on dbo.employee
AFTER INSERT, UPDATE,Delete
as
Begin
Set noCount on
if exists(select e.id from deleted e inner join inserted i on e.ID=i.id )
Begin
insert into Employee1
select id,name,0 from inserted
End
else if exists(select e.id from Employee1 e inner join deleted d on e.ID=d.id)
Begin
insert into Employee1
select id,name,2 from deleted
End
else
Begin
insert into Employee1
select id,name,1 from inserted
End
End

Update count column from data in another table

In my DB I have two tables Items(Id, ..., ToatlViews int) and ItemViews (id, ItemId, Timestamp)
In ItemViews table I store all views of an item as they come to the site. From time to time I want to call a stored procedure to update Items.ToatlViews field. I tried to do this SP using a cursor ... but the update statement is wrong. Can you help me to correct it? Can I do this without cursor?
CREATE PROCEDURE UpdateItemsViews
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
DECLARE #currentItemId int
DECLARE #currentItemCursor CURSOR
SET #currentItemCursor = CURSOR FOR SELECT Id FROM dbo.Items
OPEN #currentItemCursor
FETCH NEXT FROM #currentItemCursor INTO #currentItemId
WHILE ##FETCH_STATUS = 0
BEGIN
Update dbo.Items set TotalViews = count(*)
from dbo.ItemViews where ItemId=#currentItemId
FETCH NEXT FROM #currentItemCursor INTO #currentItemId
END
END
GO
You can use a direct UPDATE statement
update Items set TotalViews =
(select COUNT(id) from ItemViews where ItemViews.ItemId = Items.Id)
You might want to test performance for the various ways to do this, if that's important.
You could use update ... from instead of a cursor:
update i
set TotalViews = iv.cnt
from dbo.Item i
join (
select ItemId
, count(*) as cnt
from dbo.ItemViews
group by
ItemId
) iv
on i.Id = iv.ItemId
;WITH x AS
(
SELECT ItemID, c = COUNT(*)
FROM dbo.ItemViews
GROUP BY ItemID
)
UPDATE i
SET TotalViews = x.c
FROM dbo.Items AS i
INNER JOIN x
ON x.ItemID = i.ItemID;
But why do you want to store this value, when you can always get the count at runtime? You're going to have to run this update statement every time you touch the ItemViews table in any way, otherwise the count stored with Items is going to be incorrect.
What you may consider doing instead is setting up an indexed view:
CREATE VIEW dbo.ItemViewCount
WITH SCHEMABINDING
AS
SELECT ItemID, ItemCount = COUNT_BIG(*)
FROM dbo.ItemViews
GROUP BY ItemID;
GO
CREATE UNIQUE CLUSTERED INDEX x ON dbo.ItemViewCount(ItemID);
Now you can join to the view in your queries and know that the count is always up to date (without paying the penalty of scanning for the count of each item). The downside to the indexed view is that you pay that cost incrementally when there are inserts/updates/deletes to the ItemViews table.
I found this question / answer a year after it was written and answered. the answer was okay, but I was after something a bit more automatic. I ended up writing a trigger to automatically recalculate the column when a relevant row in the other table was inserted, deleted or updated.
I think it's a better solution than running something manually to do the recalculation as there isn't any possibility of someone forgetting to run the code:
CREATE TRIGGER [dbo].[TriggerItemTotalViews]
ON [dbo].[ItemViews]
AFTER INSERT, DELETE, UPDATE
AS
BEGIN
SET NOCOUNT ON;
UPDATE [Items]
SET [TotalViews] =
(
SELECT COUNT(id)
FROM [ItemViews]
WHERE [ItemViews].[ItemId] = [Items].[ItemId]
)
WHERE [Items].[ItemId] IN
(
SELECT [ItemId] FROM [INSERTED]
UNION
SELECT [ItemId] FROM [DELETED]
)
END
Same but different:
declare #productId int = 24;
declare #classificationTypeId int = 86;
update s
set CounterByProductAndClassificationType = row_num
from Samples s
join
(
select row_number() over (order by (select Id)) row_num, Id
from Samples
where
ProductId = #productId and
ClassificationTypeId = #classificationTypeId
) s_row on s.Id = s_row.Id
For who need to include zero count too
UPDATE Items as i,
(SELECT
i.Id as Id, COUNT(iv.ItemId) AS c
FROM
Items AS i
LEFT JOIN ItemViews AS iv ON i.Id = iv.ItemId
GROUP BY i.Id) AS ic
SET
i.TotalViews = ic.c
WHERE
i.Id = ic.Id

two updates using the same CTE

I have to "unremove" folders in an arborescence (thay have been flagged as removed, I change the value of the flag). Each folder can contain files or folders (which are stored in different tables). I have a CTE that defines all the folders that need to be updated.
WITH arbre(id) AS(
SELECT idDossier
FROM portail_managers_dossier
WHERE idDossier = #id
UNION ALL
SELECT d.idDossier
FROM portail_managers_dossier AS d
INNER JOIN arbre AS a
ON a.id = d.idParent)
Then I have two UPDATE request, one for each table
UPDATE portail_managers_dossier
SET dtDateSuppr = NULL
WHERE idDossier IN (SELECT id FROM arbre);
UPDATE portail_managers_document
SET dtDateSuppr = NULL
WHERE idDossier IN (SELECT id FROM arbre);
My problem is : I don't know how to merge two UPDATE requests on different tables. The CTE only exists until the end of the request, so I have to define it twice. Is there any way to write all of the above code in a single request ?
As you have discovered, CTE's will lose scope after the first update. But, instead of using a CTE, why not write the results of the query within the CTE to a temp table, and do your updates based on the contents temp table?
You can save the list of idDossier into variable from cte before execute the updates.
-- declare table variable
declare #listOfIDs table (idDossier int);
-- select ids of two tables
;WITH arbre(id) AS
(
SELECT idDossier
FROM portail_managers_dossier
WHERE idDossier = #id
UNION ALL
SELECT d.idDossier
FROM portail_managers_dossier AS d
INNER JOIN arbre AS a
ON a.id = d.idParent
)
-- saving ids
insert #listOfIDs(idDossier) select idDossier from arbre
-- execute twice updates
UPDATE portail_managers_dossier
SET dtDateSuppr = NULL
WHERE idDossier IN (SELECT idDossier FROM #listOfIDs);
UPDATE portail_managers_document
SET dtDateSuppr = NULL
WHERE idDossier IN (SELECT idDossier FROM #listOfIDs);

Resources