다중 INSERT 문 대 다중 값이있는 단일 INSERT


119

1000 INSERT 문 사용 간의 성능 비교를 실행하고 있습니다.

INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
   VALUES ('6f3f7257-a3d8-4a78-b2e1-c9b767cfe1c1', 'First 0', 'Last 0', 0)
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
   VALUES ('32023304-2e55-4768-8e52-1ba589b82c8b', 'First 1', 'Last 1', 1)
...
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
   VALUES ('f34d95a7-90b1-4558-be10-6ceacd53e4c4', 'First 999', 'Last 999', 999)

.. 대 1000 개의 값이있는 단일 INSERT 문 사용 :

INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
VALUES 
('db72b358-e9b5-4101-8d11-7d7ea3a0ae7d', 'First 0', 'Last 0', 0),
('6a4874ab-b6a3-4aa4-8ed4-a167ab21dd3d', 'First 1', 'Last 1', 1),
...
('9d7f2a58-7e57-4ed4-ba54-5e9e335fb56c', 'First 999', 'Last 999', 999)

놀랍게도 결과는 제가 생각했던 것과 반대입니다.

  • 1000 개의 INSERT 문 : 290 msec.
  • 1000 값이있는 INSERT 문 1 개 : 2800 msec.

테스트는 측정에 사용되는 SQL Server Profiler를 사용하여 MSSQL Management Studio에서 직접 실행됩니다 (그리고 SqlClient를 사용하여 C # 코드에서 비슷한 결과를 얻었습니다. 모든 DAL 레이어 왕복을 고려하면 훨씬 더 놀랍습니다)

이것은 합리적이거나 어떻게 든 설명 할 수 있습니까? 더 빠른 방법으로 인해 성능이 10 배 (!) 더 나빠지는 이유는 무엇입니까?

감사합니다.

편집 : 둘 다에 대한 실행 계획 첨부 : 실행 계획


1
이것들은 깨끗한 테스트이고, 어떤 것도 병렬로 실행되지 않고, 반복되는 데이터도 없습니다 (물론, 간단한 캐싱을 피하기 위해 각 쿼리는 다른 데이터를 사용합니다)
Borka

1
관련된 트리거가 있습니까?
AK

2
프로그램을 TVP로 변환하여 값에 대한 1000 제한을 초과하고 성능이 크게 향상되었습니다. 비교를 해보겠습니다.
paparazzo

답변:


126

추가 : SQL Server 2012는이 영역에서 일부 향상된 성능을 보여 주지만 아래에 언급 된 특정 문제를 해결하지 못하는 것 같습니다. 이것은 SQL Server 2012 이후 의 다음 주 버전에서 분명히 수정 되어야합니다 !

계획에 따르면 단일 삽입이 매개 변수화 된 프로 시저 (자동 매개 변수화 될 수 있음)를 사용하므로 이들에 대한 구문 분석 / 컴파일 시간이 최소화되어야합니다.

루프 ( script )를 설정하고 VALUES절의 수를 조정 하고 컴파일 시간을 기록 했지만이 부분을 좀 더 살펴 보겠다고 생각 했습니다.

그런 다음 컴파일 시간을 행 수로 나누어 절당 평균 컴파일 시간을 얻었습니다. 결과는 다음과 같습니다.

그래프

최대 250 개의 VALUES절이 컴파일 시간을 나타내며 절의 수는 약간의 상승 추세를 보이지만 너무 극적인 것은 아닙니다.

그래프

그러나 갑자기 변화가 있습니다.

해당 데이터 섹션이 아래에 나와 있습니다.

+------+----------------+-------------+---------------+---------------+
| Rows | CachedPlanSize | CompileTime | CompileMemory | Duration/Rows |
+------+----------------+-------------+---------------+---------------+
|  245 |            528 |          41 |          2400 | 0.167346939   |
|  246 |            528 |          40 |          2416 | 0.162601626   |
|  247 |            528 |          38 |          2416 | 0.153846154   |
|  248 |            528 |          39 |          2432 | 0.157258065   |
|  249 |            528 |          39 |          2432 | 0.156626506   |
|  250 |            528 |          40 |          2448 | 0.16          |
|  251 |            400 |         273 |          3488 | 1.087649402   |
|  252 |            400 |         274 |          3496 | 1.087301587   |
|  253 |            400 |         282 |          3520 | 1.114624506   |
|  254 |            408 |         279 |          3544 | 1.098425197   |
|  255 |            408 |         290 |          3552 | 1.137254902   |
+------+----------------+-------------+---------------+---------------+

선형 적으로 증가하던 캐시 된 계획 크기는 갑자기 떨어지지 만 CompileTime은 7 배 증가하고 CompileMemory는 증가합니다. 이것은 자동 매개 변수화 된 계획 (1,000 개의 매개 변수 포함)에서 매개 변수화되지 않은 계획 사이의 컷오프 지점입니다. 그 후에는 (주어진 시간에 처리되는 값 절의 수 측면에서) 선형 적으로 덜 효율적으로 보입니다.

왜 그래야하는지 잘 모르겠습니다. 아마도 특정 리터럴 값에 대한 계획을 컴파일 할 때 선형 적으로 확장되지 않는 일부 활동 (예 : 정렬)을 수행해야합니다.

중복 행으로 만 구성된 쿼리를 시도했을 때 캐시 된 쿼리 계획의 크기에 영향을 미치지 않는 것 같고, 상수 테이블의 출력 순서에도 영향을주지 않습니다 (그리고 정렬에 소요되는 힙 시간에 삽입 할 때) 어쨌든 무의미 할 것입니다).

