Почему последовательные клавиши GUID работают быстрее, чем последовательные клавиши INT в моем тестовом примере?

39

Задав этот вопрос, сравнивая последовательные и непоследовательные GUID, я попытался сравнить производительность INSERT на 1) таблице с первичным ключом GUID, инициализируемой последовательно с newsequentialid(), и 2) таблице с первичным ключом INT, инициализированной последовательно с identity(1,1). Я ожидал бы, что последний будет самым быстрым из-за меньшей ширины целых чисел, и также кажется более простым генерировать последовательное целое число, чем последовательный GUID. Но, к моему удивлению, INSERT в таблице с целочисленным ключом были значительно медленнее, чем в последовательной таблице 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, как в примерах Фила Сэндлера, Митча Уитта и Мартина ниже, я также обнаружил, что IDENTITY быстрее, чем должно быть. Но это не обычный способ вставки строк, и я до сих пор не понимаю, почему эксперимент поначалу не удался: даже если я опускаю GETDATE () из моего исходного примера, IDENTITY () все еще намного медленнее. Таким образом, кажется, что единственный способ сделать IDENTITY () превосходящим NEWSEQUENTIALID () - это подготовить строки для вставки во временную таблицу и выполнить множество вставок в виде пакетной вставки с использованием этой временной таблицы. В общем, я не думаю, что мы нашли объяснение этому феномену, а IDENTITY () все еще медленнее для большинства практических применений. Кто-нибудь может объяснить это?

SomeName
источник
4
Только для размышления: может ли быть так, что генерация нового GUID может быть вообще выполнена без участия таблицы, тогда как получение следующего доступного значения идентификатора временно вводит некоторую блокировку, чтобы гарантировать, что два потока / соединения не получат одинаковое значение? Я просто догадываюсь. Интересный вопрос!
злой человек
4
Кто говорит, что они делают ?? Есть много доказательств того, что они этого не делают - посмотрите, что дисковое пространство Кимберли Триппа дешево - это не главное ! сообщение в блоге - она ​​делает довольно обширный обзор, и GUID всегда явно INT IDENTITY
проигрывают
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

Прочитав расследование @ Мартина, я перезапустил предложенный 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 (окончательная первоначальная версия) - 10.50.1600.1 (X64))

Митч Пшеничный
источник
7
Наглядно иллюстрирует важнейший элемент хорошего бенчмаркинга: убедитесь, что вы измеряете только одну вещь за один раз.
Аарона
Какой план у тебя здесь? Есть ли у него SORTоператор для GUID?
Мартин Смит
@Martin: Привет, я не проверял планы (делаю несколько вещей одновременно :)). Я посмотрю чуть позже ...
Митч Уит
@Mitch - есть какие-либо отзывы по этому поводу? Я, скорее, подозреваю, что главное, что вы здесь измеряете, - это время, затрачиваемое на сортировку направляющих для больших вставок, что, хотя и интересно, не отвечает на первоначальный вопрос OP, который заключался в объяснении того, почему последовательные направляющие работали лучше, чем столбцы идентификации на одном вставки строк в тестировании ОП.
Мартин Смит
2
@ Митч - Хотя чем больше я об этом думаю, тем меньше я понимаю, почему кто-то захочет использовать в NEWSEQUENTIALIDлюбом случае. Это сделает индекс глубже, будет использовать на 20% больше страниц данных в случае OP, и гарантированно будет только увеличиваться, пока машина не будет перезагружена, поэтому имеет много недостатков по сравнению с 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связано с процессом создания, который отвечает за результаты. Для инкрементного инкремента, вычисляемого самостоятельно, тогда результаты будут намного более точными, чем можно было бы ожидать при рассмотрении только стоимости ввода-вывода.

Когда я помещаю код вставки, описанный выше, в хранимые процедуры и проверяю, 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 Inserts?

