GUID como Chave Primária

Leandro Luiz G. Cavalheiro - Oct 9 - - Dev Community

A muito se discute sobre usar ou não usar GUID ( ou UUID para outras linguagens ). Fato é, que temos vantagens e desvantagens ao usarmos essa abordagem, e que não irei abordar todas por aqui, já que existe muitos vídeos e artigos sobre o assunto. Aqui pretendo olhar para apenas um desses pontos "contra" ou pelo menos polêmico: o uso do GUID como chave primária.
A polêmica toda é em torno da perda de performance ao gravar esse dado no banco de dados, devido a sua característica de não ser ordenável, uma vez que é gerado aleatoriamente ( mesmo que ainda siga algumas regras ).
Então bora entender se realmente devemos evitar um GUID como chave primária. E lembrando, que minhas referências, logicamente serão para .NET nos códigos mostrados, mas esse assunto é agnóstico quanto a linguagem de programação.

Comumente, no .NET, usamos o gerador de GUID nativo, lá do namespace System e esse gerador nos devolve, um GUID na versão 4, que é um modelo de GUID não ordenável. Exemplo a seguir:

var myId = Guid.NewGuid();
//75bdd495-698a-487e-a792-3a5be46bdf6e
//3e448b53-b53b-42d0-9ce2-afa666b20671
Enter fullscreen mode Exit fullscreen mode

E por se tratar de um padrão já conhecido, não vou me aprofundar, mas, notamos que realmente são valores aleatórios, fazendo o banco banco trabalhar além do que deveria para reordenar essa PK a cada novo registro inserido no banco e em um possível select ordenado pela PK, o segundo valor gerado em nosso exemplo acima, viria para a primeira posição, "bagunçando" assim nossos registros.

Mas então, não é boa ideia utilizarmos GUID em uma PK, certo?
Olhando única e exclusivamente para esse ponto, realmente o uso seria desencorajado. Mas como disse no início do post, existem outros pontos a serem levados em consideração, para a escolha do um GUID como PK.

Então como solucionar ( ou minimizar ) esse efeito colateral?
O .Net gera nativamente GUIDs na versão 4, que são GUIDs randômicos, mas a boa notícia é que temos o GUID na versão 7. Na v7, esses GUIDs são ordenáveis: sim, isso mesmo ordenáveis. E melhor, ainda assim, é mantida a aleatoriedade, ou seja, temos um dado ordenável, que não exigirá trabalho extra do banco de dados ao inserir um novo registro e de quebra ainda é um valor com parte randômica, ideal para uso em nossas APIs. E lembrando, que teremos essa nova versão do GUID nativamente no .NET, que será lançado em Novembro de 2024.

Mas como é possível?
Aí que entra a "mágica".
Um GUID é formado por 16 bytes, e na v7 os 6 primeiros bytes representa a data em que foi gerado. O código a seguir exemplifica como é gerado essa parte do GUID:

Span<byte> uuidAsBytes = stackalloc byte[16];
var currentDate = BitConverter.GetBytes(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
if (BitConverter.IsLittleEndian)        
   Array.Reverse(currentDate);
currentDate.AsSpan(2..8).CopyTo(uuidAsBytes);
Enter fullscreen mode Exit fullscreen mode

ToUnixTimeMilliseconds, retorna um long, que representa os milissegundos passados desde 01/01/1970 e que pode representar um range quase infinito de datas, tanto para o passado, quanto para o futuro.
Então, nessa linha, temos os bytes que representam o ponto no tempo onde foi gerado o valor.

Temos que nos atentar, a existência de sistemas BigEndian e LittleEndian, onde resumindo, são formas que os bytes são organizados na memória. Sendo que em sistemas LittleEndian, vamos inverter os bytes, para que o byte mais significativo sejam armazenados primeiro, e isso, é importante para o próximo passo.

Como dito antes, usamos 6 bytes para representação de data, e temos 8 bytes gerados nos passos anteriores. Então, como não precisamos das representação de milhões de anos, podemos descartar os 2 primeiros bytes (aqui a importância dos bytes mais significativos primeiro), e utilizar apenas os 6 últimos, assim temos nosso carimbo de tempo, para nosso UUID.

Agora, temos a parte randômica que é mais simples:

RandomNumberGenerator.Fill(uuidAsBytes[6..]);
Enter fullscreen mode Exit fullscreen mode

Simplesmente iremos usar a RandomNumberGenerator do namespace System.Security.Cryptography, para preencher o restante do bytes, com números randômicos.

O próximo passo é ajustar o byte 6, para definir que é um GUID v7, com as seguintes ações:

uuidAsBytes[6] &= 0x0F;
uuidAsBytes[6] += 0x70;
Enter fullscreen mode Exit fullscreen mode

uuidAsBytes[6] &= 0x0F: Limpa (zera) os 4 bits mais significativos do 6º byte.
uuidAsBytes[6] += 0x70: Define os 4 bits mais significativos como 0111 para indicar que este GUID é da versão 7.

E pronto, temos nosso GUID v7.
Abaixo o código completo:

public static Guid GuidV7()
{
    Span<byte> uuidAsBytes = stackalloc byte[16];
    var currentDate = BitConverter.GetBytes(DateTimeOffset.UtcNow.ToUnixTimeMilliseconds());
    if (BitConverter.IsLittleEndian)        
        Array.Reverse(currentDate);
    currentDate.AsSpan(2..8).CopyTo(uuidAsBytes);

    RandomNumberGenerator.Fill(uuidAsBytes[6..]);
    uuidAsBytes[6] &= 0x0F;
    uuidAsBytes[6] += 0x70;
    return new(uuidAsBytes, true);
}
Enter fullscreen mode Exit fullscreen mode

E aqui um benchmark de geração de GUIDs que fiz comparando as versões 4 e 7:

Image description

Mesmo com a chegada desse recurso nativamente logo mais no .NET, sei que muitos devs só utilizam as versões LTS e como o .NET 9 não é LTS, criei um método e adicionei em um Nuget: WeNerds.Commons.

Para utilização é bem simples, basta instalar o pacote e após importar com using, chamar o método estático WeMethods.NewGuid();

using WeNerds.Commons;
var myGuidV7 = WeMethods.NewGuid();

Enter fullscreen mode Exit fullscreen mode
.
Terabox Video Player