solving sql server problems for millions of dbas and developers since 2006



SQL Server DBA Tips SQL Server Developer Tips SQL Server Business Intelligence Tips SQL Server Career Tips SQL Server Tip Categories SQL Server Tutorials SQL Server Webcasts SQL Server Whitepapers SQL Server Tools SQL Server Questions and Answers MSSQLTips Authors About MSSQLTips SQL Server User Groups MSSLQTips Giveaways MSSQLTips Advertising Options

MSSQLTips Facebook Page MSSQLTips LinkedIn Page MSSQLTips RSS Feed MSSQLTips Twitter Page MSSQLTips Google+ Page

SQL Product Highlight

Idera - SQL safe backup

Idera's SQL safe provides a high-performance backup and recovery solution for Microsoft SQL Server. SQL safe saves money by reducing database backup time by up to 50% over native backups and reducing backup disk space requirements by up to 95%. SQL safe also enables complete "hands-free" automated backup of your entire SQL Server infrastructure and ensures compliance with your organization's backup and recovery policies. From implementations with tens of SQL servers to enterprises with hundreds of servers spread around the globe, SQL safe is the only SQL Server backup and recovery solution that scales to meet the challenge.

Learn more!








Foreign Key vs. Trigger Referential Integrity in SQL Server

By: | Read Comments (7) | Print

Armando has over 24 years of industry experience and has been working with SQL Server since version 6.5.

Related Tips: 1 | 2 | 3 | 4 | 5 | 6 | More

Problem
I'm designing a table and I want to enforce a relationship between two tables (a parent table and a child table). Would I be better off just writing triggers to enforce the relationship or should I declare a foreign key constraint?  What are the pros and cons of each  approach?  Can you provide some sample code to show the various issues?

Solution
I have come across a lot of database models where the original developer had enforced basic referential integrity (RI) using triggers rather than declaratively via foreign keys. In versions prior to SQL Server 2000, triggers were one way to implement cascading deletes and updates of child tables since SQL Server versions prior to 2000 did not have this feature. However, SQL Server 2000 introduced cascading actions to foreign keys (ON DELETE CASCADE, ON UPDATE CASCADE) rendering the use of triggers for RI largely unnecessary except under specialized circumstances.