Когда 100 000 строк были вставлены в одно утверждение, я обнаружил, что разница исчезла, GUIDхотя, возможно, незначительная выгода для случая, но далеко не так очевидна. Среднее значение для 20 партий в моем тесте было

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

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

Похоже, что это полезно, поскольку есть определенный переломный момент, когда он добавит дополнительную операцию сортировки для (предположительно, последовательных!) GUID.

GUID Сортировка

Эта операция сортировки не требуется из пояснительного текста в BOL .

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

Так что мне показалось ошибкой или отсутствующей оптимизацией, что SQL Server не распознает, что выходные данные вычислимого скаляра уже будут предварительно отсортированы, как это, очевидно, уже делает для identityстолбца. ( Изменить Я сообщил об этом, и проблема с ненужной сортировкой теперь исправлена ​​в Denali )

Мартин Смит
источник
Не то, чтобы это имело большое влияние, но просто в интересах ясности, число, которое цитировал Денни, 20 кэшированных значений идентичности, неверно - оно должно быть 10.
Аарон Бертран
@AaronBertrand - Спасибо. Та статья, на которую вы ссылаетесь , наиболее информативна.
Мартин Смит
8

Все очень просто: с GUID дешевле генерировать следующий номер в строке, чем для IDENTITY (текущее значение GUID хранить не нужно, IDENTITY должно быть). Это верно даже для NEWSEQUENTIALGUID.

Вы можете сделать тест более справедливым и использовать SEQUENCER с большим кэшем, который дешевле, чем IDENTITY.

Но, как говорит MR, у GUID есть несколько основных преимуществ. На самом деле, они НАМНОГО более масштабируемы, чем столбцы IDENTITY (но только если они НЕ последовательные).

Смотрите: http://blog.kejser.org/2011/10/05/boosting-insert-speed-by-generating-scalable-keys/

Томас Кейсер
источник
Я думаю, что вы пропустили, что они используют последовательные направляющие.
Мартин Смит
Мартин: аргумент верен и для последовательного GUID. IDENTITY должен быть сохранен (чтобы вернуться к своему старому значению после перезапуска), последовательный 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
Фил Сэндлер
источник
Другой фактор, который никто не упомянул, - это модель восстановления базы данных и рост файла журнала ...
Митч Уит
@ Совпадение с новой базой данных в простой модели восстановления с данными и файлами журналов в обоих измерениях по сравнению с тем, что требуется. Я получаю результаты, аналогичные ОП.
Мартин Смит
Я только что получил время в 2,560 секунды для идентификации и 3,666 секунды для Guid (в простой модели восстановления с данными и файлами журналов, размер которых значительно превышает требуемый)
Mitch Wheat
@Mitch - На коде ОП с ним все в одной транзакции или на коде Фила?
Мартин Смит
на этот код постеров, поэтому я комментирую здесь. Я также разместил код, который использовал ...
Митч Уит
3

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

Сначала я скажу, что вы измеряете только один аспект производительности клавиш - INSERTскорость. Так что, если вы не заинтересованы только в том, чтобы как можно быстрее получить данные в таблицы, у этого животного гораздо больше.

Мои выводы были в целом похожи на ваши. Тем не менее, я хотел бы отметить , что вариации в INSERTскорости между GUIDи IDENTITY(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. Я думаю, что из-за этого вставки будут быстрее, так как не нужно распределять ключи по большому количеству страниц.

Мой личный опыт показывает, что при реализации большой базы данных с большим трафиком лучше использовать GUID, потому что это делает ее гораздо более масштабируемой для интеграции с другими системами. Это касается, в частности, репликации и ограничений int / bigint ... не то, что у вас закончатся bigints, но в конечном итоге вы это сделаете и вернетесь назад.

МИСТЕР
источник
1
У вас не заканчиваются BIGINT, никогда ... Смотрите это: sqlmag.com/blog/it-possible-run-out-bigint-values
Томас Кейсер