DEV Community

Cover image for The ALTER TABLE statement conflicted with the FOREIGN KEY constraint
Sam
Sam

Posted on Originally published at woodfireerd.com

The ALTER TABLE statement conflicted with the FOREIGN KEY constraint

You add a foreign key to a table that has been live for years, and SQL Server refuses:

ALTER TABLE dbo.SalesOrder ADD CONSTRAINT FK_SalesOrder_Customer
    FOREIGN KEY (CustomerId) REFERENCES dbo.Customer (CustomerId);
Enter fullscreen mode Exit fullscreen mode
Msg 547, Level 16, State 1, Line 1
The ALTER TABLE statement conflicted with the FOREIGN KEY constraint "FK_SalesOrder_Customer".
The conflict occurred in database "erd_article_test", table "dbo.Customer", column 'CustomerId'.
Enter fullscreen mode Exit fullscreen mode

Read that message carefully, because it sends most people to the wrong table. It names dbo.Customer and column CustomerId — the table being pointed at. There is nothing wrong with any row in it. The rows at fault are in dbo.SalesOrder: they hold a CustomerId that Customer does not have.

Find the rows

SELECT o.OrderId, o.CustomerId
FROM   dbo.SalesOrder AS o
WHERE  o.CustomerId IS NOT NULL
  AND  NOT EXISTS (SELECT 1 FROM dbo.Customer AS c WHERE c.CustomerId = o.CustomerId)
ORDER BY o.OrderId;
Enter fullscreen mode Exit fullscreen mode
OrderId  CustomerId
-------  ----------
11       99
13       404
Enter fullscreen mode Exit fullscreen mode

IS NOT NULL is not optional. A null foreign key means "points at nothing", which a foreign key allows; without that line you get every unlinked row back and the list is useless.

If you only want to know the size of the problem before deciding anything:

SELECT COUNT(*) AS Orphans
FROM   dbo.SalesOrder AS o
WHERE  o.CustomerId IS NOT NULL
  AND  NOT EXISTS (SELECT 1 FROM dbo.Customer AS c WHERE c.CustomerId = o.CustomerId);
Enter fullscreen mode Exit fullscreen mode

Three honest fixes

Create the missing parents if the values mean something — a customer deleted by hand, a load that ran out of order. The orders keep their history.

INSERT dbo.Customer (CustomerId)
SELECT DISTINCT o.CustomerId
FROM   dbo.SalesOrder AS o
WHERE  o.CustomerId IS NOT NULL
  AND  NOT EXISTS (SELECT 1 FROM dbo.Customer AS c WHERE c.CustomerId = o.CustomerId);
Enter fullscreen mode Exit fullscreen mode

Set them to null if the link is wrong but the row is real. The order survives, pointing at nobody, which is exactly what the data is telling you:

UPDATE dbo.SalesOrder
SET    CustomerId = NULL
WHERE  CustomerId IS NOT NULL
  AND  NOT EXISTS (SELECT 1 FROM dbo.Customer AS c WHERE c.CustomerId = dbo.SalesOrder.CustomerId);
Enter fullscreen mode Exit fullscreen mode

Delete them only when you know the rows are rubbish. Check what points at them first, or you will be reading this article again about the next table down.

WITH NOCHECK, and what it costs

There is a fourth option, and it is the one reached for under time pressure:

ALTER TABLE dbo.SalesOrder WITH NOCHECK ADD CONSTRAINT FK_SalesOrder_Customer
    FOREIGN KEY (CustomerId) REFERENCES dbo.Customer (CustomerId);
Enter fullscreen mode Exit fullscreen mode

That succeeds. The existing rows are not examined, and the bad ones stay exactly as they are.

What it buys you is real: from now on, nothing new can be inserted that breaks the rule.

INSERT dbo.SalesOrder (OrderId, CustomerId) VALUES (14, 777);
Enter fullscreen mode Exit fullscreen mode
Msg 547, Level 16, State 1, Line 1
The INSERT statement conflicted with the FOREIGN KEY constraint "FK_SalesOrder_Customer".
Enter fullscreen mode Exit fullscreen mode

What it costs is that the key is not trusted:

SELECT name, is_not_trusted, is_disabled
FROM   sys.foreign_keys
WHERE  name = 'FK_SalesOrder_Customer';
Enter fullscreen mode Exit fullscreen mode
name                     is_not_trusted  is_disabled
-----------------------  --------------  -----------
FK_SalesOrder_Customer   1               0
Enter fullscreen mode Exit fullscreen mode

is_not_trusted = 1 means SQL Server is enforcing the rule but will not believe it when building a query plan. It cannot eliminate the join to Customer in a query that only needs columns from SalesOrder, because as far as it knows there are rows in there with no parent — and there are. The constraint documents an intention the data does not meet.

Worth knowing: a key you never fix stays untrusted forever, and nothing warns you. This finds them all:

SELECT OBJECT_SCHEMA_NAME(parent_object_id) AS SchemaName,
       OBJECT_NAME(parent_object_id)        AS TableName,
       name                                 AS ForeignKey
FROM   sys.foreign_keys
WHERE  is_not_trusted = 1 AND is_disabled = 0;
Enter fullscreen mode Exit fullscreen mode

Making it trusted later

Once the rows are sorted out, ask SQL Server to check what it skipped:

ALTER TABLE dbo.SalesOrder WITH CHECK CHECK CONSTRAINT FK_SalesOrder_Customer;
Enter fullscreen mode Exit fullscreen mode

The doubled CHECK CHECK is not a typo: the first says check the existing rows, the second names the constraint to enable. Run it while bad rows are still there and you get Msg 547 again — the same message, for the same reason. Run it after they are gone and is_not_trusted goes to 0.

This is the step that gets forgotten, because the constraint already looks right in every tool that lists it. Add it to the migration script that does the cleanup, not to a note for later.

The part worth doing first

Before adding the key at all, know what the table is already joined to and what is pointing at it — a foreign key's effect is on the other table, so reading the table you are changing tells you very little.

That is the question WoodFireERD answers: pick a table, see what it points at and what points at it, and have the relationship come out as an ordered script, with the rows that would break it listed before anything runs.


Originally published at woodfireerd.com. Every statement in it was run against a real SQL Server before publishing — the error messages are copied from the output, not from memory.

Top comments (0)