Mysql int vs varchar comme clé primaire (InnoDB Storage Engine?

13

Je construis une application web (système de gestion de projet) et je me pose des questions à ce sujet en termes de performances.

J'ai une table Issues et à l'intérieur il y a 12 clés étrangères reliant à diverses autres tables. de ceux-ci, 8 d'entre eux, je devrais rejoindre pour obtenir le champ de titre des autres tables afin que l'enregistrement ait un sens dans une application Web, mais cela signifie ensuite faire 8 jointures, ce qui semble vraiment excessif, d'autant plus que je ne fais que tirer 1 champ pour chacune de ces jointures.

Maintenant, on m'a également dit d'utiliser une clé primaire à incrémentation automatique (sauf si le partage est un problème auquel cas je devrais utiliser un GUID) pour des raisons de permanence, mais à quel point est-il mauvais d'utiliser une varchar (longueur maximale 32) en termes de performances? Je veux dire que la plupart de ces tableaux n'auront probablement pas de nombreux enregistrements (la plupart d'entre eux devraient avoir moins de 20 ans). De plus, si j'utilise le titre comme clé primaire, je n'aurai pas à faire de jointures 95% du temps, donc pour 95% du sql, j'aurais même un impact sur les performances (je pense). Le seul inconvénient auquel je peux penser, c'est que j'aurai une utilisation d'espace disque plus élevée (mais un jour, c'est vraiment un gros problème).

La raison pour laquelle j'utilise des tables de recherche pour beaucoup de ces choses au lieu des énumérations est parce que j'ai besoin que toutes ces valeurs soient configurables par l'utilisateur final via l'application elle-même.

Quels sont les inconvénients de l'utilisation d'un varchar comme clé primaire pour une table qui n'est pas exempte d'avoir de nombreux enregistrements?

MISE À JOUR - Quelques tests

J'ai donc décidé de faire des tests de base sur ce genre de choses. J'ai 100 000 enregistrements et ce sont les requêtes de base:

Base VARCHAR FK Query

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

Base INT FK Query

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

J'ai également exécuté ces requêtes avec les ajouts suivants:

  • Sélectionnez un élément spécifique (où i.key = 43298)
  • Grouper par i.id
  • Trier par (it.title pour int FK, i.issueTypeId pour varchar FK)
  • Limite (50000, 100)
  • Regrouper et limiter ensemble
  • Grouper, ordonner et limiter ensemble

Les résultats pour ceux-ci où:

TYPE DE REQUÊTE: VARCHAR FK TIME / INT FK TIME


Requête de base: ~ 4 ms / ~ 52 ms

Sélectionnez un élément spécifique: ~ 140 ms / ~ 250 ms

Grouper par i.id: ~ 4ms / ~ 2.8sec

Trier par: ~ 231ms / ~ 2sec

Limite: ~ 67 ms / ~ 343 ms

Grouper et limiter ensemble: ~ 504 ms / ~ 2 s

Regroupez, commandez et limitez ensemble: ~ 504 ms / ~ 2,3 s

Maintenant, je ne sais pas quelle configuration je pourrais faire pour rendre l'un ou l'autre (ou les deux) plus rapide, mais il semble que le VARCHAR FK voit plus rapidement dans les requêtes de données (parfois beaucoup plus rapide).

Je suppose que je dois choisir si cette amélioration de la vitesse vaut la taille supplémentaire des données / index.

ryanzec
la source
Vos tests indiquent quelque chose. Je testerais également avec différents paramètres InnoDB (pools de tampons, etc.) car les paramètres MySQL par défaut ne sont pas vraiment optimisés pour InnoDB.
ypercubeᵀᴹ
Vous devez également tester les performances d'insertion / mise à jour / suppression car cela peut également être affecté par la taille de l'index. La seule clé en cluster de chaque table InnoDB est généralement le PK et cette colonne (PK) est également incluse dans tous les autres index. C'est probablement un gros inconvénient des gros PK dans InnoDB et de nombreux index sur la table (mais 32 octets est plutôt moyen, pas gros, donc ce n'est peut-être pas un problème).
ypercubeᵀᴹ
Vous devriez également tester avec des tables plus grandes (de l'ordre de 10 à 100 millions de lignes ou plus), si vous vous attendez à ce que vos tables dépassent 100K (ce qui n'est pas vraiment grand).
ypercubeᵀᴹ
@ypercube J'augmente donc les données à 2 millions et l'instruction select pour l'int FK ralentit de façon exponentielle où la clé étrangère varchar reste assez stable. A penser que le varchar vaut le prix des exigences disque / mémoire pour le gain dans certaines requêtes (ce qui va être critique sur cette table en particulier et quelques autres).
ryanzec
Vérifiez également vos paramètres de base de données (et en particulier InnoDB) avant de tirer des conclusions. Avec de petites tables de référence, je ne m'attendrais pas à une augmentation exponentielle
ypercubeᵀᴹ

Réponses:

9

Je respecte les règles suivantes pour les clés primaires:

