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

16

Le contexte

Je suis en train de concevoir 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 intégré de UUIDtype , ce qui est un ajustement 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-111b91875ec5ne me dit rien, alors que ACC-f8kJd9xKCd, même s'il n'est pas garanti qu'il soit unique, il me dit que j'ai affaire à un ACCobjet.

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 ACCobjet (compte) auORD table (commande). 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; Je fais besoin de place pour générer des clés sans conflits, mais UUID est surpuissant. 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}{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 {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é?

Vous trouverez ci-dessous quatre solutions possibles, mais comme je n'ai que 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, en particulier sur les opérations de jointure, est plus lente que l'utilisation INTEGER. C'est logique, mais est-ce quelque chose dont je dois m'inquiéter à cette échelle?

2. Stocker en binaire ( bytea)

Contrairement à Postgres, MySQL n'a pas de UUIDtype natif . Il existe plusieurs articles expliquant comment stocker un UUID à l'aide d'un BINARYchamp de 16 octets , au lieu d'un champ de 36 octets VARCHAR. Ces messages m'ont donné l'idée de stocker la clé au format binaire ( byteasur 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 byteac'est probablement mieux que VARCHAR, même si le programmeur doit maintenant encoder / décoder les données à chaque fois.

Je peux me tromper, mais je pense les deux byteaet VARCHARcomparerai (l'é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 byteaest la meilleure solution, mais je me demande s'il y a 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. Conserver comme UUIDmais 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 UUIDuniquement en s, mais elle encapsule l'objet avant / après les requêtes.

Par exemple, le programmeur le demande ACC-{UUID}, la base de données ignore la ACC-pièce, 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 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 en 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 chances 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 de simplement utiliser 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 du code 0000pour représenter le domaine "produits". La clé 0000:0db8:85a3:0000:0000:8a2e:0370:7334repré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 cidrle type de données?

Renato Siqueira Massaro
la source
5
Combien de nœuds distribués sont possibles? Connaissez-vous leur numéro (et leurs noms) à l'avance? Avez-vous envisagé des PK composites (multicolonnes)? Un domaine (en fonction de ma première question), plus une simple colonne série pourrait être le plus petit, le plus simple et le plus rapide ...
Erwin Brandstetter
@Phil merci! @ErwinBrandstetter En ce qui concerne l'application, elle est conçue pour évoluer automatiquement en fonction de la charge, il y a donc très peu d'informations à l'avance. J'ai pensé à utiliser (domaine, UUID) comme PK, mais cela répéterait "domaine" partout, le domaine serait encore varcharparmi beaucoup d'autres problèmes. Je ne connaissais pas les domaines de pg, ce qui est formidable à apprendre. Je vois des domaines utilisés pour valider si une requête donnée utilise le bon objet, mais cela dépendrait toujours d'un index non entier. Je ne sais pas s'il existe un moyen "sécurisé" d'utiliser serialici (sans une seule étape de verrouillage).
Renato Siqueira Massaro
1
Le domaine ne doit pas nécessairement être un varchar. Envisagez d'en faire un FK integertype et ajoutez-y une table de recherche. De cette façon, vous pouvez avoir à la fois une lisibilité humaine et vous protégerez votre composite PKdes anomalies d'insertion / mise à jour (en mettant un domaine inexistant).
yemet
1
« Je veux avoir des clés primaires avec un format comme ACC-f8kJd9xKCd. ”← Cela semble être un travail pour la bonne vieille CLÉ PRIMAIRE composite .
MDCCL

Réponses:

5

En utilisant ltree

Si IPV6 fonctionne, tant mieux. Il ne prend pas en charge "ACC". ltreeEst-ce que.

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 les 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.

Evan Carroll
la source
1

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é constant). if déclaration - suppose 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.

cohenjo
la source