조건부 INSERT 및 SELECT보다 OUTPUT이있는 MERGE가 더 나은 방법입니까?


12

"존재하지 않는 경우 삽입"상황이 종종 발생합니다. Dan Guzman의 블로그 에는이 프로세스를 스레드로부터 안전하게 만드는 방법에 대한 훌륭한 조사가 있습니다.

문자열을 a의 정수로 간단히 카탈로그 화하는 기본 테이블이 SEQUENCE있습니다. 저장 프로 시저에서 값이있는 경우 정수 키를 INSERT얻거나 결과 값을 가져와야합니다. dbo.NameLookup.ItemName열에 고유 제약 조건이 있으므로 데이터 무결성이 위험하지 않지만 예외가 발생하지는 않습니다.

그것은 IDENTITY얻을 수 없으므로 특정 경우에는 SCOPE_IDENTITY가치가 될 수 있습니다 NULL.

내 상황에서 나는 INSERT테이블의 안전 만 다루어야 하므로 MERGE다음과 같이 사용하는 것이 더 나은지 결정하려고합니다 .

SET NOCOUNT, XACT_ABORT ON;

DECLARE @vValueId INT 
DECLARE @inserted AS TABLE (Id INT NOT NULL)

MERGE 
    dbo.NameLookup WITH (HOLDLOCK) AS f 
USING 
    (SELECT @vName AS val WHERE @vName IS NOT NULL AND LEN(@vName) > 0) AS new_item
        ON f.ItemName= new_item.val
WHEN MATCHED THEN
    UPDATE SET @vValueId = f.Id
WHEN NOT MATCHED BY TARGET THEN
    INSERT
      (ItemName)
    VALUES
      (@vName)
OUTPUT inserted.Id AS Id INTO @inserted;
SELECT @vValueId = s.Id FROM @inserted AS s

내가 사용하는 우유없는이 작업을 수행 할 수있는 MERGE단지 조건으로 INSERTa로 다음 SELECT 나는이 두 번째 방법은 독자에게 명확하다고 생각,하지만 난 그것의 "더 나은"연습을 확신 아니에요

SET NOCOUNT, XACT_ABORT ON;

INSERT INTO 
    dbo.NameLookup (ItemName)
SELECT
    @vName
WHERE
    NOT EXISTS (SELECT * FROM dbo.NameLookup AS t WHERE @vName IS NOT NULL AND LEN(@vName) > 0 AND t.ItemName = @vName)

DECLARE @vValueId int;
SELECT @vValueId = i.Id FROM dbo.NameLookup AS i WHERE i.ItemName = @vName

아니면 내가 고려하지 않은 더 좋은 방법이있을 수 있습니다.

다른 질문을 검색하고 참조했습니다. 이 중 하나 : https : //.com/questions/5288283/sql-server-insert-if-not-exists-best-practice 은 내가 찾을 수있는 가장 적합하지만 내 사용 사례에는 적용되지 않는 것 같습니다. IF NOT EXISTS() THEN내가 생각하지 않는 접근법에 대한 다른 질문 은 허용됩니다.


버퍼보다 큰 테이블을 실험 해 보셨습니까? 테이블이 특정 크기에 도달하면 병합 성능이 저하되는 경험이 있습니다.
pacreely

답변:


8

시퀀스를 사용하고 있으므로 기본 키 필드 의 기본 제약 조건에있는 것과 동일한 NEXT VALUE FOR 기능 Id을 사용하여 Id미리 새 값 을 생성 할 수 있습니다. 값을 먼저 생성한다는 것은을 갖지 않아도 될 필요가 없다는 것을 SCOPE_IDENTITY의미하며, 따라서 새로운 값을 얻기 위해 OUTPUT절이 필요하거나 추가 작업을 수행 할 필요가 없습니다 SELECT. 당신은 당신이하기 전에 가치를 가질 것이고 INSERT, 당신은 심지어 엉망으로 만들 필요가 없습니다 SET IDENTITY INSERT ON / OFF:-)

전체 상황의 일부를 처리합니다. 다른 부분은 정확히 두 개의 프로세스의 동시성 문제를 동시에 처리하고 정확히 동일한 문자열에 대한 기존 행을 찾지 않고 INSERT. 문제는 발생할 수있는 고유 제약 조건 위반을 피하는 것입니다.

이러한 유형의 동시성 문제를 처리하는 한 가지 방법은이 특정 작업을 단일 스레드로 만드는 것입니다. 이를 수행하는 방법은 응용 프로그램 잠금 (세션 전체에서 작동)을 사용하는 것입니다. 효과적이지만 충돌 빈도가 상당히 낮은 상황과 같은 상황에서는 약간 무겁습니다.

