Indexando um GUID PK no SQL Server 2012

13

Meus desenvolvedores configuraram seu aplicativo para usar GUIDs como PK em praticamente todas as suas tabelas e, por padrão, o SQL Server configurou o índice clusterizado nessas PKs.

O sistema é relativamente jovem e nossas maiores tabelas têm pouco mais de um milhão de linhas, mas estamos analisando nossa indexação e queremos poder escalar rapidamente, conforme necessário em um futuro próximo.

Portanto, minha primeira inclinação foi mover o índice clusterizado para o campo criado, que é uma representação grande de um DateTime. No entanto, a única maneira de tornar o CX exclusivo seria incluir a coluna GUID nesse CX, mas solicitar primeiro a criação.

Isso tornaria a chave de cluster muito ampla e aumentaria o desempenho para gravações? As leituras também são importantes, mas as gravações são provavelmente uma preocupação maior neste momento.

njkroes
fonte
1
Como os GUIDs são gerados? NEWID ou NEWSEQUENTIALID?
swasheck
6
Agrupado desempenho guid e inserção deve ser apenas em uma frase se a palavra imediatamente anterior "performance" é minimizar
billinkc
2
Leve esses desenvolvedores para almoçar e explique a eles que, se eles usarem NEWID () novamente como chave primária, você culpará o fraco desempenho por eles. Eles perguntarão rapidamente o que fazer para evitar isso. Nesse ponto, você diz usar IDENTITY (1,1). (talvez uma pequena simplificação excessiva, mas 9 em cada 10 funcionará).
Max Vernon
3
A razão do nosso ódio ao guid é que eles são largos (16 bytes) e, quando não criados com, newsequentialidsão aleatórios. As chaves em cluster são melhores quando são estreitas e crescentes. Um GUID é o oposto: gordo e aleatório. Imagine uma estante de livros quase cheia de livros. Vem o OED e, devido à aleatoriedade das guias, ele se insere no meio da prateleira. Para manter as coisas ordenadas, a metade direita dos livros precisa ser inserida em um novo local, o que é uma tarefa demorada. É isso que o GUID está fazendo no seu banco de dados e diminuindo o desempenho.
billinkc
7
A maneira de corrigir o problema do uso de identificadores exclusivos é voltar à prancheta e não usar identificadores exclusivos . Eles não são terríveis se o sistema for pequeno, mas se você tiver pelo menos alguns milhões de tabelas de linhas (ou qualquer tabela maior que isso), você será esmagado usando identificadores exclusivos de chaves.
quer

Respostas:

20

Os principais problemas com GUIDs, especialmente os não sequenciais, são:

  • Tamanho da chave (16 bytes vs. 4 bytes para uma INT): significa que você está armazenando 4 vezes a quantidade de dados em sua chave, juntamente com o espaço adicional para quaisquer índices, se esse for o seu índice clusterizado.
  • Fragmentação de índice: é praticamente impossível manter uma coluna GUID não sequencial desfragmentada devido à natureza completamente aleatória dos valores-chave.

Então, o que isso significa para a sua situação? Tudo se resume ao seu design. Se o seu sistema é simplesmente sobre gravações e você não se preocupa com a recuperação de dados, a abordagem descrita por Thomas K é precisa. No entanto, você deve ter em mente que, ao seguir essa estratégia, está criando muitos problemas em potencial para ler e armazenar esses dados. Como Jon Seigel aponta, você também estará ocupando mais espaço e essencialmente tendo inchaço na memória.

A principal questão em torno dos GUIDs é quão necessários eles são. Os desenvolvedores gostam deles porque garantem a exclusividade global, mas é rara a ocasião em que esse tipo de exclusividade é necessário. Mas considere que se seu número máximo de valores for menor que 2.147.483.647 (o valor máximo de um número inteiro assinado de 4 bytes), provavelmente você não está usando o tipo de dados apropriado para sua chave. Mesmo usando BIGINT (8 bytes), seu valor máximo é 9.223.372.036.854.775.807. Normalmente, isso é suficiente para qualquer banco de dados não global (e muitos globais) se você precisar de algum valor de incremento automático para uma chave exclusiva.

