Mysql int vs varchar como chave primária (InnoDB Storage Engine?

13

Estou construindo um aplicativo Web (sistema de gerenciamento de projetos) e fiquei pensando sobre isso quando se trata de desempenho.

Eu tenho uma tabela de problemas e dentro dela existem 12 chaves estrangeiras vinculadas a várias outras tabelas. desses, 8 deles eu precisaria me juntar para obter o campo de título das outras tabelas para que o registro fizesse sentido em um aplicativo da web, mas significa fazer 8 junções, o que parece realmente excessivo, especialmente porque eu estou apenas entrando 1 campo para cada uma dessas junções.

Agora também me disseram para usar uma chave primária de incremento automático (a menos que o sharding seja uma preocupação e, nesse caso, eu deveria usar um GUID) por motivos de permanência, mas quão ruim é usar um desempenho varchar (comprimento máximo 32)? Quero dizer, a maioria dessas tabelas provavelmente não terá muitos registros (a maioria deles deve ter menos de 20 anos). Além disso, se eu usar o título como chave primária, não precisarei fazer junções em 95% das vezes; portanto, para 95% do sql, ocorreria um impacto no desempenho (acho). A única desvantagem em que consigo pensar é que terei é maior uso de espaço em disco (mas em um dia é realmente um grande problema).

O motivo pelo qual eu uso tabelas de pesquisa para muitas dessas coisas, em vez de enumerações, é porque eu preciso que todos esses valores sejam configuráveis ​​pelo usuário final através do próprio aplicativo.

Quais são as desvantagens de usar um varchar como chave primária para uma tabela que não tenha exceção de ter muitos registros?

UPDATE - Alguns testes

Então eu decidi fazer alguns testes básicos sobre essas coisas. Eu tenho 100000 registros e estas são as consultas base:

Consulta Base VARCHAR FK

SELECT i.id, i.key, i.title, i.reporterUserUsername, i.assignedUserUsername, i.projectTitle, 
i.ProjectComponentTitle, i.affectedProjectVersionTitle, i.originalFixedProjectVersionTitle, 
i.fixedProjectVersionTitle, i.durationEstimate, i.storyPoints, i.dueDate, 
i.issueSecurityLevelId, i.creatorUserUsername, i.createdTimestamp, 
i.updatedTimestamp, i.issueTypeId, i.issueStatusId
FROM ProjectManagement.Issues i

Consulta Base FK INT

SELECT i.id, i.key, i.title, ru.username as reporterUserUsername, 
au.username as assignedUserUsername, p.title as projectTitle, 
pc.title as ProjectComponentTitle, pva.title as affectedProjectVersionTitle, 
pvo.title as originalFixedProjectVersionTitle, pvf.title as fixedProjectVersionTitle, 
i.durationEstimate, i.storyPoints, i.dueDate, isl.title as issueSecurityLevelId, 
cu.username as creatorUserUsername, i.createdTimestamp, i.updatedTimestamp, 
it.title as issueTypeId, is.title as issueStatusId
FROM ProjectManagement2.Issues i
INNER JOIN ProjectManagement2.IssueTypes `it` ON it.id = i.issueTypeId
INNER JOIN ProjectManagement2.IssueStatuses `is` ON is.id = i.issueStatusId
INNER JOIN ProjectManagement2.Users `ru` ON ru.id = i.reporterUserId
INNER JOIN ProjectManagement2.Users `au` ON au.id = i.assignedUserId
INNER JOIN ProjectManagement2.Users `cu` ON cu.id = i.creatorUserId
INNER JOIN ProjectManagement2.Projects `p` ON p.id = i.projectId
INNER JOIN ProjectManagement2.`ProjectComponents` `pc` ON pc.id = i.projectComponentId
INNER JOIN ProjectManagement2.ProjectVersions `pva` ON pva.id = i.affectedProjectVersionId
INNER JOIN ProjectManagement2.ProjectVersions `pvo` ON pvo.id = i.originalFixedProjectVersionId
INNER JOIN ProjectManagement2.ProjectVersions `pvf` ON pvf.id = i.fixedProjectVersionId
INNER JOIN ProjectManagement2.IssueSecurityLevels isl ON isl.id = i.issueSecurityLevelId

Também executei essas consultas com as seguintes adições:

  • Selecione um item específico (onde i.key = 43298)
  • Agrupar por i.id
  • Ordenar por (it.title para int FK, i.issueTypeId para varchar FK)
  • Limite (50000, 100)
  • Agrupe e limite juntos
  • Agrupe, encomende e limite juntos

Os resultados para estes onde:

TIPO DE CONSULTA: VARCHAR FK TIME / INT FK TIME


Consulta base: ~ 4ms / ~ 52ms

Selecione um item específico: ~ 140ms / ~ 250ms

Agrupar por i.id: ~ 4ms / ~ 2.8sec

Ordenar por: ~ 231ms / ~ 2seg

Limite: ~ 67ms / ~ 343ms

Agrupe e limite juntos: ~ 504ms / ~ 2seg

Agrupe, encomende e limite juntos: ~ 504ms / ~2.3seg

Agora eu não sei qual configuração eu poderia fazer para tornar um ou outro (ou ambos) mais rápido, mas parece que o VARCHAR FK vê mais rapidamente nas consultas de dados (às vezes muito mais rápido).

Acho que tenho que escolher se essa melhoria de velocidade vale o tamanho extra de dados / índice.

ryanzec
fonte
Seu teste indica alguma coisa. Eu também testaria com várias configurações do InnoDB (buffer pools, etc.) porque as configurações padrão do MySQL não são realmente otimizadas para o InnoDB.
usar o seguinte comando
Você também deve testar o desempenho de Inserir / Atualizar / Excluir, pois isso também pode ser afetado pelo tamanho do índice. A única chave agrupada de todas as tabelas do InnoDB é geralmente a PK e essa coluna (PK) também é incluída em todos os outros índices. Essa é provavelmente uma grande desvantagem das PKs grandes no InnoDB e de muitos índices na tabela (mas 32 bytes são médios, não grandes, portanto, pode não ser um problema).
precisa saber é o seguinte
Você também deve testar com tabelas maiores (no intervalo de, digamos, 10 a 100 milhões de linhas ou mais), se espera que suas tabelas cresçam acima de 100K (o que não é muito grande).
precisa saber é o seguinte
@ypercube Então eu aumento os dados para 2 milhões e a instrução select para o int FK fica mais lenta exponencialmente onde a chave estrangeira varchar permanece bastante estável. Pense que o varchar vale o preço em requisitos de disco / memória para o ganho em consultas selecionadas (o que será crítico nessa tabela em particular e em algumas outras).
Ryanzec
Apenas verifique suas configurações de db (e particularmente o InnoDB) antes de chegar a conclusões. Com tabelas de referência pequenas, eu não esperaria aumento exponencial
ypercubeᵀᴹ

Respostas:

9

Sigo as seguintes regras para chaves primárias:

a) Não deve ter nenhum significado comercial - eles devem ser totalmente independentes do aplicativo que você está desenvolvendo; portanto, busco números inteiros gerados automaticamente. No entanto, se você precisar que colunas adicionais sejam exclusivas, crie índices exclusivos para suportar esse

