Row Locking for update - sql-server

Can anyone confirm this for me. I need to be able to write to a field in a row an 'ownership' value (Who own's the record) and need it to be the first person who selects the row for update and ignore any further selects until the row is available to write to....
My Transaction will be:
BEGIN TRANSACTION
Declare #OwnerField Varchar(20)
SET #OwnerField = SELECT OwnerField
FROM Table
WHERE RecordID = 2
IF #OwnerField IS NULL -- Can own
BEGIN
UPDATE Table
SET OwnerField = 'John Smith'
WHERE RecordID = 2
END
END TRANSACTION
As far as my knowledge goes (with Google's help) this will allow me to lock the row, check if there is a value in it, if not then write one, if so then exit..
Does this make sense?
Thank you in advance..
Derek.

Unless you want to handle the contention by producing deadlocks, don't use SERIALIZABLE for this. SERIALIZABLE will take and hold Shared (S) locks in the first query, so concurrent transactions will both read the row, and enter into a deadlock as they both try to update it. One will be killed; the other will succeed, and the SERIALIZABLE semantics are preserved.
Instead you should put a restrictive lock on the target row as you read it.
eg:
BEGIN TRANSACTION
Declare #OwnerField Varchar(20)
SET #OwnerField = SELECT OwnerField
FROM Table with (UPDLOCK,HOLDLOCK)
WHERE RecordID = 2
IF #OwnerField IS NULL -- Can own
BEGIN
UPDATE Table
SET OwnerField = 'John Smith'
WHERE RecordID = 2
END
END TRANSACTION
(UPDLOCK,HOLDLOCK) gives you the same range-locking protection of the SERIALIZABLE isolation level, but uses a restrictive lock, so multiple transactions will block on the SELECT. The second reader will block until the first has committed, and see the updated OwnerField column.

Related

Primary Key violation after checking for existence of row

Question: What things could cause the following problem?
We have a stored proc that updates or inserts a price record into a table. Very straight forward. However, at intermittent times we get a primary key violation trying to insert a row that already exists. As you see, we are checking for the existence of a record and then updating or inserting as appropriate.
This happens very rarely but the last time it happened we had 18 occurrences in a 3 minute window. We updated a lot more rows than 18 so it is not every time. This SP is called quite often. We check and there was no index maintenance going on at the time. The application that calls this SP just loops through a queue to update/insert these prices and there is only one instance of the application running.
This is running on an 2016 Availability Group with 3 servers.
ALTER PROCEDURE [dbo].[mw_UpdatePrice] #CustTypeID INT
,#ID INT
,#Price MONEY
,#OldPrice MONEY
,#ExpirationDate DATETIME
,#PriceStatusID INT
,#PriceStatusDesc VARCHAR(80)
,#FreeFreightShipviaServiceLevelID INT = NULL
,#FreeFreightShipViaServiceLevelDescription VARCHAR(150) = NULL
,#FreeFreightShipViaServiceLevelRank INT = NULL
AS
BEGIN
SET NOCOUNT ON;
IF EXISTS (
SELECT 1
FROM dbo.Price
WHERE ID = #ID
AND CustTypeID = #CustTypeID
)
BEGIN
UPDATE dbo.Price
SET Price = #Price
,OldPrice = #OldPrice
,ExpirationDate = #ExpirationDate
,PriceStatusID = #PriceStatusID
,PriceStatusDescription = #PriceStatusDesc
,ServiceLevelName = #FreeFreightShipViaServiceLevelDescription
,ServiceLevelId = #FreeFreightShipviaServiceLevelID
,ServiceLevelRank = #FreeFreightShipViaServiceLevelRank
WHERE ID = #ID
AND CustTypeID = #CustTypeID
END
ELSE
BEGIN
INSERT dbo.Price (
ID
,CustTypeID
,Price
,OldPrice
,ExpirationDate
,PriceStatusID
,PriceStatusDescription
,ServiceLevelName
,ServiceLevelID
,ServiceLevelRank
)
VALUES (
#ID
,#CustTypeID
,#Price
,#OldPrice
,#ExpirationDate
,#PriceStatusID
,#PriceStatusDesc
,#FreeFreightShipViaServiceLevelDescription
,#FreeFreightShipviaServiceLevelID
,#FreeFreightShipViaServiceLevelRank
)
END
END
As comments have mentioned, this stored procedure is not safe to call concurrently - it has race conditions for both insert and update scenarios. This can be fixed only by ensuring that each call to the stored procedure is contained in a separate transaction (add begin/end tran inside the SP, or initiate transaction from the application code), and applying HOLDLOCK to the SELECT and INSERT statements:
SELECT 1
FROM dbo.Price WITH (UPDLOCK, HOLDLOCK)
WHERE ID = #ID
AND CustTypeID = #CustTypeID
...
INSERT dbo.Price WITH (HOLDLOCK)
...
Even if this gets refactored to a MERGE statement, HOLDLOCK will still be necessary to prevent the issue. Example here.
HOLDLOCK locks the entire table and will reduce the concurrent throughput of this SP.
Aside from the concurrency problem, another possible cause of PRIMARY KEY violation could occur if ANSI_NULLS is on, potentially allowing for an application logic error where the SP is being called with NULL values for #CustTypeID or #ID, causing the application to behave in a way that wasn't intended. For example, calling with #CustTypeID = NULL and #ID = 1 would always result in an INSERT, which might not be the intended behavior.
Edit:
Added UPDLOCK as per #DavidBrowne

SQL Server Read Committed Snapshot

I am just wondering something about snapshot behavior on read committed isolation level. Let's assume that I have a table with name "A". Here is the first transaction:
Select blabla
From A
Insert Into A blabla
and second transaction does the same
Select blabla
From A
Insert Into A blabla
and assume that below timeline occurred:
Tran1: select
Tran1: insert (not yet committed)
Tran2: select (I don't know it is possible or not)
Tran2: insert
As far as I know, in standard read committed isolation level, tran2 select query would be blocked because of tran1 insert command not yet committed or rolled back. But, while "is_read_committed_snapshot" is enabled, I expect that any of lock won't acquired during insert or update command.
So what will happen to tran2?
I expect that tran2 select query won't see the data that inserted by tran1, because it would be "dirty read". But it wouldn't get block as well.
Because of the tran1 insert query does not acquire any lock, wouldn't this situation be a problem about concurrency of executing these two transactions?
I expect that any of lock won't acquired during insert or update
command.
That is wrong. Even if you have enabled RCSI, writers still block writers, and X locks are still acqiured.
What is different between RC and RCSI is reading behaviour.
When working on pessimistic RC, SELECT from Tran2 will be blocked on X lock held on A, while working on RCSI Tran2's SELECT will not be blocked, it will be provided with the last committed version of A, i.e. with the state of A before Tran1 has modificated it.
What happend then depends on your table organisation and on what you INSERT.
Some examples.
1) table A is a heap, you are doing single insert in both transactions.
In this case your INSERT in Tran2 will succeed in any case, be it the same value that you try to insert in both transactions or not, because what the server acquires in this case is IX on a table (that is compatible with IX held by Tran1), IX on a page (that is also compatible with IX held by Tran1, even if it is the same page), and X on RID (while Tran1 has X on another RID), so there is no conflict.
2) table A is clustered table, you are trying to insert the same new key in this table.
In this case your Tran2's INSERT will be blocked because of the conflict between two X lock on the same key, the first is held by Tran1, the secont is requested by Tran2 and is blocked.
3) table A is clustered table, you are trying to insert different keys in this table.
Insert2 will succeed because X lock on key requested by Tran2 will be granted as Tran1 holds IX on table, IX on page, and X on another key.
Lets say you're doing it this way:
SELECT id FROM customers
BEGIN TRAN new_tran
UPDATE customers
SET ID = '1'
WHERE ID = '01'
IF your query is something like this:
SET TRANSACTION ISOLATION LEVEL SNAPSHOT
GO
BEGIN TRAN
SELECT *
FROM customers
WHERE id = '01'
Result- Even if we have changed the value to 01, we will still see old record in session 2 (2, TWO).
Now, let’s commit transaction in session 1
Now lets say you commit the transaction, in session 2, now you'll get the new updated value:
COMMIT
SELECT *
FROM DemoTable
WHERE i = 2
You can read more about it on Pinal Dave's blog: blog.sqlauthority.com/2015/07/03/sql-server-difference-between-read-committed-snapshot-and-snapshot-isolation-level/

