Home All Groups Group Topic Archive Search About

concurrency in stored proc

Author
11 Nov 2005 2:21 PM
Bob
I am trying add an item to a table that does not allow duplicates but if the
item already exists I want to return its id.  I need an SQL equivalent to a
critical section but I am not sure how its done.  It would like something
like the below code.

--Begin Critical Section

    SELECT @id = id from domains where domains.name = @domain
     if @id is null
     begin
       INSERT Domains(name) SELECT @domain;
       SELECT @domain_id  = SCOPE_IDENTITY();
     end

-- End critical section


Thanks for any suggestions.

Author
11 Nov 2005 2:35 PM
SQL novice
Here you go ...

SELECT @id = id from domains where domains.name = @domain
if @id is null
begin
    INSERT Domains(name) Values( @domain)
    SET @domain_id  = @@IDENTITY
end
Author
11 Nov 2005 3:06 PM
ML
Actually, scope_identity() is fine, as long as there are no triggers on the
table which would insert rows into other tables.

@@identity is IMHO the worst possible way.


ML
Author
11 Nov 2005 3:07 PM
Bob
So I never have to worry about two threads calling the select part of the
proc at once and
both getting Null?  Then both call the insert forcing one to get an
error because the domain existed.

Thanks


Show quote
"SQL novice" <bal***@gmail.com> wrote in message
news:1131719749.241699.198710@g44g2000cwa.googlegroups.com...
> Here you go ...
>
> SELECT @id = id from domains where domains.name = @domain
> if @id is null
> begin
> INSERT Domains(name) Values( @domain)
>  SET @domain_id  = @@IDENTITY
> end
>
Author
11 Nov 2005 3:21 PM
ML
To answer that we'll have to see more of the procedure. Please post entire
procedure.


ML
Author
11 Nov 2005 3:52 PM
William Stacey [MVP]
That is a good question.  I would like to know the answer on that as well.
Naturally, you want to do a Monitor type operation, but how does that
translate into sql?  Wonder if you could use sql clr proc and wrap a Monitor
around the select.  That is a brute force way, but there must a sql answer
with some kind of row locking semantics.

--
William Stacey [MVP]

Show quote
"Bob" <msg***@hotmail.com> wrote in message
news:uRqxbGt5FHA.3984@TK2MSFTNGP10.phx.gbl...
> So I never have to worry about two threads calling the select part of the
> proc at once and
> both getting Null?  Then both call the insert forcing one to get an
> error because the domain existed.
>
> Thanks
>
>
> "SQL novice" <bal***@gmail.com> wrote in message
> news:1131719749.241699.198710@g44g2000cwa.googlegroups.com...
>> Here you go ...
>>
>> SELECT @id = id from domains where domains.name = @domain
>> if @id is null
>> begin
>> INSERT Domains(name) Values( @domain)
>>  SET @domain_id  = @@IDENTITY
>> end
>>
>
>
Author
11 Nov 2005 4:11 PM
Bob
OR if there was a way to catch the error and then do a select again that
would work.
i.e.

SELECT @id = id from domains where domains.name = @domain

    if @id is null
     begin
        try
            INSERT Domains(name) Values( @domain)
            SET @domain_id  = @@IDENTITY
        catch(DuplicateRecordErrorr)
            SELECT @id = id from domains where domains.name = @domain
        end catch
    end


Show quote
"William Stacey [MVP]" <william.sta***@gmail.com> wrote in message
news:e4Wcfft5FHA.3136@TK2MSFTNGP09.phx.gbl...
> That is a good question.  I would like to know the answer on that as well.
> Naturally, you want to do a Monitor type operation, but how does that
> translate into sql?  Wonder if you could use sql clr proc and wrap a
> Monitor around the select.  That is a brute force way, but there must a
> sql answer with some kind of row locking semantics.
>
> --
> William Stacey [MVP]
>
> "Bob" <msg***@hotmail.com> wrote in message
> news:uRqxbGt5FHA.3984@TK2MSFTNGP10.phx.gbl...
>> So I never have to worry about two threads calling the select part of the
>> proc at once and
>> both getting Null?  Then both call the insert forcing one to get an
>> error because the domain existed.
>>
>> Thanks
>>
>>
>> "SQL novice" <bal***@gmail.com> wrote in message
>> news:1131719749.241699.198710@g44g2000cwa.googlegroups.com...
>>> Here you go ...
>>>
>>> SELECT @id = id from domains where domains.name = @domain
>>> if @id is null
>>> begin
>>> INSERT Domains(name) Values( @domain)
>>>  SET @domain_id  = @@IDENTITY
>>> end
>>>
>>
>>
>
>
Author
11 Nov 2005 4:27 PM
ML
How about this:

select @id
            = <identity_column>
         from domains
         where (name = @domain)

if (@id is null)
    begin
        insert ...
        set @id = identity_scope()
    end


Or you can try by using TRY...CATCH on SQL 2005.


ML
Author
11 Nov 2005 5:39 PM
William Stacey [MVP]
I don't think that is thread safe.  Multiple threads could end up in the
insert block.

--
William Stacey [MVP]

Show quote
"ML" <M*@discussions.microsoft.com> wrote in message
news:A20D8A8E-4054-476E-9B77-0EC1C96202C7@microsoft.com...
> How about this:
>
> select @id
>            = <identity_column>
>         from domains
>         where (name = @domain)
>
> if (@id is null)
>    begin
>        insert ...
>        set @id = identity_scope()
>    end
>
>
> Or you can try by using TRY...CATCH on SQL 2005.
>
>
> ML
Author
11 Nov 2005 6:14 PM
ML
How?


ML
Author
11 Nov 2005 6:30 PM
William Stacey [MVP]
Say two thread enter at ~same time (say MP machine, but same issue with 1
cpu) and not domain name added yet.
Both threads get null id as none inserted yet.  Both threads move passed "if
(id is null)" test.  Now one thread will insert and return and the other
will fail on non-unique index.  And there is no error handling to catch the
error on the second thread so it just throws exception back to caller.

--
William Stacey [MVP]

Show quote
"ML" <M*@discussions.microsoft.com> wrote in message
news:ED328E47-9ABB-4A51-8EA0-CC38470DF2C7@microsoft.com...
> How?
>
>
> ML
Author
11 Nov 2005 6:41 PM
William Stacey [MVP]
Let me qualify that.  If procs are not serialized by the system, this is not
thread safe.  This begs a question about procs and atomicness.  Can procs be
entered by more then one thread at same time?  I am guessing yes.  If you
could "sync" the proc with some attribute, then this would appear to work
correctly.  Any good whitepaper on locks and threads in context of procs and
updates/deletes, etc.

--
William Stacey [MVP]

Show quote
"William Stacey [MVP]" <william.sta***@gmail.com> wrote in message
news:uLViQ4u5FHA.1032@TK2MSFTNGP11.phx.gbl...
> Say two thread enter at ~same time (say MP machine, but same issue with 1
> cpu) and not domain name added yet.
> Both threads get null id as none inserted yet.  Both threads move passed
> "if (id is null)" test.  Now one thread will insert and return and the
> other will fail on non-unique index.  And there is no error handling to
> catch the error on the second thread so it just throws exception back to
> caller.
>
> --
> William Stacey [MVP]
>
> "ML" <M*@discussions.microsoft.com> wrote in message
> news:ED328E47-9ABB-4A51-8EA0-CC38470DF2C7@microsoft.com...
>> How?
>>
>>
>> ML
>
>
Author
11 Nov 2005 8:15 PM
ML
What second thread? You're talking about two users, two sessions. Threads are
irrelevant here. In SQL there is no "out-of-the" box solution to this problem
- it must be handled programmatically.

