How to detect a record is locked? - sql

With SQL Server 2008, how can I detect if a record is locked?
EDIT:
I need to know this, so I can notify the user that the record is not accessible because the record is blocked.

In most circumstances with SQL 2008 you can do something like:
if exists(select 0 from table with (nolock) where id = #id)
and not exists(select 0 from table with(readpast) where id = #id)
begin
-- Record is locked! Do something.
end
If that is not enough (that is, you need to ignore table-level locks as well), use the NOWAIT hint that throws an error if there's a lock.

Related

SQL Server prevent all records from being updated or deleted

I am running SQL Server 2008. I written code that should be pretty safe to prevent any records being deleted or updated, but would be much happier if I could do this at the database level. Is it possible to mark a table so that once a row has been inserted it can never be modified or deleted?
Edit per comments. It seems you are actually looking for Versioning which really shouldn't be done via triggers but it can be with a performance impact and a Lot more coding.
Most appropriate method to combat your concern. Maintain transactional backups every X# of minutes so you can roll back if it gets messed up.
However sql-server does have 2 change tracking methods built in that you could explore.
Change Tracking - which simply identifies if a record was modified, and is really useful when synchronizing changes to a DB. Basically it increments a BIGINT for every row for every operation so you just have to check for records greater than the previous synchronize number.
Change Data Capture - this will capture the insert/update/delete and the state of the record (row).
For your particular worry Change Data Capture might be plausible and way more easy to maintain than triggers. I haven't tried it myself because Change Tracking was enough for my needs but here is a link to Microsoft's Documentation https://msdn.microsoft.com/en-us/library/cc645937(v=sql.110).aspx
If you insist on Triggers here would be an example, you will have to maintain the original primary key for referential integrity or you might not know which change caused the problem
CREATE TABLE TblName (
PrimaryKeyID INT IDENTITY(1,1) NOT NULL PRIMARY KEY
,Col1 INT NULL
,Col2 INT NULL
,OriginalPrimaryKeyId INT NULL
,CreateDate DATETIME DEFAULT(GETDATE())
,UpdateDate DATETIME DEFAULT(GETDATE())
,IsLatestVersion BIT NOT NULL DEFAULT(1)
)
GO
CREATE TRIGGER dbo.TrigForInsertEnforceVersionColTblName ON dbo.TblName
FOR INSERT
AS
BEGIN
BEGIN TRY
IF (SELECT COUNT(*) FROM TblName WHERE IsLatestVersion <> 1 AND OriginalPrimaryKeyId IS NULL) > 0
BEGIN
;THROW 51000, 'Attempted to insert a record identified as Previous Version without referencing another record', 1
END
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0 ROLLBACK TRANSACTION
--this will mean the loss of some Primary Keys but it is better than an
--INSTEAD of INSERT because you wont have to handle the insert code
;THROW
END CATCH
END
GO
CREATE TRIGGER dbo.TriggerName ON dbo.TblName
INSTEAD OF UPDATE, DELETE
AS
BEGIN
BEGIN TRY
IF EXISTS (
SELECT *
FROM
TblName t
INNER JOIN inserted i
ON t.PrimaryKeyID = i.PrimaryKeyID
AND (t.OriginalPrimaryKeyId <> i.OriginalPrimaryKeyId
OR t.IsLatestVersion <> i.IsLatestVersion)
)
BEGIN
;THROW 51000, 'OriginalPrimaryKeyId Column or IsLatestVersion Column was attempted to be updated', 1
END
--don't have to test count can just run the update statement
IF ((SELECT COUNT(*) FROM inserted) > 0)
BEGIN
--It's an UPDATE Operations so insert new row but maintain original primary key
--so you know what the new row is a version of
INSERT INTO dbo.TblName (Col1, Col2, OriginalPrimaryKeyId)
SELECT
i.Col1
,i.Col2
,OriginalPrimaryKeyId = CASE
WHEN t.OriginalPrimaryKeyId IS NULL THEN t.PrimaryKeyID
ELSE t.OriginalPrimaryKeyId
END
FROM
inserted i
INNER JOIN TblName t
ON i.PrimaryKeyID = t.PrimaryKeyID
END
UPDATE t
SET IsLatestVersion = 0
,UpdateDate = GETDATE()
FROM
TblName t
INNER JOIN deleted d
ON t.PrimaryKeyID = d.PrimaryKeyID
END TRY
BEGIN CATCH
IF ##TRANCOUNT > 0 ROLLBACK TRANSACTION
;THROW
END CATCH
END
Permissions discussion:
For anything row level and to block everyone including database owner role or administrators you would have to create a trigger. But those roles could always remove the trigger too and modify the table. Perhaps simple Permissions would be enough such as
on an entire role, which would be best if you put the users in a role
GRANT INSERT ON SchemaName.TableName TO RoleName
DENY UPDATE ON SchemaName.TableName TO RoleName
DENY DELETE ON SchemaName.TableName TO RoleName
OR for specific users same commands just change RoleName to username
DENY UPDATE ON SchemaName.TableName TO UserName
This would grant the ability to insert but revoke ability to update or delete a record.
You can also deny execute, alter, and a bunch more here is Microsoft's documentation: https://msdn.microsoft.com/en-us/library/ms173724.aspx
Using a trigger instead of security permissions is a lot messier and if someone with enough permissions wants to make a change they still can it will just slow them down but not by much. So if you are worried about that ability make sure you have good backups.

##ROWCOUNT check not detecting zero row count

I have a SQL Server Stored Procedure (using SQL Server 2008 R2) where it performs several different table updates. When rows have been updated I want to record information in an Audit table.
Here is my pseudo code:
UPDATE tblName SET flag = 'Y' WHERE flag = 'N'
IF ##ROWCOUNT > 0
BEGIN
INSERT INTO auditTable...etc
END
Unfortunately, even when zero rows are updated it still records the action in the audit table.
Note: There are no related triggers on the table being updated.
Any ideas why this could be happening?
Any statement that is executed in T-SQL will set the ##rowcount, even the if statement, so the general rule is to capture the value in the statement following the statement you're interested in.
So after
update table set ....
you want
Select #mycount = ##Rowcount
Then you use this value to do your flow control or messages.
As the docs state, even a simple variable assignment will set the ##rowcount to 1.
This is why it's important in this case that if you want people to diagnose the problem then you need to provide the actual code, not pseudo code.

Getting deadlocks on MS SQL stored procedure performing a read/update (put code to handle deadlocks)

I have to admit I'm just learning about properly handling deadlocks but based on suggestions I read, I thought this was the proper way to handle it. Basically I have many processes trying to 'reserve' a row in the database for an update. So I first read for an available row, then write to it. Is this not the right way? If so, how do I need to fix this SP?
CREATE PROCEDURE [dbo].[reserveAccount]
-- Add the parameters for the stored procedure here
#machineId varchar(MAX)
AS
BEGIN
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
BEGIN TRANSACTION;
declare #id BIGINT;
set #id = (select min(id) from Account_Data where passfail is null and reservedby is null);
update Account_data set reservedby = #machineId where ID = #id;
COMMIT TRANSACTION;
END
You can write this as a single statement. That will may fix the update problem:
update Account_data
set reservedby = #machineId
where ID = (select min(id) from Account_Data where passfail is null and reservedby is null);
Well, yur problem is 2that you have 2 statements - a select and an update. if those run concurrent, then the select will..... make a read lock and the update will demand a write lock. At the same time 2 machins deadlock.
Simple solution is to make the initial select demand an uddate lock (WITH (ROWLOCK, UPDLOCK) as hint). That may or may not work (depends on what else goes on) but it is a good start.
Second step - if that fails - is to use an application elvel lock (sp_getapplock) that makes sure a critical system always has only one owner and htus only exeutes transactions serially.

SQLServer lock table during stored procedure

I've got a table where I need to auto-assign an ID 99% of the time (the other 1% rules out using an identity column it seems). So I've got a stored procedure to get next ID along the following lines:
select #nextid = lastid+1 from last_auto_id
check next available id in the table...
update last_auto_id set lastid = #nextid
Where the check has to check if users have manually used the IDs and find the next unused ID.
It works fine when I call it serially, returning 1, 2, 3 ... What I need to do is provide some locking where multiple processes call this at the same time. Ideally, I just need it to exclusively lock the last_auto_id table around this code so that a second call must wait for the first to update the table before it can run it's select.
In Postgres, I can do something like 'LOCK TABLE last_auto_id;' to explicitly lock the table. Any ideas how to accomplish it in SQL Server?
Thanks in advance!
Following update increments your lastid by one and assigns this value to your local variable in a single transaction.
Edit
thanks to Dave and Mitch for pointing out isolation level problems with the original solution.
UPDATE last_auto_id WITH (READCOMMITTEDLOCK)
SET #nextid = lastid = lastid + 1
You guys have between you answered my question. I'm putting in my own reply to collate the working solution I've got into one post. The key seems to have been the transaction approach, with locking hints on the last_auto_id table. Setting the transaction isolation to serializable seemed to create deadlock problems.
Here's what I've got (edited to show the full code so hopefully I can get some further answers...):
DECLARE #Pointer AS INT
BEGIN TRANSACTION
-- Check what the next ID to use should be
SELECT #NextId = LastId + 1 FROM Last_Auto_Id WITH (TABLOCKX) WHERE Name = 'CustomerNo'
-- Now check if this next ID already exists in the database
IF EXISTS (SELECT CustomerNo FROM Customer
WHERE ISNUMERIC(CustomerNo) = 1 AND CustomerNo = #NextId)
BEGIN
-- The next ID already exists - we need to find the next lowest free ID
CREATE TABLE #idtbl ( IdNo int )
-- Into temp table, grab all numeric IDs higher than the current next ID
INSERT INTO #idtbl
SELECT CAST(CustomerNo AS INT) FROM Customer
WHERE ISNUMERIC(CustomerNo) = 1 AND CustomerNo >= #NextId
ORDER BY CAST(CustomerNo AS INT)
-- Join the table with itself, based on the right hand side of the join
-- being equal to the ID on the left hand side + 1. We're looking for
-- the lowest record where the right hand side is NULL (i.e. the ID is
-- unused)
SELECT #Pointer = MIN( t1.IdNo ) + 1 FROM #idtbl t1
LEFT OUTER JOIN #idtbl t2 ON t1.IdNo + 1 = t2.IdNo
WHERE t2.IdNo IS NULL
END
UPDATE Last_Auto_Id SET LastId = #NextId WHERE Name = 'CustomerNo'
COMMIT TRANSACTION
SELECT #NextId
This takes out an exclusive table lock at the start of the transaction, which then successfully queues up any further requests until after this request has updated the table and committed it's transaction.
I've written a bit of C code to hammer it with concurrent requests from half a dozen sessions and it's working perfectly.
However, I do have one worry which is the term locking 'hints' - does anyone know if SQLServer treats this as a definite instruction or just a hint (i.e. maybe it won't always obey it??)
How is this solution? No TABLE LOCK is required and works perfectly!!!
DECLARE #NextId INT
UPDATE Last_Auto_Id
SET #NextId = LastId = LastId + 1
WHERE Name = 'CustomerNo'
SELECT #NextId
Update statement always uses a lock to protect its update.
You might wanna consider deadlocks. This usually happens when multiple users use the stored procedure simultaneously. In order to avoid deadlock and make sure every query from the user will succeed you will need to do some handling during update failures and to do this you will need a try catch. This works on Sql Server 2005,2008 only.
DECLARE #Tries tinyint
SET #Tries = 1
WHILE #Tries <= 3
BEGIN
BEGIN TRANSACTION
BEGIN TRY
-- this line updates the last_auto_id
update last_auto_id set lastid = lastid+1
COMMIT
BREAK
END TRY
BEGIN CATCH
SELECT ERROR_NUMBER() AS ErrorNumber, ERROR_MESSAGE() as ErrorMessage
ROLLBACK
SET #Tries = #Tries + 1
CONTINUE
END CATCH
END
I prefer doing this using an identity field in a second table. If you make lastid identity then all you have to do is insert a row in that table and select #scope_identity to get your new value and you still have the concurrency safety of identity even though the id field in your main table is not identity.

SQL Server 2005 Deadlock Problem

I’m running into a deadlock problem when trying to lock some records so that no process (Windows service) picks the items to service them, then update the status and then return a recordset.
Can you please let me know why am I getting the deadlock issue when this proc is invoked?
CREATE PROCEDURE [dbo].[sp_LoadEventsTemp]
(
#RequestKey varchar(20),
#RequestType varchar(20),
#Status varchar(20),
#ScheduledDate smalldatetime = null
)
AS
BEGIN
declare #LoadEvents table
(
id int
)
BEGIN TRANSACTION
if (#scheduledDate is null)
Begin
insert into #LoadEvents (id)
(
Select eventqueueid FROM eventqueue
WITH (HOLDLOCK, ROWLOCK)
WHERE requestkey = #RequestKey
and requesttype = #RequestType
and [status] = #Status
)
END
else
BEGIN
insert into #LoadEvents (id)
(
Select eventqueueid FROM eventqueue
WITH (HOLDLOCK, ROWLOCK)
WHERE requestkey = #RequestKey
and requesttype = #RequestType
and [status] = #Status
and (convert(smalldatetime,scheduleddate) <= #ScheduledDate)
)
END
update eventqueue set [status] = 'InProgress'
where eventqueueid in (select id from #LoadEvents)
IF ##Error 0
BEGIN
ROLLBACK TRANSACTION
END
ELSE
BEGIN
COMMIT TRANSACTION
select * from eventqueue
where eventqueueid in (select id from #LoadEvents)
END
END
Thanks in advance.
Do you have a non-clustered index defined as:
CREATE NONCLUSTERED INDEX NC_eventqueue_requestkey_requesttype_status
ON eventqueue(requestkey, requesttype, status)
INCLUDE eventqueueid
and another on eventqueueid?
BTW the conversion of column scheduleddate to smalldatetime type will prevent any use of an index on that column.
First of all, as you're running SQL Server I'd recommend you to intall Performance Dashboard which is a very handy tool to identify what locks are currently being made on the server.
Performance Dahsboard Link
Second, take a trace of your SQL Server using SQL Profiler (Already Installed) and make sure that you select on the Events Selection the item Locks > Deadlock graph which will show what is causing the deadlock.
You got to have very clear on your mind what a deadlock is to start troubleshooting it.
When any access is made to any table or row on the DB a lock is made.
Lets call SPID 51 and SPID 52 (SPID = SQL Process ID)
SPID 51 locks Cell A
SPID 52 locks Cell B
if on the same transaction SPID 51 requests for the Cell B, it'll wait SPID 52 till it releases it.
if on the same transaction SPID 52 requests for the Cell A, you got a deadlock because this situation will never finish (51 waiting for 52 and 52 for 51)
Got to tell you that it ain't easy to troubleshoot, but you you to dig deeper to find the resolution
Deadlocks happen most often (in my experience) when differnt resources are locked within differnt transactions in different orders.
Imagine 2 processes using resource A and B, but locking them in different orders.
- Process 1 locks resource A, then resource B
- Process 2 locks resource B, then resource A
The following then becomes possible:
- Process 1 locks resource A
- Process 2 locks resource B
- Process 1 tries to lock resource B, then stops and waits as Process 2 has it
- Process 2 tries to lock resource A, then stops and waits as Process 1 has it
- Both proceses are waiting for each other, Deadlock
In your case we would need to see exactly where the SP falls over due to a deadlock (the update I'd guess?) and any other processes that reference that table. It could be an trigger or something, which then gets deadlocked on a different table, not the table you're updating.
What I would do is use SQL Server 2005's OUTPUT syntaxt to avoid having to use the transaction...
UPDATE
eventqueue
SET
status = 'InProgress'
WHERE
requestkey = #RequestKey
AND requesttype = #RequestType
AND status = #Status
AND (convert(smalldatetime,scheduleddate) <= #ScheduledDate OR #ScheduledDate IS NULL)
OUTPUT
inserted.*