Это ошибка в MERGE, которая не позволяет правильно реализовать FOREIGN KEY? - PullRequest
14 голосов
/ 14 октября 2011

Я использую следующие таблицы для реализации подтипов, что является очень распространенным подходом:

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');

У меня нет проблем с добавлением дочерней строки через INSERT, как показано ниже:

INSERT INTO dbo.Cars(ID, OtherData)
VALUES(1, 'Some Data');

DELETE FROM dbo.Cars;

Удивительно, но MERGE не может добавить одну дочернюю строку:

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.

Это ошибка в MERGE или я что-то упустил?

Ответы [ 2 ]

10 голосов
/ 14 октября 2011

Для меня это выглядит как определенная ошибка в MERGE.

План выполнения содержит оператор Clustered Index Merge и должен выводить [Cars].ID,[Cars].Type для проверки по таблице Vehicles.

Эксперимент показывает, что вместо передачи значения "Car" в качестве значения Type он передает пустую строку.Это можно увидеть, сняв проверочное ограничение на «Транспортные средства», затем вставив

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

Следующий оператор теперь работает

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);

Но в результате он вставляет строку, нарушающую ограничение FK.

Автомобили

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

Транспортные средства

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

Проверка ограничений сразу после этого

DBCC CHECKCONSTRAINTS  ('dbo.Cars')

Показывает строку с нарушением

Table         Constraint          Where
------------- ------------------- ------------------------------
[dbo].[Cars]  [Cars_FK_Vehicles]  [ID] = '3' AND [Type] = 'Car'
9 голосов
/ 03 ноября 2011

Причина, по которой итератор Merge выводит пустую строку для столбца Type, интересна.Оптимизатор распознает, что Type является постоянным значением, и применяет перезапись, которая удаляет этот столбец из потока, добавляя его позже в качестве вычисляемого скаляра.Это можно увидеть в действии, добавив предложение OUTPUT в оператор MERGE, чтобы выдать значение inserted.[Type].Без условия OUTPUT нет необходимости в Compute Scalar, поэтому он оптимизирован, что оставляет нас с формой плана, показанной в исходном примере.

Ошибка возникает, когда что-то между итератором Merge (которыйтакже может быть слиянием таблицы, а не слиянием кластерного индекса), а для вычисления скаляра требуется значение столбца [Type].Так как он был удален из потока (несмотря на то, что он показан в плане как выход из слияния), мы в конечном итоге ссылаемся на то, что не существует, и которое создает пустую строку.В другом мире вместо этого SQL Server будет утверждать что-то вроде нарушения нулевого указателя, но это уже другая история.

Эта проблема немного исправлена ​​в SQL Server 2012, но все еще существует ошибка.Я говорю «немного» исправлено, потому что перезапись, которая удаляет столбец с постоянными значениями, отключена (поэтому проверка FK получает реальное значение для поиска, а не пустую строку), но Compute Scalar, который добавляет строку «Car» обратнов поток все еще появляется, если OUTPUT inserted.[Type] добавлено.В идеальном мире план будет просто выводить значение, предоставленное слиянием, а не пересчитывать константу.В любом случае, это не так уж важно (просто показывает, что реализация по-прежнему немного ненадежна), но есть ошибка, связанная с удалением ссылки на столбец:

Он воспроизводится только в SQL 2012 с таблицейпеременная (все остальные типы таблиц в порядке), но воспроизводится во всех выпущенных версиях SQL Server с любым типом табличного объекта:

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;

Дело в том, что ограничение CHECK пропущено - оператор Assertон проверяет, что он добавляется в план, но затем оптимизируется, когда оптимизатор видит столбец с постоянными значениями и применяет его перезапись.Удаление Assert позволяет добавить значение «X» в таблицу, даже если оно нарушает ограничение CHECK.Как я уже сказал, вы можете воспроизвести это в 2008 R2 и более ранних версиях с реальными и временными таблицами - в 2012 году это только ошибки с табличными переменными.

Пол

...