SQL Server concurrency and generated sequence - sql-server

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

Related

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.

Insert/Update/Delete with function in SQL Server

Can we perform Insert/Update/Delete statement with SQL Server Functions. I have tried with but SQL Server error is occured.
Error:
Invalid use of side-effecting or time-dependent operator in 'DELETE' within a function.
AnyBody have any Idea why we can not use Insert/Update/Delete statements with SQL Server functions.
Waiting for your good idea's
No, you cannot.
From SQL Server Books Online:
User-defined functions cannot be used
to perform actions that modify the
database state.
Ref.
Yes, you can!))
Disclaimer: This is not a solution, it is more of a hack to test out something. User-defined functions cannot be used to perform actions that modify the database state.
I found one way to make INSERT, UPDATE or DELETE in function using xp_cmdshell.
So you need just to replace the code inside #sql variable.
CREATE FUNCTION [dbo].[_tmp_func](#orderID NVARCHAR(50))
RETURNS INT
AS
BEGIN
DECLARE #sql varchar(4000), #cmd varchar(4000)
SELECT #sql = 'INSERT INTO _ord (ord_Code) VALUES (''' + #orderID + ''') '
SELECT #cmd = 'sqlcmd -S ' + ##servername +
' -d ' + db_name() + ' -Q "' + #sql + '"'
EXEC master..xp_cmdshell #cmd, 'no_output'
RETURN 1
END
Functions in SQL Server, as in mathematics, can not be used to modify the database. They are intended to be read only and can help developer to implement command-query separation. In other words, asking a question should not change the answer. When your program needs to modify the database use a stored procedure instead.
You can't update tables from a function like you would a stored procedure, but you CAN update table variables.
So for example, you can't do this in your function:
create table MyTable
(
ID int,
column1 varchar(100)
)
update [MyTable]
set column1='My value'
but you can do:
declare #myTable table
(
ID int,
column1 varchar(100)
)
Update #myTable
set column1='My value'
Yes, you can.
However, it requires SQL CLR with EXTERNAL_ACCESS or UNSAFE permission and specifying a connection string. This is obviously not recommended.
For example, using Eval SQL.NET (a SQL CLR which allow to add C# syntax in SQL)
CREATE FUNCTION [dbo].[fn_modify_table_state]
(
#conn VARCHAR(8000) ,
#sql VARCHAR(8000)
)
RETURNS INT
AS
BEGIN
RETURN SQLNET::New('
using(var connection = new SqlConnection(conn))
{
connection.Open();
using(var command = new SqlCommand(sql, connection))
{
return command.ExecuteNonQuery();
}
}
').ValueString('conn', #conn).ValueString('sql', #sql).EvalReadAccessInt()
END
GO
DECLARE #conn VARCHAR(8000) = 'Data Source=XPS8700;Initial Catalog=SqlServerEval_Debug;Integrated Security=True'
DECLARE #sql VARCHAR(8000) = 'UPDATE [Table_1] SET Value = -1 WHERE Name = ''zzz'''
DECLARE #rowAffecteds INT = dbo.fn_modify_table_state(#conn, #sql)
Documentation: Modify table state within a SQL Function
Disclaimer: I'm the owner of the project Eval SQL.NET
You can have a table variable as a return type and then update or insert on a table based on that output.
In other words, you can set the variable output as the original table, make the modifications and then do an insert to the original table from function output.
It is a little hack but if you insert the #output_table from the original table and then say for example:
Insert into my_table
select * from my_function
then you can achieve the result.
We can't say that it is possible of not their is some other way exist to perform update operation in user-defined Function. Directly DML is not possible in UDF it is for sure.
Below Query is working perfectly:
create table testTbl
(
id int identity(1,1) Not null,
name nvarchar(100)
)
GO
insert into testTbl values('ajay'),('amit'),('akhil')
Go
create function tblValued()
returns Table
as
return (select * from testTbl where id = 1)
Go
update tblValued() set name ='ajay sharma' where id = 1
Go
select * from testTbl
Go
"Functions have only READ-ONLY Database Access"
If DML operations would be allowed in functions then function would be prety similar to stored Procedure.
No, you can not do Insert/Update/Delete.
Functions only work with select statements. And it has only READ-ONLY Database Access.
In addition:
Functions compile every time.
Functions must return a value or result.
Functions only work with input parameters.
Try and catch statements are not used in functions.
CREATE FUNCTION dbo.UdfGetProductsScrapStatus
(
#ScrapComLevel INT
)
RETURNS #ResultTable TABLE
(
ProductName VARCHAR(50), ScrapQty FLOAT, ScrapReasonDef VARCHAR(100), ScrapStatus VARCHAR(50)
) AS BEGIN
INSERT INTO #ResultTable
SELECT PR.Name, SUM([ScrappedQty]), SC.Name, NULL
FROM [Production].[WorkOrder] AS WO
INNER JOIN
Production.Product AS PR
ON Pr.ProductID = WO.ProductID
INNER JOIN Production.ScrapReason AS SC
ON SC.ScrapReasonID = WO.ScrapReasonID
WHERE WO.ScrapReasonID IS NOT NULL
GROUP BY PR.Name, SC.Name
UPDATE #ResultTable
SET ScrapStatus =
CASE WHEN ScrapQty > #ScrapComLevel THEN 'Critical'
ELSE 'Normal'
END
RETURN
END
Functions are not meant to be used that way, if you wish to perform data change you can just create a Stored Proc for that.
if you need to run the delete/insert/update you could also run dynamic statements. i.e.:
declare
#v_dynDelete NVARCHAR(500);
SET #v_dynDelete = 'DELETE some_table;';
EXEC #v_dynDelete
Just another alternative using sp_executesql (tested only in SQL 2016).
As previous posts noticed, atomicity must be handled elsewhere.
CREATE FUNCTION [dbo].[fn_get_service_version_checksum2]
(
#ServiceId INT
)
RETURNS INT
AS
BEGIN
DECLARE #Checksum INT;
SELECT #Checksum = dbo.fn_get_service_version(#ServiceId);
DECLARE #LatestVersion INT = (SELECT MAX(ServiceVersion) FROM [ServiceVersion] WHERE ServiceId = #ServiceId);
-- Check whether the current version already exists and that it's the latest version.
IF EXISTS(SELECT TOP 1 1 FROM [ServiceVersion] WHERE ServiceId = #ServiceId AND [Checksum] = #Checksum AND ServiceVersion = #LatestVersion)
RETURN #LatestVersion;
-- Insert the new version to the table.
EXEC sp_executesql N'
INSERT INTO [ServiceVersion] (ServiceId, ServiceVersion, [Checksum], [Timestamp])
VALUES (#ServiceId, #LatestVersion + 1, #Checksum, GETUTCDATE());',
N'#ServiceId INT = NULL, #LatestVersion INT = NULL, #Checksum INT = NULL',
#ServiceId = #ServiceId,
#LatestVersion = #LatestVersion,
#Checksum = #Checksum
;
RETURN #LatestVersion + 1;
END;

Search an replace text in column for Column Type Text SQL Server

What I need is to search for a string in a specific column (datatype: text) of a table and replace it with another text.
For example
Id | Text
-----------------------------
1 this is test
2 that is testosterone
If I chose to replace test with quiz, results should be
this is quiz
that is quizosterone
What I've tried so far?
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
ALTER PROC [dbo].[SearchAndReplace]
(
#FindString NVARCHAR(100)
,#ReplaceString NVARCHAR(100)
)
AS
BEGIN
SET NOCOUNT ON
SELECT CONTENT_ID as id, CONTENT_TEXT, textptr(CONTENT_TEXT) as ptr, datalength(CONTENT_TEXT) as lng
INTO #newtable6 FROM HTML_CONTENTS
DECLARE #COUNTER INT = 0
DECLARE #TextPointer VARBINARY(16)
DECLARE #DeleteLength INT
DECLARE #OffSet INT
SELECT #TextPointer = TEXTPTR(CONTENT_TEXT)
FROM #newtable6
SET #DeleteLength = LEN(#FindString)
SET #OffSet = 0
SET #FindString = '%' + #FindString + '%'
WHILE (SELECT COUNT(*)
FROM #newtable6
WHERE PATINDEX(#FindString, CONTENT_TEXT) <> 0) > 0
BEGIN
SELECT #OffSet = PATINDEX(#FindString, CONTENT_TEXT) - 1
FROM #newtable6
WHERE PATINDEX(#FindString, CONTENT_TEXT) <> 0
UPDATETEXT #newtable6.CONTENT_TEXT
#TextPointer
#OffSet
#DeleteLength
#ReplaceString
SET #COUNTER = #COUNTER + 1
END
select #COUNTER,* from #newtable6
drop table #newtable6
SET NOCOUNT OFF
I get the error:
Msg 7116, Level 16, State 4, Procedure SearchAndReplace, Line 31
Offset 1900 is not in the range of available LOB data.
The statement has been terminated.
Thank you
If you can't change your column types permanently, you can cast them on the fly:
ALTER PROC [dbo].[SearchAndReplace]
(#FindString VARCHAR(100),
#ReplaceString VARCHAR(100) )
AS
BEGIN
UPDATE dbo.HTML_CONTENTS
SET CONTENT_TEXT = cast (REPLACE(cast (CONTEXT_TEXT as varchar(max)), #FindString, #ReplaceString) as TEXT)
END
The datatype TEXT is deprecated and should not be used anymore - exactly because it's clunky and doesn't support all the usual string manipulation methods.
From the MSDN docs on text, ntext, image:
ntext, text, and image data types will
be removed in a future version of
MicrosoftSQL Server. Avoid using these
data types in new development work,
and plan to modify applications that
currently use them. Use nvarchar(max),
varchar(max), and varbinary(max)
instead.
My recommendation: convert that column to VARCHAR(MAX) and you should be fine after that!
ALTER TABLE dbo.HTML_CONTENTS
ALTER COLUMN CONTEXT_TEXT VARCHAR(MAX)
That should do it.
When your column is VARCHAR(MAX), then your stored procedures becomes totally simple:
ALTER PROC [dbo].[SearchAndReplace]
(#FindString VARCHAR(100),
#ReplaceString VARCHAR(100) )
AS
BEGIN
UPDATE dbo.HTML_CONTENTS
SET CONTENT_TEXT = REPLACE(CONTEXT_TEXT, #FindString, #ReplaceString)
END
Two observations on the side:
it would be helpful to have a WHERE clause in your stored proc, in order not to update the whole table (unless that's what you really need to do)
you're using TEXT in your table, yet your stored procedure parameters are of type NVARCHAR - try to stick to one set - either TEXT/VARCHAR(MAX) and regular VARCHAR(100) parameters, or then use all Unicode strings: NTEXT/NVARCHAR(MAX) and NVARCHAR(100). Constantly mixing those non-Unicode and Unicode strings is a mess and causes lots of conversions and unnecessary overhead

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.

Best way to do serializable updates to a table

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.

Resources