Data Points

Disabling Constraints and Triggers

John Papa

Code download available at:  Data Points 2007_04.exe(153 KB)

Contents

Disabling Foreign Keys
Disabling Triggers
Synchronizing an Offline/Mobile Database
Cursors and Information Schema Views
Disabling All Foreign Keys
Disabling All Triggers
Disabling All Triggers Quickly
Wrapping Up

Constraints are valuable tools for maintaining data integrity in databases. However, there are times when it is convenient to disable one or more of them to perform tasks such as data synchronization with an offline database. When SQL Server™ replication is used to synchronize data between databases, individual objects can be told to withhold enforcement during replication.

For example, when a foreign key constraint is defined using the NOT FOR REPLICATION statement, SQL Server will not enforce the constraint during the replication process. In fact, the NOT FOR REPLICATION statement can be used directly in T-SQL statements that define foreign key constraints, check constraints, identities, and triggers. For those who use SQL Server replication, using the NOT FOR REPLICATION statement on the appropriate objects is a good option. However, if you are instead performing a manual synchronization of your data, another option is to disable the constraints and triggers manually.

Manual synchronization is often performed in online/offline applications where there is a subset of the data to be synchronized and where greater control over how the data is synchronized is required. In this month's column, I will discuss when it might be advantageous to manually disable and enable constraints, what kinds of problems this can solve, and some tricks to help you out.

Disabling Foreign Keys

I do not advocate removing foreign key constraints from a relational database. However, there are times when you may need to temporarily relieve the referential integrity checks of one or more foreign keys, such as when you're performing a large number of inserts and updates on a series of tables and want more accurate results and better performance. Of course, you should only use this technique when it is impractical to perform massive amounts of data updates to an entire database in the proper relational order.

So when might it be a good idea to disable foreign key constraints? Imagine you have a relational database structure that has dozens of tables, all of which are related to each other in some way through foreign key constraints. The application that interacts with this database has an offline/mobile version that communicates with a second instance of the database that likely resides on a laptop. Data changes that are made to the main database might need to be synchronized with the offline/mobile database, and there are several ways to handle this.

One way to synchronize the data is to apply the inserts, updates, and deletes to the offline/mobile database in the order that adheres to the relational structure. For example, insert customers before their respective orders, and orders before their order details. Record deletion would be performed in the opposite direction (from child to parent). This technique, however, can become too complex to implement and maintain on large database structures.

Another technique is to drop the foreign key constraints, synchronize the data, and then recreate the foreign key constraints. While this approach can work, a less dramatic approach is to merely disable the foreign key constraints. Once that's done, the data can be synchronized and then the foreign keys can be enabled once again. The syntax to disable a foreign key is shown here:

-- Disable foreign key constraint
ALTER TABLE Orders
    NOCHECK CONSTRAINT 
        FK_Orders_Customers

-- Add a new Order record for a non-existent customer
INSERT INTO Orders (CustomerID) VALUES ('BLAH')

-- Select all orders for the non-existent customer
SELECT * FROM Orders WHERE CustomerID = 'BLAH'

This foreign key enforces that the CustomerID in the orders table be a valid CustomerID from the Customers table. The code disables the foreign key and then inserts an order into the Orders table. The order record that is inserted has a CustomerID that does not exist in the parent Customers table. Because the foreign key is disabled, the integrity check is bypassed and the order record is inserted successfully.

The following code demonstrates how to re-enable the foreign key constraint and then test that it is working. When this code is executed, the order is not inserted because the constraint is enforced. An error message will be returned stating that the insert statement conflicts with the foreign key constraint.

-- Enable foreign key constraint
ALTER TABLE Orders
    CHECK CONSTRAINT 
        FK_Orders_Customers

-- Add a new Order record for a non-existent customer
INSERT INTO Orders (CustomerID) VALUES ('BLEH')

When disabling foreign keys, triggers, and other constraints, it is important to make sure that no Data Manipulation Language (DML) operations are being performed on the database during that time period. This must be handled when performing the disabling options manually. If you are instead synchronizing data with SQL Server replication and the NOT FOR REPLICATION statement, this condition is handled automatically.

Disabling Triggers