SQL2005 introduces TRY...CATCH exception handling, and ever since rowversion
was introduced, concurrency violations can be prevented quite easily, as long
as this question is taken under consideration at design time. Of course some
situations cannot be handled on server alone - they are left to the client
application.

As far as threads go, each session is isolated from any other session,
regardless of the number of threads.


ML
Author
11 Nov 2005 11:03 PM
William Stacey [MVP]
> What second thread? You're talking about two users, two sessions.

The threads running sproc.  If two users execute the same sproc and around
the same time, they may be run on seperate threads - no?

> Threads are irrelevant here.

How so?

> In SQL there is no "out-of-the" box solution to this problem
> - it must be handled programmatically.

That is what we are taking about.

> As far as threads go, each session is isolated from any other session,
> regardless of the number of threads.

Hence the same problem we talking about AFAICT.

--
William Stacey [MVP]
Author
11 Nov 2005 11:26 PM
Hugo Kornelis
On Fri, 11 Nov 2005 13:30:55 -0500, William Stacey [MVP] wrote:

>Say two thread enter at ~same time (say MP machine, but same issue with 1
>cpu) and not domain name added yet.
>Both threads get null id as none inserted yet.  Both threads move passed "if
>(id is null)" test.  Now one thread will insert and return and the other
>will fail on non-unique index.  And there is no error handling to catch the
>error on the second thread so it just throws exception back to caller.

Hi William,

This can be prevented by setting the appropriate isolation level. If you
set the transaction isolation level to "SERIALIZABLE" (or add a
SERIALIZABLE locking hint to the SELECT statement), the first attempt to
read the row will issue a range lock that prevents any insertions with
the same domain name; this would cause the second connection to be
blockeed until the first one finishes and releases it's locks.

Or, if you prefer not to mess with the isolation level (it might have an
adverse effect on the performance, after all), reverse the logic a bit:

-- Step 1: insert if it doesn't exist yet
INSERT Domains (Name)
SELECT @Domain
WHERE NOT EXISTS (SELECT *
                  FROM   Domains
                  WHERE  Name = @Domain)
-- Step 2: it's always there now - retrieve ID value
SET @id = (SELECT id
           FROM   Domains
           WHERE  Name = @Domain)

Best, Hugo
--

(Remove _NO_ and _SPAM_ to get my e-mail address)
Author
11 Nov 2005 11:57 PM
William Stacey [MVP]
> This can be prevented by setting the appropriate isolation level. If you
> set the transaction isolation level to "SERIALIZABLE" (or add a
> SERIALIZABLE locking hint to the SELECT statement), the first attempt to
> read the row will issue a range lock that prevents any insertions with
> the same domain name; this would cause the second connection to be
> blockeed until the first one finishes and releases it's locks.

Thanks Hugo.  That was helpful.

Show quote
> Or, if you prefer not to mess with the isolation level (it might have an
> adverse effect on the performance, after all), reverse the logic a bit:
>
> -- Step 1: insert if it doesn't exist yet
> INSERT Domains (Name)
> SELECT @Domain
> WHERE NOT EXISTS (SELECT *
>                  FROM   Domains
>                  WHERE  Name = @Domain)
> -- Step 2: it's always there now - retrieve ID value
> SET @id = (SELECT id
>           FROM   Domains
>           WHERE  Name = @Domain)
>

That is helpful also.  Is Step 1 gaureenteed to be atomic?  If so, this
would appear to be the way to go.  Far from a DB expert, but it would appear
there might be an issue between step1 and step2 if another user happened to
delete the record between step1 and step2. for this thread.  In the c#
world, we could remove all doubt by placing a lock() {} around the whole
thing.  Not sure if above handles all cases or not.  Cheers.

--
William Stacey [MVP]


--
William Stacey [MVP]
Author
12 Nov 2005 12:17 AM
Hugo Kornelis
On Fri, 11 Nov 2005 18:57:01 -0500, William Stacey [MVP] wrote:

Show quote
>> This can be prevented by setting the appropriate isolation level. If you
>> set the transaction isolation level to "SERIALIZABLE" (or add a
>> SERIALIZABLE locking hint to the SELECT statement), the first attempt to
>> read the row will issue a range lock that prevents any insertions with
>> the same domain name; this would cause the second connection to be
>> blockeed until the first one finishes and releases it's locks.
>
>Thanks Hugo.  That was helpful.
>
>> Or, if you prefer not to mess with the isolation level (it might have an
>> adverse effect on the performance, after all), reverse the logic a bit:
>>
>> -- Step 1: insert if it doesn't exist yet
>> INSERT Domains (Name)
>> SELECT @Domain
>> WHERE NOT EXISTS (SELECT *
>>                  FROM   Domains
>>                  WHERE  Name = @Domain)
>> -- Step 2: it's always there now - retrieve ID value
>> SET @id = (SELECT id
>>           FROM   Domains
>>           WHERE  Name = @Domain)
>>
>
>That is helpful also.  Is Step 1 gaureenteed to be atomic?

Hi William,

AFAIK, all DML statements are guaranteed to be atomic in SQL Server.

I'm not sure how exactly the locking is handled internally. But I've
never heard or read that this would result in duplicate insertions. And
though I see some (theoretic) possibility of a deadlock scenario here, I
haven't seen those reported either.

>  If so, this
>would appear to be the way to go.  Far from a DB expert, but it would appear
>there might be an issue between step1 and step2 if another user happened to
>delete the record between step1 and step2. for this thread. 

Yes, that possibility still exists if the default transaction isolation
level (read committed) is used. I concentrated on the scenario of two
simulteneous execution of the same procedure, I had not yet pondered
other procs being simultaneously executed.

This could also be handled by increasong the isolation level, but in
this case there's no need to go as far as SERIALIZABLE. Using REPEATABLE
READ (or the locking hint REPEATABLEREAD) is sufficient. That means that
there won't be a range lock, but the shared lock on the row that is read
if the NOT EXISTS is false will be kept until the transaction is
finished. That shared lock will probably not block other connections
from executing the same query (since they also need a shared lock only
for the EXISTS check) - but it will prevent any DELETE or UPDATE
statement from messing with the row.

Best, Hugo
--

(Remove _NO_ and _SPAM_ to get my e-mail address)
Author
12 Nov 2005 1:34 AM
William Stacey [MVP]
Thanks Hugo.

--
William Stacey [MVP]
Author
12 Nov 2005 12:31 AM
Brian Selzer
> This can be prevented by setting the appropriate isolation level. If you
> set the transaction isolation level to "SERIALIZABLE"

Setting the isolation level is not enough.  You must enclose the SELECT and
the INSERT with a transaction.  It does no good to prevent changes during
the SELECT, but to allow changes between the SELECT and the INSERT--which is
indeed possible in a concurrent environment.  In addition, to prevent
deadlocks you must also specify WITH(UPDLOCK) or WITH(XLOCK) on the SELECT
statement.

