2009-07-09 5 views
5

기본 UPSERT 기능을 구현하려고하지만, 실제로는 기존 행을 업데이트하고 싶지 않은 경우가 있습니다.조건부 Upsert 저장 프로 시저를 구현하는 방법은 무엇입니까?

기본적으로 나는 다른 저장소간에 데이터를 동기화하려고하는데, Upsert 함수는 이동하는 것처럼 보입니다. 따라서 대체로 Sam Saffron's answer to this question을 비롯하여 다른 연구 및 읽기를 기반으로이 저장 프로 시저를 만들었습니다.

(참고 : MERGE 문은 옵션이 아니므로 MS SQL Server 2005를 사용하고 있습니다.)

CREATE PROCEDURE [dbo].[usp_UpsertItem] 
    -- Add the parameters for the stored procedure here 
    @pContentID varchar(30) = null, 
    @pTitle varchar(255) = null, 
    @pTeaser varchar(255) = null 
AS 
BEGIN 
    -- SET NOCOUNT ON added to prevent extra result sets from 
    -- interfering with SELECT statements. 
    SET NOCOUNT ON; 

    BEGIN TRANSACTION 

     UPDATE dbo.Item WITH (SERIALIZABLE) 
     SET Title = @pTitle, 
      Teaser = @pTeaser 
     WHERE ContentID = @pContentID 

     IF @@rowcount = 0 
      INSERT INTO dbo.Item (ContentID, Title, Teaser) 
      VALUES (@pContentID, @pTitle, @pTeaser) 

    COMMIT TRANSACTION 
END 

저는 기본적인 Upsert에 만족하지만 실제 업데이트는 다른 열의 값에 조건부로하고 싶습니다. Upsert 절차에 의해 더 이상의 업데이트가 수행되지 않도록 행을 "잠그는"것으로 생각하십시오. 이미 존재하는 행을 삽입하려고하지만 때문에 업데이트되지 않은 경우

UPDATE dbo.Item WITH (SERIALIZABLE) 
SET Title = @pTitle, 
    Teaser = @pTeaser 
WHERE ContentID = @pContentID 
AND RowLocked = false 

을하지만 후속 삽입합니다 (콘텐츠 ID 필드) 고유 제약 조건 위반으로 실패 : 그래서 같은 UPDATE 문을 변경할 수 있습니다 그것은 "잠겨"있었습니다.

이렇게하면 더 이상 고전적인 Upsert가 없다는 것을 의미합니다. 즉, 업데이트 또는 삽입 여부를 결정할 때마다 행을 선택해야합니다. 나는 그럴 것이라고 확신한다. 그래서 내가 정말로 묻고있는 것은 트랜잭션 분리 레벨을 올바르게하는 것이 도움이되어 프로 시저가 안전하게 실행될 수 있다는 것이다.

+0

RowLocked 란 무엇입니까 (AND RowLocked = false)? 테이블에있는 열입니까? –

+0

@AlexKuznetsov - 예, RowLocked는 테이블 열로 간주됩니다. 실제로 행이 "잠겨"있어야하는지 (즉,이 절차로 업데이트되지 않음) 지시하는 두 개의 열이 있지만 내 질문을보다 명확하게하려고 SQL을 단순화했습니다. 비록 문법이 조금 엉성함을 느낀다. 물론 "AND RowLocked = 0"이어야하고, 비트 열이라고 언급해야한다. – Matt

답변

0

업데이트/삽입 순서를 전환 할 수 있습니다. 따라서 try/catch 내에서 삽입을 수행하고 제약 조건 위반이 발생하면 업데이트를 수행하십시오. 그래도 조금 더러운 느낌.

+0

