web-dev-qa-db-fra.com

Considérations sur la clé primaire non entière

Le contexte

Je conçois une base de données (sur PostgreSQL 9.6) qui stockera les données d'une application distribuée. En raison de la nature distribuée de l'application, je ne peux pas utiliser d'entiers à incrémentation automatique (SERIAL) comme clé primaire en raison de conditions de concurrence potentielles.

La solution naturelle consiste à utiliser un UUID ou un identifiant globalement unique. Postgres est livré avec un type UUID intégré , ce qui est parfait.

Le problème que j'ai avec UUID est lié au débogage: c'est une chaîne non conviviale. L'identifiant ff53e96d-5fd7-4450-bc99-111b91875ec5 Ne me dit rien, tandis que ACC-f8kJd9xKCd, Bien qu'il ne soit pas garanti qu'il soit unique, me dit que j'ai affaire à un objet ACC.

Du point de vue de la programmation, il est courant de déboguer des requêtes d'application concernant plusieurs objets différents. Supposons que le programmeur recherche à tort un objet ACC (compte) dans la table ORD (ordre). Avec un identifiant lisible par l'homme, le programmeur identifie instantanément le problème, tout en utilisant des UUID, il passait un peu de temps à découvrir ce qui n'allait pas.

Je n'ai pas besoin de l'unicité "garantie" des UUID; J'ai ai besoin d'espace pour générer des clés sans conflits, mais l'UUID est exagéré. De plus, dans le pire des cas, ce ne serait pas la fin du monde si une collision se produisait (la base de données la rejette et l'application peut récupérer). Ainsi, les compromis envisagés, un identifiant plus petit mais convivial serait la solution idéale pour mon cas d'utilisation.

Identification des objets d'application

L'identifiant que j'ai trouvé a le format suivant: {domain}-{string}, Où {domain} Est remplacé par le domaine d'objet (compte, commande, produit) et {string} Est une chaîne générée aléatoirement . Dans certains cas, il peut même être judicieux d'insérer un {sub-domain} Avant la chaîne aléatoire. Ignorons la longueur de {domain} Et {string} Dans le but de garantir l'unicité.

Le format peut avoir une taille fixe s'il améliore les performances d'indexation/interrogation.

Le problème

Sachant que:

  • Je veux avoir des clés primaires avec un format comme ACC-f8kJd9xKCd.
  • Ces clés primaires feront partie de plusieurs tables.
  • Toutes ces clés seront utilisées sur plusieurs jointures/relations, sur une base de données 6NF.
  • La plupart des tables auront une taille moyenne à grande (en moyenne ~ 1 M de lignes; les plus grandes avec ~ 100 M lignes).

Concernant les performances, quelle est la meilleure façon de stocker cette clé?

Voici quatre solutions possibles, mais comme j'ai peu d'expérience avec les bases de données, je ne sais pas laquelle (le cas échéant) est la meilleure.

Solutions envisagées

1. Stocker sous forme de chaîne (VARCHAR)

(Postgres ne fait aucune différence entre CHAR(n) et VARCHAR(n), donc j'ignore CHAR).

Après quelques recherches, j'ai découvert que la comparaison de chaînes avec VARCHAR, spécialement sur les opérations de jointure, est plus lente que l'utilisation de INTEGER. C'est logique, mais est-ce quelque chose dont je dois m'inquiéter à cette échelle?

2. Stockez en binaire (bytea)

Contrairement à Postgres, MySQL n'a pas de type natif UUID. Il existe plusieurs articles expliquant comment stocker un UUID à l'aide d'un champ BINARY de 16 octets, au lieu d'un champ VARCHAR de 36 octets. Ces messages m'ont donné l'idée de stocker la clé au format binaire (bytea sur Postgres).

Cela économise de la taille, mais je suis plus préoccupé par les performances. J'ai eu peu de chance de trouver une explication sur laquelle la comparaison est plus rapide: binaire ou chaîne. Je pense que les comparaisons binaires sont plus rapides. S'ils le sont, alors bytea est probablement meilleur que VARCHAR, même si le programmeur doit maintenant encoder/décoder les données à chaque fois.

Je peux me tromper, mais je pense que bytea et VARCHAR compareront (égalité) octet par octet (ou caractère par caractère). Existe-t-il un moyen de "sauter" cette comparaison étape par étape et de comparer simplement "le tout"? (Je ne pense pas, mais cela ne coûte pas de vérifier).

Je pense que le stockage en tant que bytea est la meilleure solution, mais je me demande s'il existe d'autres alternatives que j'ignore. De plus, la même préoccupation que j'ai exprimée à propos de la solution 1 est vraie: les frais généraux sur les comparaisons sont-ils suffisants pour que je me préoccupe?

"Des solutions créatives

J'ai trouvé deux solutions très "créatives" qui pourraient fonctionner, je ne sais pas dans quelle mesure (c'est-à-dire si j'aurais du mal à les mettre à l'échelle sur plus de quelques milliers de lignes dans un tableau).

3. Stockez comme UUID mais avec une "étiquette" attachée

La principale raison de ne pas utiliser les UUID est que les programmeurs puissent mieux déboguer l'application. Mais que faire si nous pouvons utiliser les deux: la base de données stocke toutes les clés en tant que UUIDs uniquement, mais elle encapsule l'objet avant/après les requêtes.

Par exemple, le programmeur demande ACC-{UUID}, La base de données ignore la partie ACC-, Récupère les résultats et les renvoie tous sous la forme {domain}-{UUID}.