또한 클러스터형 인덱스가 테이블에 추가되면 계획은 여전히 ​​명시적인 정렬 단계를 표시하므로 런타임에 정렬을 피하기 위해 컴파일 타임에 정렬되지 않는 것 같습니다.

계획

디버거에서 이것을 보려고했지만 내 SQL Server 2008 버전에 대한 공용 기호를 사용할 수없는 것 같아서 대신 UNION ALLSQL Server 2005에서 동일한 구성 을 살펴 봐야했습니다 .

일반적인 스택 추적은 다음과 같습니다.

sqlservr.exe!FastDBCSToUnicode()  + 0xac bytes  
sqlservr.exe!nls_sqlhilo()  + 0x35 bytes    
sqlservr.exe!CXVariant::CmpCompareStr()  + 0x2b bytes   
sqlservr.exe!CXVariantPerformCompare<167,167>::Compare()  + 0x18 bytes  
sqlservr.exe!CXVariant::CmpCompare()  + 0x11f67d bytes  
sqlservr.exe!CConstraintItvl::PcnstrItvlUnion()  + 0xe2 bytes   
sqlservr.exe!CConstraintProp::PcnstrUnion()  + 0x35e bytes  
sqlservr.exe!CLogOp_BaseSetOp::PcnstrDerive()  + 0x11a bytes    
sqlservr.exe!CLogOpArg::PcnstrDeriveHandler()  + 0x18f bytes    
sqlservr.exe!CLogOpArg::DeriveGroupProperties()  + 0xa9 bytes   
sqlservr.exe!COpArg::DeriveNormalizedGroupProperties()  + 0x40 bytes    
sqlservr.exe!COptExpr::DeriveGroupProperties()  + 0x18a bytes   
sqlservr.exe!COptExpr::DeriveGroupProperties()  + 0x146 bytes   
sqlservr.exe!COptExpr::DeriveGroupProperties()  + 0x146 bytes   
sqlservr.exe!COptExpr::DeriveGroupProperties()  + 0x146 bytes   
sqlservr.exe!CQuery::PqoBuild()  + 0x3cb bytes  
sqlservr.exe!CStmtQuery::InitQuery()  + 0x167 bytes 
sqlservr.exe!CStmtDML::InitNormal()  + 0xf0 bytes   
sqlservr.exe!CStmtDML::Init()  + 0x1b bytes 
sqlservr.exe!CCompPlan::FCompileStep()  + 0x176 bytes   
sqlservr.exe!CSQLSource::FCompile()  + 0x741 bytes  
sqlservr.exe!CSQLSource::FCompWrapper()  + 0x922be bytes    
sqlservr.exe!CSQLSource::Transform()  + 0x120431 bytes  
sqlservr.exe!CSQLSource::Compile()  + 0x2ff bytes   

따라서 스택 추적에서 이름을 제거하면 문자열을 비교하는 데 많은 시간이 소요되는 것으로 보입니다.

이 KB 문서 는 쿼리 처리 DeriveNormalizedGroupProperties정규화 단계 라고 불렸던 것과 관련이 있음을 나타냅니다.

이 단계는 이제 binding 또는 algebrizing이라고하며 이전 구문 분석 단계의 표현식 구문 분석 트리 출력을 가져와 최적화 (이 경우 사소한 계획 최적화) [ref] 로 진행하기 위해 알고리즘 화 된 표현식 트리 (쿼리 프로세서 트리)를 출력합니다 .

원래 테스트를 다시 실행하는 실험 ( Script )을 하나 더 시도했지만 세 가지 사례를 살펴 보았습니다 .

  1. 중복이없는 길이 10 자의 이름 및 성 문자열입니다.
  2. 중복이없는 50 자 길이의 이름 및 성 문자열.
  3. 이름 및 성 길이가 모두 중복 된 10 자 문자열입니다.

그래프

문자열이 길수록 더 나쁜 결과를 얻고 반대로 중복이 많을수록 더 좋은 결과를 얻는다는 것을 분명히 알 수 있습니다. 앞서 언급했듯이 중복은 캐시 된 계획 크기에 영향을주지 않으므로 algebrized 표현 트리 자체를 구성 할 때 중복 식별 프로세스가 있어야한다고 가정합니다.

편집하다

이 정보가 활용되는 한 곳은 여기 @Lieven보여줍니다.

SELECT * 
FROM (VALUES ('Lieven1', 1),
             ('Lieven2', 2),
             ('Lieven3', 3))Test (name, ID)
ORDER BY name, 1/ (ID - ID) 

컴파일 타임에 Name열에 중복이 없음을 확인할 수 있기 때문에 1/ (ID - ID)런타임에 보조 식에 의한 정렬을 건너 뛰고 (계획의 정렬에는 ORDER BY열 이 하나만 있음 ) 0으로 나누기 오류가 발생하지 않습니다. 중복 항목이 테이블에 추가되면 정렬 연산자는 열별로 두 순서를 표시하고 예상 오류가 발생합니다.


6
가지고있는 매직 넘버는 NumberOfRows / ColumnCount = 250입니다. 3 개의 열만 사용하도록 쿼리를 변경하면 333에서 변경이 발생합니다. 매직 넘버 1000은 캐시 된 계획에서 사용되는 최대 매개 변수 수와 비슷할 수 있습니다. 목록 이있는 <ParameterList>것 이상 으로 계획을 생성하는 것이 "더 쉬울 것" <ConstantScan><Values><Row>입니다.
Mikael Eriksson 2011

1
@MikaelEriksson-동의합니다. 1000 개의 값을 가진 250 개의 행 1은 자동으로 매개 변수화됩니다. 251 개의 행은 그렇지 않은 것 같습니다. 그래도 이유가 확실하지 않습니다. 아마도 중복을 찾는 리터럴 값을 정렬하는 데 시간을 소비 할 것입니다.
Martin Smith

1
이것은 매우 미친 문제입니다. 나는 그로 인해 슬픔을 느꼈습니다. 이것은 훌륭한 답변입니다 감사합니다
사랑하지 않음

