대상 테이블의 서브 세트를 병합


71

MERGE명령문 을 사용하여 테이블에서 행을 삽입하거나 삭제 하려고 하지만 해당 행의 하위 집합에만 작용하고 싶습니다. 에 대한 설명서 MERGE에는 매우 강력한 경고가 있습니다.

일치 목적으로 사용되는 대상 테이블의 열만 지정해야합니다. 즉, 목표 테이블에서 소스 테이블의 해당 컬럼과 비교되는 컬럼을 지정하십시오. AND 절 target_table.column_x = value를 지정하는 등의 ON 절에서 대상 테이블의 행을 필터링하여 쿼리 성능을 향상시키지 마십시오. 그렇게하면 예기치 않은 잘못된 결과가 반환 될 수 있습니다.

그러나 이것이 바로 제가 일을하기 위해해야 ​​할 MERGE일입니다.

내가 가지고있는 데이터는 다음과 같이 범주에 대한 항목이 많은 표준 다 대다 조인 테이블입니다 (예 : 어떤 항목이 어떤 범주에 포함되어 있는지).

CategoryId   ItemId
==========   ======
1            1
1            2
1            3
2            1
2            3
3            5
3            6
4            5

내가해야 할 일은 특정 범주의 모든 행을 새로운 항목 목록으로 효과적으로 바꾸는 것입니다. 이 작업을 처음 시도하면 다음과 같습니다.

MERGE INTO CategoryItem AS TARGET
USING (
  SELECT ItemId FROM SomeExternalDataSource WHERE CategoryId = 2
) AS SOURCE
ON SOURCE.ItemId = TARGET.ItemId AND TARGET.CategoryId = 2
WHEN NOT MATCHED BY TARGET THEN
    INSERT ( CategoryId, ItemId )
    VALUES ( 2, ItemId )
WHEN NOT MATCHED BY SOURCE AND TARGET.CategoryId = 2 THEN
    DELETE ;

이것은 내 테스트에서 작동하는 것처럼 보이지만 MSDN이 명시 적으로하지 말라고 경고하는 것을 정확하게 수행하고 있습니다. 이로 인해 나중에 예기치 않은 문제가 발생할 것이 우려되지만 MERGE특정 필드 값 ( CategoryId = 2) 이있는 행에만 영향을 미치고 다른 범주의 행을 무시 하는 다른 방법은 없습니다 .

이 같은 결과를 얻을 수있는 "보다 정확한"방법이 있습니까? 그리고 MSDN에서 경고하는 "예기치 않거나 잘못된 결과"는 무엇입니까?


예, "예기치 않은 결과와 부정확 한 결과"에 대한 구체적인 예가 있다면이 문서가 더 유용 할 것입니다.
— AK

3
@AlexKuznetsov 여기 에 예가 있습니다 .
— Paul White

@SQLKiwi 링크 주셔서 감사합니다-IMO 문서가 원본 페이지에서 참조되면 훨씬 좋습니다.
— AK

1
@AlexKuznetsov 합의. 불행히도 2012 년의 BOL 개편은 그 중에서도 많은 것들을 깨뜨 렸습니다. 2008 R2 문서에서 꽤 잘 연결되었습니다.
— Paul White

답변:


103

MERGE문은 복잡한 구문과 훨씬 더 복잡한 구현을 가지고 있지만, 기본적 아이디어는, 두 개의 테이블을 조인 변경 (삽입, 업데이트 또는 삭제) 할 필요가 행까지 필터링하는 다음 요청 된 변경을 수행 할 수 있습니다. 다음 샘플 데이터가 제공됩니다.

DECLARE @CategoryItem AS TABLE
(
    CategoryId  integer NOT NULL,
    ItemId      integer NOT NULL,

    PRIMARY KEY (CategoryId, ItemId),
    UNIQUE (ItemId, CategoryId)
);

DECLARE @DataSource AS TABLE
(
    CategoryId  integer NOT NULL,
    ItemId      integer NOT NULL

    PRIMARY KEY (CategoryId, ItemId)
);

INSERT @CategoryItem
    (CategoryId, ItemId)
VALUES
    (1, 1),
    (1, 2),
    (1, 3),
    (2, 1),
    (2, 3),
    (3, 5),
    (3, 6),
    (4, 5);

INSERT @DataSource
    (CategoryId, ItemId)
VALUES
    (2, 2);

목표

