Is this a bug in MERGE, failing to implement FOREIGN KEY properly?

霸气de小男生 提交于 2019-11-30 08:34:57

Looks like a definite bug in MERGE to me.

The execution plan has the Clustered Index Merge operator and is supposed to output [Cars].ID,[Cars].Type for validation against the Vehicles table.

Experimentation shows that instead of passing the value "Car" as the Type value it is passing an empty string. This can be seen by removing the check constraint on Vehicles then inserting

INSERT INTO dbo.Vehicles(ID, [Type]) VALUES (3, '');

The following statement now works

MERGE dbo.Cars AS TargetTable
    USING 
        ( SELECT    3 AS ID ,
                    'Some Data' AS OtherData
        ) AS SourceData
    ON  SourceData.ID = TargetTable.ID
    WHEN NOT MATCHED 
        THEN INSERT (ID, OtherData)
        VALUES(SourceData.ID, SourceData.OtherData);

But the end result is that it inserts a row violating the FK constraint.

Cars

ID          Type  OtherData
----------- ----- ----------
3           Car   Some Data

Vehicles

ID          Type
----------- -----
1           Car
2           Truck
3           

Checking the constraints immediately afterwards

DBCC CHECKCONSTRAINTS  ('dbo.Cars')

Shows the offending row

Table         Constraint          Where
------------- ------------------- ------------------------------
[dbo].[Cars]  [Cars_FK_Vehicles]  [ID] = '3' AND [Type] = 'Car'

The reason the Merge iterator appears to output an empty string for the Type column is interesting. The optimizer recognises that Type is a constant value and applies a rewrite that removes that column from the flow, adding it back in later as a Compute Scalar. You can see this in action by adding an OUTPUT clause to the MERGE statement to emit the value of inserted.[Type]. Without the OUTPUT clause, there's no need for the Compute Scalar, so it is optimized away, leaving us with the plan shape seen in the original example.

The bug arises when something between the Merge iterator (which can also be a Table Merge rather than a Clustered Index Merge) and the Compute Scalar needs the value of the [Type] column. Since it was removed from the flow (despite showing in the plan as an output from the merge) we end up referencing something that does not exist, and that produces the empty string. In another world, SQL Server would assert instead with something like a null pointer violation, but that's another story.

This issue is a bit fixed in SQL Server 2012, but there is still a bug. I say 'a bit' fixed because the rewrite that removes the constant-value column is switched off (so the FK check gets a real value to seek on, not an empty string) but the Compute Scalar that adds the string 'Car' back into the flow still appears if OUTPUT inserted.[Type] is added. In a perfect world, the plan would simply output the value provided by the Merge rather than re-computing the constant. Anyway, that's not all that important (it just shows the implementation is still a bit flaky around the edges) but there is still a bug related to removing the column reference:

It only reproduces in SQL 2012 with a table variable (all other types of table are fine) but reproduces on all released versions of SQL Server with any type of table object:

DECLARE @Bug TABLE
(
    id INTEGER PRIMARY KEY, 
    data AS 'X' PERSISTED,
    CHECK (data = 'A')
)

MERGE @Bug AS b USING (VALUES(1)) AS u (id) ON u.id = b.id
WHEN NOT MATCHED THEN INSERT (id) VALUES (u.id)
OUTPUT INSERTED.data;

The point is that the CHECK constraint is skipped - the Assert operator that checks it is added to the plan, but then optimized away when optimizer sees the constant-value column and applies its rewrite. Removing the Assert allows the value 'X' to be added to the table even though it violates the CHECK constraint. As I say, you can reproduce this in 2008 R2 and earlier with real and temporary tables - in 2012 it only bugs out with table variables.

Paul

易学教程内所有资源均来自网络或用户发布的内容,如有违反法律规定的内容欢迎反馈
该文章没有解决你所遇到的问题?点击提问,说说你的问题,让更多的人一起探讨吧!