1
@MikaelEriksson 매직 넘버가 NumberOfRows * ColumnCount = 1000이라는 뜻입니까?
paparazzo

1
@Blam-예. 총 요소 수가 1000 개 (NumberOfRows * ColumnCount)를 초과하면 쿼리 계획 <ConstantScan><Values><Row><ParameterList>.
Mikael Eriksson 2012

23

그리 놀라운 일이 아닙니다. 작은 삽입에 대한 실행 계획이 한 번 계산 된 다음 1000 번 재사용됩니다. 계획을 파싱하고 준비하는 것은 빠르다. 델 값이 4 개뿐이기 때문이다. 반면 1000 행 계획은 4000 개의 값 (또는 C # 테스트를 매개 변수화 한 경우 4000 개의 매개 변수)을 처리해야합니다. 이는 특히 네트워크가 지나치게 느리지 않은 경우 SQL Server 로의 왕복 999 회를 제거함으로써 절약 된 시간을 쉽게 소모 할 수 있습니다.


9

문제는 쿼리를 컴파일하는 데 걸리는 시간과 관련이있을 수 있습니다.

삽입 속도를 높이려면 실제로해야 할 일은 삽입을 트랜잭션으로 래핑하는 것입니다.

BEGIN TRAN;
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
   VALUES ('6f3f7257-a3d8-4a78-b2e1-c9b767cfe1c1', 'First 0', 'Last 0', 0);
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
   VALUES ('32023304-2e55-4768-8e52-1ba589b82c8b', 'First 1', 'Last 1', 1);
...
INSERT INTO T_TESTS (TestId, FirstName, LastName, Age) 
   VALUES ('f34d95a7-90b1-4558-be10-6ceacd53e4c4', 'First 999', 'Last 999', 999);
COMMIT TRAN;

C #에서 테이블 값 매개 변수 사용을 고려할 수도 있습니다. 세미콜론으로 구분하여 단일 일괄 처리로 여러 명령을 실행하는 것도 도움이되는 또 다른 방법입니다.


1
Re : "단일 배치에서 여러 명령 실행": 약간 도움이되지만 많지는 않습니다. 그러나 나는 TRANSACTION의 래핑 (트랜스가 실제로 작동합니까 아니면 TRAN이어야합니까?) 또는 TVP를 사용하는 다른 두 가지 옵션에 확실히 동의합니다.
Solomon Rutzky

1

C ++ 프로그램 (MFC / ODBC)을 사용하여 여러 개의 100k 행이있는 테이블을 변환하려는 비슷한 상황이 발생했습니다.

이 작업은 매우 오랜 시간이 걸렸기 때문에 여러 삽입을 하나로 묶는 것으로 생각했습니다 ( MSSQL 제한 으로 인해 최대 1000 개 ). 많은 단일 삽입 문이 여기에 설명 된 것과 유사한 오버 헤드를 생성 할 것이라고 생각합니다. .

그러나 변환에 실제로는 시간이 조금 더 걸린 것으로 나타났습니다.

        Method 1       Method 2     Method 3 
        Single Insert  Multi Insert Joined Inserts
Rows    1000           1000         1000
Insert  390 ms         765 ms       270 ms
per Row 0.390 ms       0.765 ms     0.27 ms

따라서 단일 INSERT 문 (방법 1)을 사용하는 CDatabase :: ExecuteSql에 대한 1000 개의 단일 호출은 1000 개의 값 튜플 (방법 2)이있는 여러 줄 INSERT 문을 사용하는 CDatabase :: ExecuteSql에 대한 단일 호출보다 약 두 배 빠릅니다.

업데이트 : 그래서 다음으로 시도한 것은 1000 개의 개별 INSERT 문을 단일 문자열로 묶고 서버가이를 실행하도록하는 것입니다 (방법 3). 이것은 방법 1보다 조금 더 빠릅니다.

편집 : Microsoft SQL Server Express Edition (64 비트) v10.0.2531.0을 사용하고 있습니다.

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