b) Deverá executar em junções - a junção a varchars vs números inteiros é cerca de 2x a 3x mais lenta à medida que o comprimento da chave primária aumenta, portanto, você deseja que suas chaves sejam inteiras. Como todos os sistemas de computador são binários, suspeito que a string seja alterada para binária e comparada com as demais, o que é muito lento

c) Use o menor tipo de dados possível - se você espera que sua tabela tenha muito poucas colunas, digamos 52 estados dos EUA, use o menor tipo possível, talvez um CHAR (2) para o código de 2 dígitos, mas eu ainda usaria um tinyint (128) para a coluna vs um grande int que pode chegar a 2 bilhões

Além disso, você terá um desafio em cascatear suas alterações das chaves primárias para as outras tabelas se, por exemplo, o nome do projeto mudar (o que não é incomum)

Escolha números inteiros incrementais automáticos sequenciais para suas chaves primárias e obtenha as eficiências incorporadas que os sistemas de banco de dados fornecem com suporte para mudanças no futuro

Stephen Senkomago Musoke
fonte
1
Strings não são alteradas para binárias; eles são armazenados em binário desde o início. De que outra forma eles seriam armazenados? Talvez você esteja pensando em operações para permitir comparação sem distinção entre maiúsculas e minúsculas?
Jon of All Trades
6

