In che modo i database memorizzano i valori chiave dell'indice (su disco) per campi di lunghezza variabile?


16

Contesto

Questa domanda riguarda i dettagli di implementazione di basso livello degli indici in entrambi i sistemi di database SQL e NoSQL. La struttura effettiva dell'indice (albero B +, hash, SSTable, ecc.) È irrilevante in quanto la domanda riguarda specificamente le chiavi memorizzate all'interno di un singolo nodo di una di queste implementazioni.

sfondo

Nei database SQL (ad es. MySQL) e NoSQL (CouchDB, MongoDB, ecc.), Quando si crea un indice su una colonna o su un campo di dati del documento JSON, ciò che si sta effettivamente facendo fare al database è creare essenzialmente un elenco ordinato di tutti quei valori insieme a un offset di file nel file di dati principale in cui risiede il record relativo a quel valore.

(Per semplicità, potrei allontanare a mano altri dettagli esoterici di impls specifici)

Esempio SQL classico semplice

Consideriamo una tabella SQL standard che ha una semplice chiave primaria int a 32 bit su cui creiamo un indice, finiremo con un indice su disco delle chiavi intere ordinate e associate con un offset a 64 bit nel file di dati in cui il disco vive, ad esempio:

id   | offset
--------------
1    | 1375
2    | 1413
3    | 1786

La rappresentazione su disco delle chiavi nell'indice è simile alla seguente:

[4-bytes][8-bytes] --> 12 bytes for each indexed value

Attenendosi alle regole empiriche standard sull'ottimizzazione dell'I / O del disco con filesystem e sistemi di database, diciamo che memorizzi le chiavi in ​​blocchi 4KB su disco, il che significa:

4096 bytes / 12 bytes per key = 341 keys per block

Ignorando la struttura generale dell'indice (albero B +, hash, elenco ordinato, ecc.) Leggiamo e scriviamo blocchi di 341 chiavi alla volta in memoria e torniamo sul disco quando necessario.

Query di esempio

Usando le informazioni della sezione precedente, supponiamo che arrivi una query per "id = 2", la classica ricerca dell'indice DB procede come segue:

  1. Leggi la radice dell'indice (in questo caso, 1 blocco)
  2. Ricerca binaria nel blocco ordinato per trovare la chiave
  3. Ottieni l'offset del file di dati dal valore
  4. Cerca il record nel file di dati usando l'offset
  5. Restituire i dati al chiamante

Impostazione della domanda ...

Ok, ecco dove si pone la domanda ...

Il passaggio n. 2 è la parte più importante che consente l'esecuzione di queste query in tempo O (logn) ... le informazioni devono essere ordinate, MA devi essere in grado di attraversare l'elenco in modo rapido ... altro in particolare, devi essere in grado di saltare a offset ben definiti a piacimento per leggere il valore della chiave di indice in quella posizione.

Dopo aver letto nel blocco, devi essere in grado di saltare immediatamente alla 170a posizione, leggere il valore chiave e vedere se quello che stai cercando è GT o LT quella posizione (e così via e così via ...)

L'unico modo in cui potresti saltare i dati nel blocco in questo modo è se le dimensioni dei valori chiave fossero tutte ben definite, come il nostro esempio sopra (4 byte quindi 8 byte per chiave).

DOMANDA

Ok, quindi qui è dove mi sto bloccando con una progettazione dell'indice efficiente ... per colonne varchar nei database SQL o, più specificamente, campi in formato totalmente libero in database di documenti come CouchDB o NoSQL, dove qualsiasi campo che si desidera indicizzare può essere qualsiasi lunghezza come si implementano i valori chiave che si trovano all'interno dei blocchi della struttura dell'indice da cui si costruiscono gli indici?

Ad esempio, supponiamo che utilizzi un contatore sequenziale per un ID in CouchDB e che stai indicizzando i tweet ... avrai valori che vanno da "1" a "100.000.000.000" dopo alcuni mesi.

Diciamo che costruisci l'indice sul database il primo giorno, quando ci sono solo 4 tweet nel database, CouchDB potrebbe essere tentato di usare il seguente costrutto per i valori chiave all'interno dei blocchi di indice:

