I am using the following tables to implement subtypes, which is a very common approach:
CREATE TABLE dbo.Vehicles(
ID INT NOT NULL,
[Type] VARCHAR(5) NOT NULL,
CONSTRAINT Vehicles_PK PRIMARY KEY(ID),
CONSTRAINT Vehicles_UNQ_ID_Type UNIQUE(ID, [Type]),
CONSTRAINT Vehicles_CHK_ValidTypes CHECK([Type] IN ('Car', 'Truck'))
);
GO
CREATE TABLE dbo.Cars(ID INT NOT NULL,
[Type] AS CAST('Car' AS VARCHAR(5)) PERSISTED,
OtherData VARCHAR(10) NULL,
CONSTRAINT Cars_PK PRIMARY KEY(ID),
CONSTRAINT Cars_FK_Vehicles FOREIGN KEY(ID, [Type])
REFERENCES dbo.Vehicles(ID, [Type])
);
GO
-- adding parent rows
INSERT INTO dbo.Vehicles(ID, [Type])
VALUES(1, 'Car'),
(2, 'Truck');
I have no problem adding a child row via INSERT, as follows:
INSERT INTO dbo.Cars(ID, OtherData)
VALUES(1, 'Some Data');
DELETE FROM dbo.Cars;
Surprisingly, MERGE fails to add one child row:
MERGE dbo.Cars AS TargetTable
USING
( SELECT 1 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);
Msg 547, Level 16, State 0, Line 1
The MERGE statement conflicted with the FOREIGN KEY constraint "Cars_FK_Vehicles". The conflict occurred in database "Test", table "dbo.Vehicles".
The statement has been terminated.
Is this a bug in MERGE or am I missing something?
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
来源:https://stackoverflow.com/questions/7771869/is-this-a-bug-in-merge-failing-to-implement-foreign-key-properly