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?
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).
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).
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 .