I have this stored procedure:
alter procedure spGroupInsert
(#Group varchar(5))
as
if not exists (select * from tbGroup where Group = #Group)
begin
insert into tbGroup(Group)
values(#Group)
end
else
begin
waitfor delay '00:00:01'
end
The stored procedure is designed to prevent duplicates on tbGroup. Next, I need to have an UPDATE stored procedure:
alter procedure spGroupUpdate
(#GroupID int, #Group varchar(5))
as
begin
update tbGroup
set Group = #Group
where GroupID = #GroupID
end
The table should be:
GroupID Group
1 A
2 B
3 C
4 D
ff.
GroupID is identity. For Insert SP, I am really sure there won't be a problem.
But, if I execute the Update stored procedure, then I change the Group. It will be a duplicate. For instance, if I update Group A to B. Then it will make a duplicate of B.
How I can prevent this in T-SQL in my Update stored procedure?
Thank you.
ALTER TABLE tbGroup
ADD CONSTRAINT UC_Group UNIQUE (Group);
this will handle that in the update stored procedure. It will not update when there is an existing row with the same Group
alter procedure spGroupUpdate
(#GroupID int, #Group varchar(5))
as
begin
update tbGroup
set Group = #Group
WHERE GroupID = #GroupID
AND NOT EXISTS
(
SELECT *
FROM tbGroup x
WHERE x.GroupID <> #GroupID
AND x.Group = #Group
)
end
You are saying that for spGroupInsert you are sure that there won't be a problem. In fact, there is a problem.
This procedure doesn't guarantee that you'll never insert a duplicate. If two sessions are trying to insert the same value at the same time, you can easily get duplicates.
Both sessions can do the check if not exists at the same time and both can proceed with INSERT.
alter procedure spGroupInsert
(#Group varchar(5))
as
if not exists (select * from tbGroup where Group = #Group)
begin
insert into tbGroup(Group)values(#Group)
end
else
begin
waitfor delay '00:00:01'
end
The only way to guarantee that Group values are unique is to create a unique constraint, which is usually implemented as a unique index.
CREATE UNIQUE NONCLUSTERED INDEX [IX_Group] ON [dbo].[tbGroup]
(
[Group] ASC
)
GO
With such unique index in place one of the sessions from the example above would fail to INSERT the duplicate and the caller of the stored procedure would receive an error message about unique constraint violation. The caller would need to decide what to do about this error, how to handle it.
The check if not exists reduces the chances of getting this error, but it can't prevent it completely. So, with the check if not exists or without the check if not exists the INSERT can fail and your code should be able to handle this situation.
It seems that you are happy to suppress/ignore the error. In this case a simple TRY ... CATCH can be enough.
alter procedure spGroupInsert
(#Group varchar(5))
as
BEGIN
SET NOCOUNT ON; SET XACT_ABORT ON;
BEGIN TRANSACTION;
BEGIN TRY
if not exists (select * from tbGroup where Group = #Group)
begin
insert into tbGroup(Group)values(#Group);
end
COMMIT TRANSACTION;
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION;
END CATCH;
END
With this stored procedure (and unique index), if two sessions try to call it with the same Group value at the same time, only one will actually insert the value and the second would silently fail and do nothing. The caller would not know about this collision. In your case it may be acceptable.
Related
I'm trying to create a stored procedure where I'm inserting a new office into the OFFICE table I have in my database.
I want to first check whether the office I'm trying to create already exists or not.
Here is some code from where I've gotten so far, but I'm not able to quite get it right. I would greatly appreciate some input.
CREATE PROCEDURE stored_proc_new_office
AS
BEGIN
DECLARE #office_id int
SELECT #office_id = (SELECT office_id FROM inserted)
IF NOT EXISTS (SELECT 1 FROM OFFICE WHERE office_id = #office_id)
BEGIN
ROLLBACK TRANSACTION
PRINT 'Office already exists.'
END
END
Here is a bare bones example of how you can use a stored procedure to insert a new record with a check to ensure it doesn't already exist.
create procedure dbo.AddNewOffice
(
#Name nvarchar(128)
-- ... add parameters for other office details
, #NewId int out
)
as
begin
set nocount on;
insert into dbo.Office([Name]) -- ... add additional columns
select #Name -- ... add additional parameters to match the columns above
where not exists (select 1 from dbo.Office where [Name] = #Name); -- ... add any additional conditions for testing for uniqueness
-- If nothing inserted return an error code for the calling app to use to display something meaningful to the user
if ##rowcount = 0 return 99;
-- return the new id to the calling app.
set #NewId = scope_identity();
return 0;
end
I'm not sure that's the correct way making an update followed by insert in a stored procedure.
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
CREATE PROCEDURE [dbo].[io_sp_admin_add_emp]
#id BIGINT,
#lastName VARCHAR(20),
#firstName VARCHAR(20)
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRY
BEGIN TRANSACTION [TranAddEmp]
DECLARE #identity BIGINT = 0
INSERT INTO empTable(LastName, FirstName, hash_id)
VALUES (#lastName, #firstName,
HashBytes('SHA2_256', CAST(#id AS VARBINARY(50))))
SELECT #identity = ##identity
UPDATE empTable
SET rowId = incId -- both are columns in empTable
WHERE hash_id = #identity
COMMIT TRANSACTION [TranAddEmp]
END TRY
BEGIN CATCH
ROLLBACK TRANSACTION [TranAddEmp]
END CATCH
END
A simple change to your current code can give you what you're looking for.
Instead of messing around with ##Identity, which is almost never the right thing to do, you compute the hash of the #Id value once, store it in a local variable, and use it for both the insert statement and the where clause of the update statement - That is, assuming the HashId column is unique.
That being said, I'm not sure why you need the rowId column as well as the incId column - unless one of them is designed to change it's value through an update statement in the lifetime of the row - you are simply keeping redundant data.
Here's an improved version of your stored procedure:
CRETAE PROCEDURE [dbo].[io_sp_admin_add_emp]
#id BIGINT,
#lastName varchar(20),
#firstName varchar(20)
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRY
BEGIN TRANSACTION [TranAddEmp]
-- Compute the hash once, store in a local variable
DECLARE #HashId varbinary(8000) = HashBytes('SHA2_256', cast(#id as varbinary(50)))
INSERT INTO empTable(
LastName,
FirstName,
hash_id
)
VALUES(
#lastName,
#firstName,
#HashId
)
UPDATE empTable
SET rowId = incId
WHERE hash_id = #HashId
COMMIT TRANSACTION [TranAddEmp]
END TRY
BEGIN CATCH
-- make sure transaction has started and is not commited
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION [TranAddEmp]
END CATCH
END
There is a great keyword OUTPUT. As MSDN says:
Returns information from, or expressions based on, each row affected
by an INSERT, UPDATE, DELETE, or MERGE statement. These results can be
returned to the processing application for use in such things as
confirmation messages, archiving, and other such application
requirements. The results can also be inserted into a table or table
variable. Additionally, you can capture the results of an OUTPUT
clause in a nested INSERT, UPDATE, DELETE, or MERGE statement, and
insert those results into a target table or view.
You can insert your inserted id's into table through OUTPUT keyword. For example:
DECLARE #InsertedIDs TABLE (ID varbinary(8000))
INSERT INTO empTable(
LastName,
FirstName,
hash_id
)
OUTPUT HashBytes('SHA2_256', cast(INSERTED.ID as varbinary(50))) INTO #InsertedIDs(ID)
VALUES(
#lastName,
#firstName,
HashBytes('SHA2_256', cast(#id as varbinary(50)))
)
UPDATE empTable
Set rowId = incId -- both are columns in empTable
WHERE hash_id in (SELECT ID IN #InsertedIDs)
Update: This still remain a mystery. Checked the calling code and we did not find anything that would make the SP run in a loop.
For now we have split the SP into two which seems to have arrested the issue although not able to reason how that has helped out.
Database: MS SQL Server.
I have a SP which performs few operations - i.e inserts a row into 3 tables based on certain status as part of that SP being called.
It is getting called from our web application based on a user action.
We have cases, few times a day where the same row gets inserted multiple times (sometime more than 50+) with the same values in each row except that if you look at the datetime when the row was inserted there is a difference of few milliseconds. So it is unlikely that the user is initiating that action.
This SP is not running in a Transaction or with any locks however it is getting called probably concurrently multiple times as we have many concurrent users on the web application invoking this action.
My question is what is causing the same row to insert so many times? If concurrent execution of SP was an issue where we are updating same row then it is understood one may overwrite the other. However in this case each user calls in the SP with different parameters.
I have put the said operation in a Transaction to monitor the behavior however was looking to find out what exactly causes these kind of multiple inserts with same value just a few milliseconds apart?
USE [ABC]
GO
/****** Object: StoredProcedure [dbo].[AddProcessAdmittedDocUploadScrutinyWithLog] ******/
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROCEDURE [dbo].[AddProcessAdmittedDocUploadScrutinyWithLog]
(
--Insert using bulk
#stdfrm_id int,
#course_id int,
#stdfrm_scrt_apprvby int,
#stdfrm_scrt_apprvcomment varchar(max),
#sRemainingDocs varchar(max),
#DTProcessAdmittedDocUploadScrutiny AS dbo.MyDTProcessAdmittedDocUploadScrutiny READONLY
)
AS
BEGIN
DECLARE #result char
SET #result='N'
--New
declare #AuditCount int=0;
select #AuditCount=count(scrtaudit_id) from tbl_ProcessAdmittedScrutinyAuditLog
where stdfrm_id=#stdfrm_id and stdfrm_scrt_apprvby=#stdfrm_scrt_apprvby
and stdfrm_scrt_apprvcomment=#stdfrm_scrt_apprvcomment and convert(date,stdfrm_scrt_apprvon,103)=convert(date,getdate(),103)
--Checked extra conditon to avoid repeatation
if(#AuditCount=0)
BEGIN
--Call Insert
BEGIN TRY
/*Remaining Documents----------*/
DECLARE #sdtdoc_id Table (n int primary key identity(1,1), id int)
if(#sRemainingDocs is not null)
begin
--INSERT INTO #sdtdoc_id (id) SELECT Name from splitstring(#sRemainingDocs)
INSERT INTO #sdtdoc_id (id) SELECT [Value] from dbo.FN_ListToTable(#sRemainingDocs,',')
end
Declare #isRemaining int=0;
SELECT #isRemaining=Count(*) FROM #sdtdoc_id
/*Calculate stdfrm_scrt_apprvstatus*/
Declare #stdfrm_scrt_apprvstatus char(1)='A';--Approved
Declare #TotalDescripancies int;
select #TotalDescripancies=count(doc_id) from #DTProcessAdmittedDocUploadScrutiny where doc_id_scrtyn='Y'
if(#isRemaining>0)
begin
set #stdfrm_scrt_apprvstatus='H';--Discrepancies Found
end
else if exists (select count(doc_id) from #DTProcessAdmittedDocUploadScrutiny where doc_id_scrtyn='Y')
begin
if(#TotalDescripancies>0)
begin
set #stdfrm_scrt_apprvstatus='H';--Discrepancies Found
end
end
/* Check if Discrepancies Found first time then assign to Checker o.w assign to direct college like grievance*/
if(#stdfrm_scrt_apprvstatus='H')
begin
declare #countAuditLog int=0;
select #countAuditLog=count(stdfrm_id) from tbl_ProcessAdmittedScrutinyAuditLog where stdfrm_id =#stdfrm_id
if (#countAuditLog=0)
begin
set #stdfrm_scrt_apprvstatus='G'--'E';--Discrepancies Found set Edit request assign to Checker
end
--else if (#countAuditLog=1)
-- begin
--set #stdfrm_scrt_apprvstatus='G';--Discrepancies Found set Grievance assign to college
-- end
end
/*----------------------*/
/*Update status in original table-----*/
Update tbl_ProcessAdmitted set stdfrm_scrt_apprvstatus=#stdfrm_scrt_apprvstatus
,stdfrm_scrt_apprvon=getdate(),stdfrm_scrt_apprvby=#stdfrm_scrt_apprvby
,stdfrm_scrt_apprvcomment=#stdfrm_scrt_apprvcomment
where stdfrm_id =#stdfrm_id
/*Add in Main Student Log-----------*/
/********* The row here gets inserted multiple times *******************/
INSERT into tbl_ProcessAdmittedScrutinyAuditLog
(stdfrm_id, stdfrm_scrt_apprvstatus, stdfrm_scrt_apprvon, stdfrm_scrt_apprvby, stdfrm_scrt_apprvcomment )
values
(#stdfrm_id, #stdfrm_scrt_apprvstatus, getdate(), #stdfrm_scrt_apprvby, #stdfrm_scrt_apprvcomment)
DECLARE #scrtaudit_id int =##identity
/*Completed -------------------------*/
DELETE FROM tbl_ProcessAdmittedDocUploadScrutiny WHERE stdfrm_id =#stdfrm_id
SET NOCOUNT ON;
/********* The row here gets inserted multiple times *******************/
INSERT tbl_ProcessAdmittedDocUploadScrutiny
(stdfrm_id, course_id, doc_id, doc_id_scrtyn, doc_id_scrtrmrk, doc_id_comment)
SELECT #stdfrm_id, #course_id, doc_id, doc_id_scrtyn, doc_id_scrtrmrk, doc_id_comment
FROM #DTProcessAdmittedDocUploadScrutiny;
/*Scrutiny Document Log -------------------------*/
/********* The row here gets inserted multiple times *******************/
INSERT tbl_ProcessAdmittedDocUploadScrutinyAuditLog
(scrtaudit_id,stdfrm_id, course_id, doc_id, doc_id_scrtyn, doc_id_scrtrmrk, doc_id_comment)
SELECT #scrtaudit_id,#stdfrm_id, #course_id, doc_id, doc_id_scrtyn, doc_id_scrtrmrk, doc_id_comment
FROM #DTProcessAdmittedDocUploadScrutiny;
/*Remaining Documents Insert into table*/
DELETE FROM tbl_ProcessAdmittedDocUploadScrutinyRemiaing WHERE stdfrm_id =#stdfrm_id
DECLARE #Id int,#doc_id int
WHILE (SELECT Count(*) FROM #sdtdoc_id) > 0
BEGIN
Select Top 1 #Id = n,#doc_id=id From #sdtdoc_id
--Do some processing here
insert into tbl_ProcessAdmittedDocUploadScrutinyRemiaing(stdfrm_id, doc_id )
values (#stdfrm_id,#doc_id)
insert into tbl_ProcessAdmittedDocUploadScrutinyRemiaingAuditLog
(scrtaudit_id, stdfrm_id, doc_id )
values (#scrtaudit_id,#stdfrm_id,#doc_id)
DELETE FROM #sdtdoc_id WHERE n = #Id
END --Begin end While
/*End Remaining Documents-----------*/
SET #result=#stdfrm_scrt_apprvstatus
END TRY
BEGIN CATCH
SET #result='N'
insert into tbl_ErrorSql( ErrorMessage, stdfrm_id)
values(coalesce(Error_Message(),ERROR_LINE()),#stdfrm_id)
END CATCH;
--End of Call Insert
END
SELECT #result
END
I'm implementing in my application an event logging system to save some event types from my code, so I've created a table to store the log type and an Incremental ID:
|LogType|CurrentId|
|info | 1 |
|error | 5 |
And also a table to save the concrete log record
|LogType|IdLog|Message |
|info |1 |Process started|
|error |5 |some error |
So, every time I need to save a new record I call a SPROC to calculate the new id for the log type, basically: newId = (currentId + 1). But I am facing an issue with that calculation because if multiple processes calls the SPROC at the same time the "generated Id" is the same, so I'm getting log records with the same Id, and every record must be Id-unique.
This is my SPROC written for SQL Server 2005:
ALTER PROCEDURE [dbo].[usp_GetLogId]
#LogType VARCHAR(MAX)
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRANSACTION
BEGIN TRY
DECLARE #IdCreated VARCHAR(MAX)
IF EXISTS (SELECT * FROM TBL_ApplicationLogId WHERE LogType = #LogType)
BEGIN
DECLARE #CurrentId BIGINT
SET #CurrentId = (SELECT CurrentId FROM TBL_ApplicationLogId WHERE LogType = #LogType)
DECLARE #NewId BIGINT
SET #NewId = (#CurrentId + 1)
UPDATE TBL_ApplicationLogId
SET CurrentId = #NewId
WHERE LogType = #LogType
SET #IdCreated = CONVERT(VARCHAR, #NewId)
END
ELSE
BEGIN
INSERT INTO TBL_ApplicationLogId VALUES(#LogType, 0)
EXEC #IdCreated = usp_GetLogId #LogType
END
END TRY
BEGIN CATCH
DECLARE #ErrorMessage NVARCHAR(MAX)
SET #ErrorMessage = ERROR_MESSAGE()
IF ##TRANCOUNT > 0
ROLLBACK TRANSACTION;
RAISERROR (#ErrorMessage, 16, 1)
END CATCH
IF ##TRANCOUNT > 0
COMMIT TRANSACTION
SELECT #IdCreated
END
I would appreciate your help to fix the sproc to return an unique id on every call.
It has to work on SQL Server 2005. Thanks
Can you achieve what you want with an identity column?
Then you can just let SQL Server guarantee uniqueness.
Example:
create table my_test_table
(
ID int identity
,SOMEVALUE nvarchar(100)
);
insert into my_test_table(somevalue)values('value1');
insert into my_test_table(somevalue)values('value2');
select * from my_test_table
If you must issue the new ID values yourself for some reason, try using a sequence, as shown here:
if object_id('my_test_table') is not null
begin
drop table my_test_table;
end;
go
create table my_test_table
(
ID int
,SOMEVALUE nvarchar(100)
);
go
if object_id('my_test_sequence') is not null
begin
drop sequence my_test_sequence;
end;
go
CREATE SEQUENCE my_test_sequence
AS INT --other options are here: https://msdn.microsoft.com/en-us/library/ff878091.aspx
START WITH 1
INCREMENT BY 1
MINVALUE 0
NO MAXVALUE;
go
insert into my_test_table(id,somevalue)values(next value for my_test_sequence,'value1');
insert into my_test_table(id,somevalue)values(next value for my_test_sequence,'value2');
insert into my_test_table(id,somevalue)values(next value for my_test_sequence,'value3');
select * from my_test_table
One more edit: I think this is an improvement to the existing stored procedure, given the requirements. Include the new value calculation directly in the UPDATE, ultimately return the value directly from the table (not from a variable which could be out of date) and avoid recursion.
A full test script is below.
if object_id('STACKOVERFLOW_usp_getlogid') is not null
begin
drop procedure STACKOVERFLOW_usp_getlogid;
end
go
if object_id('STACKOVERFLOW_TBL_ApplicationLogId') is not null
begin
drop table STACKOVERFLOW_TBL_ApplicationLogId;
end
go
create table STACKOVERFLOW_TBL_ApplicationLogId(CurrentID int, LogType nvarchar(max));
go
create PROCEDURE [dbo].[STACKOVERFLOW_USP_GETLOGID](#LogType VARCHAR(MAX))
AS
BEGIN
SET NOCOUNT ON;
BEGIN TRANSACTION
BEGIN TRY
DECLARE #IdCreated VARCHAR(MAX)
IF EXISTS (SELECT * FROM STACKOVERFLOW_TBL_ApplicationLogId WHERE LogType = #LogType)
BEGIN
UPDATE STACKOVERFLOW_TBL_APPLICATIONLOGID
SET CurrentId = CurrentID + 1
WHERE LogType = #LogType
END
ELSE
BEGIN
--first time: insert 0.
INSERT INTO STACKOVERFLOW_TBL_ApplicationLogId(CurrentID,LogType) VALUES(0,#LogType);
END
END TRY
BEGIN CATCH
DECLARE #ErrorMessage NVARCHAR(MAX)
SET #ErrorMessage = ERROR_MESSAGE()
IF ##TRANCOUNT > 0
begin
ROLLBACK TRANSACTION;
end
RAISERROR(#ErrorMessage, 16, 1);
END CATCH
select CurrentID from STACKOVERFLOW_TBL_APPLICATIONLOGID where LogType = #LogType;
IF ##TRANCOUNT > 0
begin
COMMIT TRANSACTION
END
end
go
exec STACKOVERFLOW_USP_GETLOGID 'TestLogType1';
exec STACKOVERFLOW_USP_GETLOGID 'TestLogType1';
exec STACKOVERFLOW_USP_GETLOGID 'TestLogType1';
exec STACKOVERFLOW_USP_GETLOGID 'TestLogType2';
exec STACKOVERFLOW_USP_GETLOGID 'TestLogType2';
exec STACKOVERFLOW_USP_GETLOGID 'TestLogType2';
You want your increment and read to be atomic, with a guarantee that no other process can increment in between.
You also need to ensure that the log type exists, and again for it to be thread-safe.
Here's how I would go about that, but you would be advised to read up on how it all works in SQL Server 2005 as I have not had to deal with these things in nearly 8 years.
This should complete the two actions atomically, and also without transactions, in order to prevent threads blocking each other. (Not just performance, but also to avoid DEADLOCKs when interacting with other code.)
ALTER PROCEDURE [dbo].[usp_GetLogId]
#LogType VARCHAR(MAX)
AS
BEGIN
SET NOCOUNT ON;
-- Hold our newly created id in a temp table, so we can use OUTPUT
DECLARE #new_id TABLE (id BIGINT);
-- I think this is thread safe, doing all things in a single statement
----> Check that the log-type has no records
----> If so, then insert an initialising row
----> Output the newly created id into our temp table
INSERT INTO
TBL_ApplicationLogId (
LogType,
CurrentId
)
OUTPUT
INSERTED.CurrentID
INTO
#new_id
SELECT
#LogType, 1
FROM
TBL_ApplicationLogId
WHERE
LogType = #LogType
GROUP BY
LogType
HAVING
COUNT(*) = 0
;
-- I think this is thread safe, doing all things in a single statement
----> Ensure we don't already have a new id created
----> Increment the current id
----> Output it to our temp table
UPDATE
TBL_ApplicationLogId
SET
CurrentId = CurrentId + 1
OUTPUT
INSERTED.CurrentID
INTO
#new_id
WHERE
LogType = #LogType
AND NOT EXISTS (SELECT * FROM #new_id)
;
-- Select the result from our temp table
----> It must be populated either from the INSERT or the UPDATE
SELECT
MAX(id) -- MAX used to tell the system that it's returning a scalar
FROM
#new_id
;
END
Not much you can do here, but validate that:
table TBL_ApplicationLogId is indexed by column LogType.
#LogType sp parameter is the same data type as column LogType in table TBL_ApplicationLogId, so it can actually use the index if/when it exists.
If you have a concurrency issue, maybe forcing the lock level on table TBL_ApplicationLogId during select and update can help. Just add (ROWLOCK) after the table name, Eg: TBL_ApplicationLogId (ROWLOCK)
I want to create a trigger that will not allow to past a client out from USA.
So my code is:
CREATE TRIGGER location_tr
ON t1 FOR INSERT, UPDATE
AS
BEGIN
DECLARE #country VARCHAR(50)
SET #country = (SELECT country FROM inserted);
IF #country <> 'USA'
BEGIN
PRINT 'You cant add user out of USA!'
ROLLBACK TRANSACTION
END
ELSE
COMMIT TRANSACTION
END
So now, I can't do any UPDATE or INSERT on the table t1.
I guess I'm going wrong with a COMMIT/ROLLBACK commands. Need help. Thanks
The first issue is that your trigger does not support multiple rows. You should use EXISTS instead of setting the value of a variable. Something along these lines.
CREATE TRIGGER location_tr ON t1
FOR INSERT, UPDATE AS
BEGIN
IF EXISTS(select * from inserted where Country <> 'USA')
BEGIN
PRINT 'You cant add user out of USA!'
ROLLBACK TRANSACTION
END
END