나는 항상 "정상적인"처리를 위해 오류 처리기에 의존하지 말아야한다고 생각했다. 즉, 전형적인 유스 케이스가 예외를 발생 시킨다는 것을 안다면 예외를 발생시키기 전에 그 조건을 검사하고 처리해야한다. .그래서 나는 약간의 느낌이 든다는 것에 동의한다.) 내가 고립 수준을 올바르게 얻을 수 있다면 (나는 아직도 읽고있다) 논리가 상당히 간단하다. 그러나 나는 업서 트의 원래 이점을 잃는다. . – Matt

2

나는 지난 몇 년 동안 사용해 왔던이 트릭을 증명하기 위해 다음 스크립트를 함께 사용했다. 사용하는 경우, 용도에 맞게 수정해야합니다. 댓글은 다음과 같습니다 :

/* 
CREATE TABLE Item 
(
    Title  varchar(255) not null 
    ,Teaser  varchar(255) not null 
    ,ContentId varchar(30) not null 
    ,RowLocked bit not null 
) 


UPDATE item 
set RowLocked = 1 
where ContentId = 'Test01' 

*/ 


DECLARE 
    @Check varchar(30) 
,@pContentID varchar(30) 
,@pTitle varchar(255) 
,@pTeaser varchar(255) 

set @pContentID = 'Test01' 
set @pTitle  = 'TestingTitle' 
set @pTeaser = 'TestingTeasier' 

set @check = null 

UPDATE dbo.Item 
set 
    @Check = ContentId 
    ,Title = @pTitle 
    ,Teaser = @pTeaser 
where ContentID = @pContentID 
    and RowLocked = 0 

print isnull(@check, '<check is null>') 

IF @Check is null 
    INSERT dbo.Item (ContentID, Title, Teaser, RowLocked) 
    values (@pContentID, @pTitle, @pTeaser, 0) 

select * from Item 

여기에있는 속임수는 Update 문 내에서 지역 변수에 값을 설정할 수 있다는 것입니다. 위의 경우 "플래그"값은 업데이트가 작동하는 경우에만 설정됩니다 (즉, 업데이트 조건이 충족 됨). 그렇지 않으면 변경되지 않습니다 (여기서 null로 남음).이를 확인하고 그에 따라 처리 할 수 ​​있습니다.

트랜잭션에 대해서는 직렬화가 가능하므로 진행 방법을 제안하기 전에 트랜잭션 내에서 캡슐화해야 할 내용에 대해 자세히 알고 싶습니다.

- 부칙은

