
# Fragmentação

`DocStore` grava um arquivo por registro. Sem fragmentação, uma coleção grande
colocaria todos os arquivos em um único diretório, e as operações no diretório
ficariam mais lentas à medida que o número de entradas aumentasse. Portanto,
storh distribui os arquivos dos registros em diretórios com base em dois
caracteres do id do registro:

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

O bucket é extraído do início do último grupo de UUID (caracteres 24
e 25), que contém entropia aleatória. Dois caracteres hexadecimais geram
256 buckets, portanto uma coleção com 100k registros tem, em média, menos de
400 arquivos por diretório.

## Por que a cauda, e não o início

Os ids de UUIDv7 começam com um timestamp em milissegundos de 48 bits, então ids criados próximos uns dos outros compartilham os primeiros caracteres. Fragmentar pelo prefixo enviaria todos os picos de gravação para o mesmo diretório muito acessado, e os buckets só mudariam com o passar do tempo. Os bytes finais são aleatórios por registro, então as gravações se distribuem uniformemente desde o primeiro registro.

A fragmentação é invisível para leituras e consultas: `get()` calcula o bucket a partir do id, e `stream()` retorna os registros ordenados por id em todos os buckets, fazendo com que a paginação por cursor funcione como se a coleção fosse uma única lista ordenada.

## Em grande escala

A quantidade de buckets nunca aumenta: dois caracteres hexadecimais correspondem a 256 diretórios, qualquer que seja o tamanho da coleção. O que aumenta é a quantidade de arquivos por bucket:

| Registros | Arquivos por bucket |
| ------- | ---------------- |
| 10k     | ~40              |
| 100k    | ~390             |
| 1M      | ~3,900           |
| 10M     | ~39,000          |

Os sistemas de arquivos modernos indexam as entradas de diretório, então alguns milhares de arquivos por bucket mantêm rápidas as buscas pontuais; 1M de registros é um volume tranquilo para `get()` e consultas indexadas. Abrir um armazenamento leva tempo constante, independentemente do tamanho da coleção: a varredura de arquivos temporários órfãos só é executada quando um marcador de processo encerrado indica que um processo anterior falhou durante uma gravação. O que cresce linearmente em grande escala são as operações sobre a coleção inteira: varreduras sem índice, `reindex()` e `verify()` percorrem cada arquivo de registro, então trate-as como tarefas em lote. Acima de cerca de 10M de registros por coleção, a quantidade de arquivos por bucket e o uso total de inodes começam a importar: divida as coleções ou use o log segmentado para dados que exigem varreduras, que mantém a quantidade de arquivos baixa por construção.

## Os outros mecanismos

`SegmentedLog` e `Queue` não fragmentam os dados porque não criam um arquivo por registro. O log acrescenta dados a arquivos de segmento em `segments/`, limitados por `max_segment_bytes` (1 MiB por padrão), e a fila acrescenta eventos a um único `queue.log`. A quantidade de arquivos permanece baixa por construção.