Show quote
"Hugo Kornelis" <hugo@pe_NO_rFact.in_SPAM_fo> wrote in message
news:0p9an11fs7mb3dn1lobj7rube3blv7jclt@4ax.com...
> On Fri, 11 Nov 2005 13:30:55 -0500, William Stacey [MVP] wrote:
>
>>Say two thread enter at ~same time (say MP machine, but same issue with 1
>>cpu) and not domain name added yet.
>>Both threads get null id as none inserted yet.  Both threads move passed
>>"if
>>(id is null)" test.  Now one thread will insert and return and the other
>>will fail on non-unique index.  And there is no error handling to catch
>>the
>>error on the second thread so it just throws exception back to caller.
>
> Hi William,
>
> This can be prevented by setting the appropriate isolation level. If you
> set the transaction isolation level to "SERIALIZABLE" (or add a
> SERIALIZABLE locking hint to the SELECT statement), the first attempt to
> read the row will issue a range lock that prevents any insertions with
> the same domain name; this would cause the second connection to be
> blockeed until the first one finishes and releases it's locks.
>
> Or, if you prefer not to mess with the isolation level (it might have an
> adverse effect on the performance, after all), reverse the logic a bit:
>
> -- Step 1: insert if it doesn't exist yet
> INSERT Domains (Name)
> SELECT @Domain
> WHERE NOT EXISTS (SELECT *
>                  FROM   Domains
>                  WHERE  Name = @Domain)
> -- Step 2: it's always there now - retrieve ID value
> SET @id = (SELECT id
>           FROM   Domains
>           WHERE  Name = @Domain)
>
> Best, Hugo
> --
>
> (Remove _NO_ and _SPAM_ to get my e-mail address)
Author
12 Nov 2005 1:33 AM
William Stacey [MVP]
I think we have a winner.  Care to codify small example of that for the rest
of us :-) ?  Then maybe others can poke at it until all agree.  Thanks
Brian.

--
William Stacey [MVP]

Show quote
"Brian Selzer" <br***@selzer-software.com> wrote in message
news:ep1xfBy5FHA.252@TK2MSFTNGP15.phx.gbl...
>> This can be prevented by setting the appropriate isolation level. If you
>> set the transaction isolation level to "SERIALIZABLE"
>
> Setting the isolation level is not enough.  You must enclose the SELECT
> and the INSERT with a transaction.  It does no good to prevent changes
> during the SELECT, but to allow changes between the SELECT and the
> INSERT--which is indeed possible in a concurrent environment.  In
> addition, to prevent deadlocks you must also specify WITH(UPDLOCK) or
> WITH(XLOCK) on the SELECT statement.
>
> "Hugo Kornelis" <hugo@pe_NO_rFact.in_SPAM_fo> wrote in message
> news:0p9an11fs7mb3dn1lobj7rube3blv7jclt@4ax.com...
>> On Fri, 11 Nov 2005 13:30:55 -0500, William Stacey [MVP] wrote:
>>
>>>Say two thread enter at ~same time (say MP machine, but same issue with 1
>>>cpu) and not domain name added yet.
>>>Both threads get null id as none inserted yet.  Both threads move passed
>>>"if
>>>(id is null)" test.  Now one thread will insert and return and the other
>>>will fail on non-unique index.  And there is no error handling to catch
>>>the
>>>error on the second thread so it just throws exception back to caller.
>>
>> Hi William,
>>
>> This can be prevented by setting the appropriate isolation level. If you
>> set the transaction isolation level to "SERIALIZABLE" (or add a
>> SERIALIZABLE locking hint to the SELECT statement), the first attempt to
>> read the row will issue a range lock that prevents any insertions with
>> the same domain name; this would cause the second connection to be
>> blockeed until the first one finishes and releases it's locks.
>>
>> Or, if you prefer not to mess with the isolation level (it might have an
>> adverse effect on the performance, after all), reverse the logic a bit:
>>
>> -- Step 1: insert if it doesn't exist yet
>> INSERT Domains (Name)
>> SELECT @Domain
>> WHERE NOT EXISTS (SELECT *
>>                  FROM   Domains
>>                  WHERE  Name = @Domain)
>> -- Step 2: it's always there now - retrieve ID value
>> SET @id = (SELECT id
>>           FROM   Domains
>>           WHERE  Name = @Domain)
>>
>> Best, Hugo
>> --
>>
>> (Remove _NO_ and _SPAM_ to get my e-mail address)
>
>
Author
12 Nov 2005 9:49 AM
Brian Selzer
--Begin Critical Section
    DECLARE @_ROWCOUNT INT, @_ERROR INT
    BEGIN TRANSACTION
    SELECT @id = id from domains WITH(UPDLOCK, HOLDLOCK) where domains.name
= @domain
    SELECT @_ROWCOUNT = @@ROWCOUNT, @_ERROR = @@ERROR
    IF @_ERROR != 0 GOTO ERROR
    IF @_ROWCOUNT = 0
    BEGIN
        -- if you're inserting one row and not selecting from a table, then
use the VALUES clause
        -- that way you can differentiate at a glance between single-row
INSERTs and set-based INSERTs
        INSERT domains (name) VALUES (@domain)
        SELECT @_ROWCOUNT = @@ROWCOUNT, @_ERROR = @@ERROR
        IF @_ERROR != 0 OR @_ROWCOUNT = 0 GOTO ERROR
        -- use SET @id = @@IDENTITY if there is an INSTEAD OF INSERT trigger
on domains
        -- or SELECT @id = id where domains.name = @domain
        SET @id = SCOPE_IDENTITY()
    END
    COMMIT TRANSACTION
-- End critical section

    RETURN 0

ERROR:
    IF @@TRANCOUNT > 0 ROLLBACK TRANSACTION
    IF @RC != 0 RETURN @RC
    IF @_ERROR != 0 RETURN @_ERROR
    RETURN -1
END

