Considerazioni sulla chiave primaria non intera


16

Contesto

Sto progettando un database (su PostgreSQL 9.6) che memorizzerà i dati da un'applicazione distribuita. A causa della natura distribuita dell'applicazione, non posso usare numeri interi con incremento automatico ( SERIAL) come chiave primaria a causa di potenziali condizioni di competizione.

La soluzione naturale è utilizzare un UUID o un identificatore univoco globale. Postgres viene fornito con un built-in UUIDdi tipo , che è una misura perfetta.

Il problema che ho con UUID è legato al debug: è una stringa non umana. L'identificatore ff53e96d-5fd7-4450-bc99-111b91875ec5non mi dice nulla, mentre ACC-f8kJd9xKCd, sebbene non sia garantito per essere unico, mi dice che ho a che fare con un ACCoggetto.

Dal punto di vista della programmazione, è comune eseguire il debug di query relative ad oggetti diversi. Supponiamo che il programmatore cerchi erroneamente un ACCoggetto (account) nella ORDtabella (ordine). Con un identificatore leggibile dall'uomo, il programmatore identifica immediatamente il problema, mentre usando gli UUID trascorreva un po 'di tempo a capire cosa c'era che non andava.

Non ho bisogno dell'unicità "garantita" degli UUID; Ho non bisogno di un certo spazio per la generazione di chiavi senza conflitti, ma UUID è eccessivo. Inoltre, nel peggiore dei casi, non sarebbe la fine del mondo se si verificasse una collisione (il database la rifiuta e l'applicazione può recuperare). Quindi, considerati i compromessi, un identificatore più piccolo ma a misura d'uomo sarebbe la soluzione ideale per il mio caso d'uso.

Identificazione degli oggetti dell'applicazione

L'identificatore che ho trovato ha il seguente formato:, {domain}-{string}dove {domain}viene sostituito con il dominio dell'oggetto (account, ordine, prodotto) ed {string}è una stringa generata casualmente. In alcuni casi, potrebbe anche avere senso inserire un {sub-domain}prima della stringa casuale. Ignoriamo la lunghezza {domain}e {string}al fine di garantire l'unicità.

Il formato può avere una dimensione fissa se aiuta le prestazioni di indicizzazione / query.

Il problema

Sapendo che:

  • Voglio avere le chiavi primarie con un formato simile ACC-f8kJd9xKCd.
  • Queste chiavi primarie faranno parte di diverse tabelle.
  • Tutte queste chiavi verranno utilizzate su più join / relazioni, su un database 6NF.
  • La maggior parte delle tabelle avrà dimensioni medio-grandi (in media ~ 1 milione di righe; le più grandi con ~ 100 milioni di righe).

Per quanto riguarda le prestazioni, qual è il modo migliore per memorizzare questa chiave?

Di seguito sono riportate quattro possibili soluzioni, ma poiché ho poca esperienza con i database non sono sicuro di quale (se presente) sia il migliore.

Soluzioni considerate

1. Store as string ( VARCHAR)

(Postgres non fa alcuna differenza tra CHAR(n)e VARCHAR(n), quindi sto ignorando CHAR).

Dopo alcune ricerche, ho scoperto che il confronto delle stringhe VARCHAR, specialmente nelle operazioni di join, è più lento dell'uso INTEGER. Questo ha senso, ma è qualcosa di cui dovrei preoccuparmi su questa scala?

2. Memorizza come binario ( bytea)

A differenza di Postgres, MySQL non ha un UUIDtipo nativo . Esistono diversi post che spiegano come memorizzare un UUID usando un BINARYcampo a 16 byte , anziché uno a 36 byte VARCHAR. Questi post mi hanno dato l'idea di memorizzare la chiave come binaria ( byteasu Postgres).

Ciò consente di risparmiare dimensioni, ma sono più interessato alle prestazioni. Ho avuto poca fortuna nel trovare una spiegazione su quale confronto sia più veloce: binario o stringa. Credo che i confronti binari siano più veloci. Se lo sono, byteaprobabilmente è meglio di VARCHAR, anche se ora il programmatore deve codificare / decodificare i dati ogni volta.

Potrei sbagliarmi, ma penso entrambi byteae VARCHARconfronterò (uguaglianza) byte per byte (o carattere per carattere). C'è un modo per "saltare" questo confronto dettagliato e semplicemente confrontare "tutto"? (Non credo, ma non costa controllare).

Penso che archiviare byteasia la soluzione migliore, ma mi chiedo se ci sono altre alternative che sto ignorando. Inoltre, la stessa preoccupazione che ho espresso sulla soluzione 1 è vera: il sovraccarico sui confronti è abbastanza di cui dovrei preoccuparmi?

Soluzioni "creative"

Ho escogitato due soluzioni molto "creative" che potrebbero funzionare, non sono sicuro in che misura (cioè se avessi problemi a ridimensionarle su più di un paio di migliaia di righe in una tabella).

3. Conservare come UUIDma con una "etichetta" attaccata

Il motivo principale per non utilizzare gli UUID è che i programmatori possano eseguire meglio il debug dell'applicazione. E se potessimo usarli entrambi: il database memorizza tutte le chiavi come UUIDsolo, ma avvolge l'oggetto prima / dopo le query.

