Solibulo

Uninteresting things

A better DeleteExpiredSession stored procedure for ASP.NET sessions in SQL Server database

by softlion 6. January 2009 14:52

The standard ASP.NET 2/3/3.5 SP locks the ASPStateTempSessions table for a long time, which in turns locks running applications. This becomes very useful when SQL Server Agent jobs (especially xxx_Job_DeleteExpiredSessions) were disabled for some time for some reason.

A better SP looks like this:

   1: SET ANSI_NULLS ON
   2: GO
   3: SET QUOTED_IDENTIFIER ON
   4: GO
   5:  
   6: ALTER PROCEDURE [dbo].[DeleteExpiredSessions]
   7: AS
   8:  
   9: DECLARE @now datetime
  10: SET @now = GETUTCDATE()
  11: declare @count int
  12: set @count = 1
  13:  
  14: set ROWCOUNT 1000
  15: while @count != 0
  16: begin
  17:     begin tran
  18:     DELETE [Softlion].dbo.ASPStateTempSessions
  19:         WHERE Expires < @now
  20:     set @count = @@ROWCOUNT
  21:     commit
  22: end
  23: set ROWCOUNT 0
  24:  
  25: RETURN 0                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                                    

You may also add the hint WITH (ROWLOCK) on the delete statement to be sure no lock are set on a range which may contains rows currently used by the application.

Tags: , ,

SQL Server transaction isolation level and .NET sql library pitfall / bug !

by softlion 21. November 2008 05:49

Yesterday I found what can easily be named THE bug of the year. Even I can named it the biggest bug in the last 4 years. It affects nearly all of our past projects with different degrees of impact.

It is documented in MSDN as a feature in a small note in the description of the SqlConnection.BeginTransaction .NET method (see references below).

Let me explain the context first so you really understand the bug and how to fix it.

 

Imagine you have a web application created using ASP.NET, and a SQL Server data storage.

To construct a web page, your ASP.NET code will query 3 times data on the SQL server.

The 1st and 2nd queries are executed inside a transaction which runs under a non default isolation level  READ UNCOMMITTED.

The last query is executed alone, completely separated from the previous ones. This query is using the with(READPAST) option in one of its statement.

If you run  your application, you'll see a nice .NET error page telling you that READPAST can not be used in this transaction isolation level (it does not tell you which).

 

So first you debug your application and you check in which isolation level you are. This is easily done using this query:

 

select
      transaction_isolation_level,
      last_request_start_time,
      *
from sys.dm_exec_sessions

 

And you'll find what ? That instead of being the default READ COMMITED isolation level, the query is executed under READ UNCOMMITTED !

Have you guessed why already ? Well it is because of the "feature" of the transaction handling combined with the feature of the SQL connections pooling implemented in ADO.NET.

Connection pooling makes .NET reuse existing SQL connections which is an efficient way to use this network resource.

