Showing posts with label SQL. Show all posts
Showing posts with label SQL. Show all posts

Rolling your own identities

I must say that I am not a database guy and my SQL skills are limited. But, it is not an acceptable excuse, specially when we are working in a small team and there is no dedicated database guy. We have no choice but to hone in our database skills and get ready to switch between the roles. That is exactly what I had to do when one of the system users reported an issue where database generated the same ID for 2 different records. I delved into to the SP and found what in the database world is called “Rolling your own identities”. It is a technique (hack or shortcut or whatever you want to call it) to generate ID numbers by getting the MAX of a column and adding 1 to it. It is a widely known solution in the community for situations when displaying a primary key column (with its seed and increment values set) on the client interface is not an option. For example if it is a GUID column or it has an out of sequence ID numbers due to frequent delete and insert operations.

So, this is what I saw in the SP, which is actually a way of Rolling your own identities.


BEGIN TRANSACTION 
/* select query */ 
SELECT TOP 1 @ItemNumber = ItemNumber + 1
FROM ItemTable
WHERE {some_condition}
ORDER BY ItemNumber DESC

/* insert query */
INSERT INTO ItemTable (ItemNumber, {column2}, {column3})
VALUES (@ItemNumber, {some_value}, {some_value})
END TRANSACTION

The /*select query*/ above can also be written using MAX a function.


SELECT MAX(ItemNumber) FROM ItemTable
WHERE {some_condition}

What exactly the problem is

If you notice, select and insert queries above are within a transaction which is important in a distributed environment where multiple clients access the database (or at least this SP) at the same time. I always believed that BEGIN TRANSACTION statement is the ultimate savior and creates a critical section internally, thus preventing 2 transactions from accessing the same database resource at the same time, like a lock statement in C#. If that was true, then why it allowed 2 different transactions to execute the select query concurrently thus allowing them to have the same max ItemNumber?

Research ensues

My first clue was the transactions isolation level. It is a keyword which controls the default locking behavior. The default transaction isolation level in SQL Server is Read Committed, which means only data committed by other transaction can be read hence avoids dirty read. But, this is not what I wanted.

What do you want then?

I wanted a way to bar other users from accessing select query until transaction is finished executing select *AND* insert statements both. I think I was wanting to convert my transaction into a truly atomic unit of work in order to make sure every transaction gets a new ItemNumber whenever it runs its select statement. Since Read Committed doesn’t serve the purpose, I decided on reading more about other transaction isolation levels and locking behaviors.

Read Uncommitted:

As explained in many places on the internet, By using this level one can read the data which has been read by other transactions but not yet committed. In this case no shared and exclusive locks will be honored and dirty read will not be prevented. It can also result in Phantom data or nonrepeatable reads. I certainly didn’t want this as I needed more restrictive locking not less. I moved onto Repeatable Read.

Repeatable Read:

This one took a bit long to get into my head. This isolation level will not allow other users to update the data that has been read in the select query. But, why do they call it repeatable? Actually within a SAME transaction we would like to issue the same SELECT statement multiple times.

Transaction 1

 
SELECT ItemNumber, ItemDetail FROM ItemTable
WHERE ItemNumber < 10

Transaction 2


SET ItemDetail = {some_new_value}
WHERE ItemNumber = 5

Transaction 1 continues...

   
SELECT ItemNumber, ItemDetail FROM ItemTable
WHERE ItemNumber < 10

In order to make sure Transaction 2 doesn’t update the records we have selected between multiple reads (means we may repeat our read later in the transaction), SQL Server will maintain a lock on all the rows we have read until the transaction ends. This is certainly more restrictive locking than Read Committed. We get the ownership of the rows we read till the end of the transaction.

Unfortunately, even Repeatable Read can’t help create the kind of critical section I talked about earlier because of one reason. It does allow INSERTS. Yes, no other users/transactions can update the rows  in the transaction, but they can always insert new rows amid the rows we have already locked. The newly inserted rows are called Phantom rows. This is exactly what our problem is that we don’t want to let other users insert until transaction is finished. Hmm.. since I was desperate to find a solution, I moved on and read about the next isolation level.

