Sınama durumumda neden sıralı GUID tuşları sıralı INT tuşlarından daha hızlı çalışıyor?


39

Ardışık ve ardışık olmayan GUID'leri karşılaştırarak bu soruyu sorduktan sonra , INSERT performansını 1) bir tabloyla sırayla başlatılmış bir GUID birincil anahtarıyla newsequentialid()ve 2) sıralı bir şekilde başlatılmış bir INT birincil anahtarıyla bir tabloyu karşılaştırmaya çalıştım identity(1,1). İkincisinin tamsayıların daha küçük olması nedeniyle en hızlı olmasını beklerdim ve sıralı bir GUID'den sıralı bir tamsayı oluşturmak daha kolay görünüyor. Fakat benim için sürpriz, tamsayı anahtarlı tablodaki INSERT'lerin sıralı GUID tablosundan çok daha yavaştı.

Bu, test çalıştırmaları için ortalama zaman kullanımını (ms) gösterir:

NEWSEQUENTIALID()  1977
IDENTITY()         2223

Bunu kimse açıklayabilir mi?

Aşağıdaki deney kullanıldı:

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

GÜNCELLEME: Aşağıdaki örnekte Phil Sandler, Mitch Wheat ve Martin tarafından yapılan örneklerde olduğu gibi TEMP tablosuna dayalı eklemeleri yapmak için betiği değiştirmek, ayrıca KİMLİK'ün olması gerektiği gibi hızlı olduğunu da biliyorum. Ancak bu satırları eklemek için geleneksel bir yöntem değildir ve hala denemenin ilk başta neden yanlış gittiğini anlamıyorum: orijinal örneğimden GETDATE () atladığım halde IDENTITY () hala çok daha yavaş. Bu yüzden, IDENTITY () 'yi NEWSEQUENTIALID ()' den daha iyi hale getirmenin tek yolu, geçici bir tabloya yerleştirmek için sıraları hazırlamak ve bu geçici tabloyu kullanarak birçok eklemeyi toplu ekleme olarak yapmak gibi görünüyor. Sonuçta, fenomenle ilgili bir açıklama bulduğumuzu sanmıyorum ve IDENTITY () çoğu pratik kullanım için hala daha yavaş görünüyor. Bunu kimse açıklayabilir mi?


4
Sadece bir düşünce: Yeni bir GUID oluşturmak, tabloya hiç yer vermeden yapılabilir, oysa bir sonraki kullanılabilir kimlik değerinin alınması, iki iş parçacığının / bağlantının aynı değeri alamayacağından emin olmak için geçici olarak bir tür kilitleme sağlar. Ben sadece gerçekten tahmin ediyorum. İlginç soru!
öfkeli bir insan

4
Kim diyorlar ki ?? Yapmadıkları çok fazla kanıt var - Kimberly Tripp'in Disk alanının ucuz olduğunu görün - Önemli değil ! blog yazısı - oldukça kapsamlı bir inceleme yaptı ve GUID'ler her zaman açıkça ortaya çıkıyorINT IDENTITY
marc_s

2
Eh, yukarıdaki deney, aksini gösterir ve sonuçlar tekrarlanabilir.
someName

2
Kullanmak IDENTITYmasa kilidi gerektirmez. Kavramsal olarak MAX (id) + 1 almasını beklediğinizi görebiliyordum, ancak gerçekte bir sonraki değer saklanıyor. Aslında bir sonraki GUID'yi bulmaktan daha hızlı olması gerekir.

4
Ayrıca, muhtemelen TestGuid2 tablosu için doldurma sütunu, satırları eşit boyutta yapmak için CHAR (88) olmalıdır
Mitch Wheat

Yanıtlar:


19

@Phil Sandler'ın kodunu GETDATE () olarak adlandırmanın etkisini kaldırmak için değiştirdim (dahil olan donanım efektleri / kesintileri olabilir mi?) Ve aynı uzunlukta satırlar yaptım.

[SQL Server 2000'den bu yana zamanlama sorunları ve yüksek çözünürlüklü zamanlayıcılarla ilgili birkaç makale var, bu yüzden bu etkiyi en aza indirmek istedim.]

Verileri ve günlük dosyasını içeren basit kurtarma modelinde, gerekenin üzerinde bir boyutta yol var, işte zamanlamalar (saniye cinsinden): (Aşağıdaki kodları tam alarak yeni sonuçlarla güncellendi)

       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

Kullanılan kod:

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 soruşturmasını okuduktan sonra, her iki durumda da önerilen TOP (@num) ile tekrar koştum, yani

...
--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()
...

ve işte zamanlama sonuçları:

       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

Sorgu asla geri gelmediğinden gerçek yürütme planını alamadım! Bir hata muhtemel görünüyor. (Microsoft SQL Server 2008 R2’nin çalıştırılması (RTM) - 10.50.1600.1 (X64))


7
İyi kıyaslamanın kritik unsurunu net bir şekilde gösterir: Her seferinde sadece bir şeyi ölçtüğünüzden emin olun.
Aaron,

Ne planın var burada? SORTGUID'ler için bir işleci var mı ?
Martin Smith,

@ Martin: Merhaba, planları kontrol etmedim (aynı anda birkaç şey yapıyorum :)). Biraz sonra bakacağım ...
Mitch Wheat

@Mitch - Bu konuda herhangi bir geri bildirim var mı? Burada ölçtüğünüz ana şeyin, sıralı kılavuzların neden tek satırda kimlik sütunlarından daha iyi performans gösterdiğine dair bir açıklama yapan, OP'nin orijinal sorusuna cevap vermediği halde ilginç olanları eklemek için kılavuzları ayırmanın zamanı geldiğinden şüpheliyim. OP'nin testine satır ekler.
Martin Smith,

2
@Mitch - Ne kadar çok düşünsem de neden birilerinin neden kullanmak istediğini anlayamıyorum NEWSEQUENTIALID. Dizini daha derin hale getirecek, OP'nin durumunda% 20 daha fazla veri sayfası kullanacak ve yalnızca makine yeniden başlatılıncaya kadar artması garanti edildiğinden bir üzerinde dezavantajları var identity. Öyle görünüyor ki, bu durumda Sorgu Planı da gereksiz bir ekliyor!
Martin Smith

19

Veri dosyası 1 GB boyutunda ve günlük dosyası 3 GB değerinde (dizüstü bilgisayar makinesi, aynı sürücüdeki her iki dosya) ve kurtarma aralığı 100 dakikaya ayarlanmış (sonuçları eğriltmek için bir kontrol noktasının olmaması) basit kurtarma modelinde yeni bir veritabanında tek sıra ile size benzer sonuçlar inserts.

Üç vakayı test ettim: Her durumda aşağıdaki tablolara ayrı ayrı 100.000 satır ekleyerek 20 seri yaptım. Komut dizelerinin tamamı bu cevaplamanın revizyon tarihinde bulunabilir .

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)
  )  

Üçüncü tablo için test, artan bir Iddeğere sahip satırlar ekledi, ancak bu, bir değişkenin bir döngü içindeki değerini artırarak kendi kendine hesaplandı.

20 parti boyunca geçen zamanın ortalaması aşağıdaki sonuçları verdi.

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

Sonuç

Bu yüzden kesinlikle identitysonuçlardan sorumlu olan yaratma sürecinin ek yükü olduğu görülüyor . Kendi kendine hesaplanan artış tamsayısı için, sonuçlar yalnızca GÇ maliyeti göz önüne alındığında ne görmesi beklendiğine paraleldir.

Yukarıda açıklanan ekleme kodunu saklı yordamlara koyduğumda ve gözden sys.dm_exec_procedure_statsgeçirdiğimde aşağıdaki sonuçları veriyor.

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

Yani bu sonuçlarda total_worker_timeyaklaşık% 30 daha yüksektir. Bu temsil eder

Mikrosaniye cinsinden, derlendiğinden beri bu saklı yordamın yürütülmesiyle tüketilen toplam CPU süresi.

Bu yüzden basitçe, IDENTITYdeğeri üreten kod, CPU'dan daha yoğun olan CPU'yu gösteriyor. NEWSEQUENTIALID()(2 rakam arasındaki fark, ekleme başına ortalama 5µs olan ortalama 10231308'dir.) Ve bu tablo için bu sabit CPU maliyeti Anahtarın genişliğinin genişliğinden dolayı ortaya çıkan ek mantıksal okuma ve yazmalara göre daha yüksekti. (Not: Itzik Ben Gan da burada benzer testler yaptı ve uç başına 2 2s ceza buldu)

Peki neden daha IDENTITYfazla işlemci daha yoğun UuidCreateSequential?

Bunun bu makalede açıklandığına inanıyorum . identityÜretilen her onuncu değer için SQL Server, değişikliği sistem tablolarına diskte yazmak zorundadır.

Peki ya MultiRow Uçlar?

100.000 satır tek bir ifadeye eklendiğinde, farkın, belki de durum için küçük bir faydasıyla ortadan kalktığını, GUIDancak net sonuçların hiçbir yerinde olmadığını gördüm . Testimde ortalama 20 parti

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

Phil'in kodu ve Mitch'in ilk sonuçlarında açıkça görülmemesinin sebebi, kullanılan çoklu satır ekleme işleminde kullandığım kodun olmasıdır SELECT TOP (@NumRows). Bu, optimizasyon cihazının eklenecek satır sayısını doğru şekilde tahmin etmesini önledi.

Bu, (sözde sırayla!) GUIDS için ek bir sıralama işlemi ekleyeceği belirli bir devrilme noktası olduğu için faydası görünüyor .

GUID Sırala

Bu sıralama işlemi BOL'deki açıklayıcı metinden gerekli değildir .

Windows başlatıldığından bu yana belirli bir bilgisayarda bu işlev tarafından daha önce oluşturulan herhangi bir GUID'den daha büyük bir GUID oluşturur. Windows'u yeniden başlattıktan sonra, GUID daha düşük bir aralıktan yeniden başlayabilir, ancak yine de genel olarak benzersizdir.

Bu yüzden bana, SQL Server'ın hesaplama skalarının çıktısının zaten görünüşte identitysütun için olduğu gibi önceden sıralanacağını bilmediğini fark ettiği bir hata ya da eksik optimizasyon gibiydi . ( Düzenle bunu bildirdim ve gereksiz sıralama sorunu artık Denali’de düzeltildi )


Çok fazla etkiye sahip olmadığını, ancak sadece netlik açısından Denny alıntı, 20 önbelleğe alınmış kimlik değerlerinin yanlış olduğunu - 10 olmalıdır
Aaron Bertrand

@AaronBertrand - Teşekkürler. Bağladığın o makale en bilgilendirici.
Martin Smith

8

Oldukça basit: GUID ile, satırdaki bir sonraki sayıyı KİMLİK için olduğundan daha ucuza getirir (GUID'in mevcut değerinin depolanması gerekmez, KİMLİK olması gerekir). Bu NEWSEQUENTIALGUID için bile geçerlidir.

Testi daha adil hale getirebilir ve KİMLİK'ten daha ucuz olan, büyük bir CACHE olan bir SEQUENCER kullanabilirsiniz.

Ancak MR'ın dediği gibi, GUID'lerin bazı büyük avantajları vardır. Nitekim, KİMLİK sütunlarından çok daha fazla ölçeklenebilirler (ancak sıralı DEĞİL ise).

Bakınız: http://blog.kejser.org/2011/10/05/boosting-insert-speed-by-generating-scalable-keys/


Bence sıralı kılavuz kullandıklarını özlediniz.
Martin Smith,

Martin: Argüman sıralı GUID için de geçerlidir. KİMLİK saklanmalıdır (yeniden başlatmanın ardından eski değerlerine dönmek için), sıralı GUID'nin bu sınırlaması yoktur.
Thomas Kejser

2
Evet, yorumumdan sonra hafızada saklamak yerine sürekli saklamaktan bahsettiğinizi fark ettiniz. 2012 için IDENTITYde bir önbellek kullanıyor . Dolayısıyla burada şikayetleri
Martin Smith

4

Bu tür bir soru beni çok etkiledi. Neden cuma akşamı göndermek zorunda kaldın? :)

Testiniz SADECE INSERT performansını ölçmek için tasarlanmış olsa bile, yanıltıcı olabilecek (döngü, uzun süren bir işlem vb.) Bir dizi faktöre yol açtınız.

Sürümümün hiçbir şeyi kanıtlamadığı konusunda tam olarak ikna olmadım, ancak kimlik içindeki GUID'lerden daha iyi performans gösteriyor (evdeki bir bilgisayarda 3.2 saniye vs 6.8 saniye):

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

Kimsenin bahsetmediği diğer bir faktör ise veritabanı kurtarma modeli ve kütük dosyası büyümesi ...
Mitch Wheat

@ Yeni bir veritabanında basit kurtarma modelinde veri ve log dosyası bulunan her iki boyutta da gerekli olanın üzerinde hem de OP ile benzer sonuçlar elde ediyorum.
Martin Smith

Kimlik için sadece 2.560 saniyelik, Guid için 3.666 saniyelik zamanlarım (gerek veri gerek kayıt dosyasındaki basit kurtarma modelinde gerekenin üzerinde bir boyutta)
Mitch Wheat

@Mitch - OP'nin kodunda hepsi aynı işlemde veya Phil'in kodunda mı?
Martin Smith,

Bu afiş kodunda, bu yüzden burada yorum yapıyorum. Ayrıca kullandığım kodu da gönderdim ...
Mitch Wheat

3

Örnek komut dosyanızı birkaç kez toplu işlem sayısı ve boyutuna göre birkaç ayar yaparak koştum (ve sağladığınız için çok teşekkür ederim).

Öncelikle, tuşların performans - INSERThızının yalnızca bir kez ölçümünü yaptığınızı söyleyeceğim . Bu nedenle, özellikle tablolara mümkün olduğunca çabuk veri girmekle ilgilenmiyorsanız, bu hayvan için çok daha fazlası var.

Bulgularım genel olarak sizinkine benzerdi. Ancak, bunu içinde değişimini söz ediyorum INSERTarasındaki hız GUIDve IDENTITY(int) hafifçe daha büyük olan GUIDile daha IDENTITY- belki +/- koşular arasında% 10. Kullanılan partiler IDENTITYher seferinde% 2 - 3'ten daha az değişmiştir.

Ayrıca, test kutumun sizinkinden daha az güçlü olduğu için daha küçük sıra sayıları kullanmam gerekti.


PK bir GUID olduğunda, motorun bir indeks değil, karşılık gelen kaydın fiziksel konumunu belirlemek için bir karma algoritması kullanması mümkündür. Biriktirilmiş birincil anahtarlara sahip seyrek bir tabloya ekler, endeks ek yükünün olmaması nedeniyle birincil anahtarda bir dizine sahip bir tabloya eklenmiş olduğundan her zaman daha hızlıdır. Bu sadece bir soru - cevap hayır ise bana oy verme. Sadece otorite bağlantısını ver.

1

Bu aynı konu için yığın akışındaki başka bir konvime geri döneceğim - https://stackoverflow.com/questions/170346/what-are-the-performance-improvement-of-sequential-guid-over-standard-guid

Bildiğim bir şey, sıralı GUID'lere sahip olmanın, endeks kullanımının çok az yaprak hareketi nedeniyle daha iyi olması ve dolayısıyla HD arayışını azaltmasıdır. Bu nedenle, eklerin de daha hızlı olacağını düşünürdüm, çünkü tuşları çok sayıda sayfaya dağıtmak zorunda kalmıyor.

Kişisel deneyimim, yüksek trafik yoğunluğa sahip bir DB uyguladığınızda, GUID'leri kullanmak daha iyidir, çünkü diğer sistemlerle entegrasyon için daha ölçeklenebilir hale getirir. Bu çoğaltma için de geçerli, sen bigints tükendi, ama sonunda olacak ve döngü tekrar o spesifik olarak ve int / Bigint limitleri .... değil.


1
BIGINTs bitmez, asla ... Şuna
Thomas Kejser
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.