views:

1293

answers:

8

I'm writing a process that archives rows from a SQL Server table based on a datetime column. I want to move all the rows with a date before X, but the problem is that there are millions of rows for each date, so doing a BEGIN TRANSACTION...INSERT...DELETE...COMMIT for each date takes too long, and locks up the database for other users.

Is there a way that I can do it in smaller chunks? Maybe using ROWCOUNT or something like that?

I'd originally considered something like this:

SET ROWCOUNT 1000

DECLARE @RowsLeft DATETIME
DECLARE @ArchiveDate DATETIME

SET @ROWSLEFT = (SELECT TOP 1 dtcol FROM Events WHERE dtcol <= @ArchiveDate)

WHILE @ROWSLEFT IS NOT NULL
BEGIN

    INSERT INTO EventsBackups
    SELECT top 1000 * FROM Events

    DELETE Events

    SET @ROWSLEFT = (SELECT TOP 1 dtcol FROM Events WHERE dtcol <= @ArchiveDate)

END

But then I realized that I can't guarantee that the rows I'm deleting are the ones I just backed up. Or can I...?

UPDATE: Another options I'd considered was adding a step:

  1. SELECT TOP 1000 rows that meet my date criteria into a temp table
  2. Begin Transaction
  3. Insert from temp table into archive table
  4. Delete from source table, joining to temp table across every column
  5. Commit transaction
  6. Repeat 1-5 until no rows remain that meet the date criteria

Does anybody have an idea for how the expense of this series might compare to some of the other options discussed below?

DETAIL: I'm using SQL 2005, since somebody asked.

A: 

How about:

INSERT INTO EventsBackups
SELECT TOP 1000 * FROM Events ORDER BY YourKeyField

DELETE Events
WHERE YourKeyField IN (SELECT TOP 1000 YourKeyField FROM Events ORDER BY YourKeyField)
Aaron Alton
As an aside, this is a perfect case for sliding window partitioning, if you're able to make use of it: http://weblogs.sqlteam.com/dang/archive/2008/08/30/Sliding-Window-Table-Partitioning.aspxIt's a metadata switch, so the entire load could be done in a few seconds at most.
Aaron Alton
A: 

How about don't do it all at once?

INSERT INTO EventsBackups
SELECT * FROM Events WHERE date criteria

Then later,

DELETE FROM Events
SELECT * FROM Events INNER JOIN EventsBackup on Events.ID = EventsBackup.ID

or the equivalent.

Nothing you've said so far suggests you need a transaction.

John Saunders
It's too resource intensive to do a massive insert like that in a very active table. It needs to be "chunked" to prevent large resource waits.
Aaron Alton
But it's the Backup table that will be locked, not the Events table. Therefore is locking a problem? Then later you can perform the deletes in chunks if they're in your backup.
Robin Day
I'm using the transaction so I can rollback the insert if the delete fails. I don't want to have any records appear in the archive table that are still in the live table, since that could lead to duplicates later.I'm actually attempting to work around an application's incredibly cumbersome internal archive process, which was never meant to deal with as much data as we have, and I want to avoid anything that could possibly break it.
rwmnau
A: 

Have you got an index on the datefield? If you haven't sql may be forced to upgrade to a table lock which will lock out all your users while your archive statements run.

I think you will need an index for this operation to perform at all well! Put an index on your date field and try your operation again!

Noel Kennedy
I am using SQL 2005, and there are no indexes on the table at all, which makes the SELECT statements expensive to begin with.
rwmnau
A: 

Could you make a copy of Events, move all rows with dates >= x to that, drop Events and rename the copy Events? Or copy, truncate and then copy back? If you can afford a little downtime this would probably be the quickest approach.

John M Gant
+2  A: 

use a INSERT with an OUTPUT INTO clause to store the IDs of the inserted rows, then DELETE joining to this temp table to remove only those IDs

DECLARE @TempTable (YourKeyValue KeyDatatype not null)

