How to Effectively “Back Up” All Deleted Rows When Using DELETE in SQL Server

Deleting rows in a SQL database can sometimes be a nerve-racking experience. What if you’re deleting the wrong rows? Or what if the business later tells you they want their data back?

Fortunately SQL Server provides us with an easy way to essentially “back up” any rows affected by a DELETE operation to a table.

This article looks at using the OUTPUT ... INTO clause to save a copy of deleted rows to another table.

Example

Let’s create a table and add data. We’ll also create a logging table that we’ll use to back up any deleted rows to. Finally we’ll go ahead and delete some rows from the original table and then check the contents of the backup table.

Step 1: Create Original Table and Data

Before deleting any data, we first need data. So here we create a small table and populate it with a small amount of data (we don’t need a lot of data to demonstrate this technique):

CREATE TABLE Orders (
    OrderID INT PRIMARY KEY,
    CustomerID INT,
    OrderDate DATE
);

INSERT INTO Orders (OrderID, CustomerID, OrderDate)
VALUES (1, 1001, '2023-01-17'),
       (2, 1002, '2023-02-11'),
       (3, 1003, '2023-03-05'),
       (4, 1004, '2023-04-20'),
       (5, 1005, '2023-05-15');

Step 2: Create the Backup Table

This will be the table that contains a back up of the deleted rows:

CREATE TABLE DeletedOrdersBackup (
    OrderID INT,
    CustomerID INT,
    OrderDate DATE,
    DeletedOn DATETIME DEFAULT GETDATE()
);

Now let’s go through and delete some of those records.

Step 3: Delete Some Rows

Now to the main point of this article – delete some rows and save a back up of the deleted rows:

-- Delete the row/s
DELETE FROM Orders
OUTPUT 
    DELETED.OrderID, 
    DELETED.CustomerID, 
    DELETED.OrderDate,
    GETDATE() 
    INTO DeletedOrdersBackup (
        OrderID, 
        CustomerID, 
        OrderDate, 
        DeletedOn
        )
WHERE OrderDate < '2023-03-01';

-- Return the deleted row/s from the backup table
SELECT * FROM DeletedOrdersBackup;

Output:

OrderID  CustomerID  OrderDate                 DeletedOn               
------- ---------- ------------------------ ------------------------
1 1001 2023-01-17T00:00:00.000Z 2024-09-20T19:58:50.473Z
2 1002 2023-02-11T00:00:00.000Z 2024-09-20T19:58:50.473Z

We can see that two rows were deleted and backed up to our backup table.

Now we can easily restore data back to the original table if required. We can use the DeletedOn column to find relevant data if required (for example, if we find out that someone accidentally deleted the wrong data on a certain date/time).

Note that the backup table can’t:

A Cautious Alternative

If you’re extra cautious and decide that you want to backup deleted rows before you do the delete, you can write a SELECT statement that returns all rows to be deleted and use it to insert data into the backup table. Once you’ve confirmed that the correct data is in the backup table, then you can switch the SELECT statement over to a DELETE statement and carry out the delete operation.

Example:

-- Do the insert
INSERT INTO DeletedOrdersBackup (
        OrderID, 
        CustomerID, 
        OrderDate, 
        DeletedOn    
)
SELECT
        OrderID, 
        CustomerID, 
        OrderDate, 
        GETDATE()
FROM Orders
WHERE OrderDate < '2023-04-01';

-- Check the backup table
SELECT * FROM DeletedOrdersBackup;

Output:

OrderID  CustomerID  OrderDate                 DeletedOn               
------- ---------- ------------------------ ------------------------
1 1001 2023-01-17T00:00:00.000Z 2024-09-20T19:58:50.473Z
2 1002 2023-02-11T00:00:00.000Z 2024-09-20T19:58:50.473Z
3 1003 2023-03-05T00:00:00.000Z 2024-09-20T20:15:48.500Z

Bear in mind that this populates the backup table and provides a DeletedOn date/time before the data is actually deleted. So you might want to design it in such a way that conveys this. Or, if you’re really paranoid, you could have one table for all data identified for deletion, and another table for all data that was actually deleted.