가 기본 키 이후이 루틴을 구현하는 철저하고 단단한 방법이 정의 씨 사프란의 아이디어가있다 ----------- 아래 두번째 코멘트에서 후속 외부로 이동하여 데이터베이스로 전달됩니다 (즉, ID 컬럼을 사용하지 않고 있습니다.

나는 (ContentId 열에 기본 키 제약 조건을 추가하고, 트랜잭션에서 UPDATE 및 INSERT를 랩핑하고, 업데이트에 직렬화 가능 힌트를 추가하고) 네가 원하는 모든 작업을 수행해야합니다. 실패한 업데이트는 인덱스의 해당 부분에 대한 범위 잠금을 가볍게 치며 해당 열에 새로운 값을 삽입하려는 동시 시도를 차단합니다. 물론 N 개의 요청이 동시에 제출되면 "첫 번째"는 행을 만들고 두 번째, 세 번째 등 - 행을 따라 "잠금"을 설정하지 않으면 즉시 업데이트됩니다. 좋은 속임수!

(키 열에 인덱스가 없으면 전체 테이블을 잠글 수 있으며 범위 잠금은 새 값의 "양면"에서 행을 잠글 수 있습니다. 나는 그 중 하나를 테스트하지 않았다. 조작의 지속 시간은 [1] 밀리 세컨드 단위이어야하기 때문에 중요하지 않아야한다.)

+0

원래 샘플 코드에서 테이블 Item을 업데이트하지만 MailItem 테이블에 삽입되었습니다. upserts가 같은 테이블에 적용될 예정이 아닌가? –

+0

일치하지 않는 테이블 이름은 오타입니다 (이제 수정 됨). 나는 당신이 SELECT를 사용하여 지역 변수를 설정할 수 있다는 것을 알았지 만, UPDATE로 시도한 적이 없기 때문에 트릭을 할 수 있습니다. 직렬화 가능 트랜잭션에 관해서는, 내 (필연적으로 불완전한) 이해는 당신이 일종의 잠금을 사용하지 않는다면 고유 한 키 제약 조건 위반을 가져올 수 있고 "(직렬화 가능)로 업데이트"하면이 교착 상태가 발생한다는 것입니다. 나는 연결된 질문 (위)의 예에서 일하고 있고, 아직도 읽고있는 것/그것이 내가하는 일을 정확히 이해하는지 확인하려고 노력 중이다. – Matt

+0

위의 의견에 대한 피드백으로 내 답변을 업데이트했습니다. –

8

매우 일반적인 문제. 일부 접근법은 높은 동시성을 유지하지 못합니다. 설명과 스트레스가 여기에 테스트 :

Stress testing UPSERTs

Defensive database programming: eliminating IF statements.

를 그냥 코드를 작성하는 것만으로는 충분하지 않습니다 이러한 경우에, 당신은 높은 동시성에 을 노출해야합니다. 예를 들어, CptSkippy 의 권장 내용을 잘 모르겠지만 다음은 스트레스 테스트 방법을 보여줍니다.

CREATE PROCEDURE Testers.UpsertLoop1 
AS 
BEGIN 
DECLARE @ID INT, @i1 INT, @i2 INT, @count INT, @ret INT; 
SET @count = 0; 
WHILE @count<50000 BEGIN 
     SELECT @ID = COALESCE(MAX(ID),0) + 1 FROM dbo.TwoInts; 
    EXEC @ret=dbo.SaveTwoINTs @ID, 1, 0; 
     SET @count = @count + 1; 
END; 
END; 
GO 
CREATE PROCEDURE Testers.UpsertLoop2 
AS 
BEGIN 
DECLARE @ID INT, @i1 INT, @i2 INT, @count INT, @ret INT; 
SET @count = 0; 
WHILE @count<50000 BEGIN 
     SELECT @ID = COALESCE(MAX(ID),0) + 1 FROM dbo.TwoInts; 
    EXEC @ret=dbo.SaveTwoINTs @ID, 0, 1; 
     SET @count = @count + 1; 
END; 
END; 

는 두 개의 탭에이 절차를 실행하고 오류를 많이 얻을 수 있다는 자신에 대한 참조 :

CREATE TABLE [dbo].[TwoINTs](
     [ID] [int] NOT NULL, 
     [i1] [int] NOT NULL, 
     [i2] [int] NOT NULL, 
     [i3] [int] NOT NULL 
); 
CREATE PROCEDURE dbo.SaveTwoINTs(@ID INT, @i1 INT, @i2 INT) 
AS 
BEGIN 
     SET NOCOUNT ON; 
     SET XACT_ABORT OFF; 
     SET TRANSACTION ISOLATION LEVEL READ COMMITTED; 
     DECLARE @ret INT; 
     SET @ret=0; 
     BEGIN TRAN; 
IF EXISTS(SELECT 1 FROM dbo.TwoINTs WHERE [email protected]) BEGIN 
     UPDATE dbo.TwoINTs WITH (SERIALIZABLE) 
     SET [email protected], [email protected] WHERE [email protected]; 
     SET @[email protected]@ERROR; 
END ELSE BEGIN 
    INSERT INTO dbo.TwoINTs(ID, i1, i2, i3)VALUES(@ID, @i1, @i2, @i1); 
     SET @[email protected]@ERROR; 
END; 
COMMIT; 
RETURN @ret; 
END 
GO 

그 프로 시저를 실행하는 두 개의 루프를 설정합니다 : 테이블과 절차를 설정

Testers.UpsertLoop1 --run in one tab 
Testers.UpsertLoop1 --run in one tab 

Msg 2601, Level 14, State 1, Procedure SaveTwoINTs, Line 15 
Cannot insert duplicate key row in object 'dbo.TwoINTs' with unique index 'UNQ_TwoInts_ID'. 
The statement has been terminated. 

내가 제공 한 링크를 따라 실제로 동시성으로 작동하는 방법을 확인하십시오.

+0

@Alex +1 링크 테스트 및 스트레스 테스트 방법에 대한 조언. 나는 그것을 분명히 시도 할 것이다. – Matt

0

절차 [DBO] usp_UpsertItem] 생성 -. (저장된 여기 절차 @pContentID의 VARCHAR (30) = NULL, @pTitle의 VARCHAR (255) = NULL, @pTeaser의 VARCHAR 대한 255 매개 변수를 추가) = null BEGIN - 의 추가 결과 집합을 방지하기 위해 SET NOCOUNT ON이 추가되어 SELECT 문을 방해합니다. SET NOCOUNT ON;