Por fim, quanto ao uso de um heap versus um índice clusterizado, se você estiver gravando dados puramente, um heap seria mais eficiente, porque você está minimizando a sobrecarga para inserções. No entanto, pilhas no SQL Server são extremamente ineficientes para recuperação de dados. Minha experiência foi que um índice em cluster é sempre desejável se você tiver a oportunidade de declarar um. Vi a adição de um índice agrupado a uma tabela (mais de 4 bilhões de registros) melhorar o desempenho geral de seleção por um fator de 6.

Informação adicional:

Mike Fal
fonte
13

Não há nada errado com o GUID como chaves e clusters em um sistema OLTP (a menos que você tenha MUITOS índices na tabela que sofrem com o tamanho aumentado do cluster). Por uma questão de fato, eles são muito mais escaláveis ​​do que as colunas IDENTITY.

Existe uma crença generalizada de que o GUID é um grande problema no SQL Server - em grande parte, isso é simplesmente errado. Por uma questão de fato, o GUID pode ser significativamente mais escalável em caixas com mais de 8 núcleos:

Sinto muito, mas seus desenvolvedores estão certos. Preocupe-se com outras coisas antes de se preocupar com o GUID.

Ah, e finalmente: por que você deseja um índice de cluster em primeiro lugar? Se sua preocupação for um sistema OLTP com muitos índices pequenos, é provável que você esteja melhor com uma pilha.

Vamos agora considerar o que a fragmentação (que o GUID apresentará) faz em suas leituras. Existem três grandes problemas com a fragmentação:

  1. Página divide o E / S do disco de custo
  2. As páginas meio cheias não são tão eficientes quanto a memória
  3. Faz com que as páginas sejam armazenadas fora de ordem, o que torna a E / S sequencial menos provável

Como sua preocupação na pergunta é sobre escalabilidade, que podemos definir como "A adição de mais hardware faz o sistema acelerar mais", esse é o menor dos seus problemas. Para abordar cada um por sua vez

Anúncio 1) Se você deseja dimensionar, pode comprar E / S. Mesmo um SSD barato de Samsung / Intel de 512 GB (a alguns USD / GB) oferece mais de 100 mil IOPS. Você não consumirá isso tão cedo em um sistema de 2 soquetes. E se você se deparar com isso, compre mais um e estará pronto

Anúncio 2) Se você fizer exclusões na sua tabela, você terá meia página inteira de qualquer maneira. E mesmo se não o fizer, a memória é barata e para todos, exceto os maiores sistemas OLTP - os dados quentes devem caber ali. A busca de empacotar mais dados em páginas é subotimizadora quando você procura escala.

Anúncio 3) Uma tabela criada a partir de dados freqüentemente fragmentados e altamente fragmentados faz E / S aleatória exatamente na mesma velocidade que uma tabela preenchida sequencialmente

Com relação à associação, existem dois tipos principais de associação que você provavelmente verá em uma carga de trabalho OLTP, como: Hash e loop. Vamos analisar cada um por vez:

Junção de hash: uma junção de hash pressupõe que a tabela pequena seja varrida e a maior seja normalmente procurada. É provável que tabelas pequenas estejam na memória; portanto, a E / S não é sua preocupação aqui. Já mencionamos o fato de que as buscas têm o mesmo custo em um índice fragmentado que em um índice não fragmentado

Junção de loop: a tabela externa será procurada. Mesmo custo

Você também pode ter várias verificações incorretas de tabela - mas o GUID novamente não é sua preocupação, é a indexação adequada.

Agora, você pode ter algumas verificações de intervalo legítimas em andamento (especialmente ao ingressar em chaves estrangeiras) e, nesse caso, os dados fragmentados são menos "compactados" quando comparados aos dados não fragmentados. Mas vamos considerar o que as junções que você provavelmente verá nos dados 3NF bem indexados são:

  1. Uma junção de uma tabela que possui uma referência de chave estrangeira à chave primária da tabela que faz referência

  2. O contrário

Anúncio 1) Nesse caso, você está buscando uma única busca na chave primária - unindo n a 1. Fragmentação ou não, mesmo custo (uma busca)