Peut-être que cela serait possible avec un certain piratage avec des procédures ou des fonctions stockées, mais certaines questions viennent à l'esprit:

  • Est-ce (supprimer/ajouter le domaine à chaque requête) un surcoût substantiel?
  • Est-ce seulement possible?

Je n'ai jamais utilisé de procédures ou de fonctions stockées auparavant, donc je ne sais pas si c'est même possible. Quelqu'un peut-il faire la lumière? Si je peux ajouter une couche transparente entre le programmeur et les données stockées, cela semble une solution parfaite.

4. (Mon préféré) Stocker sous IPv6 cidr

Oui, vous l'avez bien lu. Il s'avère que le format d'adresse IPv6 résout parfaitement mon problème .

  • Je peux ajouter des domaines et des sous-domaines dans les premiers octets et utiliser les autres comme chaîne aléatoire.
  • Les cotes de collision sont OK. (Je n'utiliserais pas 2 ^ 128 cependant, mais c'est toujours OK.)
  • Les comparaisons d'égalité sont (espérons-le) optimisées, donc je pourrais obtenir de meilleures performances que d'utiliser simplement bytea.
  • Je peux en fait effectuer des comparaisons intéressantes, comme contains, selon la façon dont les domaines et leur hiérarchie sont représentés.

Par exemple, supposons que j'utilise le code 0000 Pour représenter le domaine "produits". La clé 0000:0db8:85a3:0000:0000:8a2e:0370:7334 Représenterait le produit 0db8:85a3:0000:0000:8a2e:0370:7334.

La question principale ici est: par rapport à bytea, y a-t-il un avantage ou un inconvénient principal à utiliser le type de données cidr?

Utilisation de ltree

Si IPV6 fonctionne, tant mieux. Il ne prend pas en charge "ACC". ltree fait.

Un chemin d'étiquette est une séquence de zéro ou plusieurs étiquettes séparées par des points, par exemple L1.L2.L3, représentant un chemin de la racine d'une arborescence hiérarchique vers un nœud particulier. La longueur d'un chemin d'étiquette doit être inférieure à 65 Ko, mais le garder sous 2 Ko est préférable. En pratique, ce n'est pas une limitation majeure; par exemple, le chemin d'étiquette le plus long dans le catalogue DMOZ ( http://www.dmoz.org ) est d'environ 240 octets.

Vous l'utiliseriez comme ça,

CREATE EXTENSION ltree;
SELECT replace('ACC-f8kJd9xKCd', '-', '.')::ltree;

Nous créons des exemples de données.

SELECT x, (
  CASE WHEN x%7=0 THEN 'ACC'
    WHEN x%3=0 THEN 'XYZ'
    ELSE 'COM'
  END ||'.'|| md5(x::text)
  )::ltree
FROM generate_series(1,10000) AS t(x);

CREATE INDEX ON foo USING Gist (ltree);
ANALYZE foo;


  x  |                ltree                 
-----+--------------------------------------
   1 | COM.c4ca4238a0b923820dcc509a6f75849b
   2 | COM.c81e728d9d4c2f636f067f89cc14862c
   3 | XYZ.eccbc87e4b5ce2fe28308fd9f2a7baf3
   4 | COM.a87ff679a2f3e71d9181a67b7542122c
   5 | COM.e4da3b7fbbce2345d7772b0674a318d5
   6 | XYZ.1679091c5a880faf6fb5e6087eb1b2dc
   7 | ACC.8f14e45fceea167a5a36dedd4bea2543
   8 | COM.c9f0f895fb98ab9159f51fd0297e236d

Et l'alto ..

                                                          QUERY PLAN                                                          
------------------------------------------------------------------------------------------------------------------------------
 Bitmap Heap Scan on foo  (cost=103.23..234.91 rows=1414 width=57) (actual time=0.422..0.908 rows=1428 loops=1)
   Recheck Cond: ('ACC'::ltree @> ltree)
   Heap Blocks: exact=114
   ->  Bitmap Index Scan on foo_ltree_idx  (cost=0.00..102.88 rows=1414 width=0) (actual time=0.389..0.389 rows=1428 loops=1)
         Index Cond: ('ACC'::ltree @> ltree)
 Planning time: 0.133 ms
 Execution time: 1.033 ms
(7 rows)

Voir les documents pour plus d'informations et opérateurs

Si vous créez les identifiants des produits, je préfère. Si vous avez besoin de quelque chose pour les créer, j'utiliserais UUID.

5
Evan Carroll

En ce qui concerne la comparaison des performances avec bytea. la comparaison du réseau se fait en 3 étapes: d'abord sur les bits communs de la partie réseau, puis sur la longueur de la partie réseau, puis sur l'ensemble de l'adresse non masquée. voir: network_cmp_internal

il devrait donc être un peu plus lent que le bytea qui va directement à memcmp. J'ai exécuté un test simple sur une table avec 10 millions de lignes en recherchant une seule:

  • en utilisant un identifiant numérique (entier), cela m'a pris 1000 ms.
  • en utilisant cidr, il a fallu 1300 ms.
  • en utilisant du bytea, il a fallu 1250 ms.

Je ne peux pas dire qu'il y a beaucoup de différence entre le bytea et le cidr (bien que l'écart soit resté cohérent) Juste l'instruction supplémentaire if - devinez que ce n'est pas trop mal pour des tuples de 10m.

J'espère que cela aide - j'aimerais savoir ce que vous avez fini par choisir.

1
cohenjo