It's also possible to disable a trigger when necessary. Sometimes when synchronizing data you do not want a trigger to fire. For instance, if you are updating an offline/mobile database (as in the previous example), there may be triggers on several tables that you may not be aware of. These triggers may fire when a row is inserted into their table causing the row to be reinserted into another table. But during a large data synchronization, you might not want those types of inserts to be reinserted. To avoid that eventuality, you can temporarily disable the trigger, like so:

DISABLE TRIGGER MyTriggerName ON MyTableName

Conversely, to enable it, do this:

ENABLE TRIGGER MyTriggerName ON MyTableName

The following code shows a trigger that will fire (and display a message) when one or more rows are inserted or updated in the Region table.

CREATE TRIGGER trRegion_InsertUpdate ON Region
    FOR INSERT, UPDATE
AS
    PRINT ' Trigger is running. ' 
          +  CAST(@@ROWCOUNT AS VARCHAR(10)) 
          + ' row(s) affected.'
GO

You can test this trigger by inserting a new region into the Region table, like so:

INSERT INTO Region (RegionID, RegionDescription) 
VALUES (5, 'Some New Region')

When this code is executed in a query window, the new region is inserted into the Region table, the trigger fires, and a message is displayed in the messages pane. To disable the trigger, you can execute the following code:

DISABLE TRIGGER trRegion_InsertUpdate ON Region

If you execute this code to update the new region, the data is updated but the trigger will not fire:

UPDATE Region SET RegionDescription = 'A Great Region' 
WHERE RegionID = 5

To re-enable the trigger, simply execute the following query:

ENABLE TRIGGER trRegion_InsertUpdate ON Region

Synchronizing an Offline/Mobile Database

Disabling a single trigger or foreign key has its uses, but disabling all of them in one fell swoop can be extremely useful in the offline/mobile database synchronization scenario I described. To do so, you would take the following steps: disable all foreign key constraints, disable all triggers, perform the insert, update and delete operations, enable all foreign key constraints, and finally re-enable all triggers.

By disabling the triggers and foreign key constraints, the order in which the data is modified becomes inconsequential. In a small database like Northwind, this is a minor savings; in a large database with dozens or hundreds of tables and relationships, however, this can be a huge time saver. In addition, without this technique you would have to modify the sequence of the tables in your script to insert/update/delete the data every time the database schema adds new tables and relationships. With the foreign key constraints and triggers disabled, you can just add the tables to the end of your script since order is not important.

It is critical to remember to enable the triggers and foreign keys at the end, regardless of whether the script was successful or not. For example, if your script disables the constraints and triggers and attempts to modify the data, and the attempt fails, you would still want the constraints and triggers to be enabled at the end.

Cursors and Information Schema Views

To disable all foreign keys, you must first gather a list of them and the tables to which each is applied. Here, Cursors and INFORMATION_SCHEMA views can assist you. The view named INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS will return a list of the foreign keys in the current database. To get the name of the table that the foreign key is applied to, use the view called INFORMATION_SCHEMA.TABLE_CONSTRAINTS. The following code joins these two views to return a list of all of the foreign keys and their respective tables.

SELECT
  ref.constraint_name AS FK_Name,
  fk.table_name AS FK_Table
FROM
  INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS ref
  INNER JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS fk 
  ON ref.constraint_name = fk.constraint_name
ORDER BY
  fk.table_name,
  ref.constraint_name 

Disabling All Foreign Keys

Now that you have your list, you can write a script to disable the foreign keys. You can declare and open a cursor, iterate through the list, create the T-SQL command that will disable a foreign key, and execute it for each foreign key (see Figure 1).

Figure 1 Disabling and Enabling All Foreign Keys

CREATE PROCEDURE pr_Disable_Triggers_v2 
    @disable BIT = 1