╔════════════╦════════╗
║ CategoryId ║ ItemId ║
╠════════════╬════════╣
║          1 ║      1 ║
║          2 ║      1 ║
║          1 ║      2 ║
║          1 ║      3 ║
║          2 ║      3 ║
║          3 ║      5 ║
║          4 ║      5 ║
║          3 ║      6 ║
╚════════════╩════════╝

출처

╔════════════╦════════╗
║ CategoryId ║ ItemId ║
╠════════════╬════════╣
║          2 ║      2 ║
╚════════════╩════════╝

원하는 결과는 대상의 데이터를 소스의 데이터로 바꾸는 것입니다 CategoryId = 2. MERGE위에서 설명한 설명에 따라 소스와 대상을 키에서만 조인하는 쿼리를 작성하고 WHEN절 에서만 행을 필터링해야 합니다.

MERGE INTO @CategoryItem AS TARGET
USING @DataSource AS SOURCE ON 
    SOURCE.ItemId = TARGET.ItemId 
    AND SOURCE.CategoryId = TARGET.CategoryId
WHEN NOT MATCHED BY SOURCE 
    AND TARGET.CategoryId = 2 
    THEN DELETE
WHEN NOT MATCHED BY TARGET 
    AND SOURCE.CategoryId = 2 
    THEN INSERT (CategoryId, ItemId)
        VALUES (CategoryId, ItemId)
OUTPUT 
    $ACTION, 
    ISNULL(INSERTED.CategoryId, DELETED.CategoryId) AS CategoryId,
    ISNULL(INSERTED.ItemId, DELETED.ItemId) AS ItemId
;

결과는 다음과 같습니다.

╔═════════╦════════════╦════════╗
║ $ACTION ║ CategoryId ║ ItemId ║
╠═════════╬════════════╬════════╣
║ DELETE  ║          2 ║      1 ║
║ INSERT  ║          2 ║      2 ║
║ DELETE  ║          2 ║      3 ║
╚═════════╩════════════╩════════╝
╔════════════╦════════╗
║ CategoryId ║ ItemId ║
╠════════════╬════════╣
║          1 ║      1 ║
║          1 ║      2 ║
║          1 ║      3 ║
║          2 ║      2 ║
║          3 ║      5 ║
║          3 ║      6 ║
║          4 ║      5 ║
╚════════════╩════════╝

실행 계획은 다음과 같습니다. 병합 계획

두 테이블이 모두 스캔되었습니다. CategoryId = 2대상 테이블에서 영향을받는 행만 있기 때문에 비효율적이라고 생각할 수 있습니다 . 온라인 서적에 경고가 표시되는 위치입니다. 대상에서 필요한 행만 터치하도록 최적화하려는 한 가지 잘못된 시도는 다음과 같습니다.

MERGE INTO @CategoryItem AS TARGET
USING 
(
    SELECT CategoryId, ItemId
    FROM @DataSource AS ds 
    WHERE CategoryId = 2
) AS SOURCE ON
    SOURCE.ItemId = TARGET.ItemId
    AND TARGET.CategoryId = 2
WHEN NOT MATCHED BY TARGET THEN
    INSERT (CategoryId, ItemId)
    VALUES (CategoryId, ItemId)
WHEN NOT MATCHED BY SOURCE THEN
    DELETE
OUTPUT 
    $ACTION, 
    ISNULL(INSERTED.CategoryId, DELETED.CategoryId) AS CategoryId,
    ISNULL(INSERTED.ItemId, DELETED.ItemId) AS ItemId
;

ON절의 논리가 결합의 일부로 적용됩니다. 이 경우 조인은 완전 외부 조인입니다 ( 이 이유는 이 온라인 설명서 항목 참조 ). 외부 조인의 일부로 대상 행에 범주 2 검사를 적용하면 궁극적으로 소스와 일치하지 않기 때문에 다른 값을 가진 행이 삭제됩니다.

╔═════════╦════════════╦════════╗
║ $ACTION ║ CategoryId ║ ItemId ║
╠═════════╬════════════╬════════╣
║ DELETE  ║          1 ║      1 ║
║ DELETE  ║          1 ║      2 ║
║ DELETE  ║          1 ║      3 ║
║ DELETE  ║          2 ║      1 ║
║ INSERT  ║          2 ║      2 ║
║ DELETE  ║          2 ║      3 ║
║ DELETE  ║          3 ║      5 ║
║ DELETE  ║          3 ║      6 ║
║ DELETE  ║          4 ║      5 ║
╚═════════╩════════════╩════════╝

╔════════════╦════════╗
║ CategoryId ║ ItemId ║
╠════════════╬════════╣
║          2 ║      2 ║
╚════════════╩════════╝

