SQL 2008 Merge and PK/FK Constraints

We ran into this issue a while back.  SQL 2008 has a “feature” that will not allow inserts into a table on either side of a Foreign Key relationship.  If you attempt to insert into a table involved in the FK relationship, you get an error something like:

The target table ‘TableName’ of the INSERT statement cannot be on either side of a (primary key, foreign key) relationship when the FROM clause contains a nested INSERT, UPDATE, DELETE, or MERGE statement. Found reference constraint ‘ConstraintName’.

This is documented in Connect 435031. There is one workaround documented in the ticket – drop the FK constraint, run the MERGE query, then re-create the constraint. Happily, this bug is still open as the requirement to drop a constraint just to run a MERGE statement kind of defeats the purpose of putting a constraint on the table in the first place. While dropping the constraints will work, there is another possible workaround. I was able to run this without any issues while leaving my FK Constraint in place.

  1. Create a temp table that matches the definition of the table into which you want to perform your insert.
  2. Instead of inserting into your base table like you normally would in a merge, write your code to INSERT #Tablename at the beginning of the MERGE process. You’ll still use your main table and staging table inside the MERGE INTO … USING query.
  3. After the Merge, insert into the main table using the values in your Temp table.

Here’s a shell of an example. Line 11 below contains the main difference to the MERGE query from a “normal” MERGE.


–1: Create Temp table
CREATE TABLE #MyFactTable(
ID INT NULL
, CustomerName VARCHAR(100) NULL
, SourceID INT NULL
, OutputAction VARCHAR(100) NULL
);
–2: INSERT into the temp table instead of your normal target table
— Merge query will be the same otherwise
INSERT INTO #MyFactTable (ID, CustomerName, SourceID, OutputAction)
SELECT so.ID, so.CustomerName, so.SourceID, so.output_action
FROM (
MERGE INTO dbo.MyFactTable AS t
USING Staging.MyFactTable AS s
ON ( s.ID = t.ID
AND s.NewLoad = 0 )
WHEN MATCHED AND ( s.SourceID t.SourceID )
AND s.NewLoad = 0
THEN UPDATE
SET RecordState = 0
, UpdatedDate = getdate()
WHEN NOT MATCHED BY TARGET AND s.NewLoad = 0 THEN
INSERT (ID, CustomerName, SourceID)
VALUES (s.ID, s.CustomerName, s.SourceID)
OUTPUT $action AS OutputAction
, ID
, CustomerName
, SourceID
) AS so (OutputAction, ID, CustomerName, SourceID)
WHERE OutputAction = 'UPDATE' ;
–3: Perform the final insert into your target table
INSERT INTO MyFactTable (ID, CustomerName, SourceID)
SELECT DISTINCT ID, CustomerName, SourceID
FROM #MyFactTable ;
–4: Clean up your temp objects.
DROP TABLE #MyFactTable ;

I’d love to hear if anyone else has a workaround for this that doesn’t involve dropping and recreating the FK constraint. If this is an issue for you, please vote for the Connect Ticket.

Comments
  1. Anonymous

    Thanks for the solution.

    My only suggestion would be to replace the CREATE TABLE for the temporary table with

    SELECT * into #MyFactTable
    FROM MyFactTable
    WHERE
    1=0

    so that you don't have to recreate the full table definition in the proc.

    Cheers,
    Dan

  2. Peter Schott

    Dan,
    I've tried to be a bit better about explicitly defining my tables when creating various stored procedures. That practice has saved me from surprises later on when something small changes in a table definition or something similar. I've definitely used that as a shortcut in the past, but try to avoid it inside of a stored proc or similar object.

    It's a great shortcut for creating a table structure, though. That WHERE 1=2 or 1=0 shortcut helps me quite a bit when I am in the middle of a lot of ad-hoc work.

    I hope that it helps you a bit. I know it surprised me quite a bit that this didn't just work and I figured there had to be a decent workaround. (or at least _a_ workaround)

Leave a Reply

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