소스가 XML인지 TVP인지는 큰 차이가 없습니다. 전반적인 작업은 다음과 같습니다.
- 기존 행 업데이트
- 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
);
기본 작업이 완료되었으므로 최적화 할 몇 가지 작업을 수행 할 수 있습니다.
임시 테이블에 삽입의 @@ ROWCOUNT를 캡처하고 UPDATE의 @@ ROWCOUNT와 비교하십시오. 그것들이 같다면 INSERT를 건너 뛸 수 있습니다.
OUTPUT 절을 통해 업데이트 된 ID 값을 캡처하고 임시 테이블에서 값을 삭제하십시오. 그런 다음 INSERT에는WHERE NOT EXISTS(...)
들어오는 데이터에 동기화 되지 않아야하는 (즉 삽입되거나 업데이트되지 않은) 행이있는 경우 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
이런 식으로 테이블 변수 를 사용하는 경우 들어오는 행을 임시 테이블에 덤프하지 않고 벗어날 수도 있습니다.