Por que as chaves GUID sequenciais são mais rápidas que as chaves INT seqüenciais no meu caso de teste?

39

Depois de fazer esta pergunta comparando GUIDs seqüenciais e não sequenciais, tentei comparar o desempenho INSERT em 1) uma tabela com uma chave primária GUID inicializada sequencialmente com newsequentialid()e 2) uma tabela com uma chave primária INT inicializada sequencialmente com identity(1,1). Eu esperaria que o último fosse mais rápido devido à menor largura de números inteiros, e também parece mais simples gerar um número inteiro seqüencial do que um GUID seqüencial. Mas, para minha surpresa, os INSERTs na tabela com a chave inteira eram significativamente mais lentos que a tabela GUID seqüencial.

Isso mostra o tempo médio de uso (ms) para a execução do teste:

NEWSEQUENTIALID()  1977
IDENTITY()         2223

Alguém pode explicar isso?

A seguinte experiência foi usada:

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

ATUALIZAÇÃO: Modificando o script para executar as inserções com base em uma tabela TEMP, como nos exemplos de Phil Sandler, Mitch Wheat e Martin abaixo, também acho que a IDENTITY é mais rápida do que deveria ser. Mas essa não é a maneira convencional de inserir linhas, e ainda não entendo por que o experimento deu errado no início: mesmo que eu omita GETDATE () do meu exemplo original, IDENTITY () ainda é muito mais lento. Portanto, parece que a única maneira de fazer com que IDENTITY () supere NEWSEQUENTIALID () é preparar as linhas para inserir em uma tabela temporária e executar muitas inserções como inserção em lote usando esta tabela temporária. Em suma, acho que não encontramos uma explicação para o fenômeno, e o IDENTITY () ainda parece ser mais lento para a maioria dos usos práticos. Alguém pode explicar isso?

someName
fonte
4
Apenas um pensamento: será possível gerar um novo GUID sem envolver a tabela, enquanto a obtenção do próximo valor de identidade disponível introduz algum tipo de bloqueio temporariamente para garantir que dois threads / conexões não obtenham o mesmo valor? Estou apenas adivinhando mesmo. Pergunta interessante!
pessoa com raiva
4
Quem disse que eles fazem ?? Há muitas evidências de que não existem - veja que o espaço em disco de Kimberly Tripp é barato - NÃO é esse o ponto! Blog Post - ela faz bastante uma extensa revisão, e GUIDs sempre saem perdendo claramenteINT IDENTITY
marc_s
2
Bem, o experimento acima mostra o contrário, e os resultados são repetíveis.
somename
2
O uso IDENTITYnão requer um bloqueio de tabela. Conceitualmente, pude ver que você espera que ele esteja usando MAX (id) + 1, mas, na realidade, o próximo valor é armazenado. Na verdade, deve ser mais rápido do que encontrar o próximo GUID.
4
Também, presumivelmente, a coluna de enchimento para a tabela de TestGuid2 deve ser CHAR (88) para fazer as linhas de igual tamanho
Mitch trigo

Respostas:

19

Modifiquei o código de @Phil Sandler para remover o efeito de chamar GETDATE () (pode haver efeitos / interrupções de hardware envolvidos ??) e criei linhas com o mesmo comprimento.

[Existem vários artigos desde o SQL Server 2000 relacionados a problemas de temporização e cronômetros de alta resolução, então eu queria minimizar esse efeito.]

No modelo de recuperação simples, com dados e arquivos de log em tamanho acima do necessário, eis os tempos (em segundos): (Atualizado com novos resultados com base no código exato abaixo)

       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

O código usado:

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

Depois de ler a investigação de @ Martin, corri novamente com o TOP sugerido (@num) nos dois casos, ou seja,

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

e aqui estão os resultados do timing:

       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

Não consegui obter o plano de execução real, pois a consulta nunca retornou! Parece que um erro é provável. (Executando o Microsoft SQL Server 2008 R2 (RTM) - 10.50.1600.1 (X64))

Mitch Wheat
fonte
7
Ilustra perfeitamente o elemento crítico de uma boa referência: verifique se você está medindo apenas uma coisa de cada vez.
Aaronaught 14/05
Que plano você tem aqui? Possui um SORToperador para os GUIDs?
Martin Smith
@ Martin: Oi, eu não verifiquei os planos (fazendo algumas coisas ao mesmo tempo :)). Eu vou ter um olhar um pouco mais tarde ...
Mitch Wheat
@ Mitch - Algum feedback sobre isso? Suspeito que a principal coisa que você está medindo aqui é o tempo gasto para classificar os guias para inserções grandes que, apesar de interessantes, não respondem à pergunta original do OP, que era sobre dar uma explicação de por que os guias seqüenciais tiveram melhor desempenho do que as colunas de identidade em uma única inserções de linha nos testes do OP.
Martin Smith
2
@ Mitch - Embora quanto mais eu penso sobre isso, menos eu entendo por que alguém iria querer usar de NEWSEQUENTIALIDqualquer maneira. Isso tornará o índice mais profundo, usará 20% mais páginas de dados no caso do OP e só será garantido um aumento até a reinicialização da máquina, por isso há muitas desvantagens em relação a um identity. Neste caso, parece que o Plano de Consulta adiciona outro desnecessário!
Martin Smith
19

