문 교착 상태 자체 병합


22

다음 절차가 있습니다 (SQL Server 2008 R2).

create procedure usp_SaveCompanyUserData
    @companyId bigint,
    @userId bigint,
    @dataTable tt_CoUserdata readonly
as
begin

    set nocount, xact_abort on;

    merge CompanyUser with (holdlock) as r
    using (
        select 
            @companyId as CompanyId, 
            @userId as UserId, 
            MyKey, 
            MyValue
        from @dataTable) as newData
    on r.CompanyId = newData.CompanyId
        and r.UserId = newData.UserId
        and r.MyKey = newData.MyKey
    when not matched then
        insert (CompanyId, UserId, MyKey, MyValue) values
        (@companyId, @userId, newData.MyKey, newData.MyValue);

end;

CompanyId, UserId, MyKey는 대상 테이블의 복합 키를 형성합니다. CompanyId는 부모 테이블의 외래 키입니다. 또한에 비 클러스터형 인덱스가 CompanyId asc, UserId asc있습니다.

많은 다른 스레드에서 호출 되며이 동일한 명령문을 호출하는 다른 프로세스간에 교착 상태가 지속적으로 발생합니다. 삽입 / 업데이트 경쟁 조건 오류를 방지하기 위해 "with (holdlock)"이 필요하다는 것을 이해했습니다.

두 개의 다른 스레드가 제약 조건을 검증 할 때 서로 다른 순서로 행 (또는 페이지)을 잠그고 교착 상태라고 가정합니다.

이것이 올바른 가정입니까?

이 상황을 해결하는 가장 좋은 방법은 무엇입니까 (예 : 교착 상태 없음, 멀티 스레드 성능에 미치는 영향 최소화)?

쿼리 계획 이미지 (새 탭에서 이미지를 볼 경우 읽을 수 있습니다. 작은 크기로 죄송합니다.)

  • @datatable에는 최대 28 개의 행이 있습니다.
  • 코드를 추적 한 결과 거래를 시작한 곳을 볼 수 없습니다.
  • 외래 키는 삭제시에만 계단식으로 설정되며 상위 테이블에서 삭제되지 않았습니다.

답변:


12

좋아, 두 번 이상 모든 것을 본 후에, 나는 당신의 기본 가정이 맞다고 생각합니다. 아마도 여기서 일어날 일은 :

  1. MERGE의 MATCH 부분은 인덱스가 일치하는지 확인하여 행 / 페이지가 읽 히면 읽기 잠금합니다.

  2. 일치하지 않는 행이 있으면 먼저 새로운 인덱스 행을 삽입하려고 시도하여 행 / 페이지 쓰기 잠금을 요청합니다 ...

그러나 다른 사용자가 같은 행 / 페이지에서 1 단계를 수행 한 경우 첫 번째 사용자는 업데이트에서 차단되며 ...

두 번째 사용자도 같은 페이지에 삽입해야하는 경우 교착 상태에 있습니다.

AFAIK,이 절차로 교착 상태를 얻을 수 없으며 MERGE에 TABLOCKX 힌트를 추가하는 것이 100 % 확신 할 수있는 유일한 간단한 방법이 있지만 성능에 실제로 나쁜 영향을 미칩니다.

이다 가능한 대신 TABLOCK 힌트를 추가하는 것은 큰 귀하의 성능에 영향없이 문제를 해결하기에 충분 것이다.

마지막으로 PAGLOCK, XLOCK 또는 PAGLOCK 및 XLOCK을 모두 추가 할 수도 있습니다. 다시 그 힘의 작업과 성능 수도 너무 끔찍하지. 당신은 그것을보고 시도해야합니다.


여기서 스냅 샷 격리 수준 (행 버전 관리)이 도움이 될 수 있다고 생각하십니까?
Mikael Eriksson

아마도. 또는 교착 상태 예외를 동시성 예외로 바꿀 수 있습니다.
RBarryYoung

