Untrusted Foreign Keys Still Check New Data

Blitzing

This comes up enough that I figured I’d write it down with a quick example. Foreign keys, which are next to useless in SQL Server, can become untrusted for a variety of reasons. Why? Because they don’t help with cardinality estimation, and join elimination only happens for inner joins when you don’t select any columns from one table.

How often do you write queries like that?

Becoming Untrusted

Probably the most common reasons for foreign keys become not trusted are someone using BULK INSERT without specifying the CHECK_CONSTRAINTS option, or disabling them to make loading data via another method easier, and then re-enabling them with the “wrong” syntax.

This comes up pretty often when looking at sp_BlitzIndex results with clients, who are deeply distrustful of their data.

Let’s look at an easy example.

CREATE TABLE dbo.child(child_id INT PRIMARY KEY, parent_id INT NOT NULL);
CREATE TABLE dbo.parent(parent_id INT PRIMARY KEY);

Here are two tables, which we’ll add a foreign key to. There are all sorts of fun questions we can ask about which way the relationship should go:

  • Do all parents need a child?
  • Do all children need a parent?

If one if designing a school database, you may have kids with no parent listed for any number of reasons, from death to emancipation. Likewise, you may have parents of children who graduate, but getting rid of them means adding them back if they have another kid go into the school. This is already too complicated for me.

“Pretending”

Let’s say we finally settle on which way the relationship should go, and add a foreign key.

--Add a FK
ALTER TABLE dbo.child ADD CONSTRAINT parental_advisory FOREIGN KEY (parent_id) REFERENCES dbo.parent(parent_id);

This means we can have a row in parent that doesn’t exist in child:

INSERT dbo.parent ( parent_id )
VALUES ( 1 );

But we can’t have a row in child that doesn’t exist in parent:

INSERT dbo.child ( child_id, parent_id )
VALUES ( 1, 2 );

We’ll get an error for trying this!

Msg 547, Level 16, State 0, Line 42
The INSERT statement conflicted with the FOREIGN KEY constraint "parental_advisory". The conflict occurred in database "Crap", table "dbo.parent", column 'parent_id'.

Unless…

Call A Lawyer

--Disable it
ALTER TABLE dbo.child NOCHECK CONSTRAINT parental_advisory;

If we disable the foreign key, we can put whatever data in we want, including data that violates the foreign key.

We can even re-enable the foreign key without validating data we put into the table:

--Re-enable the foreign key without validating data
ALTER TABLE dbo.child CHECK CONSTRAINT parental_advisory;

If we check on the status of the foreign key, we’ll see that it’s enabled; it’s just not trusted.

SELECT fk.name,
       fk.is_disabled,
       fk.is_not_trusted
FROM sys.foreign_keys AS fk 
WHERE fk.name = 'parental_advisory';

But just having it enabled will prevent bad new data from coming in:

INSERT dbo.child ( child_id, parent_id )
VALUES ( 2, 3 );

We’ll get the same error as above.

If you want to enable the foreign key and validate current data, you need to run the command like this:

--Re-enable the foreign key validating data
ALTER TABLE dbo.child WITH CHECK CHECK CONSTRAINT parental_advisory;

But this will fail if there’s any bad data:

Msg 547, Level 16, State 0, Line 52
The ALTER TABLE statement conflicted with the FOREIGN KEY constraint "parental_advisory". The conflict occurred in database "Crap", table "dbo.parent", column 'parent_id'.

Tracking It Down

When you have foreign key validation fail, you’ll need to clean data up that doesn’t fit. You can use queries that look something like this to do it.

SELECT * 
FROM dbo.child AS c
WHERE NOT EXISTS
(
    SELECT 1/0
    FROM dbo.parent AS p
    WHERE p.parent_id = c.parent_id
);

Thanks for reading!



2 thoughts on “Untrusted Foreign Keys Still Check New Data

  1. Yeah, it’s a good time in the “what to do after tracking it down” phase. “Do I create dummy parent rows that are meaningless (and may need dummy parent rows, themselves) or just delete the unreferenced child rows since they have no meaning or relationship and the application never complained?” To date and the work I’ve had to do (everyone’s mileage/requirement may vary), I’ve not found a reason to create dummy parents unless I had some source that would give the parent rows meaningful value.

    For the statement under adding the parental_advisory foreign key, “This means we can’t have a row in parent that doesn’t exist in child”, did you intend, “This means we *can* have a row in parent that doesn’t exist in child”?

    Thank you for your posts and videos! I really enjoy them!

Leave a Reply

Your email address will not be published. Required fields are marked *