Überlegungen zu nicht ganzzahligen Primärschlüsseln

16

Kontext

Ich entwerfe eine Datenbank (unter PostgreSQL 9.6), in der Daten aus einer verteilten Anwendung gespeichert werden. Aufgrund der Verteilung der Anwendung kann ich SERIALaufgrund möglicher Race-Bedingungen keine Auto-Increment-Ganzzahlen ( ) als Primärschlüssel verwenden.

Die natürliche Lösung besteht darin, eine UUID oder eine global eindeutige Kennung zu verwenden. Postgres wird mit einem eingebauten UUIDTyp geliefert , der perfekt passt.

Das Problem, das ich mit der UUID habe, hängt mit dem Debuggen zusammen: Es handelt sich um eine nicht menschenfreundliche Zeichenfolge. Der Bezeichner ff53e96d-5fd7-4450-bc99-111b91875ec5sagt mir nichts, ACC-f8kJd9xKCdobwohl er nicht garantiert eindeutig ist, sagt er mir, dass ich es mit einem ACCObjekt zu tun habe .

Aus Programmiersicht ist es üblich, Anwendungsabfragen zu debuggen, die mehrere verschiedene Objekte betreffen. Angenommen, der Programmierer sucht fälschlicherweise nach einem ACC(Konto-) Objekt am ORD(Bestell-) Tisch. Mit einer von Menschen lesbaren Kennung erkennt der Programmierer das Problem sofort, während er mithilfe von UUIDs einige Zeit damit verbringt, herauszufinden, was falsch war.

Ich brauche nicht die "garantierte" Eindeutigkeit von UUIDs; Ich kann zur Erzeugung von Schlüsseln ohne Konflikte etwas Platz brauchen, aber UUID ist übertrieben. Im schlimmsten Fall wäre eine Kollision nicht das Ende der Welt (die Datenbank lehnt sie ab und die Anwendung kann sich erholen). In Anbetracht der Nachteile wäre eine kleinere, aber menschenfreundliche Kennung die ideale Lösung für meinen Anwendungsfall.

Anwendungsobjekte identifizieren

Der Bezeichner, den ich erstellt habe, hat das folgende Format:, {domain}-{string}wobei er {domain}durch die Objektdomäne (Konto, Bestellung, Produkt) ersetzt wird und {string}eine zufällig generierte Zeichenfolge ist. In einigen Fällen kann es sogar sinnvoll sein, ein {sub-domain}vor dem Zufallsstring einzufügen . Ignorieren wir die Länge von {domain}und, {string}um die Einzigartigkeit zu gewährleisten.

Das Format kann eine feste Größe haben, wenn es die Indizierungs- / Abfrageleistung unterstützt.

Das Problem

Wissend, dass:

  • Ich möchte Primärschlüssel mit einem Format wie haben ACC-f8kJd9xKCd.
  • Diese Primärschlüssel sind Teil mehrerer Tabellen.
  • Alle diese Schlüssel werden für mehrere Joins / Beziehungen in einer 6NF-Datenbank verwendet.
  • Die meisten Tabellen haben eine mittlere bis große Größe (durchschnittlich ~ 1 Million Zeilen; die größten mit ~ 100 Millionen Zeilen).

Was die Leistung angeht, wie kann dieser Schlüssel am besten gespeichert werden?

Im Folgenden sind vier mögliche Lösungen aufgeführt. Da ich jedoch wenig Erfahrung mit Datenbanken habe, bin ich mir nicht sicher, welche (falls vorhanden) die beste ist.

Überlegte Lösungen

1. Speichern als String ( VARCHAR)

(Postgres macht keinen Unterschied zwischen CHAR(n)und VARCHAR(n), also ignoriere ich CHAR).

Nach einigen Recherchen habe ich herausgefunden, dass der Vergleich von Zeichenfolgen mit VARCHAR, insbesondere bei Verknüpfungsoperationen, langsamer ist als die Verwendung INTEGER. Das macht Sinn, aber ist es etwas, worüber ich mich in dieser Größenordnung sorgen sollte?

2. Speichern als binär ( bytea)

