
# Sharding

`DocStore` scrive un file per record. Senza sharding, tutti i file di una raccolta grande finirebbero in un'unica directory e le operazioni sulla directory rallenterebbero all'aumentare delle voci. storh suddivide quindi i file dei record in bucket usando due caratteri dell'id del record:

```text
pages/data/2d/018bcfe5-6800-7000-8000-2d5dba6274da.jsonc
```

Il bucket si ricava dall'inizio del gruppo finale di UUID (caratteri 24 e 25), che contiene entropia casuale. Due caratteri esadecimali danno 256 bucket, quindi una raccolta di 100k record ha in media meno di 400 file per directory.

## Perché la coda e non la testa

Gli id di UUIDv7 iniziano con un timestamp in millisecondi a 48 bit, quindi gli id creati a breve distanza condividono i caratteri iniziali. Suddividere per prefisso invierebbe ogni raffica di scritture alla stessa directory molto sollecitata, ruotando i bucket solo con il passare del tempo. I byte finali sono casuali per ogni record, quindi le scritture si distribuiscono uniformemente fin dal primo record.

Lo sharding è invisibile a letture e query: `get()` calcola il bucket dall'id e `stream()` restituisce i record ordinati per id in tutti i bucket, così la paginazione tramite cursore si comporta come se la raccolta fosse un unico elenco ordinato.

## Su larga scala

Il numero di bucket non aumenta mai: due caratteri esadecimali corrispondono a 256 directory per qualsiasi dimensione della raccolta. Aumenta invece il numero di file per bucket:

| Record | File per bucket |
| ------- | ---------------- |
| 10k     | ~40              |
| 100k    | ~390             |
| 1M      | ~3,900           |
| 10M     | ~39,000          |

I filesystem moderni indicizzano le voci delle directory, quindi qualche migliaio di file per bucket mantiene veloci le ricerche puntuali; 1M di record è gestibile per `get()` e le query indicizzate. L'apertura di uno store richiede tempo costante indipendentemente dalla dimensione della raccolta: la pulizia dei file temporanei orfani viene eseguita solo quando un marcatore di writer inattivo indica che un processo precedente è terminato in modo anomalo durante una scrittura. Ciò che cresce linearmente su larga scala sono le operazioni sull'intera raccolta: le scansioni senza indice, `reindex()` e `verify()` visitano ciascuna tutti i file dei record, quindi vanno trattate come processi batch. Oltre circa 10M di record per raccolta, iniziano a contare i numeri per bucket e l'uso totale degli inode: suddividete le raccolte oppure usate il log segmentato per dati da scansionare, che mantiene basso il numero di file per sua struttura.

## Gli altri motori

`SegmentedLog` e `Queue` non usano lo sharding perché non creano un file per record. Il log aggiunge dati ai file di segmento sotto `segments/`, con limiti definiti da `max_segment_bytes` (1 MiB per impostazione predefinita), mentre la coda aggiunge eventi a un singolo `queue.log`. Il numero di file rimane basso per sua struttura.
