xml 매개 변수를 사용하여 여러 데이터를 업데이트 할 때 병합 쿼리를 사용하지 않는 방법은 무엇입니까?


10

값 배열로 테이블을 업데이트하려고합니다. 배열의 각 항목에는 SQL Server 데이터베이스의 테이블에있는 행과 일치하는 정보가 포함됩니다. 행이 이미 테이블에 존재하면 주어진 배열의 정보로 해당 행을 업데이트합니다. 그렇지 않으면 테이블에 새 행을 삽입합니다. 나는 기본적으로 upsert를 설명했다.

이제 XML 매개 변수를 사용하는 저장 프로 시저에서이 작업을 수행하려고합니다. 테이블 반환 매개 변수가 아닌 XML을 사용하는 이유는 후자를 수행 할 때 SQL에서 사용자 지정 형식을 만들고이 형식을 저장 프로 시저와 연결해야하기 때문입니다. 저장 프로시 저나 DB 스키마에서 무언가를 변경 한 경우 저장 프로 시저와 사용자 지정 유형을 모두 다시 실행해야합니다. 이 상황을 피하고 싶습니다. 또한 TVP가 XML보다 우수하다는 점은 내 데이터 배열 크기가 1000을 초과하지 않기 때문에 현재 상황에 유용하지 않습니다. 즉, 여기에 제안 된 솔루션을 사용할 수 없습니다. SQL Server 2008에서 XML을 사용하여 여러 레코드를 삽입하는 방법

또한 비슷한 논의 ( UPSERT-MERGE 또는 @@ rowcount에 대한 더 나은 대안이 있습니까? )는 여러 행을 테이블에 upsert하려고하기 때문에 내가 요구하는 것과 다릅니다 .

XML에서 값을 upsert하기 위해 다음 쿼리 집합을 사용하기를 바랐습니다. 그러나 이것은 작동하지 않을 것입니다. 이 접근법은 입력이 단일 행일 때 작동합니다.

begin tran
   update table with (serializable) set select * from xml_param
   where key = @key

   if @@rowcount = 0
   begin
      insert table (key, ...) values (@key,..)
   end
commit tran

다음 대안은 철저한 IF EXISTS 또는 다음 형식의 변형 중 하나를 사용하는 것입니다. 그러나 나는 차선책의 효율성을 이유로 이것을 거부합니다.

IF (SELECT COUNT ... ) > 0
    UPDATE
ELSE
    INSERT

다음 옵션은 여기에 설명 된대로 Merge 문을 사용하는 것입니다 : http://www.databasejournal.com/features/mssql/using-the-merge-statement-to-perform-an-upsert.html . 그러나 병합 쿼리 관련 문제는 http://www.mssqltips.com/sqlservertip/3074/use-caution-with-sql-servers-merge-statement/ 에서 읽으십시오 . 이러한 이유로 병합을 피하려고합니다.

이제 내 질문은 : SQL Server 2008 저장 프로 시저에서 XML 매개 변수를 사용하여 여러 upsert를 얻는 다른 옵션이나 더 좋은 방법이 있습니까?

XML 매개 변수의 데이터에는 현재 레코드보다 오래되어 UPSERT되지 않아야하는 일부 레코드가 포함될 수 있습니다. 이 ModifiedDate는 XML 및 레코드가 업데이트 또는 폐기 여부를 결정하기 위해 비교해야 할 대상 테이블 모두에서 필드.


나중에 절차 를 변경하지 않으려 는 것이 실제로 TVP를 사용하지 않는 좋은 이유는 아닙니다. 데이터가 변경으로 전달되면 결국 코드를 ​​변경하게됩니다.
Max Vernon

1
@MaxVernon 내가 처음에 같은 생각을했고 그 때문에 거의 매우 비슷한 설명을했다 만은 피하기 TVP에 이유가 없습니다. 그러나 그들은 조금 더 노력을 기울이고 "1000 행을 넘지 마십시오"(때로는 또는 종종 암시 적 의미)라는 경고에 약간의 던지기입니다. 그러나 한 번에 <1000 행이 행에서 10k 번 호출되지 않는 한 XML과 크게 다르지 않다는 대답을 내야한다고 가정합니다. 그런 다음 약간의 성능 차이가 발생합니다.
Solomon Rutzky

MERGEBertrand가 지적한 문제 는 대부분의 경우와 비 효율성이며 스토퍼를 보여주지 않습니다. MS는 실제 지뢰밭이라면 그것을 공개하지 않았을 것입니다. 피하려고하는 컨볼 루션 MERGE이 저장하는 것보다 더 많은 잠재적 오류를 생성하지 않습니까?
모든 거래의 존