충돌을 처리하는 다른 방법은 충돌을 피하기보다는 충돌이 발생할 수 있음을 인정하는 것입니다. TRY...CATCH구문을 사용하면 특정 오류 (이 경우 "고유 제약 조건 위반", Msg 2601)를 효과적으로 포착하고 해당 특정 블록 으로 인해 현재 존재한다는 것을 알기 때문에 값 SELECT을 다시 실행 하여 Id값 을 얻을 수 있습니다. CATCH오류. 다른 오류는 일반적으로 처리 할 수 있습니다 RAISERROR/ RETURN또는 THROW방법.

테스트 설정 : 시퀀스, 테이블 및 고유 인덱스

USE [tempdb];

CREATE SEQUENCE dbo.MagicNumber
  AS INT
  START WITH 1
  INCREMENT BY 1;

CREATE TABLE dbo.NameLookup
(
  [Id] INT NOT NULL
         CONSTRAINT [PK_NameLookup] PRIMARY KEY CLUSTERED
        CONSTRAINT [DF_NameLookup_Id] DEFAULT (NEXT VALUE FOR dbo.MagicNumber),
  [ItemName] NVARCHAR(50) NOT NULL         
);

CREATE UNIQUE NONCLUSTERED INDEX [UIX_NameLookup_ItemName]
  ON dbo.NameLookup ([ItemName]);
GO

테스트 설정 : 저장 프로 시저

CREATE PROCEDURE dbo.GetOrInsertName
(
  @SomeName NVARCHAR(50),
  @ID INT OUTPUT,
  @TestRaceCondition BIT = 0
)
AS
SET NOCOUNT ON;

BEGIN TRY
  SELECT @ID = nl.[Id]
  FROM   dbo.NameLookup nl
  WHERE  nl.[ItemName] = @SomeName
  AND    @TestRaceCondition = 0;

  IF (@ID IS NULL)
  BEGIN
    SET @ID = NEXT VALUE FOR dbo.MagicNumber;

    INSERT INTO dbo.NameLookup ([Id], [ItemName])
    VALUES (@ID, @SomeName);
  END;
END TRY
BEGIN CATCH
  IF (ERROR_NUMBER() = 2601) -- "Cannot insert duplicate key row in object"
  BEGIN
    SELECT @ID = nl.[Id]
    FROM   dbo.NameLookup nl
    WHERE  nl.[ItemName] = @SomeName;
  END;
  ELSE
  BEGIN
    ;THROW; -- SQL Server 2012 or newer
    /*
    DECLARE @ErrorNumber INT = ERROR_NUMBER(),
            @ErrorMessage NVARCHAR(4000) = ERROR_MESSAGE();

    RAISERROR(N'Msg %d: %s', 16, 1, @ErrorNumber, @ErrorMessage);
    RETURN;
    */
  END;

END CATCH;
GO

시험

DECLARE @ItemID INT;
EXEC dbo.GetOrInsertName
  @SomeName = N'test1',
  @ID = @ItemID OUTPUT;
SELECT @ItemID AS [ItemID];
GO

DECLARE @ItemID INT;
EXEC dbo.GetOrInsertName
  @SomeName = N'test1',
  @ID = @ItemID OUTPUT,
  @TestRaceCondition = 1;
SELECT @ItemID AS [ItemID];
GO

OP로부터의 질문

왜 이것보다 낫 MERGE습니까? 절 TRY을 사용 하지 않으면 동일한 기능 을 사용할 수 WHERE NOT EXISTS없습니까?

MERGE다양한 "문제"가 있습니다 (여러 참조는 @SqlZim의 답변에 연결되어 있으므로 여기에서 해당 정보를 복제 할 필요가 없습니다). 그리고이 접근 방식에는 추가적인 잠금이 없으므로 (경쟁이 적음) 동시성에 더 좋습니다. 이 접근 방식에서는 고유 제약 조건 위반이 전혀 발생하지 않으며 모두 HOLDLOCK등이 없습니다 . 작동하는 것이 거의 보장됩니다.

이 접근 방식의 이유는 다음과 같습니다.

  1. 충돌에 대해 걱정할 필요가있을 정도로이 절차를 충분히 실행 한 경우 다음을 원하지 않습니다.
    1. 필요한 것보다 더 많은 조치를 취하십시오
    2. 필요 이상으로 모든 자원에 대한 잠금 유지
  2. 새로운 항목 ( 정확히 동시에 제출 된 새로운 항목)에서만 충돌이 발생할 수 있기 때문에 CATCH처음에 블록 으로 떨어지는 빈도는 매우 낮습니다. 1 %의 시간을 실행하는 코드 대신 99 %의 시간을 실행하는 코드를 최적화하는 것이 더 합리적입니다.

@SqlZim의 답변에 대한 의견 (강조 추가)

나는 개인적으로 가능한 한 그렇게하지 않도록 솔루션을 시도하고 조정하는 것을 선호한다 . 이 경우 잠금 장치를 사용하는 serializable것이 어려운 방법 이라고 생각하지 않으며 높은 동시성을 잘 처리 할 것이라고 확신합니다.

