2

Surprisingly can't find a match for my question. I have one table that I need to use a source of data to insert to another and then delete whatever has been inserted. This has to be accomplished in blocking manner, i.e. if the same query/SP is executed at the same time, the same records must not be moved thus creating duplicates.

I have a feeling it's something relatively simple but I am not sure I totally understand how locking works in SQL. It appears super trivial in C# (just a monitor) but SQL...

As an example, you may consider producer-consumer model, i.e. some sort of a table that serves as a queue, some threads insert into it some consume. Insertion apparently is not a problem, but consumption is what I am wondering about.

UPDATE: Two good candidate solutions for me:

  • use SELECT FOR UPDATE (need to figure out for how long row locks are hold)
  • use a field to mark records before manipulating on them

And still need to figure out that SERIALIZABLE IL thing...

Thank you everyone who took an effort and replied - this community is so great.

Schultz9999
  • 8,717
  • 8
  • 48
  • 87
  • The question is not very clear, can you give code example? So far I have understood that there are two tables (`source` and `target`) and records inserted into `source` have to be processed and then inserted in `target` and deleted from `source`. Is that correct? – Miserable Variable Oct 11 '11 at 05:29
  • I am sure Producer-Consumer pattern is understandable example. – Schultz9999 Oct 11 '11 at 05:35
  • The one part I am confused about is the duplicate insertion in source. Typically each message produced is consumed separately. In your case, duplicate concurrent insertions into source have to be ignored but if the 2nd insert comes after the 1st is deleted it will be processed is that correct? Or should it not be inserted into target? In the latter case, is it required that the row not be processed also? – Miserable Variable Oct 11 '11 at 05:41
  • Some insight on SERIALIZABLE http://www.informit.com/articles/article.aspx?p=327394&seqNum=2 – Schultz9999 Oct 11 '11 at 06:10
  • I assumed you looked at sql server queues? – Miserable Variable Oct 11 '11 at 06:13
  • 1
    @Schultz9999 - [Have you read this article?](http://rusanu.com/2010/03/26/using-tables-as-queues/) – Martin Smith Oct 11 '11 at 08:04
  • @MartinSmith: no. This is pretty cool. Thanks!! – Schultz9999 Oct 11 '11 at 17:44

4 Answers4

3

Edit 1: added one small note regarding ANSI_WARNINGS & ARITHABORT OFF.

If you use SQL Server 2008 (I see you have questions regarding this version) you could try composable DML.

Simple solution:

INSERT  Target
SELECT  q.Id, q.Name, q.Type
FROM
(
        DELETE  Source 
        OUTPUT  deleted.Id, deleted.Name, deleted.Type
        WHERE   Type = @Type --or another search condition 
) q;

Complex scenario (including errors):

1.First test case demonstrates this "technique".

2.The second test demonstrates the behavior when an error is encountered during statement execution: the statement (INSERT + DELETE OUTPUT) is canceled but the batch is still executed till last statement.

3.For the third test you can see that an error can abort the "entire" batch and the statement (INSERT + DELETE OUTPUT) is, also, canceled.

The behavior regarding errors is controlled in this script using three settings: ANSI_WARNINGS, ARITHABORT and XACT_ABORT. When both settings (ANSI_WARNINGS and ARITHABORT) are OFF then this expression 1/0 will be evaluated to NULL => so, will be INSERT ... NULL.

SET NOCOUNT ON;

CREATE TABLE dbo.Source (Id INT PRIMARY KEY, Name VARCHAR(10) NOT NULL, Type TINYINT NOT NULL);
INSERT  dbo.Source (Id, Name, Type) VALUES (1,'A',1), (2, 'B',1), (3, 'C',2), (4, 'D',2), (5, 'E',2);

CREATE TABLE dbo.Target (Id INT PRIMARY KEY, Name VARCHAR(10) NOT NULL, Type TINYINT /*NOT*/ NULL);


    --***** Test 1 Ok *****
        DECLARE @Type INT = 1;

        SELECT  'Test 1 Ok' AS Description;
        BEGIN TRAN;
        INSERT  Target
        SELECT  q.Id, q.Name, q.Type
        FROM
        (
                DELETE  Source 
                OUTPUT  deleted.Id, deleted.Name, deleted.Type
                WHERE   Type = @Type
        ) q;

        SELECT  * FROM Target;
        SELECT  * FROM Source;
        --It will be fine to COMMIT transaction but I will cancel to run the second and third test
        ROLLBACK TRAN 
        SELECT  'End of Test 1 Ok' AS Description;
        GO
    --***** End of Test 1 *****

    --***** Test 2 Err *****
        --Start another batch
        GO 
        SET ARITHABORT ON;
        SET ANSI_WARNINGS ON;
        SET XACT_ABORT OFF;

        DECLARE @Type INT = 1;

        SELECT  'Test 2 Err' AS Description, SESSIONPROPERTY('ARITHABORT') [ARITHABORT_STATUS], SESSIONPROPERTY('ANSI_WARNINGS') [ANSI_WARNINGS_STATUS];

        INSERT  Target
        --Divide by zero => Abort statement only
        SELECT  q.Id, q.Name, CASE WHEN q.Id <> 2 THEN q.Type ELSE  1/0 END 
        FROM
        (
                DELETE  Source 
                OUTPUT  deleted.Id, deleted.Name, deleted.Type
                WHERE   Type = @Type
        ) q;

        SELECT  * FROM Target;
        SELECT  * FROM Source;
        SELECT  'End of Test 2 Err' AS Description;
    --***** End of Test 2 *****

    --***** Test 3 *****
        --Start another batch
        GO 
        SET ANSI_WARNINGS OFF;
        SET ARITHABORT ON;
        SET XACT_ABORT OFF;

        DECLARE @Type INT = 1;

        SELECT  'Test 3 Err' AS Description, SESSIONPROPERTY('ARITHABORT') [ARITHABORT_STATUS], SESSIONPROPERTY('ANSI_WARNINGS') [ANSI_WARNINGS_STATUS];

        INSERT  Target
        --Divide by zero => Abort batch
        SELECT  q.Id, q.Name, CASE WHEN q.Id <> 2 THEN q.Type ELSE  1/0 END
        FROM
        (
                DELETE  Source 
                OUTPUT  deleted.Id, deleted.Name, deleted.Type
                WHERE   Type = @Type
        ) q

        --This statement is not executed 
        SELECT  * , 1 AS Statement  FROM Target;
        --This statement is not executed 
        SELECT  * , 1 AS Statement FROM Source;
        --This statement is not executed 
        SELECT  'End of Test 3 Err' AS Description

        GO --Start another batch    
        SELECT  * , 2 AS Statement FROM Target;
        SELECT  * , 2 AS Statement FROM Source;
    --***** End of Test 3 *****

    DROP TABLE dbo.Source;
    DROP TABLE dbo.Target;
Bogdan Sahlean
  • 19,233
  • 3
  • 42
  • 57
1

some suggestions...

  1. make sure your tables have proper keys or unique contraints such that duplicates can not be inserted

  2. I would use a stored procedure to do the bulk move (insert + delete) within a begin/commit transaction. I would also make sure the rows that are selected to move is done so with row level locking. (This could however have a performance impact if these tables have a lot of select requests).

  3. alternatively, you could actually lock on the C# code that invokes this action, blocking to make sure that no to user can enter the invoke the method at the same time.

mike01010
  • 5,226
  • 6
  • 44
  • 77
  • C# locking won't work in case of multiple machines. As for SQL, none of the transaction isolation levels allow blocking. I am still trying to understand what SERIALIZABLE means though but at the moment it appears SQL Server never blocks. It's just what data can be seen from one transaction or another. SERIALIZABLE though could be something different... – Schultz9999 Oct 11 '11 at 05:25
  • @Schultz9999 you are right...i wasn't sure if this was server side code or client side code (and if serverside, single instance, etc.) – mike01010 Oct 11 '11 at 05:27
  • HOLDLOCK is what I am currently looking at. They say it's similar to SERIALIZABLE IL, which I am lacking understanding of. – Schultz9999 Oct 11 '11 at 05:34
  • ok. generally i don't like locking at all to be honest. too much performance issues down the road as the db gets hit more and more. you might want to consider a different approach such as "marking" rows for delete. essentially marked' rows are never selected, and therefore you can delete them at your leisure without having to worry about much locking issues. – mike01010 Oct 11 '11 at 05:41
  • actually I think you have a good point and I like that too, mike01010. Assuming a READCOMMITED IL, updating the status field is atomic and preceding or following SELECTs should just take that flag into account. It will work for my case. Thanks! – Schultz9999 Oct 11 '11 at 05:48
1

Use select for update to lock rows from the source table, copy those rows to the destination table and then delete those. Another thread that executes the same logic will wait at the select for update call.

gkamal
  • 20,777
  • 4
  • 60
  • 57
  • hmm... that's an interesting suggestion. I'll take a look at it. In meantime, I have also found this http://sqlblog.com/blogs/merrill_aldrich/archive/2011/08/17/handy-trick-move-rows-in-one-statement.aspx. It also looks interesting however FOR UPDATE appears to be a longer lasting lock (I assume to the end of the current transaction) while OUTPUT allows execution of a couple SQL operations atomically. – Schultz9999 Oct 11 '11 at 05:37
  • FYI about FOR SELECT (some experience deadlocks): http://stackoverflow.com/questions/1483725/select-for-update-with-sql-server – Schultz9999 Oct 11 '11 at 05:44
  • mike01010 gave me a good clue but I still like your post. So +1. – Schultz9999 Oct 11 '11 at 05:50
0

If you use SQL Server 2008 you can use MERGE

Arian
  • 12,793
  • 66
  • 176
  • 300
  • MERGE statement can INSERT, UPDATE, DELETE records only from target table. So, the rows cannot be deleted from source table. – Bogdan Sahlean Oct 11 '11 at 08:27