Serializable:

This isolation level will place a range lock on all the data we have read, means whatever rows come in that range will not be allowed to be updated or deleted, and no insertion will also be possible within that range. This level does what I wanted as it is the most restrictive locking. We can use HOLDLOCK as it has the same effect as using Serializable on all tables in SELECT statement in a transaction.

/* select query */  
SELECT MAX(ItemNumber) FROM ItemTable WITH (HOLDLOCK)

But, why do they say “Serializable is prone to cause deadlock”? Should I be worried about it? I think yes, because now I am eyeing on the most optimized solution and it is fair to be threatened by every warning it gives. 

Serializable and Repeatable Read may cause deadlocks

I can’t explain this better than MSDN:

“The transaction reads data, acquiring a shared (S) lock on the resource (page or row), and then modifies the data, which requires lock conversion to an exclusive (X) lock. If two transactions acquire shared-mode locks on a resource and then attempt to update data concurrently, one transaction attempts the lock conversion to an exclusive (X) lock. The shared-mode-to-exclusive lock conversion must wait because the exclusive lock for one transaction is not compatible with the shared-mode lock of the other transaction; a lock wait occurs. The second transaction attempts to acquire an exclusive (X) lock for its update. Because both transactions are converting to exclusive (X) locks, and they are each waiting for the other transaction to release its shared-mode lock, a deadlock occurs.”

To avoid this potential deadlock problem, update (U) locks are used. Since HOLDLOCK only applies shared range locks, we can always complement this with UPDATE to turn into update range locks.


/* to see what locks HOLDLOCK applies – same can be repeated for UPDATE as well */
BEGIN TRANSACTION
SELECT MAX(ItemNumber) from Orders with(HOLDLOCK)
EXEC sp_lock @@SPID
ROLLBACK

So, that means our final query should look something like this:

 
/* select query */
BEGIN TRANSACTION 
SELECT MAX(ItemNumber) FROM ItemTable WITH (HOLDLOCK, UPDATE)

/* insert query */
INSERT INTO ItemTable (ItemNumber, {column2}, {column3})
VALUES (@ItemNumber, {some_value}, {some_value})
END TRANSACTION

We should now be able to generate our own *unique* identities in a distributed environment without worrying about deadlocks using appropriate isolation level and locking.


HTH,

Relation b/w SQL Server transaction isolation levels and locks

I had a very good discussion with a colleague of mine at work on the impact of SQL statements under the scope of a transaction. We were trying to optimize a stored procedure for the minimum execution time. While going through the SP at one point, We found a SELECT statement followed by an UPDATE statement inside a transaction, something like this:

BEGIN TRANSACTION

SELECT* from dbo.authors WHERE au_fname LIKE 'Johnson'

UPDATE authors SET au_fname = 'Johnson1' WHERE au_id = '172-32-3176'

COMMIT TRANSACTION

My colleague was of the view that, one should always keep the transaction as small as possible and SELECT statement should not unnecessarily be made part of the transaction. This helps ensure the locks will be held for a minimum period of time and maximum availability of the table or rows to other transactions. I couldn't disagree with him on this.

He added that, if the SELECT statement is not contributing in the overall outcome of the transaction, then there is no point in having it inside the transaction. In light of this argument, we can easily take the SELECT statement out of transaction I have mentioned above. However, part of his argument spurred me to do a small research on SQL Server transaction isolation level and locking where he said that all the locks will be held and table/rows will be unavailable for other transactions right from the start of the transaction.

According to my understanding, transaction isolation level defines the behavior of locks. SELECT statements acquire SHARED LOCK while UPDATE, DELETE and INSERT statements acquire EXCLUSIVE. While executing SELECT under READ COMMITTED isolation level which is the default for SQL Server, locks will immediately be released as soon as the execution of SELECT statement is finished. SQL Server will not wait for the transaction to be over and will allow other transaction to modify the table or rows. However, in case of REPEATABLE READ isolation level, every other process will have to wait for the locks regardless of SELECT statement has been executed or not. The below definition of REPEATABLE READ from MSDN helps use understand this point.