AS 
    DECLARE
        @sql VARCHAR(500),
        @tableName VARCHAR(128),
        @tableSchema VARCHAR(128)

    -- List of all tables
    DECLARE triggerCursor CURSOR
        FOR
    SELECT
        t.TABLE_NAME AS TableName,
        t.TABLE_SCHEMA AS TableSchema
    FROM
        INFORMATION_SCHEMA.TABLES t
    ORDER BY
        t.TABLE_NAME,
        t.TABLE_SCHEMA 

    OPEN triggerCursor

    FETCH NEXT FROM triggerCursor 
    INTO @tableName, @tableSchema

    WHILE ( @@FETCH_STATUS = 0 )
        BEGIN
            IF @disable = 1 
                SET @sql = ‘ALTER TABLE ‘ + @tableSchema 
                    + ‘.[‘ + @tableName + ‘] DISABLE TRIGGER ALL’ 
            ELSE 
                SET @sql = ‘ALTER TABLE ‘ + @tableSchema 
                    + ‘.[‘ + @tableName + ‘] ENABLE TRIGGER ALL’ 

            PRINT ‘Executing Statement - ‘ + @sql

            EXECUTE ( @sql )
            FETCH NEXT FROM triggerCursor
            INTO @tableName, @tableSchema
        END

    CLOSE triggerCursor
    DEALLOCATE triggerCursor

First, the foreignKeyCursor cursor is declared as the SELECT statement that gathers the list of foreign keys and their table names. Next, the cursor is opened and the initial FETCH statement is executed. This FETCH statement will read the first row's data into the local variables @foreignKeyName and @tableName.

When looping through a cursor, you can check the @@FETCH_STATUS for a value of 0, which indicates that the fetch was successful. This means the loop will continue to move forward so it can get each successive foreign key from the rowset.

@@FETCH_STATUS is available to all cursors on the connection. So if you are looping through multiple cursors, it is important to check the value of @@FETCH_STATUS in the statement immediately following the FETCH statement. @@FETCH_STATUS will reflect the status for the most recent FETCH operation on the connection. Valid values for @@FETCH_STATUS are:

  • 0 = FETCH was successful
  • -1 = FETCH was unsuccessful
  • -2 = the row that was fetched is missing

Inside the loop, the code builds the ALTER TABLE command differently depending on whether the intention is to disable or enable the foreign key constraint (using the CHECK or NOCHECK keyword). The statement is then printed as a message so its progress can be observed and then the statement is executed. Finally, when all rows have been iterated through, the stored procedure closes and deallocates the cursor.

Disabling All Triggers

The stored procedure in Figure 1 will disable or enable all foreign keys in a database. In some cases, you will want to disable all triggers during data synchronization, as well. The pr_Disable_Triggers stored procedure in Figure 2 does just that.

Figure 2 Disabling and Enabling All Triggers

CREATE PROCEDURE pr_Disable_Triggers 
@disable BIT = 1
AS 
    DECLARE
        @sql VARCHAR(500),
        @tableName VARCHAR(128),
        @triggerName VARCHAR(128),
        @tableSchema VARCHAR(128)

    -- List of all triggers and tables that exist on them
    DECLARE triggerCursor CURSOR
        FOR
    SELECT
        so_tr.name AS TriggerName,
        so_tbl.name AS TableName,
        t.TABLE_SCHEMA AS TableSchema
    FROM
        sysobjects so_tr
    INNER JOIN sysobjects so_tbl ON so_tr.parent_obj = so_tbl.id
    INNER JOIN INFORMATION_SCHEMA.TABLES t 
    ON 
        t.TABLE_NAME = so_tbl.name
    WHERE
        so_tr.type = ‘TR’
    ORDER BY
        so_tbl.name ASC,
        so_tr.name ASC

    OPEN triggerCursor

    FETCH NEXT FROM triggerCursor 
    INTO @triggerName, @tableName, @tableSchema

    WHILE ( @@FETCH_STATUS = 0 )
        BEGIN
            IF @disable = 1 
                SET @sql = ‘DISABLE TRIGGER [‘ 
                    + @triggerName + ‘] ON ‘ 
                    + @tableSchema + ‘.[‘ + @tableName + ‘]’
            ELSE 
                SET @sql = ‘ENABLE TRIGGER [‘ 
                    + @triggerName + ‘] ON ‘ 
                    + @tableSchema + ‘.[‘ + @tableName + ‘]’

            PRINT ‘Executing Statement - ‘ + @sql
            EXECUTE ( @sql )
            FETCH NEXT FROM triggerCursor 
            INTO @triggerName, @tableName,  @tableSchema
        END

    CLOSE triggerCursor
    DEALLOCATE triggerCursor

The pr_Disable_Triggers stored procedure gets a rowset containing all of triggers in the current database as well as the names of their respective tables (and their schema). Since there is no INFORMATION_SCHEMA.TRIGGERS view, I gather the list of triggers and the related information from a combination of the SQL Server 2005 system tables and INFORMATION_SCHEMA views, where possible.

