Check if a row exists, otherwise insert - sql

I need to write a T-SQL stored procedure that updates a row in a table. If the row doesn't exist, insert it. All this steps wrapped by a transaction.
This is for a booking system, so it must be atomic and reliable. It must return true if the transaction was committed and the flight booked.
I'm sure on how to use ##rowcount. This is what I've written until now. Am I on the right road?
-- BEGIN TRANSACTION (HOW TO DO?)
UPDATE Bookings
SET TicketsBooked = TicketsBooked + #TicketsToBook
WHERE FlightId = #Id AND TicketsMax < (TicketsBooked + #TicketsToBook)
-- Here I need to insert only if the row doesn't exists.
-- If the row exists but the condition TicketsMax is violated, I must not insert
-- the row and return FALSE
IF ##ROWCOUNT = 0
BEGIN
INSERT INTO Bookings ... (omitted)
END
-- END TRANSACTION (HOW TO DO?)
-- Return TRUE (How to do?)

I assume a single row for each flight? If so:
IF EXISTS (SELECT * FROM Bookings WHERE FLightID = #Id)
BEGIN
--UPDATE HERE
END
ELSE
BEGIN
-- INSERT HERE
END
I assume what I said, as your way of doing things can overbook a flight, as it will insert a new row when there are 10 tickets max and you are booking 20.

Take a look at MERGE command. You can do UPDATE, INSERT & DELETE in one statement.
Here is a working implementation on using MERGE
- It checks whether flight is full before doing an update, else does an insert.
if exists(select 1 from INFORMATION_SCHEMA.TABLES T
where T.TABLE_NAME = 'Bookings')
begin
drop table Bookings
end
GO
create table Bookings(
FlightID int identity(1, 1) primary key,
TicketsMax int not null,
TicketsBooked int not null
)
GO
insert Bookings(TicketsMax, TicketsBooked) select 1, 0
insert Bookings(TicketsMax, TicketsBooked) select 2, 2
insert Bookings(TicketsMax, TicketsBooked) select 3, 1
GO
select * from Bookings
And then ...
declare #FlightID int = 1
declare #TicketsToBook int = 2
--; This should add a new record
merge Bookings as T
using (select #FlightID as FlightID, #TicketsToBook as TicketsToBook) as S
on T.FlightID = S.FlightID
and T.TicketsMax > (T.TicketsBooked + S.TicketsToBook)
when matched then
update set T.TicketsBooked = T.TicketsBooked + S.TicketsToBook
when not matched then
insert (TicketsMax, TicketsBooked)
values(S.TicketsToBook, S.TicketsToBook);
select * from Bookings

Pass updlock, rowlock, holdlock hints when testing for existence of the row.
begin tran /* default read committed isolation level is fine */
if not exists (select * from Table with (updlock, rowlock, holdlock) where ...)
/* insert */
else
/* update */
commit /* locks are released here */
The updlock hint forces the query to take an update lock on the row if it already exists, preventing other transactions from modifying it until you commit or roll back.
The holdlock hint forces the query to take a range lock, preventing other transactions from adding a row matching your filter criteria until you commit or roll back.
The rowlock hint forces lock granularity to row level instead of the default page level, so your transaction won't block other transactions trying to update unrelated rows in the same page (but be aware of the trade-off between reduced contention and the increase in locking overhead - you should avoid taking large numbers of row-level locks in a single transaction).
See http://msdn.microsoft.com/en-us/library/ms187373.aspx for more information.
Note that locks are taken as the statements which take them are executed - invoking begin tran doesn't give you immunity against another transaction pinching locks on something before you get to it. You should try and factor your SQL to hold locks for the shortest possible time by committing the transaction as soon as possible (acquire late, release early).
Note that row-level locks may be less effective if your PK is a bigint, as the internal hashing on SQL Server is degenerate for 64-bit values (different key values may hash to the same lock id).

i'm writing my solution. my method doesn't stand 'if' or 'merge'. my method is easy.
INSERT INTO TableName (col1,col2)
SELECT #par1, #par2
WHERE NOT EXISTS (SELECT col1,col2 FROM TableName
WHERE col1=#par1 AND col2=#par2)
For Example:
INSERT INTO Members (username)
SELECT 'Cem'
WHERE NOT EXISTS (SELECT username FROM Members
WHERE username='Cem')
Explanation:
(1) SELECT col1,col2 FROM TableName WHERE col1=#par1 AND col2=#par2
It selects from TableName searched values
(2) SELECT #par1, #par2 WHERE NOT EXISTS
It takes if not exists from (1) subquery
(3) Inserts into TableName (2) step values

I finally was able to insert a row, on the condition that it didn't already exist, using the following model:
INSERT INTO table ( column1, column2, column3 )
(
SELECT $column1, $column2, $column3
WHERE NOT EXISTS (
SELECT 1
FROM table
WHERE column1 = $column1
AND column2 = $column2
AND column3 = $column3
)
)
which I found at:
http://www.postgresql.org/message-id/87hdow4ld1.fsf#stark.xeocode.com

This is something I just recently had to do:
set ANSI_NULLS ON
set QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[cjso_UpdateCustomerLogin]
(
#CustomerID AS INT,
#UserName AS VARCHAR(25),
#Password AS BINARY(16)
)
AS
BEGIN
IF ISNULL((SELECT CustomerID FROM tblOnline_CustomerAccount WHERE CustomerID = #CustomerID), 0) = 0
BEGIN
INSERT INTO [tblOnline_CustomerAccount] (
[CustomerID],
[UserName],
[Password],
[LastLogin]
) VALUES (
/* CustomerID - int */ #CustomerID,
/* UserName - varchar(25) */ #UserName,
/* Password - binary(16) */ #Password,
/* LastLogin - datetime */ NULL )
END
ELSE
BEGIN
UPDATE [tblOnline_CustomerAccount]
SET UserName = #UserName,
Password = #Password
WHERE CustomerID = #CustomerID
END
END

You could use the Merge Functionality to achieve. Otherwise you can do:
declare #rowCount int
select #rowCount=##RowCount
if #rowCount=0
begin
--insert....

INSERT INTO [DatabaseName1].dbo.[TableName1] SELECT * FROM [DatabaseName2].dbo.[TableName2]
WHERE [YourPK] not in (select [YourPK] from [DatabaseName1].dbo.[TableName1])

Full solution is below (including cursor structure). Many thanks to Cassius Porcus for the begin trans ... commit code from posting above.
declare #mystat6 bigint
declare #mystat6p varchar(50)
declare #mystat6b bigint
DECLARE mycur1 CURSOR for
select result1,picture,bittot from all_Tempnogos2results11
OPEN mycur1
FETCH NEXT FROM mycur1 INTO #mystat6, #mystat6p , #mystat6b
WHILE ##Fetch_Status = 0
BEGIN
begin tran /* default read committed isolation level is fine */
if not exists (select * from all_Tempnogos2results11_uniq with (updlock, rowlock, holdlock)
where all_Tempnogos2results11_uniq.result1 = #mystat6
and all_Tempnogos2results11_uniq.bittot = #mystat6b )
insert all_Tempnogos2results11_uniq values (#mystat6 , #mystat6p , #mystat6b)
--else
-- /* update */
commit /* locks are released here */
FETCH NEXT FROM mycur1 INTO #mystat6 , #mystat6p , #mystat6b
END
CLOSE mycur1
DEALLOCATE mycur1
go

Simple way to copy data from T1 to T2 and avoid duplicate in T2
--Insert a new record
INSERT INTO dbo.Table2(NoEtu, FirstName, LastName)
SELECT t1.NoEtuDos, t1.FName, t1.LName
FROM dbo.Table1 as t1
WHERE NOT EXISTS (SELECT (1) FROM dbo.Table2 AS t2
WHERE t1.FName = t2.FirstName
AND t1.LName = t2.LastName
AND t1.NoEtuDos = t2.NoEtu)

INSERT INTO table ( column1, column2, column3 )
SELECT $column1, $column2, $column3
EXCEPT SELECT column1, column2, column3
FROM table

The best approach to this problem is first making the database column UNIQUE
ALTER TABLE table_name ADD UNIQUE KEY
THEN INSERT IGNORE INTO table_name ,the value won't be inserted if it results in a duplicate key/already exists in the table.

Related

Alternative to Iteration for INSERT SELECT UPDATE in a sequence

I have a table with around 17k unique rows for which I need to run these set of statements in sequence
INSERT INTO TABLE1 using MASTERTABLE data (MASTERTABLE have 6 column)
SELECT value of column ID (Primary Key) of newly inserted row from TABLE1
Update that ID value in TABLE2 using a Stored Procedure
I have tried:
while loop: took around 3 hours to complete the execution
cursor: cancelled the query after executing it overnight
In my understanding I can not use JOIN as I need to execute the statements in a sequence
The questions is not detailed enough. The general idea I would like to use something like this
-- create a output table to hold new id, and key columns to join later
DECLARE #OutputTbl TABLE (ID INT, key_Columns in MASTERTABLE)
INSERT INTO TABLE1
OUTPUT INSERTED.ID, MASTERTABLE.key_columns INTO #OutputTbl
SELECT *
FROM MASTERTABLE
UPDATE T2
SET ID = o.ID
FROM TABLE2 t2
INNER JOIN OutputTbl o
ON t2.key_column = o.key_column
Maybe you can consider a TRIGGER on TABLE1 from which to call the stored procedure on TABLE2, and then you can call your INSERT as you wish/need.. one by one or in blocks..
DROP TRIGGER TR_UPD_TABLE2
GO
CREATE TRIGGER TR_UPD_TABLE2 ON TABLE1 AFTER INSERT
AS
BEGIN
SET NOCOUNT ON
DECLARE #columnID INT = NULL
IF (SELECT COUNT(*) FROM INSERTED)=1 BEGIN
-- SINGLE INSERT
SET #columnID = (SELECT columnID FROM INSERTED)
EXEC TableTwoUpdateProcedure #columnID
END ELSE BEGIN
-- MASSIVE INSERT (IF NEEDED)
SET #columnID = 0
WHILE #columnID IS NOT NULL BEGIN
SET #columnID = (SELECT MIN(columnID) FROM INSERTED WHERE columnID > #columnID)
IF #columnID IS NOT NULL BEGIN
EXEC TableTwoUpdateProcedure #columnID
END
END
END
END

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...

SQL Server: Why isn't this logic working when Chunking on inserts?

Fellow Techies--
I've got an endless loop condition happening here. Why is ##rowcount never getting set back to 0? I must not be understanding what ##rowcount really does--or I am setting the value in the wrong place. I think the value should be decrementing on each pass until I eventually hit zero.
DECLARE #ChunkSize int = 250000;
WHILE #ChunkSize <> 0
BEGIN
BEGIN TRANSACTION
INSERT TableName
(col1,col2)
SELECT TOP (#ChunkSize)
col1,col2
FROM TableName2
COMMIT TRANSACTION;
SET #ChunkSize = ##ROWCOUNT
END -- transaction block
END -- while-loop block
I'm not sure, by what you posted, how you are going to ensure you catch rows that you haven't already inserted. If you don't, it'll be an infinite loop of course. Here is a way using test data--but naturally you'd want to base it off a PK or other unique column. Perhaps you just left that part off, or I'm missing something all together. I'm just interested in what your final code is for your chunking and the logic behind it, so this is an answer and inquiry.
if object_id('tempdb..#source') is not null drop table #source
if object_id('tempdb..#destination') is not null drop table #destination
create table #source(c1 int, c2 int)
create table #destination (c1 int, c2 int)
insert into #source (c1,c2) values
(1,1),
(2,1),
(3,1),
(4,1),
(5,1),
(6,1),
(7,1),
(8,1),
(9,1),
(10,1),
(11,1),
(12,1)
DECLARE #ChunkSize int = 2;
WHILE #ChunkSize <> 0
BEGIN
INSERT INTO #destination (c1,c2)
SELECT TOP (#ChunkSize) c1,c2 FROM #source WHERE c1 NOT IN (SELECT DISTINCT c1 FROM #destination) ORDER BY ROW_NUMBER() OVER (ORDER BY C1)
SET #ChunkSize = ##ROWCOUNT
--SELECT #ChunkSize
END
select * from #source
select * from #destination
Nothing is happening because you're setting chunksize to itself without ever looking at what you've already inserted. Using your example, #Chunksize = 250000. First, select performs SELECT TOP 250000 and returns (presumably) 250000 rows. You then use ##RowCount to update #Chunksize, but the row count returned will be 250000, so you just set it to 250000 again. Which could be fine, except there is no way that number will ever change without ruling out rows that you've already inserted - you will keep inserting the same 250000 rows over and over.
You need something like NOT EXISTS to filter out the rows you've already inserted:
DECLARE #ChunkSize int = 250000;
WHILE #ChunkSize > 0
BEGIN
BEGIN TRANSACTION
INSERT INTO TableName
(col1,col2)
SELECT TOP (#ChunkSize)
col1,col2
FROM TableName2 T2
WHERE NOT EXISTS (SELECT *
FROM TableName T
WHERE T.Col1 = T2.Col1
AND T.Col2 = T2.Col2)
SET #ChunkSize = ##ROWCOUNT
PRINT CONVERT(nvarchar(10),#ChunkSize) + ' Rows Inserted.';
COMMIT TRANSACTION
END -- while-loop block
Implemented solution
In the end, I decided to pump the SQL through SSIS, where I could set the commit batch size accordingly. Had I not chosen hat route, I would have had to follow #scsimon's suggestion and basically maintain a tracking table for the records completed and the records left to cycle through.

SQL Server after update trigger

I have a problem with this trigger. I would like it to update the requested information
only to the row in question (the one I just updated) and not the entire table.
CREATE TRIGGER [dbo].[after_update]
ON [dbo].[MYTABLE]
AFTER UPDATE
AS
BEGIN
UPDATE MYTABLE
SET mytable.CHANGED_ON = GETDATE(),
CHANGED_BY=USER_NAME(USER_ID())
How do I tell the trigger that this applies only to the row in question?
Here is my example after a test
CREATE TRIGGER [dbo].UpdateTasadoresName
ON [dbo].Tasadores
FOR UPDATE
AS
UPDATE Tasadores
SET NombreCompleto = RTRIM( Tasadores.Nombre + ' ' + isnull(Tasadores.ApellidoPaterno,'') + ' ' + isnull(Tasadores.ApellidoMaterno,'') )
FROM Tasadores
INNER JOIN INSERTED i ON Tasadores.id = i.id
The inserted special table will have the information from the updated record.
Try this (update, not after update)
CREATE TRIGGER [dbo].[xxx_update] ON [dbo].[MYTABLE]
FOR UPDATE
AS
BEGIN
UPDATE MYTABLE
SET mytable.CHANGED_ON = GETDATE()
,CHANGED_BY = USER_NAME(USER_ID())
FROM inserted
WHERE MYTABLE.ID = inserted.ID
END
you can call INSERTED, SQL Server uses these tables to capture the data of the modified row before and after the event occurs.I assume in your table the name of the key is Id
I think the following code can help you
CREATE TRIGGER [dbo].[after_update]
ON [dbo].[MYTABLE]
AFTER UPDATE
AS
BEGIN
UPDATE dbo.[MYTABLE]
SET dbo.[MYTABLE].CHANGED_ON = GETDATE(),
dbo.[MYTABLE].CHANGED_BY = USER_NAME(USER_ID())
FROM INSERTED
WHERE INSERTED.Id = dbo.[MYTABLE].[Id]
END
You should be able to access the INSERTED table and retrieve ID or table's primary key. Something similar to this example ...
CREATE TRIGGER [dbo].[after_update] ON [dbo].[MYTABLE]
AFTER UPDATE AS
BEGIN
DECLARE #id AS INT
SELECT #id = [IdColumnName]
FROM INSERTED
UPDATE MYTABLE
SET mytable.CHANGED_ON = GETDATE(),
CHANGED_BY=USER_NAME(USER_ID())
WHERE [IdColumnName] = #id
Here's a link on MSDN on the INSERTED and DELETED tables available when using triggers: http://msdn.microsoft.com/en-au/library/ms191300.aspx
It is very simple to do that,
First create a copy of your table that your want keep the log for
For example you have Table dbo.SalesOrder with columns SalesOrderId, FirstName,LastName, LastModified
Your Version archieve table should be dbo.SalesOrderVersionArchieve with columns SalesOrderVersionArhieveId, SalesOrderId, FirstName,LastName, LastModified
Here is the how you will set up a trigger on SalesOrder table
USE [YOURDB]
GO
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
-- =============================================
-- Author: Karan Dhanu
-- Create date: <Create Date,,>
-- Description: <Description,,>
-- =============================================
CREATE TRIGGER dbo.[CreateVersionArchiveRow]
ON dbo.[SalesOrder]
AFTER Update
AS
BEGIN
SET NOCOUNT ON;
INSERT INTO dbo.SalesOrderVersionArchive
SELECT *
FROM deleted;
END
Now if you make any changes in saleOrder table it will show you the change in VersionArchieve table
try this solution.
DECLARE #Id INT
DECLARE #field VARCHAR(50)
SELECT #Id= INSERTED.CustomerId
FROM INSERTED
IF UPDATE(Name)
BEGIN
SET #field = 'Updated Name'
END
IF UPDATE(Country)
BEGIN
SET #field = 'Updated Country'
END
INSERT INTO CustomerLogs
VALUES(#Id, #field)
// OR
-- If you wish to update existing table records.
UPDATE YOUR_TABLE SET [FIELD]=[VALUE] WHERE {CONDITION}
I didn't checked this with older version of sql server but this will work with sql server 2012.
Try this script to create a temporary table TESTTEST and watch the order of precedence as the triggers are called in this order: 1) INSTEAD OF, 2) FOR, 3) AFTER
All of the logic is placed in INSTEAD OF trigger and I have 2 examples of how you might code some scenarios...
Good luck...
CREATE TABLE TESTTEST
(
ID INT,
Modified0 DATETIME,
Modified1 DATETIME
)
GO
CREATE TRIGGER [dbo].[tr_TESTTEST_0] ON [dbo].TESTTEST
INSTEAD OF INSERT,UPDATE,DELETE
AS
BEGIN
SELECT 'INSTEAD OF'
SELECT 'TT0.0'
SELECT * FROM TESTTEST
SELECT *, 'I' Mode
INTO #work
FROM INSERTED
UPDATE #work SET Mode='U' WHERE ID IN (SELECT ID FROM DELETED)
INSERT INTO #work (ID, Modified0, Modified1, Mode)
SELECT ID, Modified0, Modified1, 'D'
FROM DELETED WHERE ID NOT IN (SELECT ID FROM INSERTED)
--Check Security or any other logic to add and remove from #work before processing
DELETE FROM #work WHERE ID=9 -- because you don't want anyone to edit this id?!?!
DELETE FROM #work WHERE Mode='D' -- because you don't want anyone to delete any records
SELECT 'EV'
SELECT * FROM #work
IF(EXISTS(SELECT TOP 1 * FROM #work WHERE Mode='I'))
BEGIN
SELECT 'I0.0'
INSERT INTO dbo.TESTTEST (ID, Modified0, Modified1)
SELECT ID, Modified0, Modified1
FROM #work
WHERE Mode='I'
SELECT 'Cool stuff would happen here if you had FOR INSERT or AFTER INSERT triggers.'
SELECT 'I0.1'
END
IF(EXISTS(SELECT TOP 1 * FROM #work WHERE Mode='D'))
BEGIN
SELECT 'D0.0'
DELETE FROM TESTTEST WHERE ID IN (SELECT ID FROM #work WHERE Mode='D')
SELECT 'Cool stuff would happen here if you had FOR DELETE or AFTER DELETE triggers.'
SELECT 'D0.1'
END
IF(EXISTS(SELECT TOP 1 * FROM #work WHERE Mode='U'))
BEGIN
SELECT 'U0.0'
UPDATE t SET t.Modified0=e.Modified0, t.Modified1=e.Modified1
FROM dbo.TESTTEST t
INNER JOIN #work e ON e.ID = t.ID
WHERE e.Mode='U'
SELECT 'U0.1'
END
DROP TABLE #work
SELECT 'TT0.1'
SELECT * FROM TESTTEST
END
GO
CREATE TRIGGER [dbo].[tr_TESTTEST_1] ON [dbo].TESTTEST
FOR UPDATE
AS
BEGIN
SELECT 'FOR UPDATE'
SELECT 'TT1.0'
SELECT * FROM TESTTEST
SELECT 'I1'
SELECT * FROM INSERTED
SELECT 'D1'
SELECT * FROM DELETED
SELECT 'TT1.1'
SELECT * FROM TESTTEST
END
GO
CREATE TRIGGER [dbo].[tr_TESTTEST_2] ON [dbo].TESTTEST
AFTER UPDATE
AS
BEGIN
SELECT 'AFTER UPDATE'
SELECT 'TT2.0'
SELECT * FROM TESTTEST
SELECT 'I2'
SELECT * FROM INSERTED
SELECT 'D2'
SELECT * FROM DELETED
SELECT 'TT2.1'
SELECT * FROM TESTTEST
END
GO
SELECT 'Start'
INSERT INTO TESTTEST (ID, Modified0) VALUES (9, GETDATE())-- not going to insert
SELECT 'RESTART'
INSERT INTO TESTTEST (ID, Modified0) VALUES (10, GETDATE())--going to insert
SELECT 'RESTART'
UPDATE TESTTEST SET Modified1=GETDATE() WHERE ID=10-- gointo to update
SELECT 'RESTART'
DELETE FROM TESTTEST WHERE ID=10-- not going to DELETE
SELECT 'FINISHED'
SELECT * FROM TESTTEST
DROP TABLE TESTTEST
First off, your trigger as you already see is going to update every record in the table. There is no filtering done to accomplish jus the rows changed.
Secondly, you're assuming that only one row changes in the batch which is incorrect as multiple rows could change.
The way to do this properly is to use the virtual inserted and deleted tables: http://msdn.microsoft.com/en-us/library/ms191300.aspx
Trigger
special kind of stored procedure
automatically execured/fired when some event Insert/Update/Delete Occures
use when we want some event to happen automatically on certain desirable scenarios
triggers makes use of 2 tables inserted/deleted table in ssms(memory)
ONLY availabe in context of trigger(CANNOT ACCESS Outside the Trigger
when we insert/delete using trigger, a copy of row is maintained in the inserted/deleted table
inserted table - contains updated data |
deleted table - contains old data
Trigger to Update "ModifiedOn" Date Automatically when record in table is modified(UPDATED)
CREATE TRIGGER [dbo].[Trg_TableName_UpdateModifiedOn]
ON [dbo].[TableName]
AFTER UPDATE
AS
BEGIN
UPDATE [dbo].[TableName]
SET [ModifiedOn] = GetDate()
FROM [inserted]
WHERE [inserted].[PrimaryKey] = [dbo].[TableName].[PrimaryKey];
END;
CREATE TRIGGER [dbo].[after_update] ON [dbo].[MYTABLE]
AFTER UPDATE
AS
BEGIN
DECLARE #ID INT
SELECT #ID = D.ID
FROM inserted D
UPDATE MYTABLE
SET mytable.CHANGED_ON = GETDATE()
,CHANGED_BY = USER_NAME(USER_ID())
WHERE ID = #ID
END

IF EXISTS, THEN SELECT ELSE INSERT AND THEN SELECT

How do you say the following in Microsoft SQL Server 2005:
IF EXISTS (SELECT * FROM Table WHERE FieldValue='') THEN
SELECT TableID FROM Table WHERE FieldValue=''
ELSE
INSERT INTO TABLE(FieldValue) VALUES('')
SELECT TableID FROM Table WHERE TableID=SCOPE_IDENTITY()
END IF
What I'm trying to do is to see if there is a blank fieldvalue already, and if there is then return that TableID, else insert a blank fieldvalue and return the corresponding primary key.
You need to do this in transaction to ensure two simultaneous clients won't insert same fieldValue twice:
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION
DECLARE #id AS INT
SELECT #id = tableId FROM table WHERE fieldValue=#newValue
IF #id IS NULL
BEGIN
INSERT INTO table (fieldValue) VALUES (#newValue)
SELECT #id = SCOPE_IDENTITY()
END
SELECT #id
COMMIT TRANSACTION
you can also use Double-checked locking to reduce locking overhead
DECLARE #id AS INT
SELECT #id = tableID FROM table (NOLOCK) WHERE fieldValue=#newValue
IF #id IS NULL
BEGIN
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION
SELECT #id = tableID FROM table WHERE fieldValue=#newValue
IF #id IS NULL
BEGIN
INSERT INTO table (fieldValue) VALUES (#newValue)
SELECT #id = SCOPE_IDENTITY()
END
COMMIT TRANSACTION
END
SELECT #id
As for why ISOLATION LEVEL SERIALIZABLE is necessary, when you are inside a serializable transaction, the first SELECT that hits the table creates a range lock covering the place where the record should be, so nobody else can insert the same record until this transaction ends.
Without ISOLATION LEVEL SERIALIZABLE, the default isolation level (READ COMMITTED) would not lock the table at read time, so between SELECT and UPDATE, somebody would still be able to insert. Transactions with READ COMMITTED isolation level do not cause SELECT to lock. Transactions with REPEATABLE READS lock the record (if found) but not the gap.
IF EXISTS (SELECT 1 FROM Table WHERE FieldValue='')
BEGIN
SELECT TableID FROM Table WHERE FieldValue=''
END
ELSE
BEGIN
INSERT INTO TABLE(FieldValue) VALUES('')
SELECT SCOPE_IDENTITY() AS TableID
END
See here for more information on IF ELSE
Note: written without a SQL Server install handy to double check this but I think it is correct
Also, I've changed the EXISTS bit to do SELECT 1 rather than SELECT * as you don't care what is returned within an EXISTS, as long as something is
I've also changed the SCOPE_IDENTITY() bit to return just the identity assuming that TableID is the identity column
You were close:
IF EXISTS (SELECT * FROM Table WHERE FieldValue='')
SELECT TableID FROM Table WHERE FieldValue=''
ELSE
BEGIN
INSERT INTO TABLE (FieldValue) VALUES ('')
SELECT TableID FROM Table WHERE TableID=SCOPE_IDENTITY()
END
You just have to change the structure of the if...else..endif somewhat:
if exists(select * from Table where FieldValue='') then begin
select TableID from Table where FieldValue=''
end else begin
insert into Table (FieldValue) values ('')
select TableID from Table where TableID = scope_identity()
end
You could also do:
if not exists(select * from Table where FieldValue='') then begin
insert into Table (FieldValue) values ('')
end
select TableID from Table where FieldValue=''
Or:
if exists(select * from Table where FieldValue='') then begin
select TableID from Table where FieldValue=''
end else begin
insert into Table (FieldValue) values ('')
select scope_identity() as TableID
end
It sounds like your table has no key. You should be able to simply try the INSERT: if it’s a duplicate then the key constraint will bite and the INSERT will fail. No worries: you just need to ensure the application doesn't see/ignores the error. When you say 'primary key' you presumably mean IDENTITY value. That's all very well but you also need a key constraint (e.g. UNIQUE) on your natural key.
Also, I wonder whether your procedure is doing too much. Consider having separate procedures for 'create' and 'read' actions respectively.
DECLARE #t1 TABLE (
TableID int IDENTITY,
FieldValue varchar(20)
)
--<< No empty string
IF EXISTS (
SELECT *
FROM #t1
WHERE FieldValue = ''
) BEGIN
SELECT TableID
FROM #t1
WHERE FieldValue=''
END
ELSE BEGIN
INSERT INTO #t1 (FieldValue) VALUES ('')
SELECT SCOPE_IDENTITY() AS TableID
END
--<< A record with an empty string already exists
IF EXISTS (
SELECT *
FROM #t1
WHERE FieldValue = ''
) BEGIN
SELECT TableID
FROM #t1
WHERE FieldValue=''
END
ELSE BEGIN
INSERT INTO #t1 (FieldValue) VALUES ('')
SELECT SCOPE_IDENTITY() AS TableID
END
create schema tableName authorization dbo
go
IF OBJECT_ID ('tableName.put_fieldValue', 'P' ) IS NOT NULL
drop proc tableName.put_fieldValue
go
create proc tableName.put_fieldValue(#fieldValue int) as
declare #tableid int = 0
select #tableid = tableid from table where fieldValue=''
if #tableid = 0 begin
insert into table(fieldValue) values('')
select #tableid = scope_identity()
end
return #tableid
go
declare #tablid int = 0
exec #tableid = tableName.put_fieldValue('')