Ad esempio, il programmatore chiede ACC-{UUID}, il database ignora la ACC-parte, recupera i risultati e li restituisce tutti come {domain}-{UUID}.

Forse questo sarebbe possibile con alcuni hacker con procedure o funzioni memorizzate, ma vengono in mente alcune domande:

  • Questo (rimuovere / aggiungere il dominio ad ogni query) è un notevole sovraccarico?
  • È possibile?

Non ho mai usato procedure o funzioni memorizzate prima, quindi non sono sicuro che ciò sia possibile. Qualcuno può fare luce? Se posso aggiungere un livello trasparente tra il programmatore e i dati memorizzati, sembra una soluzione perfetta.

4. (Il mio preferito) Memorizza come IPv6 cidr

Sì, hai letto bene. Si scopre che il formato dell'indirizzo IPv6 risolve perfettamente il mio problema .

  • Posso aggiungere domini e sottodomini nei primi ottetti e usare quelli rimanenti come stringa casuale.
  • Le probabilità di collisione sono OK. (Non userei 2 ^ 128 però, ma è ancora OK.)
  • I confronti di uguaglianza sono (si spera) ottimizzati, quindi potrei ottenere prestazioni migliori rispetto al semplice utilizzo bytea.
  • Posso effettivamente eseguire alcuni confronti interessanti, come contains, a seconda di come sono rappresentati i domini e la loro gerarchia.

Ad esempio, supponiamo che io utilizzi il codice 0000per rappresentare il dominio "prodotti". La chiave 0000:0db8:85a3:0000:0000:8a2e:0370:7334rappresenterebbe il prodotto 0db8:85a3:0000:0000:8a2e:0370:7334.

La domanda principale qui è: rispetto a bytea, c'è qualche vantaggio o svantaggio principale nell'uso del cidrtipo di dati?


5
Quanti nodi distribuiti sono possibili? Conosci il loro numero (e nomi) in anticipo? Hai preso in considerazione i PK compositi (a più colonne)? Un dominio (a seconda della mia prima domanda), più una semplice colonna seriale potrebbe essere il più piccolo, il più semplice e il più veloce ...
Erwin Brandstetter

@Phil grazie! @ErwinBrandstetter Per quanto riguarda l'applicazione, è stata progettata per il ridimensionamento automatico in base al carico, quindi ci sono pochissime informazioni in anticipo. Ho pensato di usare (dominio, UUID) come PK, ma questo ripeterebbe "dominio" dappertutto, il dominio sarebbe ancora varchartra molti altri problemi. Non sapevo dei domini di pg, il che è fantastico da imparare. Vedo che i domini vengono utilizzati per convalidare se una determinata query utilizza l'oggetto corretto, ma continuerebbe a fare affidamento su un indice non intero. Non sono sicuro se esiste un modo "sicuro" di utilizzare serialqui (senza un passaggio di blocco).
Renato Siqueira Massaro,

1
Il dominio non deve necessariamente essere un varchar. Prendi in considerazione l'idea di renderlo un FK integertipo e aggiungi una tabella di ricerca. In questo modo puoi avere sia la leggibilità umana che proteggerai il tuo composito PKda anomalie di inserimento / aggiornamento (inserendo un dominio inesistente).
yemet


1
Voglio avere le chiavi primarie con un formato simile ACC-f8kJd9xKCd. ”← Questo sembra essere un lavoro per il buon vecchio composito PRIMARY KEY .
MDCCL,

Risposte:


5

utilizzando ltree

Se IPV6 funziona, fantastico. Non supporta "ACC". ltreelo fa.

Un percorso etichetta è una sequenza di zero o più etichette separate da punti, ad esempio L1.L2.L3, che rappresenta un percorso dalla radice di un albero gerarchico a un nodo particolare. La lunghezza di un percorso dell'etichetta deve essere inferiore a 65 kB, ma è preferibile mantenerlo al di sotto di 2 kB. In pratica questa non è una grande limitazione; ad esempio, il percorso dell'etichetta più lungo nel catalogo DMOZ ( http://www.dmoz.org ) è di circa 240 byte.

Lo useresti così,

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

Creiamo dati di esempio.

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

E viola ..

                                                          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)

Vedi i documenti per maggiori informazioni e operatori

Se stai creando gli ID del prodotto, vorrei accedervi. Se hai bisogno di qualcosa per crearli, userei UUID.


1

Per quanto riguarda il confronto delle prestazioni con bytea. il confronto della rete viene eseguito in 3 passaggi: prima sui bit comuni della parte di rete, quindi sulla lunghezza della parte di rete e quindi sull'intero indirizzo non mascherato. vedi: network_cmp_internal

quindi dovrebbe essere un po 'più lento di quello che passa a memcmp. Ho eseguito un semplice test su una tabella con 10 milioni di righe alla ricerca di una singola:

  • usando l'id numerico (intero) mi ci sono voluti 1000ms.
  • usando cidr ci sono voluti 1300ms.
  • usando bytea ci sono voluti 1250ms.

Non posso dire che ci sia molta differenza tra il bytea e il cidr (anche se il gap è rimasto consistente) Solo la iffrase aggiuntiva - suppongo che non sia poi così male per le tuple di 10m.

Spero che ti aiuti - mi piacerebbe sapere cosa hai scelto.

Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.