The sysobjects system table exists in all databases and can be queried to return a list of all triggers or tables. This system table can be joined against itself to get a list of the triggers and the tables that they operate on, as shown in the declaration of the cursor in Figure 2.

If you use this stored procedure on a database such as AdventureWorks where the tables are under specific schemas, you must prefix the name of the table with the schema name. The pr_Disable_Triggers stored procedure handles this situation by also joining to the INFORMATION_SCHEMA.TABLES view, which returns the SCHEMA_NAME for the table.

Once you have written routines that will disable and re-enable the triggers and foreign keys, you can use them in a script that will modify an offline/mobile database to keep it synchronized with a main database. For example, the following script could be used, replacing the comment in the middle with the data manipulation operations.

pr_Disable_Foreign_Keys  1
pr_Disable_Triggers 1
-- Perform data operations
pr_Disable_Foreign_Keys  0
pr_Disable_Triggers 0

Disabling All Triggers Quickly

The pr_Disable_Triggers stored procedure shown in Figure 2 could be modified to execute a slightly different T-SQL command that will disable or enable all of the triggers. There is a T-SQL statement using a flavor of the ALTER TABLE command that will disable or enable all triggers on a table. Using this technique, the pr_Disable_Triggers stored procedure can be modified to execute the following statement for each table to disable all of the triggers on that table:

ALTER TABLE MySchemaName.MyTableName DISABLE TRIGGER ALL

By using this syntax, the cursor's query would gather all of the table names but not need to get the names of the triggers themselves. Thus, the stored procedure could be modified to use this technique to loop through the list of tables and enable or disable all triggers on each table. The modified stored procedure is shown in Figure 3.

Figure 3 Disabling All Triggers with the ALTER TABLE Command

CREATE PROCEDURE pr_Disable_Foreign_Keys
    @disable BIT = 1
AS
    DECLARE
        @sql VARCHAR(500),
        @tableName VARCHAR(128),
        @foreignKeyName VARCHAR(128)

    -- A list of all foreign keys and table names
    DECLARE foreignKeyCursor CURSOR
    FOR SELECT
        ref.constraint_name AS FK_Name,
        fk.table_name AS FK_Table
    FROM
        INFORMATION_SCHEMA.REFERENTIAL_CONSTRAINTS ref
        INNER JOIN INFORMATION_SCHEMA.TABLE_CONSTRAINTS fk 
    ON ref.constraint_name = fk.constraint_name
    ORDER BY
        fk.table_name,
        ref.constraint_name 

    OPEN foreignKeyCursor

    FETCH NEXT FROM foreignKeyCursor 
    INTO @foreignKeyName, @tableName

    WHILE ( @@FETCH_STATUS = 0 )
        BEGIN
            IF @disable = 1
                SET @sql = ‘ALTER TABLE [‘ 
                    + @tableName + ‘] NOCHECK CONSTRAINT [‘ 
                    + @foreignKeyName + ‘]’
            ELSE
                SET @sql = ‘ALTER TABLE [‘ 
                    + @tableName + ‘] CHECK CONSTRAINT [‘ 
                    + @foreignKeyName + ‘]’

        PRINT ‘Executing Statement - ‘ + @sql

        EXECUTE(@sql)
        FETCH NEXT FROM foreignKeyCursor 
        INTO @foreignKeyName, @tableName
    END

    CLOSE foreignKeyCursor
    DEALLOCATE foreignKeyCursor

Wrapping Up

It is more efficient to perform multiple database operations such as those described in this article from a T-SQL script than it is to execute them all individually from a .NET application. For example, you could gather a list of foreign keys from a .NET application using ADO.NET and then execute the commands to disable each of the foreign keys. This could be extended to get the list of triggers and then disable them. All of these actions would require going back and forth between the .NET code and the database, consuming more resources than if the code were executed entirely on the database server in T-SQL.

Send your questions and comments for John to  mmdata@microsoft.com.

John Papa is a senior .NET Consultant with ASPSOFT (aspsoft.com) and a baseball fanatic who spends most of his summer nights rooting for the Yankees with his family and his faithful dog, Kadi. John, a C# MVP, has authored several books on ADO, XML, and SQL Server, and can often be found speaking at industry conferences such as VSLive.