BEGIN TRANSACTION 
    IF EXISTS (SELECT 1 FROM dbo.Item WHERE ContentID = @pContentID 
      AND RowLocked = false) 
     UPDATE dbo.Item 
     SET Title = @pTitle, Teaser = @pTeaser 
     WHERE ContentID = @pContentID 
      AND RowLocked = false 
    ELSE IF NOT EXISTS (SELECT 1 FROM dbo.Item WHERE ContentID = @pContentID) 
      INSERT INTO dbo.Item (ContentID, Title, Teaser) 
      VALUES (@pContentID, @pTitle, @pTeaser) 

COMMIT TRANSACTION 

END 것은

+2

끔찍한 코드! 동일한 조건부 연산에 대해 쿼리를 두 번 실행하고있을뿐만 아니라 단순한 "else"가 수행 할 때 "else if not exists"를 사용하고 있습니다. 더 좋은 예를 보려면 CptSkippy의 대답을보십시오. – Chris

+0

다른 해결책은 더 깔끔하고 끔찍하고 끔찍한 데 동의합니다 ... 나는 올바른 방향으로 향하고 있었습니까? – JNappi

1
BEGIN TRANSACTION 

IF EXISTS(SELECT 1 FROM dbo.Item WHERE ContentID = @pContentID) 
    UPDATE dbo.Item WITH (SERIALIZABLE) 
    SET Title = @pTitle, Teaser = @pTeaser 
    WHERE ContentID = @pContentID 
    AND RowLocked = false 
ELSE 
    INSERT INTO dbo.Item 
      (ContentID, Title, Teaser) 
    VALUES 
      (@pContentID, @pTitle, @pTeaser) 

COMMIT TRANSACTION 
+0

RowLocked 란 무엇입니까 (AND RowLocked = false)? 테이블에있는 열입니까? –

+1

나는 당신의 접근법으로 이해 한 것을 스트레스 테스트를 거쳤으며 높은 동시성을 견디지 못했습니다. –

-2

나는 거래를 드롭 것입니다.

플러스 @@ rowcount가 작동하지만 전역 변수를 조건부 검사로 사용하면 버그가 발생할 수 있습니다.

Exists() 확인 만하십시오. 아무튼 테이블을 통과해야하므로 속도가 문제가되지 않습니다.

내가 볼 수있는 한 트랜잭션이 필요하지 않습니다.

+1

rowcount의 업데이트/삽입 패턴은 삽입까지 직렬화 가능을 사용하기 때문에 안전합니다. 그렇지 않으면 삽입이 동시에 행과 일치하지 않는 갱신 시도와 충돌 할 수 있으며 고유 키가있는 경우 이중 삽입으로 인해 중복 행이 생기거나 중복 키 오류가 발생할 수 있습니다. –

관련 문제