Show quote
"William Stacey [MVP]" <william.sta***@gmail.com> wrote in message
news:uNLVfky5FHA.3976@TK2MSFTNGP15.phx.gbl...
>I think we have a winner.  Care to codify small example of that for the
>rest of us :-) ?  Then maybe others can poke at it until all agree.  Thanks
>Brian.
>
> --
> William Stacey [MVP]
>
> "Brian Selzer" <br***@selzer-software.com> wrote in message
> news:ep1xfBy5FHA.252@TK2MSFTNGP15.phx.gbl...
>>> This can be prevented by setting the appropriate isolation level. If you
>>> set the transaction isolation level to "SERIALIZABLE"
>>
>> Setting the isolation level is not enough.  You must enclose the SELECT
>> and the INSERT with a transaction.  It does no good to prevent changes
>> during the SELECT, but to allow changes between the SELECT and the
>> INSERT--which is indeed possible in a concurrent environment.  In
>> addition, to prevent deadlocks you must also specify WITH(UPDLOCK) or
>> WITH(XLOCK) on the SELECT statement.
>>
>> "Hugo Kornelis" <hugo@pe_NO_rFact.in_SPAM_fo> wrote in message
>> news:0p9an11fs7mb3dn1lobj7rube3blv7jclt@4ax.com...
>>> On Fri, 11 Nov 2005 13:30:55 -0500, William Stacey [MVP] wrote:
>>>
>>>>Say two thread enter at ~same time (say MP machine, but same issue with
>>>>1
>>>>cpu) and not domain name added yet.
>>>>Both threads get null id as none inserted yet.  Both threads move passed
>>>>"if
>>>>(id is null)" test.  Now one thread will insert and return and the other
>>>>will fail on non-unique index.  And there is no error handling to catch
>>>>the
>>>>error on the second thread so it just throws exception back to caller.
>>>
>>> Hi William,
>>>
>>> This can be prevented by setting the appropriate isolation level. If you
>>> set the transaction isolation level to "SERIALIZABLE" (or add a
>>> SERIALIZABLE locking hint to the SELECT statement), the first attempt to
>>> read the row will issue a range lock that prevents any insertions with
>>> the same domain name; this would cause the second connection to be
>>> blockeed until the first one finishes and releases it's locks.
>>>
>>> Or, if you prefer not to mess with the isolation level (it might have an
>>> adverse effect on the performance, after all), reverse the logic a bit:
>>>
>>> -- Step 1: insert if it doesn't exist yet
>>> INSERT Domains (Name)
>>> SELECT @Domain
>>> WHERE NOT EXISTS (SELECT *
>>>                  FROM   Domains
>>>                  WHERE  Name = @Domain)
>>> -- Step 2: it's always there now - retrieve ID value
>>> SET @id = (SELECT id
>>>           FROM   Domains
>>>           WHERE  Name = @Domain)
>>>
>>> Best, Hugo
>>> --
>>>
>>> (Remove _NO_ and _SPAM_ to get my e-mail address)
>>
>>
>
>
Author
12 Nov 2005 11:17 PM
ML
Beautiful!


ML
Author
13 Nov 2005 8:22 PM
Hugo Kornelis
On Fri, 11 Nov 2005 19:31:06 -0500, Brian Selzer wrote:

>> This can be prevented by setting the appropriate isolation level. If you
>> set the transaction isolation level to "SERIALIZABLE"
>
>Setting the isolation level is not enough.  You must enclose the SELECT and
>the INSERT with a transaction.

Hi Brian,

Yes, of course - I assumed that the procedure was part of a transaction,
but I should have pointed that out explicitly.

Thanks for the addition!!

Best, Hugo
--

(Remove _NO_ and _SPAM_ to get my e-mail address)
Author
6 Jan 2006 5:00 AM
William Stacey [MVP]
So is there agreement that Hugos code with addition of transaction will work
like below,
or do you still need the UPDLOCK, HOLDLOCK stuff?

Alter PROCEDURE AddDomainName
    @domainname varchar(255),
    @id int OUT
AS
BEGIN TRANSACTION
-- Step 1: Add domain name if it does not exist.
INSERT Domains (DomainName)
SELECT @domainname
WHERE NOT EXISTS (SELECT *
                  FROM   Domains
                  WHERE  DomainName = @domainname)

-- Step 2: it's always there now - retrieve ID.
SET @id = (SELECT ID
           FROM   Domains
           WHERE  DomainName = @domainname)
COMMIT TRANSACTION
go

--
William Stacey [MVP]

Show quote
"Hugo Kornelis" <hugo@pe_NO_rFact.in_SPAM_fo> wrote in message
news:g28fn1h2m4m7p75b306phdt8nm4e67gki5@4ax.com...
> On Fri, 11 Nov 2005 19:31:06 -0500, Brian Selzer wrote:
>
>>> This can be prevented by setting the appropriate isolation level. If you
>>> set the transaction isolation level to "SERIALIZABLE"
>>
>>Setting the isolation level is not enough.  You must enclose the SELECT
>>and
>>the INSERT with a transaction.
>
> Hi Brian,
>
> Yes, of course - I assumed that the procedure was part of a transaction,
> but I should have pointed that out explicitly.
>
> Thanks for the addition!!
>
> Best, Hugo
> --
>
> (Remove _NO_ and _SPAM_ to get my e-mail address)
Author
6 Jan 2006 8:06 AM
Brian Selzer
With Hugo's code, I don't think you need a transaction, but I think you
still need the UPDLOCK, HOLDLOCK stuff.  A transaction is automagically
started for the INSERT statement which encompasses the SELECT and in
particular the EXISTS clause.  But the EXISTS clause is executed before
locks are obtained for the INSERT.  Without HOLDLOCK, in a highly concurrent
environment another transaction could insert or change a row between the
time that the EXISTS clause executes and the time that the exclusive lock is
obtained for the INSERT, causing a constraint violation.  Without UPDLOCK,
two similar transactions executing simultaneously could obtain shared
range-locks on Domains (from the EXISTS clause) and then deadlock.

Try this:

INSERT Domains (DomainName)
SELECT @domainname
WHERE NOT EXISTS (SELECT *
                  FROM   Domains WITH(UPDLOCK, HOLDLOCK)
                  WHERE  DomainName = @domainname)
IF @@ERROR != 0 OR @@ROWCOUNT = 0 GOTO ERROR
SET @id = SCOPE_IDENTITY()


Show quote
"William Stacey [MVP]" <william.sta***@gmail.com> wrote in message
news:eLDxc4nEGHA.4036@TK2MSFTNGP09.phx.gbl...
> So is there agreement that Hugos code with addition of transaction will
> work like below,
> or do you still need the UPDLOCK, HOLDLOCK stuff?
>
> Alter PROCEDURE AddDomainName
>    @domainname varchar(255),
>    @id int OUT
> AS
> BEGIN TRANSACTION
> -- Step 1: Add domain name if it does not exist.
> INSERT Domains (DomainName)
> SELECT @domainname
> WHERE NOT EXISTS (SELECT *
>                  FROM   Domains
>                  WHERE  DomainName = @domainname)
>
> -- Step 2: it's always there now - retrieve ID.
> SET @id = (SELECT ID
>           FROM   Domains
>           WHERE  DomainName = @domainname)
> COMMIT TRANSACTION
> go
>
> --
> William Stacey [MVP]
>
> "Hugo Kornelis" <hugo@pe_NO_rFact.in_SPAM_fo> wrote in message
> news:g28fn1h2m4m7p75b306phdt8nm4e67gki5@4ax.com...
>> On Fri, 11 Nov 2005 19:31:06 -0500, Brian Selzer wrote:
>>
>>>> This can be prevented by setting the appropriate isolation level. If
>>>> you
>>>> set the transaction isolation level to "SERIALIZABLE"
>>>
>>>Setting the isolation level is not enough.  You must enclose the SELECT
>>>and
>>>the INSERT with a transaction.
>>
>> Hi Brian,
>>
>> Yes, of course - I assumed that the procedure was part of a transaction,
>> but I should have pointed that out explicitly.
>>
>> Thanks for the addition!!
>>
>> Best, Hugo
>> --
>>
>> (Remove _NO_ and _SPAM_ to get my e-mail address)
>
>
Author
6 Jan 2006 6:06 PM
William Stacey [MVP]
> INSERT Domains (DomainName)
> SELECT @domainname
> WHERE NOT EXISTS (SELECT *
>                  FROM   Domains WITH(UPDLOCK, HOLDLOCK)
>                  WHERE  DomainName = @domainname)
> IF @@ERROR != 0 OR @@ROWCOUNT = 0 GOTO ERROR
> SET @id = SCOPE_IDENTITY()