2
INSERT 문의 대상인 테이블에 TABLOCK 힌트를 지정하면 TABLOCKX 힌트를 지정하는 것과 동일한 효과가 있습니다. (출처 : msdn.microsoft.com/en-us/library/bb510625.aspx )
tuespetre

31

테이블 변수가 하나의 값만 보유한 경우에는 문제가 없습니다. 여러 행을 사용하면 교착 상태에 대한 새로운 가능성이 있습니다. 동일한 회사에 대해 (1, 2) 및 (2, 1)을 포함하는 테이블 변수로 실행되는 두 개의 동시 프로세스 (A & B)가 있다고 가정하십시오.

프로세스 A는 대상을 읽고 행을 찾은 다음 값 '1'을 삽입합니다. 값 '1'에 독점 행 잠금을 보유합니다. 프로세스 B는 대상을 읽고 행을 찾은 다음 값 '2'를 삽입합니다. 값 '2'에 독점 행 잠금을 보유합니다.

이제 프로세스 A는 행 2를 처리해야하고 프로세스 B는 행 1을 처리해야합니다. 두 프로세스 모두 다른 프로세스가 보유한 독점 잠금과 호환되지 않는 잠금이 필요하기 때문에 진행할 수 없습니다.

여러 행이있는 교착 상태를 피하려면 매번 같은 순서로 행을 처리하고 테이블에 액세스해야합니다 . 질문에 표시된 실행 계획의 테이블 변수는 힙이므로 행에 본질적인 순서가 없습니다 (이는 보장되지 않지만 삽입 순서로 읽을 가능성이 높습니다).

기존 계획

일관된 행 처리 순서가 없으면 교착 상태가 발생합니다. 두 번째 고려 사항은 키 고유성 보장이 없다는 것은 올바른 할로윈 보호를 제공하기 위해 테이블 ​​스풀이 필요하다는 것을 의미합니다. 스풀은 열성적인 스풀입니다. 즉, 모든 행이 삽입 연산자에 대해 읽고 재생되기 전에 tempdb 작업 테이블에 기록됩니다 .

TYPE클러스터링을 포함하도록 테이블 변수를 재정의합니다 PRIMARY KEY.

DROP TYPE dbo.CoUserData;

CREATE TYPE dbo.CoUserData
AS TABLE
(
    MyKey   integer NOT NULL PRIMARY KEY CLUSTERED,
    MyValue integer NOT NULL
);

실행 계획에 클러스터 된 인덱스의 스캔이 표시되고 고유성 보장으로 옵티마이 저가 테이블 스풀을 안전하게 제거 할 수 있습니다.

기본 키로

MERGE128 개 스레드 에서 명령문을 5000 회 반복 한 테스트 에서 클러스터 된 테이블 변수에 교착 상태가 발생하지 않았습니다. 나는 이것이 관찰에 기초한 것임을 강조해야한다. 클러스터 된 테이블 변수는 기술적 으로 행을 다양한 순서로 생성 할 수 있지만 일관된 순서의 가능성이 크게 향상됩니다. 물론 모든 새로운 누적 업데이트, 서비스 팩 또는 새 버전의 SQL Server에 대해 관찰 된 동작을 다시 테스트해야합니다.

테이블 변수 정의를 변경할 수없는 경우 다른 대안이 있습니다.

MERGE dbo.CompanyUser AS R
USING 
    (SELECT DISTINCT MyKey, MyValue FROM @DataTable) AS NewData ON
    R.CompanyId = @CompanyID
    AND R.UserID = @UserID
    AND R.MyKey = NewData.MyKey
WHEN NOT MATCHED THEN 
    INSERT 
        (CompanyID, UserID, MyKey, MyValue) 
    VALUES
        (@CompanyID, @UserID, NewData.MyKey, NewData.MyValue)
OPTION (ORDER GROUP);

또한 명시적인 정렬을 도입하면서 스풀 (및 행 순서 일관성)을 제거 할 수 있습니다.

