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 UUID
di tipo , che è una misura perfetta.
Il problema che ho con UUID è legato al debug: è una stringa non umana. L'identificatore ff53e96d-5fd7-4450-bc99-111b91875ec5
non mi dice nulla, mentre ACC-f8kJd9xKCd
, sebbene non sia garantito per essere unico, mi dice che ho a che fare con un ACC
oggetto.
Dal punto di vista della programmazione, è comune eseguire il debug di query relative ad oggetti diversi. Supponiamo che il programmatore cerchi erroneamente un ACC
oggetto (account) nella ORD
tabella (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 UUID
tipo nativo . Esistono diversi post che spiegano come memorizzare un UUID usando un BINARY
campo a 16 byte , anziché uno a 36 byte VARCHAR
. Questi post mi hanno dato l'idea di memorizzare la chiave come binaria ( bytea
su 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, bytea
probabilmente è meglio di VARCHAR
, anche se ora il programmatore deve codificare / decodificare i dati ogni volta.
Potrei sbagliarmi, ma penso entrambi bytea
e VARCHAR
confronterò (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 bytea
sia 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 UUID
ma 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 UUID
solo, 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 0000
per rappresentare il dominio "prodotti". La chiave 0000:0db8:85a3:0000:0000:8a2e:0370:7334
rappresenterebbe 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 cidr
tipo di dati?
varchar
tra 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 serial
qui (senza un passaggio di blocco).
varchar
. Prendi in considerazione l'idea di renderlo un FK
integer
tipo e aggiungi una tabella di ricerca. In questo modo puoi avere sia la leggibilità umana che proteggerai il tuo composito PK
da anomalie di inserimento / aggiornamento (inserendo un dominio inesistente).
text
è preferibile varchar
. Guarda depesz.com/2010/03/02/charx-vs-varcharx-vs-varchar-vs-text e postgresql.org/docs/current/static/datatype-character.html
ACC-f8kJd9xKCd
. ”← Questo sembra essere un lavoro per il buon vecchio composito PRIMARY KEY .