"REPEATABLE READ specifies that statements cannot read data that has been modified but not yet committed by other transactions and that no other transactions can modify data (but can be add) that has been read by the current transaction until the current transaction completes"

BEGIN TRANSACITON plays no role in defining the scope of the locks which are not applied until SQL Server reaches to a particular statement (SELECT, INSERET, UPDATE, DELETE). Based upon the definitions of different isolation levels and locks, we can easily understand the association b/w them. In the light of this analysis, for the above transaction, I reached to an understanding that, it will make no difference whether we keep the SELECT statement inside or outside the transaction. It might take longer to come back because of the SELECT statement but, it will not prevent other transaction from acquiring locks until it reaches to UPDATE statement. I supported my argument by running the following test:

Process 1:

Use Pubs

SET TRANSACTION ISOLATION LEVEL REPEATABLE READ

BEGIN TRANSACTION

select * from authors WHERE au_fname like 'Johnson' /* Statement 1 */

Insert Into authors values('172-32-4188', 'ben', 'Johnson', '406 496-7229', 'address', 'city', 'CA', 94445, 1) /* Statement 2 */

select * from authors WHERE au_fname like 'Johnson' /* Statement 3 */

Commit Transaction

Process 2:

Use Pubs

Insert Into authors values('172-32-4188', 'ben', 'Johnson', '406 496-7229', 'address', 'city', 'CA', 94445, 1) //will get executed

UPDATE authors set au_fname = 'Johnson1' where au_id = '172-32-3176' /* will have to wait until transaction in Process 1 is finished.

Can't delete/rebuild full text catalog

A couple of days ago while working with SQL Server, I found myself in a catch 22 situation .We recently moved our database server and copied all objects including full text catalogs to a new location. While enabling the full text search on the new database, I realized that full text catalogs are still pointing to old location and needs changing. Frankly speaking, I didn't know how to do that and decided to drop the index and recreate as an easy way around it. However, in an effort to do so, I got an error saying "Full text is not enabled on the database. Run sp_fulltext_database 'enable'". When I ran that sp, I got another error saying "F:\MSSQL\FTData\SH_FT_CA..." doesn't exist". It was interesting enough to know that I couldn't drop the catalogue because of being disabled, but couldn't enable it either as it didn't exist at all. Wow! What a deadlock.

Well, with a little effort, I figured out a manual work around which is nothing but running an update command on sysfulltextcatalogs table. However, before I do that, I had to enable the updates for the current server by executing sp_configure for 'allow_updates' option. Long story short, following set of queries helped me update the path and eventually made it possible to drop the catalogues.


SELECT * FROM sysfulltextcatalogs

SP_CONFIGURE 'allow_updates', 0 GO RECONFIGURE WITH OVERRIDE GO

UPDATE SYSFULLTEXTCATALOGS SET path = 'D:\Microsoft SQL Server\MSSQL\FTData' WHERE ftcatid = 5

Sort By Column Value

We do sorting by column names almost everyday while writing database queries. Below is one such simple query

SELECT customerid, employeeid, orderdate  FROM dbo.orders  ORDER BY customerid

But, today I faced a situation where I had to write a query that returns all the rows however some of the rows appeared on the top in the list if they match a specified criteria. Lets take the example of above query where we want to get a list of all the customers but customers having customerid 'VINET' should appear on top. I came up with the below query:

DECLARE @customerid AS VARCHAR(100)  SET @customerid='VINET'  SELECT customerid, employeeid, orderdate,  (CASE @customerid WHEN '' THEN  null WHEN customerid THEN customerid END) AS 'sortColumn'  FROM orders  ORDER BY sortColumn DESC, employeeid

In the above query, 'sortColumn'  is a temporary column that is being used just to sort the whole result set by its value. 

This might not be a perfect solution as I am not a SQL expert but, I am content with it for the time being as long as it servers the purpose. If you think there is a much better way of doing the same thing, then you are more than welcome to share it here.