SQL Server select for update

I am struggling to find a SQL Server replacement for select for update that works.
I have a master table that contains a column which is used for next order number. The application does a select from update on this row, reads the current value (while locked) adds one to this value and then updates the row, then uses the number it received. This process works perfectly on all databases I've tried but for SQL Server which does not seem to have any process for selecting data for exclusive use.
How do I do a locked read and update of something like a next order number from a sequence table is SQL Server?
BTW, I know I can use things like IDENTITY cols and stuff, to do this, but in this case I must read from this existing column. Get the value and inc it, and do it in a safe locked manner to avoid 2 users getting the same value.
UPDATE::
Thank you, that works for this case :)
DECLARE #Output char(30)
UPDATE scheme.sysdirm
SET #Output = key_value = cast(key_value as int)+1
WHERE system_key='OPLASTORD'
SELECT #Output
I have one other place I do something similar. I read and lock a stock record too.
SELECT STOCK
FROM PRODUCT
WHERE ID = ? FOR UPDATE.
I then do some validation and the do
UPDATE PRODUCT SET STOCK = ?
WHERE ID=?
I can't just use your above method here, as the value I update is based on things I do from the stock I read. But I need to ensure no one else can mess with the stock while I do this. Again, easy on other DB's with SELECT FOR UPDATE... is there a SQL Server workaround?? :)
You can simple do an UPDATE that also reads out the new value into a SQL Server variable:
DECLARE #Output INT
UPDATE dbo.YourTable
SET #Output = YourColumn = YourColumn + 1
WHERE ID = ????
SELECT #Output
Since it's an atomic UPDATE statement, it's safe against concurrency issues (since only one connection can get an update locks at any one given time). A potential second session that wants to get the incremented value at the same time will have to wait until the first one completes, thus getting the next value from the table.
As an alternative you can use the OUTPUT clause of the UPDATE statement, although this will insert into a table variable.
Create table YourTable
(
ID int,
YourColumn int
)
GO
INSERT INTO YourTable VALUES (1, 1)
GO
DECLARE #Output TABLE
(
YourColumn int
)
UPDATE YourTable
SET YourColumn = YourColumn + 1
OUTPUT inserted.YourColumn INTO #Output
WHERE ID = 1
SELECT TOP 1 YourColumn
FROM #Output
**** EDIT
If you want to ensure that no-one can change the data after you have read it, you can use a repeatable read. You should be aware that any reads of any tables you do will be locked for Update (pessimistic locking) and may cause Deadlocking. You can also sue the SELECT ... FROM TABLE (UPDLOCK) hint within a transaction.
SET TRANSACTION ISOLATION LEVEL REPEATABLE READ
BEGIN TRANSACTION
SELECT STOCK
FROM PRODUCT
WHERE ID = ?
.....
...
UPDATE Product
SET Stock = nnn
WHERE ID = ?
COMMIT TRANSACTION

