Please start any new threads on our new site at https://forums.sqlteam.com. We've got lots of great SQL Server experts to answer whatever question you can come up with.

 All Forums
 SQL Server 2000 Forums
 Transact-SQL (2000)
 Transactions problem

Author  Topic 

zurbum
Yak Posting Veteran

55 Posts

Posted - 2005-11-11 : 15:56:24
I already posted this question in "General SQL", but haven't received an reply yet, so I am reposting it here in hope someone will be able to help me.

I've created 2 tables
TEST(TEST_ID int identity, TEST_NAME varchar(20))
and
SAVE(SAVE_ID int identity, TEST_ID int)

I filled TEST table with random data.

In query analyzer I opened 3 connection and wrote
BEGIN TRANSACTION TEST
DECLARE @id int

SET @id = (SELECT MAX(TEST_ID) FROM TEST WHERE TEST_ID NOT IN (SELECT TEST_ID FROM SAVE))

SELECT * FROM TEST WHERE TEST_ID=@id

INSERT INTO SAVE (TEST_ID) VALUES (@id)
WAITFOR DELAY '00:00:10'
COMMIT TRANSACTION TEST

The idea is to read the data with MAX(Test_id) which has not been read yet(not in SAVE table), saving this id in table SAVE and display the record. Let say if more users try to get the data at the same time, no one should receive the same data.

Then I run query in QueryAnalyzer.connection 1, then 2 and then 3.
The results were not as expected. In connection 2 and 3 I got the same record from table TEST. I tried with SET TRANSACTION ISOLATION LEVEL SERIALIZABLE but then the second connection was killed
"Transaction (Process ID 54) was deadlocked on lock resources with another process and has been chosen as the deadlock victim. Rerun the transaction."

Can anyone help me to solve this problem. Thank you.

SamC
White Water Yakist

3467 Posts

Posted - 2005-11-11 : 16:29:46
I don't think you need to bracket the following code in a transaction to get what you want.

Try this:

INSERT INTO SAVE (Test_ID)
SELECT MAX(TEST_ID) AS MAXTEST_ID
FROM TEST
WHERE TEST_ID NOT IN (SELECT TEST_ID FROM SAVE)

SET @id = SCOPE_IDENTITY() -- Should return the ID inserted into SAVE
Go to Top of Page

zurbum
Yak Posting Veteran

55 Posts

Posted - 2005-11-11 : 20:43:01
I need to call a store procedure, which from tables gets result.

The problem is that if my store procedure is called from 2 different computers in the same time(not always, but happens), both receive same record, which should not happen.

Imagine a call center, where store procedure pick up phone number from a table and insert this number in called numbers table. If two computers do a select for record which is not in Already called and not in waiting quive... it happens that both received the same phone number which I don't want.

With my example I have just simplified my problem.
Go to Top of Page

TG
Master Smack Fu Yak Hacker

6065 Posts

Posted - 2005-11-11 : 22:26:48
You're going to need a different methodology to avoid deadlocking and collisions. One way is to "reserve" a row, then get a reference to the row you just reserved, then process the row then flag it as processed. You don't need to use a "save" table for this but you could if you want to. Here is an example of what I mean:

use northwind

create table queuestatus (statusid int primary key, statusDesc varchar(25) not null)
go

create table queue
(queueID int identity(1,1) primary key clustered
,queuestatusid int references queuestatus (statusid) not null
,processerid int not null
,updateDate datetime not null)
go
create nonclustered index processerid_queueStatusID on queue (processerid, queueStatusid) with fillfactor=80
go

insert queuestatus (statusid,statusDesc)
select 0,'unprocessed' union
select 1,'Reserved' union
select 2,'processed'

set rowcount 100
insert queue (queuestatusid, processerid, updateDate)
select 0, 0, getdate()
from sysobjects
set rowcount 0

go

create procedure reserveQueueRow
@processerid int
,@queueid int output
as
set nocount on
--reserve a row
update queue set
processerid = @processerid
,queuestatusid = 1
,updateDate = getdate()
where queueID = (select max(queueID)
from queue
where queuestatusid = 0
and processerid = 0)
if @@rowcount = 0
return -1 --all rows processed

--get the id of the reserved row
select @queueID = queueID
from queue
where processerid = @processerid
and queuestatusid = 1

if @queueid is null
return -2 --problem

return 0 --successfull

go

create proc queueStatus_upd
@queueID int
,@queuestatusID int
as
set nocount on
update queue set
queuestatusid = 2 --processed
,updateDate = getdate()
where queueID = @queueID
go
--------------------------------------------------------------------
--in multiple QA windows:
while 1=1
begin
declare @qid int
,@rc int

exec @rc = reserveQueueRow @processerid = @@spid, @queueid = @qid output
if @rc = -1
begin
raiserror('all rows processed', 10,1)
break
end

if @qid is null
begin
raiserror ('some problem', 17,1)
return
end

exec queueStatus_upd @queueID = @qid, @queueStatusID = 2
--can also add id to another table if you want

waitfor delay '00:00:00.250'
end
--------------------------------------------------------------------
go
drop proc reserveQueueRow
drop proc queueStatus_upd
drop table queue
drop table queuestatus


Be One with the Optimizer
TG
Go to Top of Page

SamC
White Water Yakist

3467 Posts

Posted - 2005-11-12 : 13:41:53
quote:
Originally posted by zurbum

The problem is that if my store procedure is called from 2 different computers in the same time(not always, but happens), both receive same record, which should not happen.

I'm probably wrong (again), but I thought the solution I posted was atomic and would handle race conditions from multiple users.