In our case,  a connection is created by the first query and is used by the whole transaction. When closed by code, the connection is not closed but moved to the connections pool (with a TTL so it won't last forever if not used).

For the third query the code creates a new connection, which the library converts to a 'get the opened connection from my connections pool'.

 

Now read the note from MSDN :

After a transaction is committed or rolled back, the isolation level of the transaction persists for all subsequent commands that are in autocommit mode (the SQL Server default). This can produce unexpected results, such as an isolation level of REPEATABLE READ persisting and locking other users out of a row. To reset the isolation level to the default (READ COMMITTED), execute the Transact-SQL SET TRANSACTION ISOLATION LEVEL READ COMMITTED statement, or call SqlConnection..::.BeginTransaction followed immediately by SqlTransaction..::.Commit. For more information on SQL Server isolation levels, see "Isolation Levels in the Database Engine" in SQL Server 2005 Books Online.

 

What happens here is that the first connection is returned to the pool but the transaction isolation level is not reset to its default value. So our 3rd query will execute and fail unexpectedly because the with(READPAST) option is not compatible with an isolation level equals to read uncommited.

 

How to fix this ?

Before closing your connection, after a transaction where you know you change the default isolation level, just execute this code wich resets it to the default READ COMMITED or wathever you set to be the default:

myConnection.BeginTransaction().Commit();

 

 

References:

SqlConnection.BeginTransaction .NET method and the note explaining the "feature".

.NET SQL connection pooling (ADO.NET 2.0)

 

Tags: ,

uniqueidentifier in SQL xml type are upper case strings but C# Guid are lower case strings

by softlion 20. November 2008 10:00

A modern way to put a list of IDs in a stored procedure parameter is to type the parameter as xml (instead of the conventional CSV like varchar). With the nodes() method you can inner join directly the xml 'table' with one of your database table without first filling a temporary table. And building the xml fragment with LINQ TO XML (ie: XDocument class) is a piece of cake.

 

If these ids are C# Guids (or tsql uniqueidentifier) and you optimize your query (like msdn tells you), you may fall in the uppercase/lowercase trap.

 

-- Optimized query (working)
select * from MyGames a
left join MyUsers b on a.Winners.exist('//winner[upper-case(@userId)=sql:column("b.UserId")]')=1

 

In this XQuery optimized comparison, T-SQL converts a uniqueidentifier to an upper case string. The Winners xml column has been filled using C# XDocument, which uses by default the C# method Guid.ToString() to convert a guid ... to lowercase.

 

So we have SQL converting by default uniqueidentifier to uppercase, and C# converting by default Guid to lowercase.
If you rely on defaults any comparison done between uniqueidentifier in this cutting edge xml/sql technology then it will not work.



Notes:
The xml type is a new native type available since SQL Server 2005. I use it essentially to simplify the model for 1-N relations by storing ids of the N table in a field of the 1 table instead of creating this dummy relationship table I always hated. This without giving up performance, as long as you setup the new special xml indexes for the xml typed field and provide the XSD schema to SQL. See references below for the full documentation.


A uniqueidentifier is a 16 bytes (128 bits) value which is used by the default implementation of User/Membership provider of the ASP.NET framework for all unique IDs.



With the xml data type, SQL 2005 introduced "methods" that can be used to efficiently query the xml data and use it as if it was a normal table : exists(..), value(..), ...
A common parameter for these methods is an XQuery string, where XQuery is a SQL Server specific language based on XPath, with at least 2 useful efficients extensions in the sql namespace:
sql:variable(..) which is replaced by the value of an existing variable
sql:column(..) which is replaced by the value of any table row available in the t-sql query




References:
SQL Server xml data type

XML indexes
XML type query methods
XQuery language reference

 

Tags: ,

Using HashBytes to create a database independant row id

by softlion 4. August 2008 21:58

Transfering rows from a development database to a production database can quickly become a nightmare.
To avoid this you should think your data model to deal with this new constraint: each row must be uniquely identified by a value which does not change in time and can be recomputed in a similary table in another database at anytime.

 

This unique value can be a hash. A hash is an asymetric cryptography function which always generates the same value when the same input is specified, and has a 99,9% chance to generate a different values with different inputs.


That means to generate our small hash to identify a row, we should find a primary key on our table which does not depend on an identity column. We can not use variable date fields nor uniqueidentifier fields as they are generated on row creation and will be different if we create a row with the same data on another database. This primary key can be a composite string key made of parts of the other table columns.

 

Using the HashBytes function with the SHA1 hash function in SQL 2005 generates a binary(20) result. With SQL 2000 you may use the checksum() function.

 

Example:

CREATE TABLE [dbo].[ITEM](
    [ITEMID] int IDENTITY(1,1) NOT NULL,
    [NAME] nvarchar(50) NOT NULL,
    [DESCRIPTION] nvarchar(200) NOT NULL,
    [HASH] AS (hashbytes('SHA1',[NAME]+[DESCRIPTION])),
CONSTRAINT [PK_ITEM_ITEMID] PRIMARY KEY CLUSTERED
(
    [ITEMID] ASC
),
CONSTRAINT [IX_ITEM_HASH] UNIQUE NONCLUSTERED
(
    [HASH] ASC
))
 

Limitation: HashBytes input string is limited to 8000 bytes.
Tip: you may use the undocumented function master.dbo.fn_varbintohexstr(binary hash value) to convert the binary result to a string prefixed with 0x.

Tags: