Чому послідовні ключі GUID працюють у моєму тесті швидше, ніж послідовні клавіші INT?


39

Задавши це питання, порівнюючи послідовні та непослідовні GUID, я спробував порівняти продуктивність INSERT на 1) таблиці з первинним ключем GUID, ініціалізованою послідовно newsequentialid(), та 2) таблиці з первинним ключем INT, ініціалізованим послідовно identity(1,1). Я б очікував, що остання буде найшвидшою через меншу ширину цілих чисел, а також видається простішим для отримання послідовного цілого числа, ніж послідовного GUID. Але на мій подив, ВСТАВКИ на столі з цілим ключем були значно повільнішими, ніж послідовна таблиця GUID.

Це показує середнє використання часу (мс) для тестових прогонів:

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

ОНОВЛЕННЯ: Змінюючи сценарій для виконання вставок на основі таблиці TEMP, як у прикладах Філа Сандлера, Мітча Пшениці та Мартіна нижче, я також вважаю, що ІДЕНТИЧНІСТЬ швидша, як і повинна бути. Але це не звичайний спосіб вставлення рядків, і я досі не розумію, чому експеримент спочатку пішов не так: навіть якщо я опускаю GETDATE () з мого оригінального прикладу, IDENTITY () все ще відбувається повільніше. Отже, здається, що єдиний спосіб перемогти IDENTITY () перевершити NEWSEQUENTIALID () - це підготувати рядки до вставки у тимчасовій таблиці та виконати безліч вставок як пакетної вставки за допомогою цієї таблиці temp. Загалом, я не думаю, що ми знайшли пояснення цьому явищу, і IDENTITY () все ще здається повільнішим для більшості практичних звичаїв. Хтось може це пояснити?


4
Тільки думка: чи може бути так, що генерувати новий GUID можна, навіть не залучаючи таблицю, тоді як отримання наступного наявного значення ідентифікації вводить якийсь замок тимчасово, щоб два потоки / з'єднання не отримали однакове значення? Я просто здогадуюсь насправді. Цікаве запитання!
розлючена людина

4
Хто каже, що роблять ?? Є багато доказів, яких вони не мають - бачити, що місця на диску Кімберлі Триппа є дешевим - це НЕ СУМНО! допис у блозі - вона робить досить обширний огляд, а GUID завжди чітко втрачається доINT IDENTITY
marc_s

2
Ну, експеримент вище показаний навпаки, і результати повторювані.
деякіНазви

2
Для використання IDENTITYне потрібно блокування таблиці. Концептуально я міг бачити, що ви можете очікувати, що він буде приймати MAX (id) + 1, але насправді наступне значення зберігається. Насправді це має бути швидше, ніж пошук наступного GUID.

4
Також, мабуть, стовпчик наповнювача для таблиці TestGuid2 повинен бути CHAR (88), щоб рядки були однаковими за розміром
Мітч Пшеничний

Відповіді:


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

Прочитавши розслідування @ Мартіна, я повторно зіткнувся з запропонованим 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

Якого плану ви тут? Чи є у нього SORTоператор для GUID?
Мартін Сміт

@Martin: Привіт, я не перевіряв плани (робив декілька речей одразу :)). Погляну трохи пізніше ...
Мітч Пшеничний

@Mitch - Будь-який відгук про це? Я скоріше підозрюю, що головне, що ви тут вимірюєте, - це час, який потрібен для сортування посібників для великих вставок, які, хоча цікаво, не відповідають початковому питанню ОП, яке стосувалося пояснення того, чому послідовні посібники працюють краще, ніж стовпці ідентичності на одиночному рядкові вставки в тестуванні ОП.
Мартін Сміт

2
@Mitch - Хоча чим більше я думаю про це, тим менше я розумію, чому хто-небудь хотів би використати NEWSEQUENTIALIDвсе одно. Це зробить індекс глибшим, використає на 20% більше сторінок даних у випадку ОП, і гарантується, що він постійно збільшуватиметься, поки машина не перезавантажиться, тому матиме чимало недоліків у порівнянні з identity. Просто в цьому випадку здається, що План запитів додає ще один непотрібний!
Мартін Сміт

19

На новій базі даних у простій моделі відновлення з файлом даних розміром 1 ГБ та файлом журналу в 3 ГБ (портативна машина, обидва файли на одному накопичувачі) та інтервалом відновлення встановлено 100 хвилин (щоб уникнути перевірки результатів перекосу результатів) подібні вам результати з одним рядком inserts.

Я перевірив три випадки: для кожного випадку я робив 20 партій, вставляючи 100 000 рядків окремо в наступні таблиці. Повні сценарії можна знайти в історії ревізії цієї відповіді .

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% вище. Це являє собою

Загальна кількість часу процесора в мікросекундах, яке було спожито виконанням цієї збереженої процедури з моменту її складання.

Таким чином, це просто здається, ніби код, який генерує IDENTITYзначення, є більш інтенсивним процесором, ніж той, який генерує NEWSEQUENTIALID()(Різниця між двома цифрами - 10231308, що в середньому становить приблизно 5 мкс на вставку.), І що для цього визначення таблиці ця фіксована вартість процесора була достатньо високою, щоб переважувати додаткові логічні читання та записи, що виникають із-за більшої ширини ключа. (Примітка: Іцик Бен Ган провів подібне тестування і знайшов штраф у розмірі 2 мкс за вставку)

То чому ж IDENTITYінтенсивніший процесор, ніж UuidCreateSequential?

Я вважаю, що це пояснено в цій статті . Для кожного десятого identityствореного значення SQL Server повинен записати зміни в системні таблиці на диску

Що з вкладками MultiRow?

Коли 100000 рядків вставлено в одне твердження, я виявив, що різниця зникла, мабуть, незначна користь для GUIDсправи, але ніде не так чіткі результати скорочення. Середній показник для 20 партій у моєму тесті був

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

Причина того, що в ньому не передбачено покарання в коді Філа та першому наборі результатів Мітча, тому що так сталося, що код, який я використовував, робив використовуваний багаторядковий вставку SELECT TOP (@NumRows). Це завадило оптимізатору правильно оцінити кількість рядків, які будуть вставлені.

Це здається корисним, оскільки існує певна переломна точка, в якій це додасть додаткову операцію сортування для (нібито послідовних!) GUIDS.

Сортування GUID

Така операція сортування не потрібна в пояснювальному тексті в BOL .

Створює GUID, який перевищує будь-який GUID, раніше створений цією функцією на визначеному комп'ютері з моменту запуску Windows. Після перезапуску Windows GUID може запуститися знову з нижчого діапазону, але все ще є унікальним у всьому світі.

Отже, мені здалося помилкою або відсутністю оптимізації, що SQL Server не визнає, що вихід обчислювального скаляра буде вже попередньо відсортований, як це, мабуть, вже робиться для identityстовпця. ( Редагувати. Я повідомив про це, і непотрібна проблема сортування тепер виправлена ​​в Деналі )


Не те, що це має чималий вплив, але просто в інтересах ясності число, яке Денні цитував, 20 кешованих значень ідентичності, є невірним - воно повинно бути 10.
Аарон Бертран

@AaronBertrand - Дякую Ця стаття, яку ви пов’язали, є найбільш інформативною.
Мартін Сміт

8

Досить просто: за допомогою GUID дешевше генерувати наступне число у рядку, ніж це для IDENTITY (Поточне значення GUID не потрібно зберігати, ідентифікатор повинен бути). Це справедливо навіть для NEWSEQUENTIALGUID.

Ви можете зробити тест більш справедливим і використовувати СЕКВЕНЦЕР з великим КАШ - що дешевше, ніж ІДЕНТИЧНІСТЬ.

Але, як говорить MR, є деякі основні переваги GUID. Власне кажучи, вони набагато масштабніші, ніж стовпці ІДЕНТИМНОСТІ (але лише якщо вони НЕ є послідовними).

Дивіться: http://blog.kejser.org/2011/10/05/boosting-insert-speed-by-generating-scalable-keys/


Я думаю, ти пропустив, що вони використовують послідовні настанови.
Мартін Сміт

Мартін: аргумент справедливий і для послідовного GUID. Ідентифікатор повинен бути збережений (для повернення до старого значення після перезавантаження), послідовний GUID не має цього обмеження.
Томас Кейсер

2
Так, зрозумів, що після мого коментаря ви говорили про постійне зберігання, а не про збереження в пам'яті. 2012 рік також використовує кеш-пам'ять IDENTITY. звідси скарги
Мартін Сміт

4

Мене захоплює такий тип питань. Чому вам довелося розміщувати це в ніч на п’ятницю? :)

Я думаю, навіть якщо ваш тест призначений ТІЛЬКИ для вимірювання продуктивності INSERT, ви (можливо) ввели ряд факторів, які можуть ввести в оману (циклічне завершення, тривала транзакція тощо).

Я не повністю впевнений, що моя версія нічого не підтверджує, але ідентичність працює краще, ніж GUID-файли в ній (3,2 секунди проти 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 на новій базі даних у простій моделі відновлення з даними та файлом журналу, розміром в один і той же розмір, ніж потрібно, я отримую подібні результати до ОП.
Мартін Сміт

Щойно я отримав таймінги 2,560 секунд для Identity і 3,666 секунди для Guid (у простій моделі відновлення з даними та файлом журналу в обидва розміри, ніж потрібно)
Mitch Wheat

@Mitch - Про код ОП з усіма в одній транзакції чи за кодом Філа?
Мартін Сміт

на цей код плакатів, тому я коментую тут. Я також опублікував код, який я використав ...
Mitch Wheat

3

Я декілька разів запускав ваш зразок сценарію, роблячи декілька налаштувань для кількості та розміру партії (і дуже дякую за надання).

Спочатку скажу, що ви вимірюєте лише один раз аспект продуктивності ключів - INSERTшвидкість. Тож якщо ви конкретно не переймаєтесь тим, щоб якомога швидше вносити дані в таблиці, для цієї тварини набагато більше

Мої висновки були загалом схожі на ваші. Тим НЕ менше, я хотів би зазначити , що варіації в INSERTшвидкості між GUIDі IDENTITY(INT) трохи більше , з GUIDчим IDENTITY- може бути +/- 10% між запусками. Партії, які використовували, IDENTITYщоразу варіювали менше 2 - 3%.

Також зауважте, моє тестове поле явно менш потужне, ніж ваше, тому мені довелося використовувати менший кількість рядків.


Коли ПК є GUID, чи можливо, що двигун використовує не індекс, а алгоритм хешування для визначення фізичного розташування відповідного запису? Вставки в розріджену таблицю з хешованими первинними ключами завжди швидші, ніж вставки в таблицю з індексом на первинному ключі через відсутність накладних індексів. Це лише питання - не голосуйте, якщо відповідь є ні. Просто надайте посилання органу влади.

1

Я повернусь до іншого перегляду стаціонарного потоку для цієї ж теми - https://stackoverflow.com/questions/170346/what-are-the-performance-improvement-of-sequences-guid-over-standard-guid

Одне, що я знаю, це те, що мати послідовні GUID - це те, що використання індексу краще за рахунок дуже невеликого руху листків, а отже, зменшення пошуку HD. Я б подумав, що через це вставки також будуть швидшими, оскільки не потрібно розподіляти клавіші на великій кількості сторінок.

Мій особистий досвід полягає в тому, що, коли ви реалізуєте великий БД з високим трафіком, краще використовувати GUID, оскільки це робить його набагато більш масштабованим для інтеграції з іншими системами. Зокрема, це стосується реплікації та обмежень int / bigint .... не для того, щоб у вас закінчилися bigints, але, зрештою, ви будете і повертатися назад.


1
У вас не закінчується BIGINT, ніколи ... Дивіться це: sqlmag.com/blog/it-possible-run-out-bigint-values
Thomas Kejser
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.