Italiano

Strumenti per sviluppatori · Generatore UUID

Chiavi primarie UUID casuali e frammentazione B-Tree: cosa succede realmente

· Perché è importante

uuid crittografia API del browser

Un diagramma ad albero B che mostra inserti sequenziali che riempiono una pagina rispetto a inserti casuali sparsi su più pagine
Illustrazione vettoriale originale ToolAcre

Le chiavi v4 casuali vengono inserite in pagine di indice casuali e gli indici cluster ne pagano il prezzo. Questo post spiega il meccanismo, il costo di archiviazione del testo rispetto al binario e dove gli UUID ordinati nel tempo cambiano l'immagine.

Gli inserti rallentano man mano che la tabella cresce: il sintomo che spinge i DBA a riflettere sulla scelta delle chiavi

L'inserimento di una riga con un UUID casuale come chiave primaria in un database con un indice cluster fa sì che il database inserisca la nuova riga in una posizione casuale all'interno della struttura ad albero B. Le chiavi sequenziali vengono aggiunte alla pagina foglia più a destra, mantenendo tutti gli inserimenti all'interno di una piccola area residente nella cache. Le chiavi casuali spargono gli inserti nell'intero indice, costringendo il database ad attraversare e modificare pagine distanti nell'archiviazione fisica. Man mano che la tabella cresce e l'albero si approfondisce, ogni inserimento tocca più pagine e provoca più operazioni I/O. L'operazione che sembrava economica su mille righe diventa costosa su un milione. Questo non è un problema teorico; si manifesta come un degrado misurabile nel throughput di inserimento.

Come si riempie un albero B in cluster: le chiavi sequenziali vengono aggiunte all'ultima pagina; i tasti casuali toccano le pagine dell'intero indice

Il meccanismo è fondamentale per il funzionamento degli alberi B perché mantengono l'ordine delle chiavi ordinato all'interno delle pagine foglia. Quando inserisci una riga con la chiave 10,001 in una tabella che ha già archiviato le chiavi da 1 a 10,000, il database sa dove appartiene quella riga: alla fine, nella pagina esistente più a destra se c'è spazio o in una nuova pagina aggiunta a destra. Quando inserisci una riga con un UUID casuale come 7524fae2-7dec-11d0-a765-00a0c91e6bf6 nella stessa tabella, il database deve esplorare l'albero per trovare la pagina foglia contenente le chiavi in ​​quell'intervallo UUID, individuare la posizione esatta all'interno di quella pagina e inserire la riga. Se la pagina è piena, si divide, spostando metà del suo contenuto in una nuova pagina e aggiornando il nodo genitore.

Suddivisioni delle pagine e pressione della cache: perché l'inserimento casuale costa di più I/O e perché l'effetto aumenta con la dimensione della tabella

Le chiavi casuali creano uno schema di inserimento nel caso peggiore perché ogni inserimento finisce in una posizione casuale nell'albero invece di accodarsi alla pagina più a destra. Il database deve cercare la pagina corretta, il che in genere costa diverse letture di pagina, una per livello dell'albero. Quindi deve modificare quella pagina, il che potrebbe innescare una divisione che si propaga lungo l'albero. Vengono modificate più pagine, vengono eseguite più scritture e il pool di buffer di memoria si riempie con pagine provenienti da diverse regioni dell'albero anziché rimanere focalizzato sul punto di inserimento attivo. La cache non aumenta e I/O diventa il collo di bottiglia, con la velocità di inserimento che si stabilizza man mano che la tabella cresce su scale più grandi.

Testo o binario: stringhe di 36 caratteri rispetto a 16-byte colonne uuid native o binarie (16) e effetto sulla dimensione dell'indice

Il costo non è uniforme tra i motori di database perché sistemi diversi ottimizzano la gestione delle pagine in modo diverso. I database con compressione aggressiva, dimensioni di pagina ridotte o operazioni in memoria possono mostrare differenze di prestazioni minori tra chiavi sequenziali e casuali. I database con pagine di grandi dimensioni, dischi meccanici o limiti di memoria rigidi mostreranno un drammatico degrado. Il problema è osservabile e misurabile: misura i tassi di inserimento nelle righe 1,000, righe 100,000 e righe 1,000,000. Se la velocità al secondo diminuisce drasticamente su scala più ampia, si verifica la penalità di inserimento casuale con la configurazione specifica dell'hardware e del database.

