Best way to do serializable updates to a table - sql-server

I'm a long time Firebird user and it has a feature called Generators (I think Oracle also has it and it's called Sequences). I'm new to SQL Server and I need to simulate the same feature. I can't use an identity field to solve my problem. I need a named series of values, not a unique number for each row.
My biggest concern was about more than one user calling the procedure at the same time and getting duplicated values, so I decided to lock using the SERIALIZABLE isolation level.
So I came up with this code (I'm using SQL Server 2005):
CREATE TABLE dbo.GENERATORS
(
[NAME] VARCHAR(30) NOT NULL,
[VALUE] INT,
CONSTRAINT UNQ_GENERATORS_NAME UNIQUE NONCLUSTERED ( [NAME] )
)
GO
CREATE PROCEDURE GEN_ID
(
#GENERATOR_NAME VARCHAR(30)
)
AS
DECLARE #RETURN_VALUE INT ;
SET NOCOUNT ON
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE
BEGIN TRANSACTION
UPDATE GENERATORS
SET [VALUE] = [VALUE] + 1
WHERE [NAME] = #GENERATOR_NAME
IF ##ROWCOUNT = 0
BEGIN
INSERT INTO dbo.GENERATORS ( [NAME], [VALUE] )
VALUES ( #GENERATOR_NAME, 1 )
SET #RETURN_VALUE = 1
END
ELSE
BEGIN
SELECT #RETURN_VALUE = [VALUE]
FROM GENERATORS
WHERE [NAME] = #GENERATOR_NAME
END
COMMIT TRANSACTION
RETURN #RETURN_VALUE
GO
So my questions are:
Is this a good solution?
Is there any better way?
Thanks.

Instead of UPDATE and SELECT, to get the value back that you just added, you can do
UPDATE GENERATORS
SET #RETURN_VALUE = [VALUE] = [VALUE] + 1
WHERE [NAME] = #GENERATOR_NAME
which may remove the need for isolation level
I would prefer to return #RETURN_VALUE as an OUTPUT parameter, rather than as a Return value - leaving the Return Value free for any error code during execution.

Related

Using SQL Server triggers, how to keep data in two identical tables the same without going in a infinite loop?

I know that probably the best way to acomplish this would be to make some changes in the application code to save all changes in both tables, but the company ordered to make this happens with database logic, using triggers. So, there are two different databases, both with a table named User, and they both have the same model already. What I insert/update in User on database X have to be inserted/updated in table User on database Y, and vice-versa.
I managed to make the insert and update trigger going in one direction (database X -> database Y), but now i'm thinking that when I create the trigger on database Y, a loop would happen. What is missing or what can I do to make the trigger loop not happen?
This is what I have created for now on one of the databases:
---insert trigger
USE [DATABASE_Y]
CREATE TRIGGER [dbo].[TR_USER_OnInsert] ON [dbo].[USER]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON
INSERT INTO [DATABASE_X].[dbo].[USER] (
usu_id,
usu_name,
usu_block,
usu_login,
usu_password
)
SELECT
usu_id,
usu_name,
usu_block,
usu_login,
usu_password
FROM INSERTED
SET NOCOUNT OFF
END
---update trigger
USE [DATABASE_Y]
ALTER TRIGGER [dbo].[TR_USER_OnUpdate] ON [dbo].[USER]
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON
UPDATE X
SET
X.usu_id = INSERTED.usu_id,
X.usu_name = INSERTED.usu_name,
X.usu_block = INSERTED.usu_block,
X.usu_login = INSERTED.usu_login,
X.usu_password = INSERTED.usu_password
FROM [DATABASE_X].[dbo].[USER] X
INNER JOIN inserted ON X.usu_login = inserted.usu_login
SET NOCOUNT OFF
END
Interesting approach. There are a number of different ways to solve this problem, most of them are better than using triggers.
That said...
As part of your triggered code, create some variables in memory then run a select on the target database to fill them before running your update / insert. Use some simple logic to check that the values aren't already set to those same values, and avoid getting into a loop.
something like this for your update trigger:
USE [DATABASE_Y]
ALTER TRIGGER [dbo].[TR_USER_OnUpdate] ON [dbo].[USER]
AFTER UPDATE
AS
BEGIN
SET NOCOUNT ON
DECLARE #id as varchar(100)
DECLARE #name as varchar(100)
DECLARE #block as varchar(100)
DECLARE #login as varchar(100)
DECLARE #password as varchar(100)
SELECT #id = X.usu_id,
#name = X.usu_name,
#block = X.usu_block,
#login = X.usu_login,
#password = X.usu_password
FROM [DATABASE_X].[dbo].[USER] X
WHERE X.usu_login = inserted.usu_login
IF #id <> INSERTED.usu_id
OR #name <> INSERTED.usu_name
OR #block <> INSERTED.usu_block
OR #login <> INSERTED.usu_login
OR #password <> INSERTED.usu_password
BEGIN
UPDATE X
SET
X.usu_id = INSERTED.usu_id,
X.usu_name = INSERTED.usu_name,
X.usu_block = INSERTED.usu_block,
X.usu_login = INSERTED.usu_login,
X.usu_password = INSERTED.usu_password
FROM [DATABASE_X].[dbo].[USER] X
INNER JOIN inserted ON X.usu_login = inserted.usu_login
END
SET NOCOUNT OFF
END
I don't know what your datatypes are so I just assumed varchar(100) for everything.
Make sure that all of the columns are NOT NULL. If you have any Nullable columns, make sure you add ISNULL logic to the variable comparisons.
The insert trigger is a little easier, since you just need to check that the userId you're trying to insert doesn't already exist:
USE [DATABASE_Y]
CREATE TRIGGER [dbo].[TR_USER_OnInsert] ON [dbo].[USER]
AFTER INSERT
AS
BEGIN
SET NOCOUNT ON
IF NOT EXISTS(SELECT 1 FROM [DATABASE_X].[dbo].[USER] WHERE usu_login = INSERTED.usu_login)
BEGIN
INSERT INTO [DATABASE_X].[dbo].[USER] (
usu_id,
usu_name,
usu_block,
usu_login,
usu_password
)
SELECT
usu_id,
usu_name,
usu_block,
usu_login,
usu_password
FROM INSERTED
END
SET NOCOUNT OFF
END

How to get and use the value returned by a stored procedure to a INSERT INTO... SELECT... statement

I am just new in SQL language and still studying it. I'm having hard time looking for answer on how can I use the stored procedure and insert value into a table.
I have this stored procedure:
CREATE PROCEDURE TestID
AS
SET NOCOUNT ON;
BEGIN
DECLARE #NewID VARCHAR(30),
#GenID INT,
#BrgyCode VARCHAR(5) = '23548'
SET #GenID = (SELECT TOP (1) NextID
FROM dbo.RandomIDs
WHERE IsUsed = 0
ORDER BY RowNumber)
SET #NewID = #BrgyCode + '-' + CAST(#GenID AS VARCHAR (30))
UPDATE dbo.RandomIDs
SET dbo.RandomIDs.IsUsed = 1
WHERE dbo.RandomIDs.NextID = #GenID
SELECT #NewID
END;
and what I'm trying to do is this:
INSERT INTO dbo.Residents([ResidentID], NewResidentID, [ResLogdate],
...
SELECT
[ResidentID],
EXEC TestID ,
[ResLogdate],
....
FROM
source.dbo.Resident;
There is a table dbo.RandomIDs containing random 6 digit non repeating numbers where I'm pulling out the value via the stored procedure and updating the IsUsed column of the table to 1. I'm transferring data from one database to another database and doing some processing on the data while transferring. Part of the processing is generating a new ID with the required format.
But I can't get it to work Sad I've been searching the net for hours now but I'm not getting the information that I need and that the reason for my writing. I hope someone could help me with this.
Thanks,
Darren
your question is little bit confusing, because you have not explained what you want to do. As i got your question, you want to fetch random id from randomids table and after performed some processing on nextid you want to insert it into resident table [newresidentid] and end of the procedure you fetch data from resident table. if i get anything wrong feel free to ask me.
your procedure solution is following.
CREATE PROCEDURE [TestId]
AS
SET NOCOUNT ON;
BEGIN
DECLARE #NEWID NVARCHAR(30)
DECLARE #GENID BIGINT
DECLARE #BRGYCODE VARCHAR(5) = '23548'
DECLARE #COUNT INTEGER
DECLARE #ERR NVARCHAR(20) = 'NO IDS IN RANDOM ID'
SET #COUNT = (SELECT COUNT(NEXTID) FROM RandomIds WHERE [IsUsed] = 0)
SET #GENID = (SELECT TOP(1) [NEXTID] FROM RandomIds WHERE [IsUsed] = 0 ORDER BY [ID] ASC)
--SELECT #GENID AS ID
IF #COUNT = 0
BEGIN
SELECT #ERR AS ERROR
END
ELSE
BEGIN
SET #NEWID = #BRGYCODE + '-' + CAST(#GENID AS varchar(30))
UPDATE RandomIds SET [IsUsed] = 1 WHERE [NextId] = #GENID
INSERT INTO Residents ([NewResidentId] , [ResLogDate] ) VALUES (#NEWID , GETDATE())
SELECT * FROM Residents
END
END
this procedure will fetch data from your randomids table and perform some processing on nextid than after it directs insert it into resident table and if you want to insert some data through user you can use parameter after declaring procedure name
E.G
CREATE PROCEDURE [TESTID]
#PARAM1 DATATYPE,
#PARAM2 DATATYPE
AS
BEGIN
END
I'm not convinced that your requirement is a good one but here is a way to do it.
Bear in mind that concurrent sessions will not be able to read your update until it is committed so you have to kind of "lock" the update so you will get a block until you're going to commit or rollback. This is rubbish for concurrency, but that's a side effect of this requirement.
declare #cap table ( capturedValue int);
declare #GENID int;
update top (1) RandomIds set IsUsed=1
output inserted.NextID into #cap
where IsUsed=0;
set #GENID =(select max( capturedValue) from #cap )
A better way would be to use an IDENTITY or SEQUENCE to solve your problem. This would leave gaps but help concurrency.

Duplicate Auto Numbers generated in SQL Server

Be gentle, I'm a SQL newbie. I have a table named autonumber_settings like this:
Prefix | AutoNumber
SO | 112320
CA | 3542
A whenever a new sales line is created, a stored procedure is called that reads the current autonumber value from the 'SO' row, then increments the number, updates that same row, and return the number back from the stored procedure. The stored procedure is below:
ALTER PROCEDURE [dbo].[GetAutoNumber]
(
#type nvarchar(50) ,
#out nvarchar(50) = '' OUTPUT
)
as
set nocount on
declare #currentvalue nvarchar(50)
declare #prefix nvarchar(10)
if exists (select * from autonumber_settings where lower(autonumber_type) = lower(#type))
begin
select #prefix = isnull(autonumber_prefix,''),#currentvalue=autonumber_currentvalue
from autonumber_settings
where lower(autonumber_type) = lower(#type)
set #currentvalue = #currentvalue + 1
update dbo.autonumber_settings set autonumber_currentvalue = #currentvalue where lower(autonumber_type) = lower(#type)
set #out = cast(#prefix as nvarchar(10)) + cast(#currentvalue as nvarchar(50))
select #out as value
end
else
select '' as value
Now, there is another procedure that accesses the same table that duplicates orders, copying both the header and the lines. On occasion, the duplication results in duplicate line numbers. Here is a piece of that procedure:
BEGIN TRAN
IF exists
(
SELECT *
FROM autonumber_settings
WHERE autonumber_type = 'SalesOrderDetail'
)
BEGIN
SELECT
#prefix = ISNULL(autonumber_prefix,'')
,#current_value=CAST (autonumber_currentvalue AS INTEGER)
FROM autonumber_settings
WHERE autonumber_type = 'SalesOrderDetail'
SET #new_auto_number = #current_value + #number_of_lines
UPDATE dbo.autonumber_settings
SET autonumber_currentvalue = #new_auto_number
WHERE autonumber_type = 'SalesOrderDetail'
END
COMMIT TRAN
Any ideas on why the two procedures don't seem to play well together, occasionally giving the same line numbers created from scratch as lines created by duplication.
This is a race condition or your autonumber assignment. Two executions have the potential to read out the same value before a new one is written back to the database.
The best way to fix this is to use an identity column and let SQL server handle the autonumber assignments.
Barring that you could use sp_getapplock to serialize your access to autonumber_settings.
You could use repeatable read on the selects. That will lock the row and block the other procedure's select until you update the value and commit.
Insert WITH (REPEATABLEREAD,ROWLOCK) after the from clause for each select.

SQL Server - Auto-incrementation that allows UPDATE statements

When adding an item in my database, I need it to auto-determine the value for the field DisplayOrder. Identity (auto-increment) would be an ideal solution, but I need to be able to programmatically change (UPDATE) the values of the DisplayOrder column, and Identity doesn't seem to allow that. For the moment, I use this code:
CREATE PROCEDURE [dbo].[AddItem]
AS
DECLARE #DisplayOrder INT
SET #DisplayOrder = (SELECT MAX(DisplayOrder) FROM [dbo].[MyTable]) + 1
INSERT INTO [dbo].[MyTable] ( DisplayOrder ) VALUES ( #DisplayOrder )
Is it the good way to do it or is there a better/simpler way?
A solution to this issue from "Inside Microsoft SQL Server 2008: T-SQL Querying"
CREATE TABLE dbo.Sequence(
val int IDENTITY (10000, 1) /*Seed this at whatever your current max value is*/
)
GO
CREATE PROC dbo.GetSequence
#val AS int OUTPUT
AS
BEGIN TRAN
SAVE TRAN S1
INSERT INTO dbo.Sequence DEFAULT VALUES
SET #val=SCOPE_IDENTITY()
ROLLBACK TRAN S1 /*Rolls back just as far as the save point to prevent the
sequence table filling up. The id allocated won't be reused*/
COMMIT TRAN
Or another alternative from the same book that allocates ranges easier. (You would need to consider whether to call this from inside or outside your transaction - inside would block other concurrent transactions until the first one commits)
CREATE TABLE dbo.Sequence2(
val int
)
GO
INSERT INTO dbo.Sequence2 VALUES(10000);
GO
CREATE PROC dbo.GetSequence2
#val AS int OUTPUT,
#n as int =1
AS
UPDATE dbo.Sequence2
SET #val = val = val + #n;
SET #val = #val - #n + 1;
You can set your incrementing column to use the identity property. Then, in processes that need to insert values into the column you can use the SET IDENITY_INSERT command in your batch.
For inserts where you want to use the identity property, you exclude the identity column from the list of columns in your insert statement:
INSERT INTO [dbo].[MyTable] ( MyData ) VALUES ( #MyData )
When you want to insert rows where you are providing the value for the identity column, use the following:
SET IDENTITY_INSERT MyTable ON
INSERT INTO [dbo].[MyTable] ( DisplayOrder, MyData )
VALUES ( #DisplayOrder, #MyData )
SET IDENTITY_INSERT MyTable OFF
You should be able to UPDATE the column without any other steps.
You may also want to look into the DBCC CHECKIDENT command. This command will set your next identity value. If you are inserting rows where the next identity value might not be appropriate, you can use the command to set a new value.
DECLARE #DisplayOrder INT
SET #DisplayOrder = (SELECT MAX(DisplayOrder) FROM [dbo].[MyTable]) + 1
DBCC CHECKIDENT (MyTable, RESEED, #DisplayOrder)
Here's the solution that I kept:
CREATE PROCEDURE [dbo].[AddItem]
AS
DECLARE #DisplayOrder INT
BEGIN TRANSACTION
SET #DisplayOrder = (SELECT ISNULL(MAX(DisplayOrder), 0) FROM [dbo].[MyTable]) + 1
INSERT INTO [dbo].[MyTable] ( DisplayOrder ) VALUES ( #DisplayOrder )
COMMIT TRANSACTION
One thing you should do is to add commands so that your procedure's run as a transaction, otherwise two inserts running at the same time could produce two rows with the same value in DisplayOrder.
This is easy enough to achieve: add
begin transaction
at the start of your procedure, and
commit transaction
at the end.
You way works fine (with a little modification) and is simple. I would wrap it in a transaction like #David Knell said. This would result in code like:
CREATE PROCEDURE [dbo].[AddItem]
AS
DECLARE #DisplayOrder INT
BEGIN TRANSACTION
SET #DisplayOrder = (SELECT MAX(DisplayOrder) FROM [dbo].[MyTable]) + 1
INSERT INTO [dbo].[MyTable] ( DisplayOrder ) VALUES ( #DisplayOrder )
COMMIT TRANSACTION
Wrapping your SELECT & INSERT in a transaction guarantees that your DisplayOrder values won't be duplicated by AddItem. If you are doing a lot of simultaneous adding (many per second), there may be contention on MyTable but for occasional inserts it won't be a problem.

SQL Server concurrency and generated sequence

I need a sequence of numbers for an application, and I am hoping to leverage the abilities of SQL Server to do it. I have created the following table and procedure (in SQL Server 2005):
CREATE TABLE sequences (
seq_name varchar(50) NOT NULL,
seq_value int NOT NULL
)
CREATE PROCEDURE nextval
#seq_name varchar(50)
AS
BEGIN
DECLARE #seq_value INT
SET #seq_value = -1
UPDATE sequences
SET #seq_value = seq_value = seq_value + 1
WHERE seq_name = #seq_name
RETURN #seq_value
END
I am a little concerned that without locking the table/row another request could happen concurrently and end up returning the same number to another thread or client. This would be very bad obviously. Is this design safe in this regard? Is there something I can add that would add the necessary locking to make it safe?
Note: I am aware of IDENTITY inserts in SQL Server - and that is not what I am looking for this in particular case. Specifically, I don't want to be inserting/deleting rows. This is basically to have a central table that manages the sequential number generator for a bunch of sequences.
The UPDATE will lock the row exclusively so your concurrency concerns are not founded. But use of #variable assignment in UPDATE statements is relying on undefined behavior. It's true, it will work, but rather rely on defined behavior: use the OUTPUT clause.
CREATE PROCEDURE nextval
#seq_name varchar(50)
, #seq_value INT output
AS
BEGIN
DECLARE #ot TABLE (seq_value INT)
UPDATE sequences
SET seq_value = seq_value + 1
OUTPUT INSERTED.seq_value INTO #ot
WHERE seq_name = #seq_name
SELECT #seq_value = seq_value FROM #ot;
END
Try this code:
ALTER PROCEDURE Item_Category_Details
#Item_Category varchar(20)
,#Active VARCHAR(10)
AS
DECLARE #Item_Category_Code varchar(20)
DECLARE #seqNo AS INTEGER
SELECT #seqNo=ISNULL(Item_Code,0) FROM Seq_Master
SET #seqNo=#seqNo+1
SET #Item_Category_Code=#seqNo
PRINT #Item_Category_Code
INSERT Item_Category_Master
(Item_Category_Code
,Item_Category
,Active
)
VALUES
(
#Item_Category_Code
,#Item_Category
,#Active
)
UPDATE Seq_Master SET Item_Code=#seqNo

Resources