Nos seus testes, você não está comparando a diferença de desempenho varchar x int keys, mas o custo de várias junções. Não é de surpreender que a consulta de uma tabela seja mais rápida do que a junção de muitas tabelas.
Uma desvantagem da chave primária varchar é aumentar o tamanho do índice como atxdba apontou. Mesmo que sua tabela de pesquisa não possua outros índices, exceto PK (o que é bastante improvável, mas possível), cada tabela que referenciar a pesquisa terá um índice nessa coluna. Outra coisa ruim das chaves primárias naturais é que o valor delas pode mudar, causando muitas atualizações em cascata. Nem todos os RDMS, por exemplo, Oracle, permitem que você tenha . Em geral, alterar o valor da chave primária considerando uma prática muito ruim. Não quero dizer que chaves primárias naturais são sempre más; se os valores de pesquisa são pequenos e nunca mudam, acho que isso pode ser aceitável.
on update cascade

Uma opção que você pode considerar é implementar a visão materializada. O Mysql não o suporta diretamente, mas você pode obter a funcionalidade desejada com gatilhos nas tabelas subjacentes. Então você terá uma tabela que tem tudo o que você precisa para exibir. Além disso, se o desempenho for aceitável, não lute com o problema que não existe no momento.

a1ex07
fonte
3

A maior desvantagem é a repetição do PK. Você apontou um aumento no uso do espaço em disco, mas, para ficar claro, o aumento do tamanho do índice é sua maior preocupação. Como o innodb é um índice clusterizado, todo índice secundário armazena internamente uma cópia do PK que ele usa para, finalmente, encontrar registros correspondentes.

Você diz que as tabelas devem ser "pequenas" (20 linhas são realmente muito pequenas). Se você tiver RAM suficiente para definir o innodb_buffer_pool_size igual a

select sum(data_length+index_length) from information_schema.tables where engine='innodb';

Então faça isso e você provavelmente estará sentado bonito. Como regra geral, você deseja deixar pelo menos 30% a 40% da memória total do sistema para outras despesas gerais e cache de disco mysql. E isso assumindo que é um servidor de banco de dados dedicado. Se houver outras coisas em execução no sistema, você precisará levar em consideração os requisitos deles.

atxdba
fonte
1

Além da resposta @atxdba - que explicava por que usar o numérico seria melhor para o espaço em disco, eu gostaria de acrescentar dois pontos:

  1. Se sua tabela de problemas for baseada no VARCHAR FK e digamos que você tenha 20 pequenos VARCHAR (32) FK, seu registro poderá ter 20x32 bytes de comprimento, enquanto você mencionou que as outras tabelas são tabelas de pesquisa, então INT FK pode ser o TINYINT FK para 20 campos e registros de 20 bytes. Sei por centenas de registros que não vai mudar muito, mas quando você chegar a vários milhões, acho que vai gostar de economizar espaço

  2. Para o problema de velocidade, consideraria o uso de índices de cobertura, pois para esta consulta você não está recuperando tanta quantidade de dados das tabelas de pesquisa, eu procuraria cobrir o índice e faria novamente o teste fornecido com o VARCHAR FK / W / COVERING ÍNDICE E INT regular FK.

Espero que possa ajudar,

Spredzy
fonte