테스트 케이스에서 순차 GUID 키가 순차 INT 키보다 더 빠른 성능을 보이는 이유는 무엇입니까?


39

순차 및 비 순차 GUID를 비교하는 질문을 한 후 1) GUID 기본 키가 순차적으로 초기화 newsequentialid()된 테이블 및 2) INT 기본 키가 순차적으로 초기화 된 테이블 의 INSERT 성능을 비교하려고했습니다 identity(1,1). 정수의 너비가 더 작기 때문에 후자가 가장 빠를 것으로 예상하고 순차 GUID보다 순차 정수를 생성하는 것이 더 단순 해 보입니다. 그러나 놀랍게도 정수 키가있는 테이블의 INSERT는 순차적 GUID 테이블보다 상당히 느 렸습니다.

테스트 실행의 평균 시간 사용량 (ms)이 표시됩니다.

NEWSEQUENTIALID()  1977
IDENTITY()         2223

누구든지 이것을 설명 할 수 있습니까?

다음 실험이 사용되었습니다.

SET NOCOUNT ON

CREATE TABLE TestGuid2 (Id UNIQUEIDENTIFIER NOT NULL DEFAULT NEWSEQUENTIALID() PRIMARY KEY,
SomeDate DATETIME, batchNumber BIGINT, FILLER CHAR(100))

CREATE TABLE TestInt (Id Int NOT NULL identity(1,1) PRIMARY KEY,
SomeDate DATETIME, batchNumber BIGINT, FILLER CHAR(100))

DECLARE @BatchCounter INT = 1
DECLARE @Numrows INT = 100000


WHILE (@BatchCounter <= 20)
BEGIN 
BEGIN TRAN

DECLARE @LocalCounter INT = 0

    WHILE (@LocalCounter <= @NumRows)
    BEGIN
    INSERT TestGuid2 (SomeDate,batchNumber) VALUES (GETDATE(),@BatchCounter)
    SET @LocalCounter +=1
    END

SET @LocalCounter = 0

    WHILE (@LocalCounter <= @NumRows)
    BEGIN
    INSERT TestInt (SomeDate,batchNumber) VALUES (GETDATE(),@BatchCounter)
    SET @LocalCounter +=1
    END

SET @BatchCounter +=1
COMMIT 
END

DBCC showcontig ('TestGuid2')  WITH tableresults
DBCC showcontig ('TestInt')  WITH tableresults

SELECT batchNumber,DATEDIFF(ms,MIN(SomeDate),MAX(SomeDate)) AS [NEWSEQUENTIALID()]
FROM TestGuid2
GROUP BY batchNumber

SELECT batchNumber,DATEDIFF(ms,MIN(SomeDate),MAX(SomeDate)) AS [IDENTITY()]
FROM TestInt
GROUP BY batchNumber

DROP TABLE TestGuid2
DROP TABLE TestInt

업데이트 : 아래의 Phil Sandler, Mitch Wheat 및 Martin의 예제와 같이 TEMP 테이블을 기반으로 삽입을 수행하도록 스크립트를 수정하면 IDENTITY가 더 빠릅니다. 그러나 이것이 행을 삽입하는 일반적인 방법은 아니며 처음에는 왜 실험이 잘못되었는지 이해하지 못합니다. 원래 예제에서 GETDATE ()를 생략하더라도 IDENTITY ()는 여전히 느립니다. 따라서 IDENTITY ()가 NEWSEQUENTIALID ()를 능가하는 유일한 방법은 임시 테이블에 삽입 할 행을 준비하고이 임시 테이블을 사용하여 일괄 삽입으로 많은 삽입을 수행하는 것 같습니다. 결론적으로, 우리가 현상에 대한 설명을 찾지 못했다고 생각하며 IDENTITY ()는 여전히 대부분의 실제 사용에서 느리게 보입니다. 누구든지 이것을 설명 할 수 있습니까?


4
생각 만 : 테이블을 전혀 사용하지 않고 새 GUID를 생성 할 수 있지만 다음 사용 가능한 ID 값을 얻으면 일시적으로 일종의 잠금이 발생하여 두 개의 스레드 / 연결이 동일한 값을 얻지 못할 수 있습니까? 나는 단지 정말로 추측하고있다. 재미있는 질문!
화난 사람

4
누가 그렇게합니까 ?? 그들이하지 않은 많은 증거가 있습니다-Kimberly Tripp의 디스크 공간이 저렴하다는 것을 참조하십시오 -그게 요점 아닙니다 ! 블로그 게시물-그녀는 광범위한 리뷰를하고 GUID는 항상 명확하게 풀 INT IDENTITY
marc_s

2
위의 실험은 그 반대를 보여 주며 결과는 반복 가능합니다.
someName

2
사용 IDENTITY에는 테이블 잠금이 필요하지 않습니다. 개념적으로 MAX (id) + 1을 취할 것으로 예상 할 수 있지만 실제로 다음 값이 저장됩니다. 실제로 다음 GUID를 찾는 것보다 빠릅니다.

4
또한 TestGuid2 테이블의 필러 열은 행 크기를 동일하게하기 위해 CHAR (88)이어야합니다.
Mitch Wheat

답변:


19

@Phil Sandler의 코드를 수정하여 GETDATE () 호출의 효과를 제거하고 (하드웨어 효과 / 중단이있을 수 있습니까 ??) 행을 같은 길이로 만들었습니다.

[SQL Server 2000 이후로 타이밍 문제 및 고해상도 타이머와 관련된 몇 가지 기사가 있었으므로 그 영향을 최소화하고 싶었습니다.]

데이터와 로그 파일이 필요한 간단한 복구 모델에서 필요한 시간을 초과하는 타이밍은 다음과 같습니다 (초) : (아래 정확한 코드를 기반으로 새로운 결과로 업데이트 됨)

       Identity(s)  Guid(s)
       ---------    -----
       2.876        4.060    
       2.570        4.116    
       2.513        3.786   
       2.517        4.173    
       2.410        3.610    
       2.566        3.726
       2.376        3.740
       2.333        3.833
       2.416        3.700
       2.413        3.603
       2.910        4.126
       2.403        3.973
       2.423        3.653
    -----------------------
Avg    2.650        3.857
StdDev 0.227        0.204

사용 된 코드 :

SET NOCOUNT ON

CREATE TABLE TestGuid2 (Id UNIQUEIDENTIFIER NOT NULL DEFAULT NEWSEQUENTIALID() PRIMARY KEY,
SomeDate DATETIME, batchNumber BIGINT, FILLER CHAR(88))

CREATE TABLE TestInt (Id Int NOT NULL identity(1,1) PRIMARY KEY,
SomeDate DATETIME, batchNumber BIGINT, FILLER CHAR(100))

DECLARE @Numrows INT = 1000000

CREATE TABLE #temp (Id int NOT NULL Identity(1,1) PRIMARY KEY, rowNum int, adate datetime)

DECLARE @LocalCounter INT = 0

--put rows into temp table
WHILE (@LocalCounter < @NumRows)
BEGIN
    INSERT INTO #temp(rowNum, adate) VALUES (@LocalCounter, GETDATE())
    SET @LocalCounter += 1
END

--Do inserts using GUIDs
DECLARE @GUIDTimeStart DateTime = GETDATE()
INSERT INTO TestGuid2 (SomeDate, batchNumber) 
SELECT adate, rowNum FROM #temp
DECLARE @GUIDTimeEnd  DateTime = GETDATE()

--Do inserts using IDENTITY
DECLARE @IdTimeStart DateTime = GETDATE()
INSERT INTO TestInt (SomeDate, batchNumber) 
SELECT adate, rowNum FROM #temp
DECLARE @IdTimeEnd DateTime = GETDATE()

SELECT DATEDIFF(ms, @IdTimeStart, @IdTimeEnd) AS IdTime, DATEDIFF(ms, @GUIDTimeStart, @GUIDTimeEnd) AS GuidTime

DROP TABLE TestGuid2
DROP TABLE TestInt
DROP TABLE #temp
GO

@Martin의 조사를 읽은 후 두 경우 모두 제안 된 TOP (@num)으로 다시 실행됩니다.

...
--Do inserts using GUIDs
DECLARE @num INT = 2147483647; 
DECLARE @GUIDTimeStart DATETIME = GETDATE(); 
INSERT INTO TestGuid2 (SomeDate, batchNumber) 
SELECT TOP(@num) adate, rowNum FROM #temp; 
DECLARE @GUIDTimeEnd DATETIME = GETDATE();

--Do inserts using IDENTITY
DECLARE @IdTimeStart DateTime = GETDATE()
INSERT INTO TestInt (SomeDate, batchNumber) 
SELECT TOP(@num) adate, rowNum FROM #temp;
DECLARE @IdTimeEnd DateTime = GETDATE()
...

타이밍 결과는 다음과 같습니다.

       Identity(s)  Guid(s)
       ---------    -----
       2.436        2.656
       2.940        2.716
       2.506        2.633
       2.380        2.643
       2.476        2.656
       2.846        2.670
       2.940        2.913
       2.453        2.653
       2.446        2.616
       2.986        2.683
       2.406        2.640
       2.460        2.650
       2.416        2.720

    -----------------------
Avg    2.426        2.688
StdDev 0.010        0.032

쿼리가 반환되지 않았으므로 실제 실행 계획을 얻을 수 없었습니다! 버그 일 가능성이 높습니다. (Microsoft SQL Server 2008 R2 (RTM)-10.50.1600.1 (X64) 실행)


7
훌륭한 벤치마킹의 중요한 요소를 깔끔하게 보여줍니다. 한 번에 한 가지만 측정하십시오.
Aaronaught

여기 어떤 계획이 있습니까? SORTGUID에 대한 연산자가 있습니까?
Martin Smith

@ 마틴 : 안녕, 나는 계획을 확인하지 않았다 (한 번에 몇 가지 일을 :)). 좀 나중에 살펴 볼게요 ...
Mitch Wheat

@Mitch-이것에 대한 의견이 있으십니까? 차라리 당신이 여기서 측정하고있는 주요한 것은 큰 인서트에 대한 가이드를 정렬하는 데 걸린 시간이라고 생각합니다. 흥미로운 것은 OP의 원래 질문에 답하지는 않지만 순차적 인 guid가 왜 단일 컬럼의 ID 열보다 더 나은지에 대한 설명을 제공하는 것에 관한 설명은 아닙니다 OP 테스트에서 행 삽입.
마틴 스미스

2
@ 미치-그것에 대해 더 많이 생각할수록 왜 NEWSEQUENTIALID어쨌든 누군가가 왜 사용하고 싶어하는지 이해하지 못합니다 . 그것은 인덱스를 더 깊게 만들고 OP의 경우 20 % 더 많은 데이터 페이지를 사용하며 컴퓨터가 재부팅 될 때까지만 증가한다는 보장이 있으므로에 비해 많은 단점이 있습니다 identity. 이 경우 쿼리 계획에 불필요한 추가 계획이 추가되는 것 같습니다!
마틴 스미스

19

1GB 크기의 데이터 파일과 3GB 로그 파일 (랩탑 컴퓨터, 동일한 드라이브에있는 두 파일) 및 복구 간격을 100 분으로 설정 한 간단한 복구 모델의 새로운 데이터베이스에서 (체크 포인트가 결과를 왜곡하지 않도록) 하나의 행으로 비슷한 결과를 얻을 수 있습니다 inserts.

세 가지 경우를 테스트했습니다. 각 경우마다 100,000 개의 행을 다음 표에 개별적으로 삽입하는 20 개의 배치를 수행했습니다. 전체 스크립트는이 답변의 개정 내역에서 찾을 수 있습니다 .

CREATE TABLE TestGuid
  (
     Id          UNIQUEIDENTIFIER NOT NULL DEFAULT NEWSEQUENTIALID() PRIMARY KEY,
     SomeDate    DATETIME, batchNumber BIGINT, FILLER CHAR(100)
  )

CREATE TABLE TestId
  (
     Id          Int NOT NULL identity(1, 1) PRIMARY KEY,
     SomeDate    DATETIME, batchNumber BIGINT, FILLER CHAR(100)
  )

CREATE TABLE TestInt
  (
     Id          Int NOT NULL PRIMARY KEY,
     SomeDate    DATETIME, batchNumber BIGINT, FILLER  CHAR(100)
  )  

세 번째 테이블의 경우 테스트는 증분 Id값으로 행을 삽입 했지만 루프에서 변수 값을 증분하여 자체 계산되었습니다.

20 개 배치에 걸리는 평균 시간은 다음과 같습니다.

NEWSEQUENTIALID() IDENTITY()  INT
----------------- ----------- -----------
1999              2633        1878

결론

따라서 identity결과를 담당 하는 작성 프로세스의 오버 헤드 인 것 같습니다 . 자체 계산 증분 정수의 경우 결과는 IO 비용 만 고려할 때 예상되는 것과 훨씬 더 일치합니다.

위에서 설명한 삽입 코드를 저장 프로 시저에 넣고 검토 sys.dm_exec_procedure_stats하면 다음과 같은 결과가 나타납니다.

proc_name      execution_count      total_worker_time    last_worker_time     min_worker_time      max_worker_time      total_elapsed_time   last_elapsed_time    min_elapsed_time     max_elapsed_time     total_physical_reads last_physical_reads  min_physical_reads   max_physical_reads   total_logical_writes last_logical_writes  min_logical_writes   max_logical_writes   total_logical_reads  last_logical_reads   min_logical_reads    max_logical_reads
-------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- -------------------- --------------------
IdentityInsert 20                   45060360             2231067              2094063              2645079              45119362             2234067              2094063              2660080              0                    0                    0                    0                    32505                1626                 1621                 1626                 6268917              315377               276833               315381
GuidInsert     20                   34829052             1742052              1696051              1833055              34900053             1744052              1698051              1838055              0                    0                    0                    0                    35408                1771                 1768                 1772                 6316837              316766               298386               316774

따라서 그 결과 total_worker_time는 약 30 % 더 높습니다. 이것은 나타냅니다

이 저장 프로 시저가 컴파일 된 이후 실행되어 소비 된 총 CPU 시간 (마이크로 초)입니다.

따라서 IDENTITY값 을 생성하는 코드가 NEWSEQUENTIALID()(인서트 당 약 5µs의 평균 인 2231의 차이는 10231308입니다.)이 테이블 정의의 경우 고정 CPU 비용이 키의 너비가 더 커서 발생하는 추가 논리적 읽기 및 쓰기를 능가하기에 충분히 높았습니다. (NB : Itzik Ben Gan은 여기서 유사한 테스트를 수행 했으며 인서트 당 2µs의 패널티를 발견했습니다)

그렇다면 왜 IDENTITYCPU보다 더 많은 CPU를 사용 UuidCreateSequential합니까?

나는 이것이이 기사에서 설명된다고 믿는다 . 열 번째 identity값이 생성 될 때마다 SQL Server는 디스크의 시스템 테이블에 변경 사항을 기록해야합니다.

MultiRow Insert는 어떻습니까?

단일 문에 100,000 행이 삽입되면 차이가 사라져서 약간의 이점이 GUID있었지만 명확한 결과는 없었습니다. 내 테스트에서 20 배치의 평균은

NEWSEQUENTIALID() IDENTITY()
----------------- -----------
1016              1088

Phil의 코드와 Mitch의 첫 번째 결과 세트에 명백한 페널티가없는 이유는 내가 사용한 다중 행 삽입을 수행하는 데 사용 된 코드가 발생했기 때문 SELECT TOP (@NumRows)입니다. 이를 통해 옵티마이 저가 삽입 될 행 수를 올바르게 추정하지 못했습니다.

이것은 (추정 적으로 순차적입니다!)에 대한 추가 정렬 작업을 추가 할 특정 티핑 포인트가 있기 때문에 유익한 것으로 보입니다 GUID.

GUID 정렬

이 정렬 작업은 BOL의 설명 텍스트에 필요하지 않습니다 .

Windows가 시작된 이후 지정된 컴퓨터에서이 기능으로 이전에 생성 된 GUID보다 큰 GUID를 작성합니다. Windows를 다시 시작한 후 GUID는 더 낮은 범위에서 다시 시작할 수 있지만 여전히 전 세계적으로 고유합니다.

따라서 SQL Server에서 계산 스칼라의 출력이 이미 identity열에 대해 이미 수행 된 것처럼 미리 정렬되어 있음을 인식하지 못하는 버그 나 최적화가없는 것 같습니다 . ( 편집 내가 이것을보고 불필요한 정렬 문제가 이제 Denali에서 수정되었습니다 )


그것은 많은 영향을 미치지는 않지만 명확성을 위해 단지 20 개의 캐시 된 ID 값인 Denny가 인용 한 숫자는 정확하지 않습니다. 10이어야합니다.
Aaron Bertrand

@AaronBertrand-감사합니다. 연결 한 기사 가 가장 유익합니다.
Martin Smith

8

매우 간단합니다. GUID를 사용하면 IDENTITY보다 줄에서 다음 숫자를 생성하는 것이 더 저렴합니다 (GUID의 현재 값을 저장할 필요가 없으며 IDENTITY가 있어야 함). NEWSEQUENTIALGUID의 경우에도 마찬가지입니다.

테스트를보다 공정하게 만들고 큰 CACHE가있는 SEQUENCER를 사용할 수 있습니다. 이는 IDENTITY보다 저렴합니다.

그러나 MR이 말했듯이 GUID에는 몇 가지 주요 이점이 있습니다. 실제로 IDENTITY 열보다 확장 성이 뛰어납니다 (단순하지 않은 경우에만).

참조 : http://blog.kejser.org/2011/10/05/boosting-insert-speed-by-generating-scalable-keys/


나는 그들이 순차적 guid를 사용하고 있다고 생각하지 않았다고 생각합니다.
Martin Smith

Martin :이 주장은 순차 GUID에서도 마찬가지입니다. IDENTITY를 저장해야합니다 (다시 시작한 후 이전 값으로 돌아 가기 위해) 순차적 GUID에는이 제한이 없습니다.
Thomas Kejser

2
예, 제 의견 후에 메모리에 저장하는 대신 지속적으로 저장하는 것에 대해 이야기하고 있음을 깨달았습니다. 2012는 캐시도 사용합니다 IDENTITY. 따라서 여기에 불만
Martin Smith

4

이런 유형의 질문에 매료되었습니다. 금요일 밤에 왜 게시해야 했습니까? :)

테스트가 INSERT 성능을 측정하기위한 것 일지라도, 오해의 소지가있는 여러 가지 요인 (루핑, 장기 실행 트랜잭션 등)을 도입했을 수 있습니다.

내 버전이 아무것도 입증하지 못한다고 확신하지만, ID는 GUID보다 성능이 뛰어납니다 (홈 PC의 경우 3.2 초 vs 6.8 초).

SET NOCOUNT ON

CREATE TABLE TestGuid2 (Id UNIQUEIDENTIFIER NOT NULL DEFAULT NEWSEQUENTIALID() PRIMARY KEY,
SomeDate DATETIME, batchNumber BIGINT, FILLER CHAR(100))

CREATE TABLE TestInt (Id Int NOT NULL identity(1,1) PRIMARY KEY,
SomeDate DATETIME, batchNumber BIGINT, FILLER CHAR(100))

DECLARE @Numrows INT = 1000000

CREATE TABLE #temp (Id int NOT NULL Identity(1,1) PRIMARY KEY, rowNum int)

DECLARE @LocalCounter INT = 0

--put rows into temp table
WHILE (@LocalCounter < @NumRows)
BEGIN
    INSERT INTO #temp(rowNum) VALUES (@LocalCounter)
    SET @LocalCounter += 1
END

--Do inserts using GUIDs
DECLARE @GUIDTimeStart DateTime = GETDATE()
INSERT INTO TestGuid2 (SomeDate, batchNumber) 
SELECT GETDATE(), rowNum FROM #temp
DECLARE @GUIDTimeEnd  DateTime = GETDATE()

--Do inserts using IDENTITY
DECLARE @IdTimeStart DateTime = GETDATE()
INSERT INTO TestInt (SomeDate, batchNumber) 
SELECT GETDATE(), rowNum FROM #temp
DECLARE @IdTimeEnd DateTime = GETDATE()

SELECT DATEDIFF(ms, @IdTimeStart, @IdTimeEnd) AS IdTime
SELECT DATEDIFF(ms, @GUIDTimeStart, @GUIDTimeEnd) AS GuidTime

DROP TABLE TestGuid2
DROP TABLE TestInt
DROP TABLE #temp

아무도 언급하지 않은 다른 요인은 데이터베이스 복구 모델과 로그 파일 증가입니다.
Mitch Wheat

데이터와 로그 파일이 모두 필요한 간단한 복구 모델의 새 데이터베이스에서 @Mitch는 필요한 것보다 크기가 작습니다. OP와 비슷한 결과를 얻습니다.
마틴 스미스

방금 Identity의 경우 2.560 초, Guid의 경우 3.666 초의 타이밍을 얻었습니다 (필요한 것보다 크기가 큰 데이터 및 로그 파일이 포함 된 간단한 복구 모델)
Mitch Wheat

@Mitch-OP의 코드가 모두 같은 트랜잭션이나 Phil의 코드에 있습니까?
마틴 스미스

이 포스터 코드에서 제가 여기에 댓글을 달았습니다. 또한 사용한 코드를 게시했습니다.
Mitch Wheat

3

샘플 스크립트를 여러 번 실행하여 배치 수와 크기를 약간 조정했습니다 (제공해 주셔서 대단히 감사합니다).

먼저 키 성능- INSERT속도 의 한 측면 만 측정한다고 말하겠습니다 . 따라서 가능한 빨리 테이블에 데이터를 가져 오는 데 관심이 없다면이 동물에는 더 많은 것이 있습니다.

내 발견은 일반적으로 당신과 비슷했습니다. 그러나, 나는이 점에서 차이 말할 것 INSERT사이의 속도 GUIDIDENTITY(INT 것은) 조금있다 GUID보다 IDENTITY- 어쩌면 +/- 실행 사이 10 %. 사용한 배치는 IDENTITY매번 2-3 % 미만으로 다양했습니다.

또한 테스트 상자는 사용자보다 강력하지 않으므로 행 수를 줄이십시오.


PK가 GUID 인 경우 엔진이 색인이 아니라 해싱 알고리즘을 사용하여 해당 레코드의 실제 위치를 결정할 수 있습니까? 해시 된 기본 키가있는 스파 스 테이블에 삽입하는 것은 인덱스 오버 헤드가 없기 때문에 기본 키에 인덱스가있는 테이블에 삽입하는 것보다 항상 빠릅니다. 그것은 단지 질문 일뿐입니다. 대답이 아니오 인 경우 투표하지 마십시오. 당국에 링크를 제공하십시오.

1

-이 같은 주제에 대한 유래에 또 다른 전환을 다시 참조거야 https://stackoverflow.com/questions/170346/what-are-the-performance-improvement-of-sequential-guid-over-standard-guid

내가 아는 한 가지는 순차적 GUID가 있다는 것은 리프 이동이 거의 없기 때문에 인덱스 사용이 더 좋고 HD 탐색을 줄이는 것입니다. 이 때문에 많은 페이지에 키를 배포 할 필요가 없으므로 삽입도 더 빠를 것이라고 생각합니다.

저의 개인적인 경험은 트래픽이 많은 DB를 구현할 때 다른 시스템과의 통합을 위해 훨씬 확장 성이 뛰어 나기 때문에 GUID를 사용하는 것이 좋습니다. 그것은 특히 복제 및 int / bigint 한도에 해당합니다. ... bigint가 부족하지는 않지만 결국에는 다시 돌아 가게됩니다.


1
BIGINTs가 부족하지 않습니다. 절대 참조 : sqlmag.com/blog/it-possible-run-out-bigint-values
Thomas Kejser
당사 사이트를 사용함과 동시에 당사의 쿠키 정책개인정보 보호정책을 읽고 이해하였음을 인정하는 것으로 간주합니다.
Licensed under cc by-sa 3.0 with attribution required.