Im Gegensatz zu Postgres hat MySQL keinen nativen UUIDTyp. In mehreren Beiträgen wird das Speichern einer UUID mithilfe eines 16-Byte- BINARYFelds anstelle eines 36-Byte - Felds erläutert VARCHAR. Diese Posts brachten mich auf die Idee, den Schlüssel als Binärdatei ( byteaauf Postgres) zu speichern .

Das spart Größe, aber ich bin mehr auf Leistung bedacht. Ich hatte wenig Glück, eine Erklärung zu finden, bei der der Vergleich schneller ist: binäre oder String-Vergleiche. Ich glaube, binäre Vergleiche sind schneller. Wenn dies der Fall ist, byteaist dies wahrscheinlich besser als VARCHAR, obwohl der Programmierer die Daten jetzt jedes Mal codieren / decodieren muss.

Ich könnte mich irren, aber ich denke beides byteaund VARCHARwerde (Gleichheits-) Byte für Byte (oder Zeichen für Zeichen) vergleichen. Gibt es eine Möglichkeit, diesen schrittweisen Vergleich zu "überspringen" und einfach "das Ganze" zu vergleichen? (Ich denke nicht, aber es kostet keine Überprüfung).

Ich denke, Speichern als byteaist die beste Lösung, aber ich frage mich, ob es noch andere Alternativen gibt, die ich ignoriere. Die gleiche Besorgnis, die ich zu Lösung 1 geäußert habe, gilt auch: Reicht der Aufwand für Vergleiche aus, um den ich mich sorgen sollte?

"Kreative Lösungen

Ich habe zwei sehr "kreative" Lösungen gefunden, die funktionieren könnten. Ich bin mir nur unsicher, in welchem ​​Umfang (dh wenn ich Probleme hätte, sie auf mehr als ein paar tausend Zeilen in einer Tabelle zu skalieren).

3. Speichern als, UUIDjedoch mit einem "Etikett" versehen

Der Hauptgrund, keine UUIDs zu verwenden, besteht darin, dass Programmierer die Anwendung besser debuggen können. Was aber, wenn wir beide verwenden können: Die Datenbank speichert alle Schlüssel nur als UUIDs, umschließt jedoch das Objekt, bevor / nachdem Abfragen durchgeführt wurden.

Der Programmierer fragt beispielsweise nach ACC-{UUID}, die Datenbank ignoriert den ACC-Teil, ruft die Ergebnisse ab und gibt sie alle als zurück {domain}-{UUID}.

Möglicherweise wäre dies mit etwas Hackerei mit gespeicherten Prozeduren oder Funktionen möglich, aber einige Fragen kommen in den Sinn:

  • Ist dies (Entfernen / Hinzufügen der Domäne bei jeder Abfrage) ein erheblicher Aufwand?
  • Ist das überhaupt möglich?

Ich habe noch nie gespeicherte Prozeduren oder Funktionen verwendet, daher bin ich mir nicht sicher, ob dies überhaupt möglich ist. Kann jemand Licht ins Dunkel bringen? Wenn ich eine transparente Ebene zwischen dem Programmierer und den gespeicherten Daten einfügen kann, scheint dies eine perfekte Lösung zu sein.

4. (Mein Favorit) Als IPv6 speichern cidr

Ja, du hast es richtig gelesen. Es stellt sich heraus, dass das IPv6-Adressformat mein Problem perfekt löst .

  • Ich kann in den ersten Oktetten Domänen und Unterdomänen hinzufügen und die übrigen als Zufallszeichenfolge verwenden.
  • Die Kollisionswahrscheinlichkeiten sind in Ordnung. (Ich würde zwar nicht 2 ^ 128 verwenden, aber es ist immer noch OK.)
  • Gleichstellungsvergleiche werden (hoffentlich) optimiert, sodass ich möglicherweise eine bessere Leistung erhalte als nur die Verwendung bytea.
  • Ich kann tatsächlich einige interessante Vergleiche durchführen, containsje nachdem, wie die Domänen und ihre Hierarchie dargestellt werden.

Angenommen, ich verwende Code 0000, um die Domäne "Produkte" darzustellen. Schlüssel 0000:0db8:85a3:0000:0000:8a2e:0370:7334würde das Produkt darstellen 0db8:85a3:0000:0000:8a2e:0370:7334.

Die Hauptfrage ist hier: Gibt es im Vergleich zu Datentypen byteaeinen Hauptvorteil oder einen Hauptnachteil cidr?