Can you devise a test to show that it isn't? To help it break, you could declare a UNIQUE CONSTRAINT on the table:

SAVE(SAVE_ID int identity, TEST_ID int UNIQUE NOT NULL)

Sam
Go to Top of Page

TG
Master Smack Fu Yak Hacker

6065 Posts

Posted - 2005-11-12 : 15:37:15
>>I'm probably wrong (again),
I don't think you're wrong, Sam. Your solution would solve the collision and deadlocking issues.

However there's not that much control (the you have it currently). For instance, if all rows in Test exist in Save then max(test_id) will be NULL and the insert will still be attempted. I think if this is for controlling a call center, they will want a more robust solution that can handle all situations gracefully.

Be One with the Optimizer
TG
Go to Top of Page

zurbum
Yak Posting Veteran

55 Posts

Posted - 2005-11-14 : 02:15:10
SamC and TG thank you very much!!! TG your approach helped, also SamC is not wrong, it is just that I need to do a lot of IFs, checking different tables... so just INSERT with SELECT won't help in my case.
Go to Top of Page

zurbum
Yak Posting Veteran

55 Posts

Posted - 2005-11-19 : 19:01:57
Iam still having difficulties.
I was 100% that
INSERT INTO TABLE1 (A,B,C)
SELECT Something1, Something2, (SELECT TOP 1 Something3 FROM (SELECT...JOIN........WHERE don't exist in TABLE1)

is an atomic operation, but it is not. If I run the query at the same time from more connections, I still receive the same result. Any clues why?

In MS SQL exists atomic operations?
Go to Top of Page

SamC
White Water Yakist

3467 Posts

Posted - 2005-11-19 : 22:25:49
quote:
Originally posted by zurbum

INSERT INTO TABLE1 (A,B,C)
SELECT Something1, Something2, (SELECT TOP 1 Something3 FROM (SELECT...JOIN........WHERE don't exist in TABLE1)

Post your full query so there's no guessing on our part.

What result exactly are you getting?
Go to Top of Page

zurbum
Yak Posting Veteran

55 Posts

Posted - 2005-11-20 : 10:56:51
Here it is (simplified again)

CREATE TABLE [dbo].[CALLED] (
[CALLED_ID] [int] IDENTITY (1, 1) NOT NULL ,
[FORCALL_ID] [int] NOT NULL
) ON [PRIMARY]
GO

CREATE TABLE [dbo].[FORCALL] (
[FORCALL_ID] [int] NOT NULL ,
[FORCALL_NUMBER] [char] (10) NOT NULL
) ON [PRIMARY]
GO


INSERT INTO FORCALL(FORCALL_ID, FORCALL_NUMBER) VALUES (1,'111')
INSERT INTO FORCALL(FORCALL_ID, FORCALL_NUMBER) VALUES (2,'222')
INSERT INTO FORCALL(FORCALL_ID, FORCALL_NUMBER) VALUES (3,'333')
INSERT INTO FORCALL(FORCALL_ID, FORCALL_NUMBER) VALUES (4,'444')
INSERT INTO FORCALL(FORCALL_ID, FORCALL_NUMBER) VALUES (5,'555')
INSERT INTO FORCALL(FORCALL_ID, FORCALL_NUMBER) VALUES (6,'666')
INSERT INTO FORCALL(FORCALL_ID, FORCALL_NUMBER) VALUES (7,'777')
INSERT INTO FORCALL(FORCALL_ID, FORCALL_NUMBER) VALUES (8,'888')
INSERT INTO FORCALL(FORCALL_ID, FORCALL_NUMBER) VALUES (9,'999')
INSERT INTO FORCALL(FORCALL_ID, FORCALL_NUMBER) VALUES (10,'000')

Now create 3 or more connections with
WAITFOR TIME '12:50:00' --we want to start all at the same time

INSERT INTO CALLED (FORCALL_ID)
SELECT TOP 1 FORCALL_ID FROM FORCALL FC WHERE NOT EXISTS (SELECT FORCALL_ID FROM CALLED WHERE FORCALL_ID=FC.FORCALL_ID)

and start them in query analayzer and wait for time, so all will start at the same time.
I've receive results that 2/3 have the same FORCALL_ID which should not happen:
SELECT * FROM CALLED

CALLED_ID FORCALL_ID
1 1 (this is OK)
2 1 (this shouldn't happen !!!)
3 2 (this is OK)


This is the simplified query. In my query I have to join more tables,.... The problem is the same as in case above and that is the problem of receiving the same number to call for.
Go to Top of Page

TG
Master Smack Fu Yak Hacker

6065 Posts

Posted - 2005-11-21 : 12:36:22
I think the issue is that the "where not exists(subquery)" is not part of the transaction. try this with your same test:

INSERT INTO CALLED (FORCALL_ID)
SELECT TOP 1 fc.FORCALL_ID
FROM FORCALL FC
left join called ex
on ex.forcall_id = fc.forcall_id
where ex.forcall_id is null


Be One with the Optimizer
TG
Go to Top of Page

zurbum
Yak Posting Veteran

55 Posts

Posted - 2005-11-22 : 05:09:01
Tried

INSERT INTO CALLED (FORCALL_ID)
SELECT TOP 1 fc.FORCALL_ID
FROM FORCALL fc
left join CALLED ex
on ex.FORCALL_ID = fc.FORCALL_ID
where ex.CALLED_ID is null

without results.

Result is
CALLED_ID FORCALL_ID
1 1 (ok)
3 1 (not ok)
2 1 (not ok)
Go to Top of Page
   

- Advertisement -