근본 원인은 외부 조인 ON절 에서 술어 가 WHERE절에 지정된 경우 와 다르게 작동하는 것과 동일한 이유 입니다. MERGE구문 (지정된 조항에 따라 조인 구현은) 그냥 열심히이 그렇다고 볼 수 있도록.

온라인에서 안내 합니다 (확대 최적화 성능 항목)를 사용하여 표현된다 정확한 의미를 확인합니다 지침 제공 MERGE, 사용자가 반드시 최적화 합법적으로 다시 정렬 할 수있는 방법에 대한 모든 구현 세부 사항 또는 계정을 이해하지 않고, 구문을 실행 효율성을위한 것.

이 문서는 조기 필터링을 구현하는 세 가지 방법을 제공합니다.

WHEN절에 필터링 조건을 지정하면 올바른 결과가 보장되지만 소스 및 대상 테이블에서 더 많은 행을 읽고 처리해야한다는 것을 의미 할 수 있습니다 (첫 번째 예에서 볼 수 있음).

필터링 조건이 포함 된 뷰 를 업데이트하면 뷰 를 통해 업데이트하기 위해 변경된 행에 액세스 할 수 있어야하므로 올바른 결과가 보장되지만 전용 뷰가 필요하고 뷰를 업데이트하기위한 이상한 조건을 따르는 뷰가 필요합니다.

공통 테이블 표현식을 사용 하면 ON절에 술어를 추가 할 때와 비슷한 위험이 있지만 약간 다른 이유가 있습니다. 대부분의 경우 안전하지만 실행 계획에 대한 전문가 분석을 통해이를 확인하고 광범위한 실제 테스트를 수행해야합니다. 예를 들면 다음과 같습니다.

WITH TARGET AS 
(
    SELECT * 
    FROM @CategoryItem
    WHERE CategoryId = 2
)
MERGE INTO TARGET
USING 
(
    SELECT CategoryId, ItemId
    FROM @DataSource
    WHERE CategoryId = 2
) AS SOURCE ON
    SOURCE.ItemId = TARGET.ItemId
    AND SOURCE.CategoryId = TARGET.CategoryId
WHEN NOT MATCHED BY TARGET THEN
    INSERT (CategoryId, ItemId)
    VALUES (CategoryId, ItemId)
WHEN NOT MATCHED BY SOURCE THEN
    DELETE
OUTPUT 
    $ACTION, 
    ISNULL(INSERTED.CategoryId, DELETED.CategoryId) AS CategoryId,
    ISNULL(INSERTED.ItemId, DELETED.ItemId) AS ItemId
;

보다 최적의 계획으로 올바른 결과 (반복되지 않음)가 생성됩니다.

병합 계획 2

계획은 대상 테이블에서 카테고리 2에 대한 행만 읽습니다. 대상 테이블이 큰 경우 이는 중요한 성능 고려 사항이 될 수 있지만 MERGE구문을 사용하여이 문제점을 잘못 이해하는 것은 너무 쉽습니다 .

때로는 MERGE별도의 DML 작업으로 작성하기가 더 쉽습니다 . 이 접근 방식은 단일 사람들 보다 더 나은 성능을 발휘할 수 있으며 MERGE, 종종 사람들을 놀라게합니다.

DELETE ci
FROM @CategoryItem AS ci
WHERE ci.CategoryId = 2
AND NOT EXISTS 
(
    SELECT 1 
    FROM @DataSource AS ds 
    WHERE 
        ds.ItemId = ci.ItemId
        AND ds.CategoryId = ci.CategoryId
);

INSERT @CategoryItem
SELECT 
    ds.CategoryId, 
    ds.ItemId
FROM @DataSource AS ds
WHERE
    ds.CategoryId = 2;

나는 이것이 정말로 오래된 질문이라는 것을 알고 있지만 "공통 테이블 표현식을 사용하면 ON 절에 술어를 추가하는 것과 비슷한 위험이 있지만 약간 다른 이유로 발생할 수 있습니다." BOL에도 비슷한 경고가 있습니다. "이 방법은 ON 절에 추가 검색 기준을 지정하는 것과 유사하며 잘못된 결과를 생성 할 수 있습니다.이 방법을 사용하지 않는 것이 좋습니다." CTE 방법은 사용 사례를 해결하는 것처럼 보이지만 고려하지 않은 시나리오가 있는지 궁금합니다.
— Henry Lee
당사 사이트를 사용함과 동시에 당사의 쿠키 정책과 개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.