Anúncio 2) Nesse caso, você está ingressando na mesma chave, mas pode recuperar mais de uma linha (busca por intervalo). A junção neste caso é de 1 a n. No entanto, na tabela estrangeira que você procura, você está procurando a chave SAME, que tem a mesma probabilidade de estar na mesma página em um índice fragmentado que em um índice não fragmentado.

Considere essas chaves estrangeiras por um momento. Mesmo se você tivesse sequenciado "perfeitamente" nossas chaves primárias - qualquer coisa que aponte para essa chave ainda será não sequencial.

Obviamente, você pode estar rodando em uma máquina virtual em alguma SAN em algum banco com baixo custo e alto processo. Então, todo esse conselho será perdido. Mas se esse é o seu mundo, a escalabilidade provavelmente não é o que você está procurando - você está procurando desempenho e alta velocidade / custo - que são coisas diferentes.

Thomas Kejser
fonte
1
Comentários não são para discussão prolongada; esta conversa foi movida para o bate-papo .
Paul White 9
5

Thomas: alguns de seus argumentos fazem todo sentido e eu concordo com todos eles. Se você usa SSDs, o saldo do que você otimiza muda. Aleatório vs seqüencial não é a mesma discussão que disco giratório.

Concordo especialmente que ter uma visão pura do banco de dados é terrivelmente errado. Tornar seu aplicativo lento e não escalonável para melhorar apenas o desempenho do banco de dados pode ser bastante equivocado.

O grande problema com IDENTITY (ou sequência, ou qualquer outra coisa) gerada no banco de dados) é que é terrivelmente lento, pois requer uma ida e volta ao banco de dados para criar uma chave, e isso cria automaticamente um gargalo no seu banco de dados, reforça que os aplicativos devem faça uma chamada ao banco de dados para começar a usar uma chave. Criar um GUID resolve isso usando o aplicativo para criar a chave, é garantido que seja globalmente exclusivo (por definição), e as camadas do aplicativo podem usá-lo para passar o registro antes de incorrer em uma viagem de ida e volta ao banco de dados.

Mas eu costumo usar uma alternativa aos GUIDs. Minha preferência pessoal por um tipo de dados aqui é um BIGINT globalmente exclusivo gerado pelo aplicativo. Como alguém faz isso? No exemplo mais trivial, você adiciona uma função pequena e MUITO leve ao seu aplicativo para fazer o hash de um GUID. Supondo que sua função hash seja rápida e relativamente rápida (consulte CityHash do Google para um exemplo: http://google-opensource.blogspot.in/2011/04/introducing-cityhash.html - verifique se todas as etapas de compilação estão corretas, (ou a variante FNV1a de http://tools.ietf.org/html/draft-eastlake-fnv-03 para código simples), você obtém o benefício dos identificadores exclusivos gerados pelo aplicativo e de um valor-chave de 64 bits com o qual as CPUs funcionam melhor com .

Existem outras maneiras de gerar BIGINTs e, nesses dois algos, há uma chance de colisões de hash - ler e tomar decisões conscientes.

Mark Stacey
fonte
2
Sugiro que você edite sua resposta como resposta à pergunta do OP e não (como é agora) como resposta à resposta de Thomas. Você ainda pode destacar as diferenças entre Thomas (e MikeFal) e sua sugestão.
precisa saber é o seguinte
2
Por favor, envie sua resposta para a pergunta. Caso contrário, removeremos para você.
JNK
2
Obrigado pelos comentários Mark. Quando você edita sua resposta (o que acho que fornece um contexto muito bom), eu mudaria uma coisa: IDENTITY não requer uma ida e volta adicional ao servidor se você for cuidadoso com o INSERT. Você sempre pode voltar SCOPE_IDENTITY () no lote que chama INSERT ..
Thomas Kejser
1
Em relação a "é terrivelmente lento, pois requer uma ida e volta ao DB para criar uma chave" - ​​você pode pegar quantas você precisar em uma ida e volta.
AK
Em relação a "você pode pegar quantas você precisar em uma viagem de ida e volta" - Você não pode fazer isso com colunas IDENTITY ou qualquer outro método em que esteja basicamente usando DEFAULT no nível do banco de dados.
Avi cereja