계획 정렬

이 계획은 또한 동일한 테스트를 사용하여 교착 상태를 만들지 않았습니다. 아래의 복제 스크립트 :

CREATE TYPE dbo.CoUserData
AS TABLE
(
    MyKey   integer NOT NULL /* PRIMARY KEY */,
    MyValue integer NOT NULL
);
GO
CREATE TABLE dbo.Company
(
    CompanyID   integer NOT NULL

    CONSTRAINT PK_Company
        PRIMARY KEY (CompanyID)
);
GO
CREATE TABLE dbo.CompanyUser
(
    CompanyID   integer NOT NULL,
    UserID      integer NOT NULL,
    MyKey       integer NOT NULL,
    MyValue     integer NOT NULL

    CONSTRAINT PK_CompanyUser
        PRIMARY KEY CLUSTERED
            (CompanyID, UserID, MyKey),

    FOREIGN KEY (CompanyID)
        REFERENCES dbo.Company (CompanyID),
);
GO
CREATE NONCLUSTERED INDEX nc1
ON dbo.CompanyUser (CompanyID, UserID);
GO
INSERT dbo.Company (CompanyID) VALUES (1);
GO
DECLARE 
    @DataTable AS dbo.CoUserData,
    @CompanyID integer = 1,
    @UserID integer = 1;

INSERT @DataTable
SELECT TOP (10)
    V.MyKey,
    V.MyValue
FROM
(
    VALUES
        (1, 1),
        (2, 2),
        (3, 3),
        (4, 4),
        (5, 5),
        (6, 6),
        (7, 7),
        (8, 8),
        (9, 9)
) AS V (MyKey, MyValue)
ORDER BY NEWID();

BEGIN TRANSACTION;

    -- Test MERGE statement here

ROLLBACK TRANSACTION;

8

SQL_Kiwi가 매우 훌륭한 분석을 제공했다고 생각합니다. 데이터베이스에서 문제를 해결해야하는 경우 그의 제안을 따라야합니다. 물론 업그레이드하거나 서비스 팩을 적용하거나 인덱스 또는 인덱싱 된 뷰를 추가 / 변경할 때마다 여전히 작동하는지 다시 테스트해야합니다.

다른 세 가지 대안이 있습니다.

  1. 삽입이 충돌하지 않도록 삽입을 직렬화 할 수 있습니다. 트랜잭션 시작시 sp_getapplock을 호출하고 MERGE를 실행하기 전에 독점 잠금을 획득 할 수 있습니다. 물론 스트레스 테스트를해야합니다.

  2. 하나의 스레드가 모든 삽입을 처리하도록하여 앱 서버가 동시성을 처리하도록 할 수 있습니다.

  3. 교착 상태 후에 자동으로 재 시도 할 수 있습니다. 동시성이 높은 경우 가장 느린 방법 일 수 있습니다.

어느 쪽이든 솔루션 만이 성능에 미치는 영향을 결정할 수 있습니다.

일반적으로 시스템에 교착 상태가 발생하지는 않지만 교착 상태가 발생하지는 않습니다. 2011 년에 우리는 한 번의 배포에서 실수를했으며 같은 시나리오에 따라 몇 시간 만에 교착 상태가 발생했습니다. 나는 그것을 곧 고쳤다. 그리고 그것은 그 해의 모든 교착 상태였다.

우리는 대부분 시스템에서 접근법 1을 사용하고 있습니다. 그것은 우리에게 정말 잘 작동합니다.


-1

또 다른 가능한 접근 방법-때때로 잠금 및 성능 문제가있는 병합을 발견했습니다. 옵션 (MaxDop x) 쿼리 옵션을 사용하는 것이 좋습니다.

희미하고 먼 과거 SQL Server에는 Insert Row Level Locking 옵션이 있었지만 이것은 사망 한 것으로 보이지만 ID가있는 클러스터 PK는 삽입을 깨끗하게 실행해야합니다.

당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.