Transaction Concurrency Isolation - Why can I update a subset of another transactions records?

I'm trying to understand a problem I have run into that I don't believe should be possible when dealing with transactions utilizing the read committed isolation level. I have a table that is being used as a queue. In one thread (connection 1) I insert multiple batches of 20 records into each table. Each batch of 20 records is performed inside a transaction. In a second thread (connection 2) I perform an update to change the status of the records that have been inserted into the queue, which also occurs inside a transaction. When running concurrently, it is my expectation that the number of rows affected by the update (connection 2) should be a multiple of 20, since connection 1 is inserting rows in the table inserts in batches of 20 rows within a transaction.
But my testing shows this is not always the case, and on occasion I'm able to update a subset of records from connection 1's batch. Should this be possible or am I missing something about transactions, concurrency, and isolation levels? Below is a set of test scripts I created to reproduce this issue in T-SQL.
This script inserts 20,000 records into the table in transaction batches of 20.
USE ReadTest
GO
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
GO
SET NOCOUNT ON
DECLARE #trans_id INTEGER
DECLARE #cmd_id INTEGER
DECLARE #text_str VARCHAR(4000)
SET #trans_id = 0
SET #text_str = 'Placeholder String Value'
-- First empty the table
DELETE FROM TABLE_A
WHILE #trans_id < 1000 BEGIN
SET #trans_id = #trans_id + 1
SET #cmd_id = 0
BEGIN TRANSACTION
-- Insert 20 records into the table per transaction
WHILE #cmd_id < 20 BEGIN
SET #cmd_id = #cmd_id + 1
INSERT INTO TABLE_A ( transaction_id, command_id, [type], status, text_field )
VALUES ( #trans_id, #cmd_id, 1, 1, #text_str )
END
COMMIT
END
PRINT 'DONE'
This script updates the records in the table, changing the status from 1 to 2 and then checks the rowcount from the update operation. When the rowcount is not a multiple of 20, and print statement indicates this and the number of rows affected.
USE ReadTest
GO
SET TRANSACTION ISOLATION LEVEL READ COMMITTED
GO
SET NOCOUNT ON
DECLARE #loop_counter INTEGER
DECLARE #trans_id INTEGER
DECLARE #count INTEGER
SET #loop_counter = 0
WHILE #loop_counter < 100000 BEGIN
SET #loop_counter = #loop_counter + 1
BEGIN TRANSACTION
UPDATE TABLE_A SET status = 2
WHERE status = 1
and type = 1
SET #count = ##ROWCOUNT
COMMIT
IF ( #count % 20 <> 0 ) BEGIN
-- Records in concurrent transaction inserting in batches of 20 records before commit.
PRINT '*** Rowcount not a multiple of 20. Count = ' + CAST(#count AS VARCHAR) + ' ***'
END
IF #count > 0 BEGIN
-- Delete the records where the status was changed.
DELETE TABLE_A WHERE status = 2
END
END
PRINT 'DONE'
This script creates the test queue table in a new database called ReadTest.
USE master;
GO
IF EXISTS (SELECT * FROM sys.databases WHERE name = 'ReadTest')
BEGIN;
DROP DATABASE ReadTest;
END;
GO
CREATE DATABASE ReadTest;
GO
ALTER DATABASE ReadTest
SET ALLOW_SNAPSHOT_ISOLATION OFF
GO
ALTER DATABASE ReadTest
SET READ_COMMITTED_SNAPSHOT OFF
GO
USE ReadTest
GO
CREATE TABLE [dbo].[TABLE_A](
[ROWGUIDE] [uniqueidentifier] NOT NULL,
[TRANSACTION_ID] [int] NOT NULL,
[COMMAND_ID] [int] NOT NULL,
[TYPE] [int] NOT NULL,
[STATUS] [int] NOT NULL,
[TEXT_FIELD] [varchar](4000) NULL
CONSTRAINT [PK_TABLE_A] PRIMARY KEY NONCLUSTERED
(
[ROWGUIDE] ASC
) ON [PRIMARY]
) ON [PRIMARY]
ALTER TABLE [dbo].[TABLE_A] ADD DEFAULT (newsequentialid()) FOR [ROWGUIDE]
GO
You expectations are completely misplaced. You have never expressed in your query the requirement to 'dequeue' exactly 20 rows. The UPDATE can return 0, 19, 20, 21 or 1000 rows and all results are correct, as long as the status is 1 and type is 1. If you expect that the 'dequeue' occurs in the order of the 'enqueue' (which is somehow eluded to in your question, but never explicitly stated) then your 'dequeue' operation must contain an ORDER BY clause. Had you add such an explicitly stated requirement then your expectation that 'dequeue' always return an entire batch of 'enqueue' rows (ie. multiple of 20 rows) would be one step closer to being a reasonable expectation. As things stand right now, is, as I said, completely misplaced.
For a lengthier discussion see Using Tables as Queues.
I shouldn't be concerned that while one transaction is committing a
batch of 20 inserted records, another concurrent transaction is only
able to update a subset of those records and not all 20?
Basically the question boils down to If I SELECT while I INSERT, how many inserted rows will I see?. You only have a right to be concerned if the isolation level is declared as SERIALIZABLE. None of the other isolation levels make any prediction about how many rows inserted while the UPDATE was running will be visible. Only SERIALIZABLE states that the outcome has to be the same as running the two statements one after another (ie. serialized, hence the name). While the technical details of how the UPDATE 'sees' only part of the INSERT batch are easy to understand once you consider physical order and the lack of ORDER BY clause, the explanation is irrelevant. The fundamental issue is that the expectation is non-warranted. Even if the 'issue' is 'fixed' by adding a proper ORDER BY and the correct clustered index key (the article linked above explains the details), the expectation is still non-warranted. It will still be perfectly legal for the UPDATE to 'see' 1, 19 or 21 rows, although it will be unlikely to happen.
I guess I've always understood READ COMMITTED to only read committed
data, and that a transaction commit is an atomic operation, making all
the changes that occurred in the transaction available at once.
That is correct. What is incorrect is to expect that a concurrent SELECT (or update) to see the entire change, irrelevant of where it happens to be in the execution. Open an SSMS query and run the following:
use tempdb;
go
create table test (a int not null primary key, b int);
go
insert into test (a, b) values (5,0)
go
begin transaction
insert into test (a, b) values (10,0)
Now open a new SSMS query and run the following:
update test
set b=1
output inserted.*
where b=0
This will block behind the uncommitted INSERT. Now go back to first query and run the following:
insert into test (a, b) values (1,0)
commit
When this commits, the second SSMS query will finish, and it will return two rows, not three. QED. This is READ COMMITTED. What you expect is SERIALIZABLE execution (in which case the example above will deadlock).
It could happen like this:
The writer/inserter writes 20 rows (does not commit)
The reader/updater reads one row (which is not committed - it discards it)
The writer/inserter commits
The reader/updater reads 19 rows which are now committed thus visible
I believe that only an isolation level of serializable (or snapshot isolation which is more concurrent) fixes this.

SQL Server decrementing inventory

I am using SQL Server 2008. I have a table where orders with SKU are recorded, a table for inventory that has counts and a table where the relationship between the SKU sold and inventory items is recorded.
In the end, I got the report like this
Inventory CurrentQuantity OpenedOrder
SKU1 300 50
SKU2 100 10
Each order will be processed individually. How can I have the database automatically update the inventory tablet after each order is processed?
i.e
If the order has 2 SKU1 in it got processed, the the inventory table will automatically show 298.
Thanks
I would use a Stored Procedure, and perform the order insert and quantity update in one hit:
CREATE PROC dbo.ProcessOrder
#Item int,
#Quantity int
AS
BEGIN
--Update order table here
INSERT INTO dbo.Orders(ItemID,Quantity)
VALUES (#ItemID, #Quantity)
--Update Inventory here
UPDATE dbo.Inventory
SET CurrentQuantity = CurrentQuantity - Quantity
WHERE ItemID = #ItemID
END
I think what you are looking for is a trigger
Basically, set up a trigger that will update the appropriate columns using the inserted/updated data given. Without a full schema set, that is the best answer I can give at this time
I wouldn't be looking at a trigger myself for this.
My check out process
Start a transaction
Check stock level.
If OK, (optional validation / authorisation)
Add a check out record
Reduce the stock
Possibly add some record to invoice teh recipent etc.
Commit the transaction
While you could do it with triggers, I simply fail to see the point, a nice simple clear and all in one place SP_CheckOut stored procedure is where I'd be going.
I would normally advise to use a trigger but stock manipulation is that kind of operation that's usually done a lot of times, sometimes on batches and this is not the best scenario for triggers to be honest.
I think PKG's idea is very good, but you should never forget to add transaction control to it, otherwise you can endup with non-matching stocks:
CREATE PROC dbo.ProcessOrder
#Item int,
#Quantity int
AS
BEGIN
begin transaction my_tran
begin try
--Update order table here
INSERT INTO dbo.Orders(ItemID,Quantity)
VALUES (#ItemID, #Quantity)
--Update Inventory here
UPDATE dbo.Inventory
SET CurrentQuantity = CurrentQuantity - Quantity
WHERE ItemID = #ItemID
commit transaction
end try
begin catch
rollback transaction
--raise error if necessary
end catch
END
you can use the trigger,also use the procedure,and the specific steps on the top,use the procedure need to open the atuo exec feature in mastaer DB.

Resources