INSERT INTO EventsBackups
    (columns1,column2, column3)
    OUTPUT INSERTED.primaryKeyValue
    INTO @TempTable
    SELECT
        top 1000
        columns1,column2, column3
        FROM Events

DELETE Events
    FROM Events
        INNER JOIN @TempTable  t ON Events.PrimaryKey=t.YourKeyValue
KM
I like this solution. Note that your final join will be:ON Events.PrimaryKey = t.primaryKeyValuerather thanON Events.PrimaryKey = t.YourKeyValueJust to keep the example consistent ;-)
Aaron Alton
@Aaron Alton, t.YourKeyValue comes from my @tempTable, which I define in my code, there is no @TempTable .primaryKeyValue. The OUTPUT INSERTED.primaryKeyValue needs changed to be INSERTED.his key value.
KM
I really like this solution as well, except that there's no column that's a key. There can be repeat rows in the table with the same timestamp :(I really like this, though, and it's worth an upvote.
rwmnau
if there is no primary key, add one, make it an IDENTITY.
KM
See my post bellow. OUTPUT clause is a good hint, but use it on the DELETE to return the deleted rows straight into the INSERT.Also see my blog about this: http://rusanu.com/2008/04/09/chained-updates/
Remus Rusanu
A: 

Here's what I ended up doing:

SET @CleanseFilter = @startdate
WHILE @CleanseFilter IS NOT NULL
BEGIN
 BEGIN TRANSACTION

  INSERT INTO ArchiveDatabase.dbo.MyTable
  SELECT *
    FROM dbo.MyTable
   WHERE startTime BETWEEN @startdate AND @CleanseFilter

  DELETE dbo.MyTable
   WHERE startTime BETWEEN @startdate AND @CleanseFilter

 COMMIT TRANSACTION

 SET @CleanseFilter = (SELECT MAX(starttime)
    FROM (SELECT TOP 1000
                 starttime
     FROM dbo.MyTable
           WHERE startTime BETWEEN @startdate AND @enddate
        ORDER BY starttime) a)
END

I'm not pulling exactly 1000, just 1000ish, so it handles repeats in the time column appropriately (something I worried about when I considered using ROWCOUNT). Since there are often repeats in the time column, I see it regularly move 1002 or 1004 rows/iteration, so I know it's getting everything.

I'm submitting this as an answer so it can be judged up against the other solutions people have provided. Let me know if there's something obviously wrong with this method. Thanks for your help, everybody, and I'll accept whichever answer has the most votes in a few days.

rwmnau
if you have no key, and don't want to add one, use my answer, but change it up. do the delete with the OUTPUT INTO, just capture all the columns into the temp table and then insert from a select of that temp table.
KM
You need to be very careful with the SQL you have posted. If you don't use SERIALIZABLE transaction isolation, your DELETE will not be guaranteed to only be deleting the rows your SELECT chose. Look up non-repeatable reads, and phantom reads. If you do go with the SQL you posted, the only way for SERIALIZABLE to be guaranteed by SQL server (without an index on the date column) is to table lock, which will kill performance like it has never been killed before!
Noel Kennedy
cant edit comments*for Serializable isolation level to be executed
Noel Kennedy
A: 

Another option would be to add a trigger procedure to the Events table that does nothing but add the same record to the EventsBackup table.

That way the EventsBackup is always up to date, and all you do is periodically purge records from your Events table.

Ron

Ron Savage
+2  A: 

Just INSERT the result of the DELETE:

WHILE 1=1
BEGIN

    WITH EventsTop1000 AS (
    SELECT TOP 1000 * 
     FROM Events
      WHERE <yourconditionofchoice>)
    DELETE EventsTop1000
     OUTPUT DELETED.* 
     INTO EventsBackup;

    IF (@@ROWCOUNT = 0)
     BREAK;
END

This is atomic and consistent.

Remus Rusanu