[1-byte][8-bytes] <-- 9 bytes
4096 / 9 = 455 keys per block

Ad un certo punto questo si interrompe e è necessario un numero variabile di byte per memorizzare il valore chiave negli indici.

Il punto è ancora più evidente se decidi di indicizzare un campo di lunghezza variabile come un "tweet_message" o qualcosa del genere.

Dato che la chiave stessa ha una lunghezza totalmente variabile e che il database non ha modo di indovinare in modo intelligente una "dimensione massima della chiave" quando l'indice viene creato e aggiornato, come vengono effettivamente archiviate queste chiavi all'interno dei blocchi che rappresentano segmenti degli indici in questi database ?

Ovviamente se le tue chiavi sono di dimensioni variabili e leggi in un blocco di chiavi, non solo non hai idea di quante chiavi siano effettivamente nel blocco, ma non hai idea di come saltare in mezzo all'elenco per fare un binario cercali.

È qui che mi fanno inciampare.

Con i campi di tipo statico nei classici database SQL (come bool, int, char, ecc.), Capisco che l'indice può semplicemente pre-definire la lunghezza della chiave e attenersi ad essa ... ma in questo mondo di archivi di dati di documenti, sono perplesso su come stanno modellando efficacemente questi dati su disco in modo tale che possano ancora essere scansionati in tempo O (logn) e apprezzerebbero qualsiasi chiarimento qui.

Per favore fatemi sapere se sono necessari chiarimenti!

Aggiornamento (risposta di Greg)

Si prega di vedere i miei commenti allegati alla risposta di Greg. Dopo una settimana di ricerche in più, penso che si sia davvero imbattuto in un suggerimento meravigliosamente semplice e performante che in pratica è estremamente facile da implementare e utilizzare, fornendo grandi vittorie nell'evitare la deserializzazione dei valori chiave che non ti interessano.

Ho esaminato 3 implementazioni DBMS separate (CouchDB, kivaloo e InnoDB) e tutte gestiscono questo problema deserializzando l'intero blocco nella struttura interna dei dati prima di cercare i valori all'interno del loro ambiente di esecuzione (erlang / C).

Questo è ciò che penso sia così geniale nel suggerimento di Greg; una normale dimensione del blocco di 2048 avrebbe normalmente 50 o meno offset, risultando in un blocco di numeri molto piccolo che dovrebbe essere letto.

Aggiornamento (potenziali svantaggi del suggerimento di Greg)

Per continuare al meglio questo dialogo con me stesso, ho realizzato i seguenti svantaggi di questo ...

  1. Se ogni "blocco" è diretto con dati di offset, non è possibile consentire che le dimensioni del blocco vengano modificate nella configurazione in un secondo momento lungo la strada poiché si potrebbe finire per leggere i dati che non sono iniziati correttamente con un'intestazione o un blocco che conteneva più intestazioni.

  2. Se stai indicizzando valori chiave enormi (supponi che qualcuno stia cercando di indicizzare una colonna di carattere (8192) o BLOB (8192)), è possibile che le chiavi non si adattino in un singolo blocco e debbano essere traboccate su due blocchi affiancati . Ciò significa che il tuo primo blocco dovrebbe avere un'intestazione offset e il secondo blocco inizierà immediatamente con i dati chiave.

La soluzione a tutto questo è avere una dimensione di blocco del database fissa che non è regolabile e sviluppare strutture di dati di blocchi di intestazione attorno ad esso ... ad esempio, si fissano tutte le dimensioni di blocco su 4KB (in genere il più ottimale comunque) e si scrive un valore molto piccolo intestazione di blocco che include il "tipo di blocco" all'inizio. Se è un blocco normale, immediatamente dopo l'intestazione del blocco dovrebbe essere l'intestazione degli offset. Se si tratta di un tipo "overflow", immediatamente dopo l'intestazione del blocco sono dati chiave grezzi.

Aggiornamento (potenziale eccezionale)

Dopo che il blocco viene letto come una serie di byte e gli offset decodificati; tecnicamente potresti semplicemente codificare la chiave che stai cercando in byte grezzi e quindi fare confronti diretti sul flusso di byte.