The following table examines some of the considerations in using either method:

    Enforcing RI: Foreign Keys vs Triggers
    Foreign Keys Triggers
    Checked before the data modification is made Executed after the data modification has been made
    Do not extend transactions Extend the life of a transaction as it executes code to check RI (can be extended even longer if an integrity violation is encountered which requires ROLLBACK)
    Can be added with an ALTER TABLE statement; No special coding required Coding is required
    Prevent TRUNCATE of a parent table Do not prevent TRUNCATE of a parent table; DELETE triggers are not fired when a TRUNCATE is issued
    Table relationships can be easily seen in data model diagrams Table relationships are not readily seen in data model diagrams
     

    In the following example, we'll examine some of the drawbacks of using triggers to enforce data integrity. The following example data model has a parent table called saved_report and child table called saved_report_parameters. Using these tables, users are able to store custom copies of an application's canned reports along with the parameters he/she used to produce a custom report. We'll also create a delete trigger maintaining the relationship between them. In our example application, we do not cascade deletes through child rows; Child rows are maintained in a separate UI.

    In a Management Studio connection, run the following script:


    -- create a parent table
    create table dbo.saved_report
    (
    saved_report_id int identity(1,1) not null primary key,
    saved_name nvarchar(40) not null unique,
    )
    go

    insert into dbo.saved_report (saved_name) select 'Customer Acme Sales Report - 2007'
    go

    -- create a the child table
    create table dbo.saved_report_parameters
    (
    saved_report_parameters_id int identity(1,1) not null primary key,
    saved_report_id int not null,
    parameter_name nvarchar(20) not null,
    parameter_value nvarchar(20) not null,
    constraint uq_saved_report_parameter unique (saved_report_id, parameter_name, parameter_value)
    )
    go

    insert into dbo.saved_report_parameters (saved_report_id, parameter_name, parameter_value)
    select 1, 'customerid', '39'
    insert into dbo.saved_report_parameters (saved_report_id, parameter_name, parameter_value)
    select 1, 'asofdate', '2007-12-31'
    go

    -- create a trigger to enforce the parent/child relationship if the parent is deleted
    create trigger dbo.tr_saved_report_delete on dbo.saved_report
    for delete
    as

    set nocount on

    if exists (select 1 from dbo.saved_report_parameters s
            join deleted d on d.saved_report_id = s.saved_report_id)
    begin
        raiserror ('Delete not allowed; child rows exist', 16, 10)
        waitfor delay '00:00:10' -- delay a bit to allow the showing of held locks in a separate connection
        rollback
    end
    go
     

    The application allows the user to delete saved reports via a UI which calls a stored procedure. However, any child rows must be deleted via a separate UI before the parent can be deleted. Run the following command in a new Management Studio connection to delete all saved reports:
     

    delete from dbo.saved_report
    go

    In a separate Management Studio connection, run the following command while the delete is running:


    select resource_type,
            resource_associated_entity_id,
            request_mode, request_type
    from sys.dm_tran_locks
    go

    You'll see that the trigger is part of the transaction as illustrated by the held locks.

     
    When the trigger completes, you'll see the following message in the results pane of the delete query
     
    What if the stored procedure was coded to perform a TRUNCATE as opposed to a DELETE if the user chooses to delete all saved reports? Well, when performing wholesale purging of all of a table's rows, a TRUNCATE can be faster since this operation is only minimally logged (only page deallocations are recorded in the transaction log). Let's issue a TRUNCATE.
     

    truncate table dbo.saved_report
    go

    After the TRUNCATE completes, we'll examine the parent and child tables

    select * from dbo.saved_report
    select * from dbo.saved_report_parameters
    go
     

    Since DELETE triggers are not fired when a TRUNCATE is issued, we've just orphaned the child rows.

    If you re-create the tables without the trigger and declare the following foreign key, you will see that your data is protected from a stray TRUNCATE.

    alter table dbo.saved_report_parameters
    add constraint fk_saved_report_parameters
    foreign key (saved_report_id)
    references dbo.saved_report(saved_report_id)
    go
     

    truncate table dbo.saved_report
    go

     

     

    I believe in using foreign keys over triggers when enforcing basic referential integrity. I am very diligent about using declarative constraints in my data models for good reasons:

    • I don't have the time to code and maintain triggers or stored procedures to handle database integrity for every table I add to my model
    • I don't have the time to create repair scripts if a bug is exposed in one of these routines.
    • I also want to keep my transactions short so I don't end up in blocking situations.
    • Furthermore, foreign keys and the other constraint options (i.e. CHECK, UNIQUE, PRIMARY KEY) are the database's last line of defense in the prevention of orphaned and invalid data due to coding errors in the logic tier as well as the database tier.

    Next Steps



    Related Tips: 1 | 2 | 3 | 4 | 5 | 6 | More | Become a paid author


    Last Update: 5/29/2008

    Share: Share 






    Comments and Feedback:

    Monday, September 28, 2009 - 1:52:12 PM - mrsocks Read The Tip

     Thank you for the article.

    I am trying to find the best way to maintain some info across a number of child tables by a value in a parent table. 

    Table structures are as such:

    (parent)

    Author_Info
    |id|name|active|
    |1|Poe|1|
    |2|Roth||1|
    |3|Twain|0|

    (child)

    Books
    |id|author_id|title|active|
    |1|1|Tell tale heart|1|
    |2|1|The Pit and the Pendulum|1|
    |3|2|Cabin Fever|1|
    |4|2|Hostel|1|
    |5|3|Tom Swayer|0|
    |6|3|Huck Finn|0|

    Currently, I have a trigger that when a author's active column is changed, it updates the 'active' column in the all the records in the 'Books' table  that match that authro's id.

    I am chaning this to use a multi-column FK on (author_id, active) against (id, active) of the Author_Info table that ON DELETE No Action, ON UPDATE Casecade. Is this a better solution?

     

    Thanks.


    Monday, September 28, 2009 - 5:46:55 PM - aprato Read The Tip

    Rather than store an active flag in the child tables, just store it in the parent table and let the parent dictate if the titles are active.  That way, you don't need a trigger and you don't have any db overhead in maintaining the child table entries.  The way you described it, it sounds like either the titles are all active or all inactive depending on the status of the parent (author).

     


    Tuesday, September 29, 2009 - 6:41:42 AM - mrsocks Read The Tip

     Thanks for the reply.

    Yeah, that was what I was thinking about after writting this question out. I thought there was a reason our application needed it to be done that way, but we'll see. I have been testing out some things doing it this way since i posted this yesterday.

    Thanks again.


    Tuesday, January 19, 2010 - 2:08:09 PM - DreVinci Read The Tip

    Thanks for the article.  I have one question though:

    At the moment, I have a relationship between two tables product and customer and they're linked by an order_id column in each table and using a foreign key relationship.  However, when inputting data into the product table I want it to make sure that the order_id in the customer table already exists, meaning that you won't be able to enter product until the parent order_id in the customer is created.

    To clear it up some...
    Customer table has primary key "order_id".
    Product table has foreign key "order_id".
    I want to make sure that when you enter the foreign key, there must be a matching primary key in customer.

    Is this possible?  If so, would this be done using a trigger?  If so, how would I go about doing that?  As I'm clueless with pl/sql at the moment.  (In 10th grade, we dont start pl/sql until next semester D=)


    Tuesday, January 19, 2010 - 5:06:11 PM - aprato Read The Tip

    Hi... it's nice to see such a young person interested in databases.  I think you'll find it to be a rewarding endeavor.

     In your case, I think what you're looking for is 3 tables

    1. a Customer table
    2. a Product table
    3. an Orders table (and probably a Line_Item table)

     You would have a PK on each table (i.e. Customer_id, Product_id, and Order_id)

     The Orders table would contain the Customer_id and Product_id in addition to Order_id  (along with any other columns you'd need).   You would add a FK for the Customer_id and Product_id which would, in effect, say that "You can't have an order unless (a) you have a valid customer and a (b) a valid product".

     I recommend you download the AdventureWorks sample database from CodePlex where you can examine a sample Orders schema that contains sample data that you can experiment with.

    Let me know if you're not clear and I can try to work it through a bit more with you.


    Tuesday, January 19, 2010 - 6:44:03 PM - DreVinci Read The Tip

    Aha, that was exactly how I planned it at the beginning, however it didn't work out because an order can have multiple products attached to it.  Thus, I'd need a bunch of optional foreign key product fields? Lol...(And I only got rid of my Orders table because my programming teacher's motto is "Simple, simple simple" hence she told me to get rid of what she thought was redundant data in the Orders table >_<)  Lol.

    But now that I think about it....Instead I can bring back the Orders table, and have a reference to the Customer (like you said).  But instead of referencing the Products in the Orders, I'll reference the Orders in the Products.  This way, we'll have a Customer and his info.  Then the Order will reference the Customer_ID and have w/e info.  And each Product will have a reference to that Order_ID.  Oh snap!  Might that work?

    Noooo...Because this way I'll still be able to input a value into product.order_id for an Order that hasn't been created yet.....My goal is to have some type of constraint that will stop me from inserting a foreign key value that has no matching primary key already in the database.  Basically, if the primary key is not in the database, you can't enter this foreign key.  So, I went full circle.  Is this possible with triggers?

    Lol, thanks for the help and the recommendation towards AdventureWorks, although I've got almost 400records plus some old stuff from Oracle I can use...and I apologize for being so confusing.  ^_^  Perhaps I'm missing something, and the way you explained it was correct....


    Tuesday, January 19, 2010 - 11:05:55 PM - aprato Read The Tip

     <<Aha, that was exactly how I planned it at the beginning, however it didn't work out because an order can have multiple products attached to it.>>

    That's where a LineItem table comes into play.  In the classic sense, a line item represents an ordered product in a customer's order

    Think of it this way.   Think of the nouns that you need for your model by forming
    a statement about what you're trying to solve:

    A customer can order one or many products 

    You would need:

    A customer
    A product
    An order
    A line item (to represent more than 1 product per order)

    Now think about how they relate to each other.  This would determine your FK constraints.

    Can a customer exist without a product?  Yes, so there's no relation

    But...

    Can an order exist without a customer?  No.  So customer_id would be a FK in the Orders table.

    When you get into transactions, you'd need to be aware that triggers can extend the life of the transaction which could cause a condition called blocking where readers can't get data they need becuase someone is writing data and vice versa.  They have their place but they're not my choice for data checking.


     



    Post a Comment or Question

    Keep it clean and stay on the subject or we may delete your comment.
    Your email address is not published. Required fields are marked with an asterisk (*)

    *Name   *Email   Notify for updates
    Comments
    *Enter Code refresh code


     
    Sponsor Information
    "SQL diagnostic manager delivers response in minutes, not hours!"

    SQL Monitor – For database professionals who need results on Day One. Try it online.

    SQL Servers keeping you up at night? Contact the Edgewood SQL Server DBAs for a Health Check. Prices start at $995.

    Get SQL Server Tips Straight from Kevin Kline.

    Solving SQL Server problems for millions of DBAs and Devs since 2006. Join now.

    Valuable SQL Server web casts on Performance Tuning, Development, Administration, SSIS and more...


    Copyright (c) 2006-2012 Edgewood Solutions, LLC All rights reserved
    privacy | disclaimer | copyright | advertise | about
    authors | contribute | feedback | giveaways | user groups
    Some names and products listed are the registered trademarks of their respective owners.


    Edgewood Solutions LLC | MSSharePointTips.com | MSSQLTips.com