Em um novo banco de dados no modelo de recuperação simples, com o arquivo de dados dimensionado em 1 GB e o arquivo de log em 3 GB (laptop, ambos arquivos na mesma unidade) e o intervalo de recuperação definido como 100 minutos (para evitar um ponto de verificação distorcido os resultados), vejo resultados semelhantes a você com a única linha inserts.

Testei três casos: para cada caso, fiz 20 lotes de inserção de 100.000 linhas individualmente nas tabelas a seguir. Os scripts completos podem ser encontrados no histórico de revisões desta resposta .

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

Para a terceira tabela, o teste inseriu linhas com um Idvalor incremental, mas este foi calculado automaticamente incrementando o valor de uma variável em um loop.

A média do tempo gasto nos 20 lotes deu os seguintes resultados.

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

Conclusão

Definitivamente, parece haver uma sobrecarga no identityprocesso de criação responsável pelos resultados. Para o número inteiro incremental auto-calculado, os resultados são muito mais alinhados com o que seria esperado ver ao considerar apenas o custo de IO.

Quando coloco o código de inserção descrito acima nos procedimentos armazenados e revisto, sys.dm_exec_procedure_statsele fornece os seguintes resultados

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

Portanto, nesses resultados total_worker_timeé cerca de 30% maior. Isto representa

Quantidade total de tempo de CPU, em microssegundos, consumido pelas execuções deste procedimento armazenado desde que ele foi compilado.

Portanto, simplesmente parece que o código que gera o IDENTITYvalor é mais intensivo em CPU do que o que gera a NEWSEQUENTIALID()(A diferença entre os dois números é 10231308, que calcula a média de cerca de 5µs por inserção) e que, para esta tabela, define esse custo fixo da CPU foi suficientemente alto para compensar as leituras e gravações lógicas adicionais incorridas devido à maior largura da chave. (Nota: Itzik Ben Gan fez testes semelhantes aqui e encontrou uma penalidade de 2µs por inserção)

Então, por que é IDENTITYmais intensivo em CPU do que UuidCreateSequential?

Eu acredito que isso é explicado neste artigo . Para cada décimo identityvalor gerado, o SQL Server precisa gravar a alteração nas tabelas do sistema em disco

E as Pastilhas MultiRow?

Quando as 100.000 linhas são inseridas em uma única declaração, descobri que a diferença desapareceu, ainda com um pequeno benefício para o GUIDcaso, mas nem de longe com resultados claros. A média de 20 lotes no meu teste foi

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

A razão pela qual não tem a penalidade aparente no código de Phil e no primeiro conjunto de resultados de Mitch é porque aconteceu que o código que eu costumava fazer a inserção de várias linhas usada SELECT TOP (@NumRows). Isso impediu que o otimizador calculasse corretamente o número de linhas que serão inseridas.

Isso parece ser benéfico, pois há um certo ponto de inflexão no qual ele adicionará uma operação de classificação adicional para os (supostamente seqüenciais!) GUIDS.

Classificação GUID

Esta operação de classificação não é necessária no texto explicativo em BOL .

Cria um GUID maior que qualquer GUID gerado anteriormente por esta função em um computador especificado desde que o Windows foi iniciado. Após reiniciar o Windows, o GUID pode ser iniciado novamente a partir de um intervalo mais baixo, mas ainda é globalmente exclusivo.

Portanto, pareceu-me um erro ou falta de otimização que o SQL Server não reconhece que a saída do escalar de computação já estará pré-classificada, como aparentemente já faz para a identitycoluna. ( Editar eu relatei isso e o problema desnecessário de classificação agora está corrigido no Denali )

Martin Smith
fonte
Não que isso tenha muito impacto, mas apenas por uma questão de clareza, o número que Denny citou, com 20 valores de identidade em cache, está incorreto - deveria ser 10. #
Aaron Bertrand
@AaronBertrand - Obrigado. Esse artigo que você vinculou é mais informativo.
Martin Smith
8