Thanks again.  I saw the following in the Insert docs, not sure if this
applies here, but it sounds like it may:

"The ability to specify the HOLDLOCK, SERIALIZABLE, READCOMMITTED,
REPEATABLEREAD, or UPDLOCK hints on tables that are targets of INSERT
statements will be removed in a future version of SQL Server. These hints do
not affect the performance of INSERT statements. Avoid using them in new
development work, and plan to modify applications that currently use them."

What do you think?  Cheers.
--
wjs
Author
7 Jan 2006 12:04 AM
Brian Selzer
Nope, it doesn't apply.  It would apply were the statement like this:

INSERT Domains WITH(HOLDLOCK) (DomainName)
SELECT @domainname
WHERE NOT EXISTS (SELECT *
                 FROM   Domains WITH(UPDLOCK, HOLDLOCK)
                 WHERE  DomainName = @domainname)
IF @@ERROR != 0 OR @@ROWCOUNT = 0 GOTO ERROR
SET @id = SCOPE_IDENTITY()

Locks on tables referenced within the EXISTS clause above are handled
separately from the locks on the target table.  It's just coincidental that
the target table and the table in the EXISTS clause happen to be the same.
Even if you had enclosed the SELECT clause in a stored procedure and issued
INSERT...EXEC, the locks would be obtained in the same order.   The
statement below refers to the target of the INSERT--the table that is about
to receive data.

Show quote
"William Stacey [MVP]" <william.sta***@gmail.com> wrote in message
news:u9sQgvuEGHA.336@TK2MSFTNGP14.phx.gbl...
>> INSERT Domains (DomainName)
>> SELECT @domainname
>> WHERE NOT EXISTS (SELECT *
>>                  FROM   Domains WITH(UPDLOCK, HOLDLOCK)
>>                  WHERE  DomainName = @domainname)
>> IF @@ERROR != 0 OR @@ROWCOUNT = 0 GOTO ERROR
>> SET @id = SCOPE_IDENTITY()
>
> Thanks again.  I saw the following in the Insert docs, not sure if this
> applies here, but it sounds like it may:
>
> "The ability to specify the HOLDLOCK, SERIALIZABLE, READCOMMITTED,
> REPEATABLEREAD, or UPDLOCK hints on tables that are targets of INSERT
> statements will be removed in a future version of SQL Server. These hints
> do not affect the performance of INSERT statements. Avoid using them in
> new development work, and plan to modify applications that currently use
> them."
>
> What do you think?  Cheers.
> --
> wjs
>
Author
7 Jan 2006 12:16 AM
Hugo Kornelis
On Fri, 6 Jan 2006 13:06:07 -0500, William Stacey [MVP] wrote:

Show quote
>> INSERT Domains (DomainName)
>> SELECT @domainname
>> WHERE NOT EXISTS (SELECT *
>>                  FROM   Domains WITH(UPDLOCK, HOLDLOCK)
>>                  WHERE  DomainName = @domainname)
>> IF @@ERROR != 0 OR @@ROWCOUNT = 0 GOTO ERROR
>> SET @id = SCOPE_IDENTITY()
>
>Thanks again.  I saw the following in the Insert docs, not sure if this
>applies here, but it sounds like it may:
>
>"The ability to specify the HOLDLOCK, SERIALIZABLE, READCOMMITTED,
>REPEATABLEREAD, or UPDLOCK hints on tables that are targets of INSERT
>statements will be removed in a future version of SQL Server. These hints do
>not affect the performance of INSERT statements. Avoid using them in new
>development work, and plan to modify applications that currently use them."
>
>What do you think?  Cheers.

Hi William,

This pertains to locking hints on the INSERT part of the INSERT
statement, like this:

INSERT Domains WITH (TABLOCK) (DomainName)
SELECT ....
WHERE  ....

The hints specified in the above quote are useless since inserted rows
will always be locked exclusively until the end of the transaction.

--
Hugo Kornelis, SQL Server MVP
Author
7 Jan 2006 12:13 AM
Hugo Kornelis
On Fri, 6 Jan 2006 03:06:06 -0500, Brian Selzer wrote:

>With Hugo's code, I don't think you need a transaction, but I think you
>still need the UPDLOCK, HOLDLOCK stuff.  A transaction is automagically
>started for the INSERT statement which encompasses the SELECT and in
>particular the EXISTS clause.

Hi Brian,

But that implicit transaction does not extend beyond the INSERT
statement. This could cause the SELECT statement that follows to fail if
the row just inserted gets changed or deleted by another connection
between these two steps. (I know, it's very unlikely, but at leastt
theoretically impossible - so why not play safe and keep the explicit
transaction?)

(snip)
>Try this:
>
> INSERT Domains (DomainName)
> SELECT @domainname
> WHERE NOT EXISTS (SELECT *
>                  FROM   Domains WITH(UPDLOCK, HOLDLOCK)
>                  WHERE  DomainName = @domainname)
>IF @@ERROR != 0 OR @@ROWCOUNT = 0 GOTO ERROR
>SET @id = SCOPE_IDENTITY()

This would set @id to NULL if the WHERE clause in the INSERT statement
evaluates to FALSE, since there would be no rows inserted. (Or, if an
other INSERT statement has already been executed in the same scope, the
identity value generated for that INSERT statement would be returned!)

--
Hugo Kornelis, SQL Server MVP
Author
7 Jan 2006 2:15 AM
Brian Selzer
Hi Hugo,

I'm almost positive that the implicit transaction extends to the SELECT
clause of the INSERT...SELECT statement.  It's difficult to prove this, but
I did a profiler trace of SQLTransaction events for a typical
INSERT...SELECT statement, and there's only one entry with "Begin" for the
EventSubClass, and only one entry for "Commit."  In addition, locks were
aquired on both of the affected tables--first the object of the SELECT, and
then on the target of the INSERT--between the Begin and Commit
SQLTransaction events.  I was able to make the batch block by holding locks
in another Query Analyzer window, so it's clear that the SELECT was
operating within the context of a transaction.

--Brian

Show quote
"Hugo Kornelis" <h***@perFact.REMOVETHIS.info> wrote in message
news:ql1ur1t0ck46mcqrmi6tsh3ncndh0f20p0@4ax.com...
> On Fri, 6 Jan 2006 03:06:06 -0500, Brian Selzer wrote:
>
>>With Hugo's code, I don't think you need a transaction, but I think you
>>still need the UPDLOCK, HOLDLOCK stuff.  A transaction is automagically
>>started for the INSERT statement which encompasses the SELECT and in
>>particular the EXISTS clause.
>
> Hi Brian,
>
> But that implicit transaction does not extend beyond the INSERT
> statement. This could cause the SELECT statement that follows to fail if
> the row just inserted gets changed or deleted by another connection
> between these two steps. (I know, it's very unlikely, but at leastt
> theoretically impossible - so why not play safe and keep the explicit
> transaction?)
>
> (snip)
>>Try this:
>>
>> INSERT Domains (DomainName)
>> SELECT @domainname
>> WHERE NOT EXISTS (SELECT *
>>                  FROM   Domains WITH(UPDLOCK, HOLDLOCK)
>>                  WHERE  DomainName = @domainname)
>>IF @@ERROR != 0 OR @@ROWCOUNT = 0 GOTO ERROR
>>SET @id = SCOPE_IDENTITY()
>
> This would set @id to NULL if the WHERE clause in the INSERT statement
> evaluates to FALSE, since there would be no rows inserted. (Or, if an
> other INSERT statement has already been executed in the same scope, the
> identity value generated for that INSERT statement would be returned!)
>
> --
> Hugo Kornelis, SQL Server MVP
Author
7 Jan 2006 9:28 PM
Hugo Kornelis
On Fri, 6 Jan 2006 21:15:18 -0500, Brian Selzer wrote:

(snip top-post)
Show quote
>"Hugo Kornelis" <h***@perFact.REMOVETHIS.info> wrote in message
>news:ql1ur1t0ck46mcqrmi6tsh3ncndh0f20p0@4ax.com...
>> On Fri, 6 Jan 2006 03:06:06 -0500, Brian Selzer wrote:
>>
>>>With Hugo's code, I don't think you need a transaction, but I think you
>>>still need the UPDLOCK, HOLDLOCK stuff.  A transaction is automagically
>>>started for the INSERT statement which encompasses the SELECT and in
>>>particular the EXISTS clause.
>>
>> Hi Brian,
>>
>> But that implicit transaction does not extend beyond the INSERT
>> statement. This could cause the SELECT statement that follows to fail if
>> the row just inserted gets changed or deleted by another connection
>> between these two steps. (I know, it's very unlikely, but at leastt
>> theoretically impossible - so why not play safe and keep the explicit
>> transaction?)
(paste top-post)

>Hi Hugo,
>
>I'm almost positive that the implicit transaction extends to the SELECT
>clause of the INSERT...SELECT statement.
(snip)

Hi Brian,

And so am I.

The SELECT statement that could (theoretically) fail is the second one,
the one that gets the identity value of the row that was either just
inserted or already existing. Here's the code William referred to, with
some comments:


Alter PROCEDURE AddDomainName
    @domainname varchar(255),
    @id int OUT
AS
BEGIN TRANSACTION
-- Step 1: Add domain name if it does not exist.
-- *** No need for a transaction for just this step;
-- *** single statements are always ACID.
INSERT Domains (DomainName)
SELECT @domainname
WHERE NOT EXISTS (SELECT *
                  FROM   Domains
                  WHERE  DomainName = @domainname)

-- Step 2: it's always there now - retrieve ID.
-- *** Here's the reason why we need a transaction.
-- *** Without it, the row that prevented the insert in the first step
-- *** might possibly be removed when it's read here.
SET @id = (SELECT ID
           FROM   Domains
           WHERE  DomainName = @domainname)

-- *** Now it's safe to end the transaction.
COMMIT TRANSACTION
go

--
Hugo Kornelis, SQL Server MVP
Author
8 Jan 2006 1:13 AM
Brian Selzer
I must have been half asleep when I read your post.  Sorry.

Show quote
"Hugo Kornelis" <h***@perFact.REMOVETHIS.info> wrote in message
news:6bc0s19ilo0f7da6s166h8ckh7ekha0nn7@4ax.com...
> On Fri, 6 Jan 2006 21:15:18 -0500, Brian Selzer wrote:
>
> (snip top-post)
>>"Hugo Kornelis" <h***@perFact.REMOVETHIS.info> wrote in message
>>news:ql1ur1t0ck46mcqrmi6tsh3ncndh0f20p0@4ax.com...
>>> On Fri, 6 Jan 2006 03:06:06 -0500, Brian Selzer wrote:
>>>
>>>>With Hugo's code, I don't think you need a transaction, but I think you
>>>>still need the UPDLOCK, HOLDLOCK stuff.  A transaction is automagically
>>>>started for the INSERT statement which encompasses the SELECT and in
>>>>particular the EXISTS clause.
>>>
>>> Hi Brian,
>>>
>>> But that implicit transaction does not extend beyond the INSERT
>>> statement. This could cause the SELECT statement that follows to fail if
>>> the row just inserted gets changed or deleted by another connection
>>> between these two steps. (I know, it's very unlikely, but at leastt
>>> theoretically impossible - so why not play safe and keep the explicit
>>> transaction?)
> (paste top-post)
>
>>Hi Hugo,
>>
>>I'm almost positive that the implicit transaction extends to the SELECT
>>clause of the INSERT...SELECT statement.
> (snip)
>
> Hi Brian,
>
> And so am I.
>
> The SELECT statement that could (theoretically) fail is the second one,
> the one that gets the identity value of the row that was either just
> inserted or already existing. Here's the code William referred to, with
> some comments:
>
>
> Alter PROCEDURE AddDomainName
>    @domainname varchar(255),
>    @id int OUT
> AS
> BEGIN TRANSACTION
> -- Step 1: Add domain name if it does not exist.
> -- *** No need for a transaction for just this step;
> -- *** single statements are always ACID.
> INSERT Domains (DomainName)
> SELECT @domainname
> WHERE NOT EXISTS (SELECT *
>                  FROM   Domains
>                  WHERE  DomainName = @domainname)
>
> -- Step 2: it's always there now - retrieve ID.
> -- *** Here's the reason why we need a transaction.
> -- *** Without it, the row that prevented the insert in the first step
> -- *** might possibly be removed when it's read here.
> SET @id = (SELECT ID
>           FROM   Domains
>           WHERE  DomainName = @domainname)
>
> -- *** Now it's safe to end the transaction.
> COMMIT TRANSACTION
> go
>
> --
> Hugo Kornelis, SQL Server MVP
Author
8 Jan 2006 1:16 AM
Brian Selzer
(snip)
> This would set @id to NULL if the WHERE clause in the INSERT statement
> evaluates to FALSE, since there would be no rows inserted. (Or, if an
> other INSERT statement has already been executed in the same scope, the
> identity value generated for that INSERT statement would be returned!)

I don't think so. That's why I used,

IF @@ERROR != 0 OR @@ROWCOUNT = 0 GOTO ERROR



Show quote
"Hugo Kornelis" <h***@perFact.REMOVETHIS.info> wrote in message
news:ql1ur1t0ck46mcqrmi6tsh3ncndh0f20p0@4ax.com...
> On Fri, 6 Jan 2006 03:06:06 -0500, Brian Selzer wrote:
>
>>With Hugo's code, I don't think you need a transaction, but I think you
>>still need the UPDLOCK, HOLDLOCK stuff.  A transaction is automagically
>>started for the INSERT statement which encompasses the SELECT and in
>>particular the EXISTS clause.
>
> Hi Brian,
>
> But that implicit transaction does not extend beyond the INSERT
> statement. This could cause the SELECT statement that follows to fail if
> the row just inserted gets changed or deleted by another connection
> between these two steps. (I know, it's very unlikely, but at leastt
> theoretically impossible - so why not play safe and keep the explicit
> transaction?)
>
> (snip)
>>Try this:
>>
>> INSERT Domains (DomainName)
>> SELECT @domainname
>> WHERE NOT EXISTS (SELECT *
>>                  FROM   Domains WITH(UPDLOCK, HOLDLOCK)
>>                  WHERE  DomainName = @domainname)
>>IF @@ERROR != 0 OR @@ROWCOUNT = 0 GOTO ERROR
>>SET @id = SCOPE_IDENTITY()
>
> This would set @id to NULL if the WHERE clause in the INSERT statement
> evaluates to FALSE, since there would be no rows inserted. (Or, if an
> other INSERT statement has already been executed in the same scope, the
> identity value generated for that INSERT statement would be returned!)
>
> --
> Hugo Kornelis, SQL Server MVP
Author
8 Jan 2006 12:23 PM
Hugo Kornelis
On Sat, 7 Jan 2006 20:16:15 -0500, Brian Selzer wrote:

>(snip)
>> This would set @id to NULL if the WHERE clause in the INSERT statement
>> evaluates to FALSE, since there would be no rows inserted. (Or, if an
>> other INSERT statement has already been executed in the same scope, the
>> identity value generated for that INSERT statement would be returned!)
>
>I don't think so. That's why I used,
>
>IF @@ERROR != 0 OR @@ROWCOUNT = 0 GOTO ERROR

Hi Brian,

I guess I was half asleep as well. I missed that.

Your version is better if the situation that the row already exists
shuld not arise, and should therefor be treated as an error condition.

My version is better if the "row already exists" case is an expected,
non-error case: the routine is designed to add a row if it's not there,
then return the identity value of the row that either already existed or
was just inserted.

I thought the latter was what was asked for in this thread, but I may
well be confused with another discussion, here or in another group. I
don't feel like going back up this thread to check - if the original
poster is still here, (s)he should pick either your or my suggestion,
depending on what (s)he really needs.

--
Hugo Kornelis, SQL Server MVP
Author
6 Jan 2006 6:24 PM
ML
But we need to take into account that any insert triggers will fire even if
no rows are inserted. I'm not saying this is a problem. I'm simply pointing
it out. :)


ML

---
http://milambda.blogspot.com/
Author
7 Jan 2006 12:09 AM
Hugo Kornelis
On Fri, 6 Jan 2006 00:00:25 -0500, William Stacey [MVP] wrote:

Show quote
>So is there agreement that Hugos code with addition of transaction will work
>like below,
>or do you still need the UPDLOCK, HOLDLOCK stuff?
>
>Alter PROCEDURE AddDomainName
>    @domainname varchar(255),
>    @id int OUT
>AS
>BEGIN TRANSACTION
>-- Step 1: Add domain name if it does not exist.
>INSERT Domains (DomainName)
>SELECT @domainname
>WHERE NOT EXISTS (SELECT *
>                  FROM   Domains
>                  WHERE  DomainName = @domainname)
>
>-- Step 2: it's always there now - retrieve ID.
>SET @id = (SELECT ID
>           FROM   Domains
>           WHERE  DomainName = @domainname)
>COMMIT TRANSACTION
>go

Hi William,

I think that you'd need to add a HOLDLOCK hint to Domains in the NOT
EXISTS subquery. Basically, there are two scenario's:

A. Row is new:
   1. Subquery returns no row, NOT EXISTS is true --> Row inserted
   2. Because of INSERT, row is exclusive-locked and this lock is held
for entire transaction
   3. Final SELECT retrieves row just inserted.

B. Row already exists:
   1. Subquery returns row, NOT EXISTS is false --> No row inserted
   2. Row is shared-locked and this lock is by default not held
   3. Final SELECT retrieves row just read.

In scenario B, there is a small chance that the row is deleted or
changed after steps 1 and 2 but before step 3. In that case, the
subquery won't return any rows and @id will be set to NULL. Adding the
HOLDLOCK hint prevents that - the lock is held during the transaction,
so that no other connection can change or delete the row.

--
Hugo Kornelis, SQL Server MVP
Author
11 Nov 2005 6:40 PM
David Gugick
William Stacey [MVP] wrote:
> I don't think that is thread safe.  Multiple threads could end up in
> the insert block.
>

I'm getting confused about this part of the discussion. I assume we're
not disputing that multiple users can run the same stored procedure at
the same time. I also assume we're not disputing that performing a
SELECT to see if a row exists in a table, and having that SELECT fail,
will not prevent two users from possibly hitting the INSERT command at
the same time. I don't think thread-safe is the correct term here.
Thread-safe (at least to me) implies the prevention of multiple threads
in the same program hitting the same data structure and stepping on each
others data. If we're using the term here in the context of preventing
two users from trying to insert the same key value at the same time, I
don't think this can be done if the key row does not already exist. If
it did, you could SELECT the row and hold an update lock on the data in
a transaction to prevent other users from getting past the same SELECT
portion - but inthat case there is no insert to be done. If the row is
not there in the first place, there's nothing to lock and each user
could hit the INSERT at the same time. Constraints would obviously
prevent data integrity issues.

So what are we really talking about here? Still confused...


--
David Gugick
Quest Software
www.imceda.com
www.quest.com
Author
11 Nov 2005 7:05 PM
William Stacey [MVP]
Just that the second thread will now throw dup exception and client has to
handle it.  IMO, this should be handled correctly inside the sproc.  The
sproc should add item if not exist and return id of new or existing row, and
only throw exception if a real unhandled exception occurs.  The client could
catch the error and retry, but I don't think that is right way to handle
this.  I also don't like the idea of using try/catch blocks in this way in
the "normal" logic path as exceptions are relatively expensive and should be
used for real exceptions per best practices.  That said, I don't currently
have a better idea then Bob's, but I would sure think this is a common DB
pattern, that probably has a simple (and fast) solution.

--
William Stacey [MVP]

Show quote
"David Gugick" <david.gugick-nospam@quest.com> wrote in message
news:usmQY9u5FHA.964@tk2msftngp13.phx.gbl...
> William Stacey [MVP] wrote:
>> I don't think that is thread safe.  Multiple threads could end up in
>> the insert block.
>>
>
> I'm getting confused about this part of the discussion. I assume we're not
> disputing that multiple users can run the same stored procedure at the
> same time. I also assume we're not disputing that performing a SELECT to
> see if a row exists in a table, and having that SELECT fail, will not
> prevent two users from possibly hitting the INSERT command at the same
> time. I don't think thread-safe is the correct term here. Thread-safe (at
> least to me) implies the prevention of multiple threads in the same
> program hitting the same data structure and stepping on each others data.
> If we're using the term here in the context of preventing two users from
> trying to insert the same key value at the same time, I don't think this
> can be done if the key row does not already exist. If it did, you could
> SELECT the row and hold an update lock on the data in a transaction to
> prevent other users from getting past the same SELECT portion - but inthat
> case there is no insert to be done. If the row is not there in the first
> place, there's nothing to lock and each user could hit the INSERT at the
> same time. Constraints would obviously prevent data integrity issues.
>
> So what are we really talking about here? Still confused...
>
>
> --
> David Gugick
> Quest Software
> www.imceda.com
> www.quest.com
>
Author
11 Nov 2005 8:25 PM
ML
> I also don't like the idea of using try/catch blocks in this way in
> the "normal" logic path as exceptions are relatively expensive and should be
> used for real exceptions per best practices.

So, you're saying basic business logic should be implemented in the platform
itself? How? Should a failed insert become an update? Should a failed insert
be ignored by design? And should it become a select? Or maybe "prophesied
exclusive locking" could be introduced - one that can see into the future?

IMHO that's exactly why TRY...CATCH was introduced - plus it enables us to
handle more exceptions on the server rather than propagating them to the
client.

Either way, I'd much rather see exceptions than end up with unexpected
results.