나는 "그리고 신중할 때"라고 개정하기 위해이 첫 문장에 동의 할 것입니다. 기술적으로 가능한 것이 있다고해서 상황 (예 : 의도 된 사용 사례)이 혜택을받을 것이라는 의미는 아닙니다.

이 접근 방식에서 볼 수있는 문제는 제안 된 것보다 더 많이 잠겨 있다는 것입니다. "직렬화 가능"에 대해 인용 된 문서, 특히 다음을 강조하여 읽어야합니다 (강조 추가).

  • 다른 트랜잭션은 현재 트랜잭션이 완료 될 때까지 현재 트랜잭션의 명령문이 읽은 키 범위에 해당하는 키 값을 가진 새 행을 삽입 할 수 없습니다 .

다음은 예제 코드의 주석입니다.

SELECT [Id]
FROM   dbo.NameLookup WITH (SERIALIZABLE) /* hold that key range for @vName */

작동 단어에는 "범위"가 있습니다. 잠금은의 값에만 국한된 @vName것이 아니라보다 정확하게 시작 되는 범위입니다 .이 새로운 값이 들어가야하는 위치 (즉, 새로운 값이 맞는 곳의 기존 키 값 사이)이지만 값 자체는 아닙니다. 즉, 현재 조회중인 값에 따라 다른 프로세스가 새 값을 삽입하지 못하도록 차단됩니다. 조회가 범위 상단에서 수행되는 경우 동일한 위치를 차지할 수있는 것을 삽입하면 차단됩니다. 예를 들어, "a", "b"및 "d"값이 존재하는 경우 한 프로세스가 "f"에서 SELECT를 수행하는 경우 "g"또는 "e"값을 삽입 할 수 없습니다 ( 그 중 하나가 "d"바로 뒤에 올 것입니다.) 그러나 "예약 된"범위에 위치하지 않으므로 "c"값을 삽입 할 수 있습니다.

다음 예제는이 동작을 보여줍니다.