@JonofAllTrades 공정하게 말하면, 내가 제안한 것은 실제로와 비교하여 복잡하지 않습니다 MERGE. MERGE의 INSERT 및 UPDATE 단계는 여전히 개별적으로 처리됩니다. 내 접근 방식의 주요 차이점은 업데이트 된 레코드 ID를 보유하는 테이블 변수와 해당 테이블 변수를 사용하여 들어오는 데이터의 임시 테이블에서 해당 레코드를 제거하는 DELETE 쿼리입니다. 그리고 소스가 임시 테이블로 덤프하는 대신 @ XMLparam.nodes ()에서 직접 제공 될 수 있다고 가정하지만 여전히 엣지 케이스 중 하나에서 자신을 찾는 것에 대해 걱정할 필요가없는 추가 항목은 많지 않습니다. ).
Solomon Rutzky

답변:


12

소스가 XML인지 TVP인지는 큰 차이가 없습니다. 전반적인 작업은 다음과 같습니다.

  1. 기존 행 업데이트
  2. INSERT 누락 된 행

INSERT를 먼저 수행하면 모든 행이 존재하여 업데이트를 받고 방금 삽입 한 행에 대해 반복 된 작업을 수행하므로 순서대로 수행합니다.

이외에도이를 달성하기위한 다양한 방법과 그 밖의 일부 효율성을 조정할 수있는 다양한 방법이 있습니다.

최소한으로 시작합시다. XML 추출은이 작업에서 가장 비싼 부분 중 하나 일 가능성이 높기 때문에 (가장 비싸지는 않은 경우) 두 번 수행 할 필요는 없습니다 (두 개의 작업을 수행해야 함). 따라서 임시 테이블을 만들고 XML에서 데이터를 추출합니다.

CREATE TABLE #TempImport
(
  Field1 DataType1,
  Field2 DataType2,
  ...
);

INSERT INTO #TempImport (Field1, Field2, ...)
  SELECT tab.col.value('XQueryForField1', 'DataType') AS [Field1],
         tab.col.value('XQueryForField2', 'DataType') AS [Field2],
         ...
  FROM   @XmlInputParam.nodes('XQuery') tab(col);

거기에서 우리는 UPDATE와 INSERT를 수행합니다.

UPDATE tab
SET    tab.Field1 = tmp.Field1,
       tab.Field2 = tmp.Field2,
       ...
FROM   [SchemaName].[TableName] tab
INNER JOIN #TempImport tmp
        ON tmp.IDField = tab.IDField
        ... -- more fields if PK or alternate key is composite

INSERT INTO [SchemaName].[TableName]
  (Field1, Field2, ...)
  SELECT tmp.Field1, tmp.Field2, ...
  FROM   #TempImport tmp
  WHERE  NOT EXISTS (
                       SELECT  *
                       FROM    [SchemaName].[TableName] tab
                       WHERE   tab.IDField = tmp.IDField
                       ... -- more fields if PK or alternate key is composite
                     );

기본 작업이 완료되었으므로 최적화 할 몇 가지 작업을 수행 할 수 있습니다.

  1. 임시 테이블에 삽입의 @@ ROWCOUNT를 캡처하고 UPDATE의 @@ ROWCOUNT와 비교하십시오. 그것들이 같다면 INSERT를 건너 뛸 수 있습니다.

  2. OUTPUT 절을 통해 업데이트 된 ID 값을 캡처하고 임시 테이블에서 값을 삭제하십시오. 그런 다음 INSERT에는WHERE NOT EXISTS(...)

  3. 들어오는 데이터에 동기화 되지 않아야하는 (즉 삽입되거나 업데이트되지 않은) 행이있는 경우 UPDATE를 수행하기 전에 해당 레코드를 제거해야합니다.

CREATE TABLE #TempImport
(
  Field1 DataType1,
  Field2 DataType2,
  ...
);

DECLARE @ImportRows INT;
DECLARE @UpdatedIDs TABLE ([IDField] INT NOT NULL);