Alternative ordinate nel tempo: in che modo UUIDv7 e ULID mantengono l'unicità durante l'inserimento alla fine dell'indice

Gli UUID come stringhe consumano 36 caratteri in formato testo o 16 bytes come tipo UUID binario a seconda del formato di archiviazione scelto. Una stringa di caratteri 36 in una colonna UTF-8 o ASCII è 36 bytes, rispetto a 8 bytes per un numero intero di 64 bit. L'indice su una colonna string-UUID è tre volte più grande di un indice su una colonna intera, presupponendo che non vengano applicate tecniche di compressione. Indici più grandi significano che meno pagine di indice rientrano nel pool di buffer, il che significa meno riscontri nella cache quando si attraversa l'albero. Un indice più piccolo che si adatta a RAM offre prestazioni migliori rispetto a un indice di grandi dimensioni che deve essere letto dal disco su ogni query, indipendentemente dall'ordine di inserimento o dai modelli di carico di lavoro.

Esempio pratico: lo stesso carico di lavoro di inserimento descritto per una tabella a chiave casuale e a chiave ordinata nel tempo, qualitativamente, senza benchmark inventati

La differenza di archiviazione è importante sia per gli indici primari che per quelli secondari perché ogni indice secondario che include la chiave primaria deve archiviare il valore UUID completo di caratteri 36 o 16 byte binario UUID. Ciò rende gli indici secondari sostanzialmente più grandi degli indici che utilizzano una chiave intera per le ricerche di chiave primaria. La replica, i backup e i set di risultati delle query crescono tutti proporzionalmente con la dimensione dell'indice maggiore. Il generatore ToolAcre UUID produce valori compatibili con il formato binario; memorizzarli come tipi binari(16) o GUID a seconda del database consente di risparmiare spazio rispetto a varchar(36) e migliora l'efficienza della cache su tutta la linea. Questa ottimizzazione dello storage è fondamentale per i sistemi su larga scala.

Cosa non copre: tabelle e database organizzati in heap in cui la chiave primaria non è raggruppata, dove l'effetto è minore

Un'ottimizzazione comune consiste nell'archiviare UUID come binario internamente e visualizzarlo come stringa solo quando necessario per API o interfacce utente. Le operazioni di indice e unione funzionano sulla forma binaria compatta; la risposta API o il codice dell'applicazione viene convertito in una rappresentazione di stringa. Alcuni database offrono tipi GUID o UUID integrati che gestiscono automaticamente questa conversione. Altri richiedono operazioni di casting esplicite. La differenza di prestazioni tra le colonne 36-byte e 16-byte è reale: una tabella con un milione di righe e una chiave di colonna 36-byte rispetto a 16-byte UUID differisce di 20 MB per livello di indice, che può fare la differenza tra un indice che si adatta alla cache L3 e la richiesta un recupero della memoria.

Conclusione: conosci il tuo indice prima di scegliere una versione: il generatore ToolAcre produce UUID casuali; usa il post per decidere se si adatta al tuo motore di archiviazione

Il compromesso tra prestazioni di inserimento, dimensione dell'indice e caratteristiche della query richiede decisioni architetturali basate su modelli di carico di lavoro. Una chiave di stringa sequenziale potrebbe essere veloce da inserire se le stringhe sono in ordine crescente, ad esempio stringhe basate su timestamp, ma consumerebbe lo stesso spazio degli UUID casuali e perderebbe informazioni temporali. Un UUID casuale è semanticamente più pulito e non presenta componenti di timestamp da perdere, ma è più lento da inserire in un indice cluster e ha uno spazio di archiviazione complessivamente maggiore. Le alternative ordinate nel tempo come UUIDv7 combinano i vantaggi mantenendo la località di inserimento evitando perdite di timestamp negli identificatori casuali della versione 4.