Muito simples: com o GUID, é mais barato gerar o próximo número na linha do que para o IDENTITY (o valor atual do GUID não precisa ser armazenado, o IDENTITY deve ser). Isso vale mesmo para NEWSEQUENTIALGUID.

Você poderia tornar o teste mais justo e usar um SEQUENCER com um CACHE grande - mais barato que o IDENTITY.

Mas, como o MR diz, existem algumas vantagens importantes nos GUIDs. Por uma questão de fato, eles são MUITO mais escalonáveis ​​que as colunas IDENTITY (mas somente se NÃO forem seqüenciais).

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

Thomas Kejser
fonte
Eu acho que você perdeu que eles estão usando guias seqüenciais.
Martin Smith
Martin: o argumento também é verdadeiro para o GUID seqüencial. IDENTITY deve ser armazenado (para retornar ao seu valor antigo após uma reinicialização), o GUID seqüencial não possui essa limitação.
Thomas Kejser
2
Sim, percebi que, depois do meu comentário, você estava falando sobre armazenar persistentemente, em vez de armazenar na memória. 2012 também usa um cache IDENTITY. daqui reclamações aqui
Martin Smith
4

Estou fascinado por esse tipo de pergunta. Por que você teve que publicá-lo na sexta à noite? :)

Acho que, mesmo que seu teste tenha como único objetivo medir o desempenho do INSERT, você (pode) introduziu uma série de fatores que podem ser enganosos (loop, uma transação de longa duração etc.)

Não estou totalmente convencido de que minha versão prove alguma coisa, mas a identidade tem um desempenho melhor que os GUIDs (3,2 segundos versus 6,8 segundos em um PC doméstico):

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
Phil Sandler
fonte
O outro fator que ninguém tenha mencionado é banco de dados de modelo de recuperação e arquivo de log crescimentos ...
Mitch Wheat
@Mitch em um novo banco de dados no modelo de recuperação simples, com dados e arquivos de log de tamanho igual ao necessário, obtenho resultados semelhantes ao OP.
Martin Smith
Eu só tenho horários de 2.560 segundos de identidade, e 3.666 segundos para Guid (em modelo de recuperação simples com dados e arquivo de log tanto maneira sized sobre o que é necessário)
Mitch Trigo
@ Mitch - No código do OP com tudo na mesma transação ou no código de Phil?
Martin Smith
nesse código de pôsteres, é por isso que estou comentando aqui. Eu também postou o código que usei ...
Mitch Wheat
3

Eu executei seu exemplo de script várias vezes, fazendo alguns ajustes na contagem e no tamanho do lote (e muito obrigado por fornecê-lo).

Primeiro, direi que você está medindo apenas uma vez o aspecto do desempenho das teclas - a INSERTvelocidade. Portanto, a menos que você esteja especificamente preocupado apenas em obter dados nas tabelas o mais rápido possível, há muito mais nesse animal.

Minhas descobertas foram em geral semelhantes às suas. No entanto, eu mencionaria que a variação na INSERTvelocidade entre GUIDe IDENTITY(int) é um pouco maior com do GUIDque com IDENTITY- talvez +/- 10% entre as execuções. Os lotes utilizados IDENTITYvariaram menos de 2 a 3% a cada vez.

Observe também que minha caixa de teste é claramente menos poderosa que a sua, então tive que usar contagens de linhas menores.

que nojo
fonte
Quando o PK é um GUID, é possível que o mecanismo não use um índice, mas um algoritmo de hash para determinar a localização física do registro correspondente? As inserções em uma tabela esparsa com chaves primárias com hash são sempre mais rápidas do que as inseridas em uma tabela com um índice na chave primária devido à ausência da sobrecarga do índice. É apenas uma pergunta - não me vote se a resposta for Não. Basta fornecer o link para a autoridade.
1

Vou me referir a outra conv. No stackoverflow para este mesmo tópico - https://stackoverflow.com/questions/170346/what-are-the-performance-improvement-of-sequential-guid-over-standard-guid

Uma coisa que eu sei é que ter GUIDs seqüenciais é que o uso do índice é melhor devido a muito pouco movimento de folhas e, portanto, reduzindo a busca por HD. Eu pensaria que, por isso, as inserções também seriam mais rápidas, pois não precisam distribuir as chaves por um grande número de páginas.

Minha experiência pessoal é que, quando você está implementando um banco de dados grande e de alto tráfego, é melhor usar GUIDs, porque o torna muito mais escalável para integração com outros sistemas. Isso vale para a replicação, especificamente, e os limites int / bigint ... não que você fique sem bigints, mas eventualmente acabará, e retornará.

SR
fonte
11
Você não ficar sem valores BIGINT, nunca mais ... ver isto: sqlmag.com/blog/it-possible-run-out-bigint-values
Thomas Kejser