ML
Author
11 Nov 2005 11:09 PM
William Stacey [MVP]
Not sure we are talking about the same thing any longer.  The proc should
handle the test-and-set atomically without needing to throw an internal
exception to get that specific job done.  Try/Catch is good, but should be
used to handle true exceptions (i.e. things we did not expect to happen).
This exception, I hope and suspect, can be avoided by the implementation
(which we are trying to find).

--
William Stacey [MVP]

Show quote
"ML" <M*@discussions.microsoft.com> wrote in message
news:A93CFB6F-CD1C-4C84-8509-22FB38282ACF@microsoft.com...
>> I also don't like the idea of using try/catch blocks in this way in
>> the "normal" logic path as exceptions are relatively expensive and should
>> be
>> used for real exceptions per best practices.
>
> So, you're saying basic business logic should be implemented in the
> platform
> itself? How? Should a failed insert become an update? Should a failed
> insert
> be ignored by design? And should it become a select? Or maybe "prophesied
> exclusive locking" could be introduced - one that can see into the future?
>
> IMHO that's exactly why TRY...CATCH was introduced - plus it enables us to
> handle more exceptions on the server rather than propagating them to the
> client.
>
> Either way, I'd much rather see exceptions than end up with unexpected
> results.
>
>
> ML
Author
12 Nov 2005 12:14 AM
ML
Ok. First of all - when you say thread, do you actually mean connection? It
sure seems so. :)

> Not sure we are talking about the same thing any longer.  The proc should
> handle the test-and-set atomically without needing to throw an internal
> exception to get that specific job done.  Try/Catch is good, but should be
> used to handle true exceptions (i.e. things we did not expect to happen).

TRY...CATCH is the general (generic, if you will) solution. It handles most
exceptions...

> This exception, I hope and suspect, can be avoided by the implementation
> (which we are trying to find).

....and I agree - some are better handled without the TRY...CATCH, but I
don't think the platform should handle them, since their nature is (in most
cases) purely business-related.

In case of an insert that should not happen, because it already happened,
although not detected by the current session (does that sound weird or is it
just me? :), then using serializable transaction isolation level would be the
natural option. The only trouble being that it blocks *everything*. Thus
transactions (or sessions) not intended to insert data should make use of the
readpast locking hint. Which still leaves us with transactions that intend to
insert rows - using readpast might lead to exceptions as might using nolock.

What about the combination of both?

1) If any of the rows returned by the nolock-select that are not returned by
the readpast-select correspond to the row I'm trying to insert, then the
insert will fail if the other transaction is rolled back => better not insert.

2) However, if none of the rows returned by the nolock-select that are not
returned by the readpast-select correspond to the row I'm trying to insert,
the insert won't fail, since no other transaction is trying to insert the
same data => continue with insert, get scope_identity, insert details.

3) And, if the row I'm trying to insert is returned by both the
readpast-select and the nolock-select, then I don't need to insert at all, so
just get identity and insert details.

I think a combination of the three states can lead us to a solution. Any
other thoughts?


ML
Author
12 Nov 2005 1:30 AM
William Stacey [MVP]
> Ok. First of all - when you say thread, do you actually mean connection?
> It
> sure seems so. :)

No.  As I said, 1+ threads in the *server (i.e. the server process).
Connection will boil down to threads in the server, so talking in terms of
threads is proper for this discussion I think.

> TRY...CATCH is the general (generic, if you will) solution. It handles
> most
> exceptions...

I am a big fan of try/catch, I use it all the time.  My point is still this.
Using try/catch in this way is, IMO, not good.  Catching exceptions are
expensive and should not be used as a control flow construct if it can be
avoided.  If we can do the test-set atomic, we don't need the try/catch and
will let unexpected exceptions flow back to client.

> ...and I agree - some are better handled without the TRY...CATCH, but I
> don't think the platform should handle them,

I never said the platform should handle them.  I would agree.

I think Brian and Hugo may have the answer here.

--
William Stacey [MVP]
Author
12 Nov 2005 12:04 PM
ML
> No.  As I said, 1+ threads in the *server (i.e. the server process).
> Connection will boil down to threads in the server, so talking in terms of
> threads is proper for this discussion I think.

If the DML tasks of a single connection get split amongst several threads
for purposes other than supporting parallelism, we're - pardon my language -
completely screwed, and can start looking for jobs in the fast-food business.
This simply cannot happen, since it absolutely breaks the atomicity of a DML
task.

Even Brian's example won't work if the server actually allows threads to
"run amok". But there's only one way to know for sure - we should test it. Or
ask MS. Or better yet, test it.


ML
Author
12 Nov 2005 5:20 PM
William Stacey [MVP]
> If the DML tasks of a single connection get split amongst several threads
> for purposes other than supporting parallelism, we're - pardon my
> language -
> completely screwed, and can start looking for jobs in the fast-food
> business.
> This simply cannot happen, since it absolutely breaks the atomicity of a
> DML
> task.

Hi ML.  We are not on same page yet.  Not single connection worried about
here.  Multiple connections (i.e. from diff users or different jobs) that
run the same sproc at ~same time, which AFAIK, start new threads.  So if
multiple threads are running same proc, we need to design for that depending
on spoc.  I think Brian has got it above.  Cheers.  Have a great weekend
guys.

--
William Stacey [MVP]
Author
11 Nov 2005 5:00 PM
David Gugick
Bob wrote:
> So I never have to worry about two threads calling the select part of
> the proc at once and
> both getting Null?  Then both call the insert forcing one to get an
> error because the domain existed.

That's a possiblilty, but is easily handled with with constraints or
indexing. If you want to check if a row exists before the insert, I
think that's fine, despite the added overhead. But why is it a problem
if the insert fails because someone inserted a row before that time. The
insert will error out and the application can easily handle that. There
would be no damage to the data. Which inevitably leads us to ask: Why
not just perform the insert and forget the checking and the overhead it
adds since that actually reduces concurrency overall.

--
David Gugick
Quest Software
www.imceda.com
www.quest.com
Author
11 Nov 2005 7:51 PM
jxstern
On Fri, 11 Nov 2005 10:07:15 -0500, "Bob" <msg***@hotmail.com> wrote:
>So I never have to worry about two threads calling the select part of the
>proc at once and
>both getting Null?  Then both call the insert forcing one to get an
>error because the domain existed.

You don't worry about thread safety in the relational world, you worry
about transactions and isolation levels.  It comes to the same thing.

J.
Author
11 Nov 2005 2:37 PM
David Gugick
Bob wrote:
Show quote
> I am trying add an item to a table that does not allow duplicates but
> if the item already exists I want to return its id.  I need an SQL
> equivalent to a critical section but I am not sure how its done.  It
> would like something like the below code.
>
> --Begin Critical Section
>
>    SELECT @id = id from domains where domains.name = @domain
>     if @id is null
>     begin
>       INSERT Domains(name) SELECT @domain;
>       SELECT @domain_id  = SCOPE_IDENTITY();
>     end
>
> -- End critical section
>
>
> Thanks for any suggestions.

That looks fine. Just make sure you have an index on the "Name" column.
If you have a clustered index on ID and a non-clustered index on "Name"
then you can eliminate a bookmark lookup operation on the SELECT.

--
David Gugick
Quest Software
www.imceda.com
www.quest.com

AddThis Social Bookmark Button