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.
fonte
Respostas:
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
fonte
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.
fonte
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
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.
fonte
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:
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
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,
fonte