Renato Siqueira Massaro
quelle
5
Wie viele verteilte Knoten sind möglich? Kennen Sie ihre Nummer (und Namen) im Voraus? Haben Sie zusammengesetzte (mehrspaltige) PKs in Betracht gezogen? Eine Domain (abhängig von meiner ersten Frage) und eine einfache serielle Spalte sind möglicherweise die kleinsten, einfachsten und schnellsten ...
Erwin Brandstetter,
@ Phil, danke! @ErwinBrandstetter Bezüglich der Anwendung wurde sie so konzipiert, dass sie sich automatisch an die Last anpasst, sodass im Voraus nur sehr wenige Informationen vorliegen. Ich habe darüber nachgedacht, (Domain, UUID) als PK zu verwenden, aber dies würde "Domain" überall wiederholen, Domain wäre immer noch varcharunter vielen anderen Problemen. Ich wusste nichts über die Domains von pg, was sehr interessant ist. Ich sehe, dass Domänen verwendet werden, um zu überprüfen, ob eine bestimmte Abfrage das richtige Objekt verwendet, aber es würde immer noch einen nicht ganzzahligen Index voraussetzen. Ich bin nicht sicher, ob es eine "sichere" Verwendungsweise serialgibt (ohne einen Sperrschritt).
Renato Siqueira Massaro
1
Die Domain muss nicht unbedingt eine sein varchar. Überlegen Sie, ob Sie einen FK integerTyp festlegen und eine Nachschlagetabelle hinzufügen möchten. Auf diese Weise können Sie sowohl die menschliche Lesbarkeit als auch den Composite PKvor Einfüge- / Aktualisierungsanomalien schützen (eine nicht vorhandene Domain einrichten).
Yemet
1
Ich möchte Primärschlüssel mit einem Format wie ACC-f8kJd9xKCd. ”← Das scheint ein Job für den guten alten zusammengesetzten PRIMARY KEY zu sein .
MDCCL

Antworten:

5

Verwenden ltree

Wenn IPV6 funktioniert, ist das großartig. "ACC" wird nicht unterstützt. ltreetut.

Ein Beschriftungspfad ist eine Folge von null oder mehr durch Punkte getrennten Beschriftungen, z. B. L1.L2.L3, die einen Pfad von der Wurzel eines hierarchischen Baums zu einem bestimmten Knoten darstellen. Die Länge eines Beschriftungspfads muss weniger als 65 KB betragen. Eine Länge unter 2 KB ist jedoch vorzuziehen. In der Praxis ist dies keine wesentliche Einschränkung. Beispielsweise beträgt der längste Etikettenpfad im DMOZ-Katalog ( http://www.dmoz.org ) ungefähr 240 Byte.

Sie würden es so verwenden,

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

Wir erstellen Beispieldaten.

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

Und Bratsche ..

                                                          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)

Weitere Informationen und Operatoren finden Sie in den Dokumenten

Wenn Sie die Produkt-IDs erstellen, würde ich Baum. Wenn Sie etwas brauchen, um sie zu erstellen, würde ich UUID verwenden.

Evan Carroll
quelle
1

Nur in Bezug auf den Leistungsvergleich mit bytea. Der Vergleich des Netzwerks erfolgt in drei Schritten: zuerst mit den gemeinsamen Bits des Netzwerkteils, dann mit der Länge des Netzwerkteils und dann mit der gesamten nicht maskierten Adresse. Siehe: network_cmp_internal

es sollte also etwas langsamer sein als bytea, was direkt zu memcmp führt. Ich habe einen einfachen Test für eine Tabelle mit 10 Millionen Zeilen durchgeführt, um nach einer einzigen zu suchen:

  • Mit der numerischen ID (Integer) habe ich 1000ms gebraucht.
  • mit cidr hat es 1300ms gedauert.
  • mit bytea hat es 1250ms gedauert.

Ich kann nicht sagen, dass es einen großen Unterschied zwischen bytea und cidr gibt (obwohl die Lücke konstant blieb). Nur die zusätzliche ifAussage - denke, das ist nicht schlecht für 10-Millionen-Tupel.

Hoffe, es hilft - würde gerne hören, was Sie am Ende gewählt haben.

cohenjo
quelle