Una volta trovata la chiave che stai cercando, il puntatore può essere decodificato e seguito.

Un altro fantastico effetto collaterale dell'idea di Greg! Il potenziale per l'ottimizzazione del tempo della CPU qui è abbastanza grande che l'impostazione di una dimensione di blocco fissa potrebbe valere la pena solo per ottenere tutto questo.


Per chiunque fosse interessato a questo argomento, lo sviluppatore principale di Redis si è imbattuto in questo preciso problema mentre cercava di implementare il componente "disk store" defunto per Redis. Inizialmente ha optato per una dimensione della chiave statica "abbastanza grande" di 32 byte, ma ha realizzato il potenziale per problemi e invece ha optato per la memorizzazione dell'hash delle chiavi (sha1 o md5) solo per avere una dimensione coerente. Questo uccide la capacità di fare query a distanza, ma bilancia bene l'albero FWIW. Dettagli qui redis.hackyhack.net/2011-01-12.html
Riyad Kalla

Altre informazioni che ho trovato. Sembra che SQLite abbia un limite su quanto possono essere grandi le chiavi o in realtà tronca il valore della chiave in qualche limite superiore e mette il resto in una "pagina di overflow" sul disco. Questo può rendere orribili le richieste di chiavi enormi, dato che l'I / O casuale raddoppia. Scorri verso il basso fino alla sezione "Pagine B-tree" qui sqlite.org/fileformat2.html
Riyad Kalla

Risposte:


7

È possibile memorizzare il proprio indice come un elenco di offset di dimensioni fisse nel blocco contenente i dati chiave. Per esempio:

+--------------+
| 3            | number of entries
+--------------+
| 16           | offset of first key data
+--------------+
| 24           | offset of second key data
+--------------+
| 39           | offset of third key data
+--------------+
| key one |
+----------------+
| key number two |
+-----------------------+
| this is the third key |
+-----------------------+

(beh, i dati chiave sarebbero ordinati in un esempio reale, ma ottieni l'idea).

Si noti che ciò non riflette necessariamente il modo in cui i blocchi di indice sono effettivamente costruiti in alcun database. Questo è solo un esempio di come è possibile organizzare un blocco di dati di indice in cui i dati chiave sono di lunghezza variabile.


Greg, non ho ancora scelto la tua risposta come risposta defacto perché spero in un po 'più di feedback e sto facendo ulteriori ricerche su altri DBMS (sto aggiungendo i miei commenti alla Q originale). Finora l'approccio più comune sembra essere un limite superiore e quindi il resto della chiave in una tabella di overflow che viene controllata solo quando è necessaria la chiave completa. Non così elegante. La tua soluzione ha un po 'di eleganza che mi piace, ma nel caso limite in cui i tasti aumentano le dimensioni della tua pagina, il tuo modo avrebbe comunque bisogno di una tabella di overflow o semplicemente non consentirla.
Riyad Kalla,

Ho esaurito lo spazio ... In breve, se il progettista di database potrebbe vivere con alcuni limiti rigidi sulla dimensione della chiave, penso che il tuo approccio sia il più efficiente e flessibile. Bella combinazione di spazio ed efficienza della CPU. Le tabelle di overflow sono più flessibili, ma possono essere sbalorditive per l'aggiunta di I / O casuali alle ricerche di chiavi che traboccano costantemente. Grazie per l'input su questo!
Riyad Kalla,

Greg, ci ho pensato sempre di più, cercando soluzioni alternative e penso che tu l'abbia inchiodato con l'idea dell'intestazione offset. Se mantenessi piccoli i tuoi blocchi potresti scappare con offset a 8 bit (1 byte), con blocchi più grandi a 16 bit sarebbe il più sicuro anche fino a blocchi di 128 KB o 256 KB che dovrebbero essere ragionevoli (assumerebbe chiavi da 4 o 8 byte). La grande vittoria è quanto si può leggere e veloce nei dati di offset e quanta deserializzazione si risparmia di conseguenza. Ottimo consiglio, grazie ancora.
Riyad Kalla,

Questo è anche l'approccio utilizzato in UpscaleDB: upscaledb.com/about.html#varlength
Mathieu Rodic
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.