a) Ne devrait pas avoir de sens commercial - ils devraient être totalement indépendants de l'application que vous développez, donc je préfère les nombres numériques générés automatiquement. Cependant, si vous avez besoin de colonnes supplémentaires pour être uniques, créez des index uniques pour les prendre en charge.

b) Devrait fonctionner dans les jointures - la jointure à varchars vs entiers est environ 2x à 3x plus lente à mesure que la longueur de la clé primaire augmente, donc vous voulez avoir vos clés sous forme d'entiers. Étant donné que tous les systèmes informatiques sont binaires, je soupçonne que son coz la chaîne est changé en binaire puis par rapport aux autres qui est très lent

c) Utilisez le plus petit type de données possible - si vous vous attendez à ce que votre table contienne très peu de colonnes, dites 52 États américains, alors utilisez le plus petit type possible, peut-être un CHAR (2) pour le code à 2 chiffres, mais j'irais quand même pour un petit nombre (128) pour la colonne vs un gros int qui peut aller jusqu'à 2 milliards

Vous aurez également du mal à mettre en cascade vos modifications des clés primaires vers les autres tables si, par exemple, le nom du projet change (ce qui n'est pas rare)

Optez pour des nombres entiers à incrémentation automatique séquentielle pour vos clés primaires et gagnez en efficacité intégrée que les systèmes de base de données fournissent avec la prise en charge des modifications futures

Stephen Senkomago Musoke
la source
1
Les chaînes ne sont pas modifiées en binaire; ils sont stockés en binaire depuis le début. Sinon, comment seraient-ils stockés? Vous pensez peut-être à des opérations permettant une comparaison insensible à la casse?
Jon of All Trades
6

Dans vos tests, vous ne comparez pas la différence de performances entre les clés varchar et int, mais plutôt le coût de plusieurs jointures. Il n'est pas surprenant que l'interrogation d'une table soit plus rapide que la jonction de plusieurs tables.
Un inconvénient de la clé primaire varchar est l'augmentation de la taille de l'index comme l' a souligné atxdba . Même si votre table de recherche n'a pas d'autres index à l'exception de PK (ce qui est assez peu probable, mais possible), chaque table qui référence la recherche aura un index sur cette colonne.
Une autre mauvaise chose à propos des clés primaires naturelles, c'est que leur valeur peut changer, ce qui provoque de nombreuses mises à jour en cascade. Tous les RDMS, par exemple Oracle, ne vous laissent même pason update cascade. En général, changer la valeur de la clé primaire est considéré comme une très mauvaise pratique. Je ne veux pas dire que les clés primaires naturelles sont toujours mauvaises; si les valeurs de recherche sont petites et ne changent jamais, je pense que cela peut être acceptable.

Une option que vous voudrez peut-être envisager est d'implémenter la vue matérialisée. Mysql ne le prend pas directement en charge, mais vous pouvez obtenir la fonctionnalité souhaitée avec des déclencheurs sur les tables sous-jacentes. Vous aurez donc une table qui a tout ce dont vous avez besoin pour afficher. De plus, si les performances sont acceptables, ne vous débattez pas avec le problème qui n'existe pas pour le moment.

a1ex07
la source
3

Le plus gros inconvénient est la répétition du PK. Vous avez souligné une augmentation de l'utilisation de l'espace disque, mais pour être clair, l'augmentation de la taille de l'index est votre plus grande préoccupation. Étant donné que innodb est un index clusterisé, chaque index secondaire stocke en interne une copie du PK qu'il utilise pour finalement trouver les enregistrements correspondants.

Vous dites que les tables sont censées être "petites" (en effet, 20 lignes sont très petites). Si vous avez suffisamment de RAM pour définir la taille innodb_buffer_pool_size égale à

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

Ensuite, faites-le et vous serez probablement bien assis. En règle générale, vous devez laisser au moins 30% à 40% de la mémoire totale du système pour les autres surcharges mysql et la mise en cache. Et cela suppose qu'il s'agit d'un serveur de base de données dédié. Si d'autres éléments sont en cours d'exécution sur le système, vous devrez également prendre en compte leurs besoins.

atxdba
la source
1

En plus de la réponse @atxdba - qui vous expliquait pourquoi l'utilisation du numérique serait préférable pour l'espace disque, je voulais ajouter deux points:

  1. Si votre table Issues est basée sur VARCHAR FK, et disons que vous avez 20 petites VARCHAR (32) FK, votre enregistrement peut atteindre une longueur de 20x32 octets, tandis que, comme vous l'avez mentionné, les autres tables sont des tables de recherche, donc INT FK pourrait être TINYINT FK qui font pour 20 champs un enregistrement de 20 octets. Je sais que pour plusieurs centaines d'enregistrements, cela ne changera pas grand-chose, mais lorsque vous atteindrez plusieurs millions, je suppose que vous apprécierez d'économiser de l'espace

  2. Pour le problème de vitesse, j'envisagerais d'utiliser des index de couverture, car il semble que pour cette requête, vous ne récupérez pas autant de données des tables de recherche que j'irais pour couvrir l'index et refaire le test que vous avez fourni avec VARCHAR FK / W / COVERING INDEX ET INT FK régulier.

J'espère que cela pourrait aider,

Spredzy
la source