BEGIN TRY

  INSERT INTO #TempImport (Field1, Field2, ...)
    SELECT tab.col.value('XQueryForField1', 'DataType') AS [Field1],
           tab.col.value('XQueryForField2', 'DataType') AS [Field2],
           ...
    FROM   @XmlInputParam.nodes('XQuery') tab(col);

  SET @ImportRows = @@ROWCOUNT;

  IF (@ImportRows = 0)
  BEGIN
    RAISERROR('Seriously?', 16, 1); -- no rows to import
  END;

  -- optional: test to see if it helps or hurts
  -- ALTER TABLE #TempImport
  --   ADD CONSTRAINT [PK_#TempImport]
  --   PRIMARY KEY CLUSTERED (PKField ASC)
  --   WITH FILLFACTOR = 100;


  -- optional: remove any records that should not be synced
  DELETE tmp
  FROM   #TempImport tmp
  INNER JOIN [SchemaName].[TableName] tab
          ON tab.IDField = tmp.IDField
          ... -- more fields if PK or alternate key is composite
  WHERE  tmp.ModifiedDate < tab.ModifiedDate;

  BEGIN TRAN;

  UPDATE tab
  SET    tab.Field1 = tmp.Field1,
         tab.Field2 = tmp.Field2,
         ...
  OUTPUT INSERTED.IDField
  INTO   @UpdatedIDs ([IDField]) -- capture IDs that are updated
  FROM   [SchemaName].[TableName] tab
  INNER JOIN #TempImport tmp
          ON tmp.IDField = tab.IDField
          ... -- more fields if PK or alternate key is composite

  IF (@@ROWCOUNT < @ImportRows) -- if all rows were updates then skip, else insert remaining
  BEGIN
    -- get rid of rows that were updates, leaving only the ones to insert
    DELETE tmp
    FROM   #TempImport tmp
    INNER JOIN @UpdatedIDs del
            ON del.[IDField] = tmp.[IDField];

    -- OR, rather than the DELETE, maybe add a column to #TempImport for:
    -- [IsUpdate] BIT NOT NULL DEFAULT (0)
    -- Then UPDATE #TempImport SET [IsUpdate] = 1 JOIN @UpdatedIDs ON [IDField]
    -- Then, in below INSERT, add:  WHERE [IsUpdate] = 0

    INSERT INTO [SchemaName].[TableName]
      (Field1, Field2, ...)
      SELECT tmp.Field1, tmp.Field2, ...
      FROM   #TempImport tmp
  END;

  COMMIT TRAN;

END TRY
BEGIN CATCH
  IF (@@TRANCOUNT > 0)
  BEGIN
    ROLLBACK;
  END;

  -- THROW; -- if using SQL 2012 or newer, use this and remove the following 3 lines
  DECLARE @ErrorMessage NVARCHAR(4000) = ERROR_MESSAGE();
  RAISERROR(@ErrorMessage, 16, 1);
  RETURN;
END CATCH;

이 모델을 Imports / ETL에서 여러 번 사용했습니다. 수백만 행이 넘는 총 20k 세트 중 1000 개가 넘는 행이나 500 개가있는 일괄 처리가 가능합니다. 그러나 임시 테이블에서 업데이트 된 행의 DELETE와 [IsUpdate] 필드 업데이트 간의 성능 차이를 테스트하지 않았습니다.


한 번에 최대 1000 개의 행을 가져올 수 있기 때문에 TVP를 통해 XML을 사용하기로 한 결정에 대해 유의하십시오 (질문에 언급 됨).

이것이 몇 번이고 여기저기서 호출되는 경우 TVP의 작은 성능 향상으로 인해 추가 유지 관리 비용이 들지 않을 수 있습니다 (사용자 정의 테이블 유형, 앱 코드 변경 등을 변경하기 전에 프로세스를 삭제해야 할 필요가 있음) . 그러나 4 백만 행을 가져 와서 한 번에 1000 개를 전송하는 경우 4000 개의 실행 (및 분석 된 XML에 대해 400 만 행), 심지어 몇 번만 실행하면 약간의 성능 차이가 발생합니다. 눈에 띄는 차이를 더하십시오.

즉, 앞에서 설명한 방법은 SELECT FROM @XmlInputParam을 SELECT FROM @TVP로 바꾸는 것 외에는 변경되지 않습니다. TVP는 읽기 전용이므로 TVP에서 삭제할 수 없습니다. 나는 WHERE NOT EXISTS(SELECT * FROM @UpdateIDs ids WHERE ids.IDField = tmp.IDField)간단한 대신에 최종 SELECT (INSERT에 묶여 있음)에 단순히를 추가 할 수 있다고 생각합니다 WHERE IsUpdate = 0. @UpdateIDs이런 식으로 테이블 변수 를 사용하는 경우 들어오는 행을 임시 테이블에 덤프하지 않고 벗어날 수도 있습니다.

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