(쿼리 탭 (예 : 세션) # 1)

INSERT INTO dbo.NameLookup ([ItemName]) VALUES (N'test5');

BEGIN TRAN;

SELECT [Id]
FROM   dbo.NameLookup WITH (SERIALIZABLE) /* hold that key range for @vName */
WHERE  ItemName = N'test8';

--ROLLBACK;

(쿼리 탭 (예 : 세션) # 2)

EXEC dbo.NameLookup_getset_byName @vName = N'test4';
-- works just fine

EXEC dbo.NameLookup_getset_byName @vName = N'test9';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1

EXEC dbo.NameLookup_getset_byName @vName = N'test7';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1

EXEC dbo.NameLookup_getset_byName @vName = N's';
-- works just fine

EXEC dbo.NameLookup_getset_byName @vName = N'u';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1

마찬가지로, 값 "C"가 존재하고 값 "A"가 선택되어 (따라서 잠긴 경우) "D"값을 삽입 할 수 있지만 "B"값은 삽입 할 수 없습니다.

(쿼리 탭 (예 : 세션) # 1)

INSERT INTO dbo.NameLookup ([ItemName]) VALUES (N'testC');

BEGIN TRAN

SELECT [Id]
FROM   dbo.NameLookup WITH (SERIALIZABLE) /* hold that key range for @vName */
WHERE  ItemName = N'testA';

--ROLLBACK;

(쿼리 탭 (예 : 세션) # 2)

EXEC dbo.NameLookup_getset_byName @vName = N'testD';
-- works just fine

EXEC dbo.NameLookup_getset_byName @vName = N'testB';
-- hangs until you either hit "cancel" in this query tab,
-- OR issue a COMMIT or ROLLBACK in query tab #1

공평하게, 나의 제안 된 접근법에서, 예외가있을 때,이 "직렬화 가능한 트랜잭션"접근법에서는 일어나지 않을 트랜잭션 로그에 4 개의 엔트리가있을 것입니다. 그러나 위에서 언급했듯이 예외가 1 % (또는 5 %)의 시간이 발생하면 초기 SELECT가 일시적으로 INSERT 작업을 차단하는 경우보다 훨씬 적은 영향을 미칩니다.

이 "직렬화 가능한 트랜잭션 + OUTPUT 절"접근 방식의 또 다른 사소한 문제는 OUTPUT(현재 사용중인) 절이 데이터를 결과 세트로 다시 보내는 것입니다. 결과 집합에는 간단한 OUTPUT매개 변수 보다 더 많은 오버 헤드 (아마도 양쪽에서 : 내부 커서를 관리하는 SQL Server 및 DataReader 개체를 관리하는 응용 프로그램 계층)가 더 필요합니다 . 단일 스칼라 값만 처리하고 있고 실행 빈도가 높다고 가정하면 결과 집합의 추가 오버 헤드가 더해질 수 있습니다.

OUTPUT절은 OUTPUT매개 변수 를 리턴하는 방식으로 사용될 수 있지만 임시 테이블 또는 테이블 변수를 작성하고 해당 임시 테이블 / 테이블 변수의 값을 OUTPUT매개 변수로 선택하려면 추가 단계가 필요합니다 .

추가 설명 : @SqlZim의 응답에 대한 응답 (업데이트 된 답변) @SqlZim의 응답에 대한 응답 (원래 답변으로) 동시성 및 성능에 관한 내 진술에 대한 ;-)

이 부분이 길이가 길면 미안하지만이 시점에서 우리는 두 가지 접근 방식의 뉘앙스에 달려 있습니다.

나는 정보가 제시된 방식 serializable이 원래의 질문에 제시된 시나리오에서 사용할 때 발생할 수있는 잠금의 양에 대한 잘못된 가정으로 이어질 수 있다고 생각합니다 .

예, 공정하지만 편견임을 인정합니다.

  1. 인간이 최소한 어느 정도 편견을 갖지 못하는 것은 불가능하며 최소한 최소한도를 유지하려고 노력합니다.
  2. 주어진 예는 단순하지만, 과도하게 복잡하게하지 않으면 서 행동을 전달하기위한 예시적인 목적으로 사용되었습니다. 과도한 빈도를 암시하지는 않았지만 명시 적으로 달리 명시하지 않았으며 실제로 존재하는 것보다 더 큰 문제를 암시하는 것으로 읽을 수 있음을 이해합니다. 나는 아래를 명확히하려고 노력할 것이다.
  3. 또한 두 개의 기존 키 (두 번째 "쿼리 탭 1"및 "쿼리 탭 2"블록) 사이의 범위를 잠그는 예도 포함 시켰습니다.
  4. 나는 INSERT고유 제약 조건 위반으로 인해 실패 할 때마다 4 개의 추가 Tran Log 항목이 있다는 접근 방식의 "숨겨진 비용"을 찾았습니다 . 나는 다른 답변 / 게시물에서 언급 한 것을 보지 못했습니다.

@gbn의 "JFDI"접근 방식과 관련하여 Michael J. Swart의 "The Ugly Pragmatism For The Win"게시물과 Aaron Bertrand의 Michael 게시물에 대한 의견 (어떤 시나리오에서 성능이 저하되었는지를 보여주는 테스트와 관련)과 "Michael J의 적응에 대한 귀하의 의견 @gbn의 Try Catch JFDI 절차에 대한 Stewart의 적응 ":

기존 값을 선택하는 것보다 새 값을 더 자주 삽입하는 경우 @srutzky 버전보다 성능이 우수 할 수 있습니다. 그렇지 않으면이 버전보다 @srutzky 버전을 선호합니다.

"JFDI"접근법과 관련된 gbn / Michael / Aaron 토론과 관련하여 gbn의 "JFDI"접근법에 대한 제안을 동일시하는 것은 올바르지 않습니다. " SELECT가져 오기 또는 삽입"작업의 특성으로 인해 ID기존 레코드 의 값 을 가져 오려면 명시 적으로 수행해야 합니다. 이 SELECT는 IF EXISTS검사 역할을 하므로이 검사 방식은 Aaron 테스트의 "CheckTryCatch"변형과 동일합니다. Michael의 재 작성 코드 (Michael의 적응에 대한 최종 적응)에는 WHERE NOT EXISTS먼저 동일한 점검을 수행하는 것이 포함됩니다 . 따라서 내 제안 (Michael의 최종 코드 및 최종 코드의 적응과 함께)은 실제로 그 CATCH모든 블록에 실제로 영향을 미치지 않습니다 . 두 개의 세션이있는 상황 만 가능합니다.ItemNameINSERT...SELECT정확히 같은 순간에 두 세션이 모두 같은 순간에 "참"을 받고 WHERE NOT EXISTS, 따라서 두 순간이 모두 정확히 같은 순간에 시도합니다 INSERT. 이 특정 시나리오 는 다른 프로세스가 정확히 같은 순간에 시도하지 않을 때 기존 시나리오를 선택 ItemName하거나 새 것을 삽입하는 것보다 훨씬 덜 자주 발생합니다 .ItemName

위의 모든 것을 염두에두고 : 왜 내 접근 방식을 선호합니까?

먼저 "직렬화 가능"접근 방식에서 잠금이 발생하는 것을 살펴 보자. 위에서 언급했듯이 잠긴 "범위"는 새 키 값이 맞는 위치의 기존 키 값에 따라 다릅니다. 해당 방향으로 기존 키 값이없는 경우 범위의 시작 또는 끝은 각각 색인의 시작 또는 끝일 수도 있습니다. 다음 색인과 키가 있다고 가정합니다 (색인 ^의 시작을 $나타내며 끝 을 나타냄).

Range #:    |--- 1 ---|--- 2 ---|--- 3 ---|--- 4 ---|
Key Value:  ^         C         F         J         $

세션 55가 다음과 같은 키 값을 삽입하려고 시도하는 경우 :

  • A그런 다음 범위 # 1 (에서 ^~까지 C)이 잠 깁니다. 세션 56은 B고유하고 유효한 (아직) 인 경우에도 값을 삽입 할 수 없습니다 . 그러나 세션 (56)의 값을 삽입 할 수 있습니다 D, G하고 M.
  • D그런 다음 범위 # 2 (에서 C~까지 F)가 잠 깁니다. 세션 56은 E(아직) 값을 삽입 할 수 없습니다 . 그러나 세션 (56)의 값을 삽입 할 수 있습니다 A, G하고 M.
  • M그런 다음 범위 # 4 (에서 J~까지 $)가 잠 깁니다. 세션 56은 X(아직) 값을 삽입 할 수 없습니다 . 그러나 세션 (56)의 값을 삽입 할 수 있습니다 A, D하고 G.

더 많은 키 값이 추가됨에 따라 키 값 사이의 범위가 좁아 지므로 같은 범위에서 싸우는 동시에 여러 값이 삽입 될 확률 / 빈도가 줄어 듭니다. 분명히 이것은 문제 가 아니며 , 다행히 시간이 지남에 따라 실제로 감소하는 문제인 것처럼 보입니다.

내 접근 방식의 문제는 위에서 설명했습니다. 두 세션이 동일한 키 값을 동시에 삽입하려고 할 때만 발생합니다 . 이와 관련하여 그것은 일어날 가능성이 더 높은 것입니다. 두 개의 서로 다르지만 가까운 키 값이 동시에 시도되거나 동일한 키 값이 동시에 시도됩니까? 대답은 삽입을하는 응용 프로그램의 구조에 있다고 생각하지만 일반적으로 말하자면 동일한 범위를 공유하는 두 가지 다른 값이 삽입 될 가능성이 더 높다고 가정합니다. 그러나 실제로 아는 유일한 방법은 OP 시스템에서 두 가지를 모두 테스트하는 것입니다.

다음으로 두 가지 시나리오와 각 접근 방식이 시나리오를 처리하는 방법을 살펴 보겠습니다.

  1. 고유 키 값에 대한 모든 요청 :

    이 경우 CATCH제 제안 의 블록은 절대 입력되지 않으므로 "문제"(예 : 4 개의 트랜 로그 항목과 처리 시간)가 입력되지 않습니다. 그러나 "직렬화 가능"방식에서는 모든 인서트가 독창적 임에도 불구하고 항상 같은 범위에서 다른 인서트를 막을 가능성이 항상 있습니다 (매우 길지는 않지만).

  2. 동일한 키 값에 대한 요청 빈도가 높은 빈도 :

    이 경우, 존재하지 않는 키 값에 대한 수신 요청 측면에서 매우 낮은 수준의 고유성으로 인해 CATCH제안 의 블록이 정기적으로 입력됩니다. 이로 인해 실패한 각 삽입은 자동 롤백하고 4 개의 항목을 트랜잭션 로그에 기록해야합니다. 이는 매번 약간의 성능 저하입니다. 그러나 전체 작업이 실패해서는 안됩니다 (적어도 이것으로 인한 것이 아닙니다).

    이전 버전의 "업데이트 된"접근 방식에 교착 상태가 발생할 수있는 문제가있었습니다.이를 해결하기위한 updlock힌트가 추가되었으며 더 이상 교착 상태가 발생하지 않습니다.그러나 "직렬화 가능"접근 방식 (업데이트되고 최적화 된 버전조차도)에서는 작업이 교착 상태가됩니다. 왜? 이 serializable동작 INSERT은 읽혀 져서 잠긴 범위의 작업 만 방지 하기 때문에 ; SELECT해당 범위에서 작업을 방해하지는 않습니다 .

    serializable접근 방식은,이 경우, 추가 오버 헤드가없는 것 같다, 내가 제안하고있는 것보다 약간 더 수행 할 수 있습니다.

성능에 관한 많은 / 대부분의 토론과 마찬가지로 결과에 영향을 줄 수있는 요소가 너무 많기 때문에 실제로 어떻게 수행되는지에 대한 감각을 가질 수있는 유일한 방법은 대상 환경에서 실행하는 것입니다. 그 시점에서 그것은 의견의 문제가되지 않습니다 :).


7

업데이트 된 답변


@srutzky 님의 답변

이 "직렬화 가능한 트랜잭션 + OUTPUT 절"접근 방식의 또 다른 사소한 문제는 OUTPUT 절 (현재 사용법에서)이 데이터를 결과 세트로 다시 전송한다는 것입니다. 결과 집합에는 간단한 OUTPUT 매개 변수보다 더 많은 오버 헤드 (아마도 양쪽에서 : 내부 커서를 관리하는 SQL Server 및 DataReader 개체를 관리하는 응용 프로그램 계층)가 더 필요합니다. 단일 스칼라 값만 처리하고 있고 실행 빈도가 높다고 가정하면 결과 집합의 추가 오버 헤드가 더해질 수 있습니다.

나는 동의하며, 같은 이유로 prudent 일 때 출력 매개 변수를 사용합니다 . 초기 답변에 출력 매개 변수를 사용하지 않는 것은 실수였습니다. 게으르고있었습니다.

다음과 함께 출력 매개 변수 추가 최적화를 사용하여 수정 절차 next value for@srutzky는 그의 대답에 설명은 :

create procedure dbo.NameLookup_getset_byName (@vName nvarchar(50), @vValueId int output) as
begin
  set nocount on;
  set xact_abort on;
  set @vValueId = null;
  if nullif(@vName,'') is null                                 
    return;                                        /* if @vName is empty, return early */
  select  @vValueId = Id                                              /* go get the Id */
    from  dbo.NameLookup
    where ItemName = @vName;
  if @vValueId is not null                                 /* if we got the id, return */
    return;
  begin try;                                  /* if it is not there, then get the lock */
    begin tran;
      select  @vValueId = Id
        from  dbo.NameLookup with (updlock, serializable) /* hold key range for @vName */
        where ItemName = @vName;
      if @@rowcount = 0                    /* if we still do not have an Id for @vName */
      begin;                                         /* get a new Id and insert @vName */
        set @vValueId = next value for dbo.IdSequence;      /* get next sequence value */
        insert into dbo.NameLookup (ItemName, Id)
          values (@vName, @vValueId);
      end;
    commit tran;
  end try
  begin catch;
    if @@trancount > 0 
      begin;
        rollback transaction;
        throw;
      end;
  end catch;
end;

업데이트 참고 사항 : updlockselect를 포함하면 이 시나리오에서 적절한 잠금을 얻습니다. 만 사용하는 경우이 교착 상태를 야기 할 수 있다고 지적했다 @srutzky 덕분에, serializableselect.

참고 : 그렇지 않을 수도 있지만, 가능하면 after @vValueId포함 값을 사용하여 프로 시저를 호출 합니다. 그렇지 않으면 제거 할 수 있습니다.set @vValueId = null;set xact_abort on;


키 범위 잠금 동작의 @srutzky의 예와 관련하여 :

@srutzky는 테이블에서 하나의 값만 사용하고 키 범위 잠금을 설명하기 위해 테스트를 위해 "다음"/ "무한대"키를 잠급니다. 그의 테스트는 이러한 상황에서 어떤 일이 발생하는지 보여 주지만, 정보가 제시되는 방식 serializable은 원래 질문에 제시된 시나리오에서 사용할 때 발생할 수있는 잠금의 양에 대한 잘못된 가정으로 이어질 수 있다고 생각합니다 .

그가 키 범위 잠금에 대한 그의 설명과 예를 제시하는 방식에서 편견 (아마도 거짓)을 인식하더라도 여전히 정확합니다.


더 많은 연구 끝에 2011 년부터 Michael J. Swart : Mythbusting : Concurrent Update / Insert Solutions에 의해 특히 관련있는 블로그 기사를 찾았습니다 . 그는 정확성과 동시성을 위해 여러 가지 방법을 테스트합니다. 방법 4 : 증가 된 격리 + 미세 조정 잠금 은 Sam Saffron의 포스트 삽입 또는 업데이트 패턴 (SQL Server 용 ) 및 원래 테스트에서 그의 기대치를 충족시키는 유일한 방법 (나중에 결합 merge with (holdlock))을 기반으로합니다.

2016 년 2 월 Michael J. Swart는 Ugly Pragmatism For The Win을 게시했습니다 . 이 게시물에서 그는 잠금을 줄이기 위해 Saffron upsert 절차에 대해 추가 조정을 수행했습니다 (위의 절차에 포함).

이러한 변경을 한 후 Michael은 자신의 절차가 더 복잡해지기 시작하여 Chris라는 동료와상의 한 것에 대해 기뻐하지 않았습니다. Chris는 원본 Mythbusters 게시물을 모두 읽고 모든 주석을 읽고 @gbn TRY CATCH JFDI 패턴에 대해 물었습니다 . 이 패턴은 @srutzky의 답변과 비슷하며 Michael이 그 인스턴스에서 사용했던 솔루션입니다.

마이클 제이 스와트 :

어제 나는 동시성을 수행하는 가장 좋은 방법에 대해 마음이 바뀌었다. Mythbusting : 동시 업데이트 / 삽입 솔루션에 몇 가지 방법을 설명합니다. 내가 선호하는 방법은 격리 수준을 높이고 미세 조정 잠금을 높이는 것입니다.

적어도 그것은 내 취향이었습니다. 최근에 gbn이 의견에서 제안한 방법을 사용하도록 접근 방식을 변경했습니다. 그는 그의 방법을“TRY CATCH JFDI 패턴”이라고 설명합니다. 일반적으로 나는 그런 해결책을 피합니다. 경험에 따르면 개발자는 제어 흐름에 대한 오류나 예외를 잡는 데 의존해서는 안됩니다. 그러나 나는 어제 그 규칙을 어겼습니다.

그건 그렇고, 패턴 "JFDI"에 대한 gbn의 설명을 좋아합니다. 그것은 시아파 Labeouf의 동기 부여 비디오를 생각 나게한다.


제 생각에는 두 솔루션이 모두 가능합니다. 나는 여전히 격리 수준과 미세 조정 잠금을 높이는 것을 선호하지만 @srutzky의 답변 도 유효하며 특정 상황에서 더 성능이 좋을 수도 있고 그렇지 않을 수도 있습니다.

아마도 앞으로도 Michael J. Swart가했던 것과 같은 결론에 도달 할 것입니다.


필자가 선호하는 것은 아니지만 여기에 Michael J. Stewart가 @gbn의 Try Catch JFDI 절차를 채택한 결과 는 다음과 같습니다.

create procedure dbo.NameLookup_JFDI (
    @vName nvarchar(50)
  , @vValueId int output
  ) as
begin
  set nocount on;
  set xact_abort on;
  set @vValueId = null;
  if nullif(@vName,'') is null                                 
    return;                     /* if @vName is empty, return early */
  begin try                                                 /* JFDI */
    insert into dbo.NameLookup (ItemName)
      select @vName
      where not exists (
        select 1
          from dbo.NameLookup
          where ItemName = @vName);
  end try
  begin catch        /* ignore duplicate key errors, throw the rest */
    if error_number() not in (2601, 2627) throw;
  end catch
  select  @vValueId = Id                              /* get the Id */
    from  dbo.NameLookup
    where ItemName = @vName
  end;

기존 값을 선택하는 것보다 새 값을 더 자주 삽입하는 경우 @srutzky의 version 보다 성능이 우수 할 수 있습니다 . 그렇지 않으면 이 버전 보다 @srutzky 버전 을 선호합니다 .

Michael J Swart의 게시물에 대한 Aaron Bertrand의 의견은 그가 수행 한 관련 테스트와 연결되어이 교환을 이끌었습니다. 추악한 실용주의에 대한 의견 섹션에서 발췌 :

그러나 JFDI는 호출 실패 비율에 따라 전체적으로 성능이 저하되는 경우가 있습니다. 예외를 제기하면 상당한 오버 헤드가 발생합니다. 나는 이것을 몇 개의 게시물에서 보여주었습니다.

http://sqlperformance.com/2012/08/t-sql-queries/error-handling

https://www.mssqltips.com/sqlservertip/2632/checking-for-potential-constraint-violations-before-entering-sql-server-try-and-catch-logic/

Aaron Bertrand의 코멘트 — 2016 년 2 월 11 일 오전 11시 49 분

답글 :

당신은 맞습니다 Aaron, 그리고 우리는 그것을 테스트했습니다.

우리의 경우, 실패한 통화의 백분율은 0이었습니다 (가장 가까운 백분율로 반올림했을 때).

나는 당신이 가능한 한 많이 규칙을 따르는 것에 대해 사례별로 평가한다는 점을 설명한다고 생각합니다.

또한 필수가 아닌 WHERE NOT EXISTS 절을 추가 한 이유도 여기에 있습니다.

Michael J. Swart의 코멘트 — 2016 년 2 월 11 일 오전 11시 57 분


새로운 링크 :


원래 답변


나는 특히 단일 행을 다룰 때 Sam Saffron upsert 접근법을 사용 하는 것을 선호합니다 merge.

이 upsert 방법을 다음과 같이이 상황에 적용합니다.

declare @vName nvarchar(50) = 'Invader';
declare @vValueId int       = null;

if nullif(@vName,'') is not null /* this gets your where condition taken care of before we start doing anything */
begin tran;
  select @vValueId = Id
    from dbo.NameLookup with (serializable) 
    where ItemName = @vName;
  if @@rowcount > 0 
    begin;
      select @vValueId as id;
    end;
    else
    begin;
      insert into dbo.NameLookup (ItemName)
        output inserted.id
          values (@vName);
      end;
commit tran;

나는 당신의 이름과 일치 할 것이고,와 serializable동일하게 holdlock하나를 선택하고 그 사용에서 일관성을 유지하십시오. 나는 serializable그것을 지정할 때 사용 된 것과 같은 이름이기 때문에 사용하는 경향이 있습니다 set transaction isolation level serializable.

값을 사용 serializable하거나 holdlock범위 잠금은 절의 값을 포함하는 값 @vName을 선택하거나 삽입 할 때 다른 조작이 대기하게 dbo.NameLookup하는 값에 따라 수행 where됩니다.

범위 잠금이 제대로 작동하려면 ItemName사용시 적용되는 열에 색인이 있어야합니다 merge.


다음 절차가 어떻게 보이는지입니다 주로 다음과 같은 오류 처리 ERLAND Sommarskog의 백서를 사용 throw. throw오류가 발생하는 방식이 아닌 경우 나머지 절차와 일치하도록 오류를 변경하십시오.

create procedure dbo.NameLookup_getset_byName (@vName nvarchar(50) ) as
begin
  set nocount on;
  set xact_abort on;
  declare @vValueId int;
  if nullif(@vName,'') is null /* if @vName is null or empty, select Id as null */
    begin
      select Id = cast(null as int);
    end 
    else                       /* else go get the Id */
    begin try;
      begin tran;
        select @vValueId = Id
          from dbo.NameLookup with (serializable) /* hold key range for @vName */
          where ItemName = @vName;
        if @@rowcount > 0      /* if we have an Id for @vName select @vValueId */
          begin;
            select @vValueId as Id; 
          end;
          else                     /* else insert @vName and output the new Id */
          begin;
            insert into dbo.NameLookup (ItemName)
              output inserted.Id
                values (@vName);
            end;
      commit tran;
    end try
    begin catch;
      if @@trancount > 0 
        begin;
          rollback transaction;
          throw;
        end;
    end catch;
  end;
go

위의 절차에서 진행중인 작업을 요약하려면 : set nocount on; set xact_abort on;항상 그렇듯이 입력 변수가 is null비어 있거나 select id = cast(null as int)결과가 비어 있는 경우. 그것이 null이거나 비어 있지 않다면, 그 자리 가 없을 때를 대비하여 Id변수 를 가져옵니다 . 이 있으면 보내십시오. 존재하지 않는 경우 삽입하여 new를 보내십시오 .IdId

한편, 동일한 값에 대한 ID를 찾으려고하는이 절차에 대한 다른 호출은 첫 번째 트랜잭션이 완료 될 때까지 기다렸다가 선택 및 반환합니다. 이 절차에 대한 다른 호출이나 다른 값을 찾는 다른 명령문은 계속 진행되지 않으므로 계속 진행됩니다.

@srutzky에 동의하면 충돌을 처리하고 이러한 종류의 문제에 대한 예외를 삼킬 수 있지만 개인적으로 가능한 경우 그렇게하지 않도록 솔루션을 시도하고 조정하는 것을 선호합니다. 이 경우 잠금 장치를 사용하는 serializable것이 어려운 방법 이라고 생각하지 않으며 높은 동시성을 잘 처리 할 것이라고 확신합니다.

테이블 힌트에 대한 SQL Server 설명서에서serializableholdlock 인용 / :

직렬화 가능

HOLDLOCK과 같습니다. 트랜잭션이 완료되었는지 여부에 관계없이 필요한 테이블 또는 데이터 페이지가 더 이상 필요하지 않은 즉시 공유 잠금을 해제하는 대신 트랜잭션이 완료 될 때까지 공유 잠금을 유지하여 공유 잠금을 더 제한적으로 만듭니다. 스캔은 SERIALIZABLE 격리 수준에서 실행되는 트랜잭션과 동일한 의미로 수행됩니다. 격리 수준에 대한 자세한 내용은 SET TRANSACTION ISOLATION LEVEL (Transact-SQL)을 참조하십시오.

트랜잭션 격리 수준에 대한 SQL Server 설명서에서 인용serializable

SERIALIZABLE 다음을 지정합니다.

  • 명령문은 수정되었지만 아직 다른 트랜잭션에 의해 커밋되지 않은 데이터를 읽을 수 없습니다.

  • 다른 트랜잭션은 현재 트랜잭션이 완료 될 때까지 현재 트랜잭션이 읽은 데이터를 수정할 수 없습니다.

  • 다른 트랜잭션은 현재 트랜잭션이 완료 될 때까지 현재 트랜잭션의 명령문이 읽은 키 범위에 해당하는 키 값을 가진 새 행을 삽입 할 수 없습니다.


위의 솔루션과 관련된 링크 :

MERGE는 희미한 역사를 가지고 있으며 코드가 모든 구문에서 원하는 방식으로 작동하는지 확인하기 위해 더 많은 관심을 기울이는 것 같습니다. 관련 merge기사 :

마지막 링크, 켄드라 리틀거친 비교 mergeinsert with left join 그녀는 "나는이에 철저한 부하 테스트를하지 않았다"라는 경고와를,하지만 여전히 좋은 읽기입니다.

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