Partie 9 : Découper le stockage des tables en pages
Lecture 5 min. âą
Table des matiĂšres
Les articles de la série
- Partie 1 : Construire le REPL
- Partie 2 : Sérialisation des données
- Partie 3 : Database
- Partie 4 : Tables
- Partie 5 : Parser les commandes, création de la boßte à outils
- Partie 6 : Parser les commandes SQL
- Partie 7 : Tuples de données
- Partie 8 : Contraindre les données via un schéma
- Partie 9 : Découper le stockage des tables en pages
- Partie 10 : Clefs primaires
- Partie 11 : Expressions Logiques SQL
- Partie 12 : Scans et filtrage
- Partie 13 : UTF-8 et caractÚres échappés
- Partie 14 : Attributs nullifiables
- Partie 15 : Index secondaires
- Partie 16 : Directive EXPLAIN
- Partie 17 : Logical Plan
- Partie 18 : Exécution du plan logique
- Partie 19 : Suppression d'enregistrements
- Partie 20 : Réindexation et compactation de tables
Bonjour Ă toutes et tous đ
Petit article (en comparaison des autres đ ).
Bien que lâon soit en train de rĂ©implĂ©menter tout et nâimporte quoi pour notre plaisir et notre apprentissage Ă la fois dans Rust et dans la crĂ©ation de base de donnĂ©es.
Notre objectif est de créer un sqlite, et qui dit sqlite, dit persistance sur disque.
Pour le moment, toutes nos opĂ©rations sont in-memory, on peut se permettre de scanner la mĂ©moire pour un coup modique. MĂȘme si on a plusieurs centaines de mĂ©gaoctets le dĂ©lais de scan de la table reste acceptable.
Par contre si on commence Ă stocker sur disque, un problĂšme de taille (et câest le cas de le dire) va intervenir: lire un fichier est LENT !!
Pour Ă©viter cette lenteur, il faut absolument le âmonter en mĂ©moireâ, mais on ne va pas monter 2Go de base de donnĂ©es!
La solution est de le découper en plusieurs unité logique que nous allons appeler des pages.
Ces pages seront bien plus petites que le fichier de la base de donnĂ©es et permettront par la suite de faire des opĂ©rations de cache trĂšs intĂ©ressantes. đ
Ces dans ces pages que nous allons stocker nos données et donc nos tuples.

La notion cruciale que nous allons devoir respecter pour espĂ©rer rĂ©cupĂ©rer quoique ce soit de page est que chaque donnĂ©es doit avoir rigoureusemet la mĂȘme taille.
Heureusement, câest dĂ©jĂ la cas grĂące Ă notre SchĂ©ma
qui contraint dĂ©sormais nos tuples Ă respecter toutes les rĂšgles que lâon lui dicte avant insertion des dites donnĂ©es dans la table.
Nous pouvons alors implémenter le trait Size
sur le Schéma
Cela est possible car chaque ColumnType
connaĂźt trĂšs exactement la place quâil prend. On peut donc sommer ces emplacements pour obtenir la taille du tuple en mĂ©moire.
Nous allons nous retrouver dans cette configuration.

Chaque tuple dâune table prend la mĂȘme place et on peut alors les sĂ©rialiser les uns aprĂšs les autres.
Ici jâai reprĂ©sentĂ© deux pages de 96 cases et des tuples de 6 cases.
En faisant la division on se retrouve avec 96 / 6 = 16
tuples stockables dans une page. Le 17Ăšme se trouvant dans la page suivante (on commence Ă 0 les index).
Mais attention! Si la taille du tuple nâest pas aussi sympathique, comme 7 cases par exemple, nous allons observer un phĂ©nomĂšne de fragmentation de la donnĂ©e qui se retrouve sur plus dâune page, on nomme ça du dĂ©salignement.
En effet 96 / 7 = 13.7..
, le 14Úme élément va se retrouver coupé en deux.

Pour contrer ce phénomÚne, nous allons condamner les cases qui provoquent ce désalignement.

Ainsi, le début 14Úme élément est désormais déplacé dans la deuxiÚme page, ce qui résout le désalignement.
Câest bien beau de pouvoir stocker de la donnĂ©e dans nos pages, encore faut-il pouvoir les lire aprĂšs coup.
Pour cela, nous allons utiliser une arme des plus commode : les mathĂ©matiques !! đ
DĂ©jĂ calculons la taille de la page. Prenons arbitrairement 4096 octets, câest la taille dâun bloc de donnĂ©es dans plusieurs systĂšme de fichier, ça accĂšlerera les opĂ©rations.
Nous lâappellerons en majuscules : PAGE_SIZE
.
Pour éviter les désalignements en fonction de la taille du tuple row_size
, nous devons tronquer cette taille.
page_size = (PAGE_SIZE // row_size) * row_size
Cela nous donne un page_size
en minuscule cette fois.
Attention
Le symbole
//
est la division entiĂšre, on ne peut pas simplifier âen haut et en basâ parrow_size
.
Pour retrouver la position du tuple dâindex row_number
, il faut dâabord trouver sa page.
page_number = (row_number * row_size) // page_size
^---------------------^
index global
Ce qui revient Ă mettre Ă plat toute nos pages tronquĂ©es et Ă dĂ©finir lâindex global dans cette immense tableau, puis Ă diviser cette index par la taille dâune page tronquĂ©e.
Pour trouver la position du tuple dans cette page son offset
, on va alors utiliser la mĂȘme technique, on calcul lâindex global global_index
, mais cette fois ci au lieu de diviser, on soustrait les tailles de pages qui ne sont pas la page sélectionnée.
offset = global_index - page_number * page_size
En cumulant ces deux informations, il est possible de retrouver notre tuple dans les pages, vive les maths! đ€©
Matérialisons tout ça.
Dâabord, on se créé une structure Page
qui sera notre page.
pub const PAGE_SIZE: usize = 4096;
Note
Si on demande la taille dâune page, on aura trĂšs exactement
4096
bytes.
On lui ajoute deux mĂ©thodes qui permettent dâaccĂ©der en lecture et en Ă©criture Ă une zone de la page.
On peut maintenant définir un objet Pager
qui aura pour rĂŽle de maintenir les pages.
Lors de la création du pager, on y calcule notre fameuse taille tronquée.
On peut alors dĂ©finir notre mĂ©thode qui rĂ©alise lâopĂ©ration mathĂ©matique de rĂ©cupĂ©ration le page et de son offset.
On dĂ©fini pour lâoccasion une structure de Position
Pour finalement définir deux méthodes qui permettent respectivement de récupérer une slice en lecture et en écriture, positionné au début du tuple demandé.
On peut alors finalement modifier la table pour lui rajouter le Pager
et lui retirer la slice de data.
Et finalement utiliser le pager dans nos commandes dâinsertions
Et de sĂ©lection, pour le moment on dĂ©clare une erreur si la page nâexiste pas, dans lâavenir on dira que la donnĂ©e nâexiste pas.
Conclusion
Un article court et globalement technique, les tests unitaires nâont pas bougĂ© dâun poil car les modifications sont purement internes.
Mais le travail que nous avons faut va nous ouvrir la voie dans les prochaines parties sur plein dâoptimisations trĂšs intĂ©ressantes comme du cache LRU.
Dans la prochaine partie, nous verrons les index primaires et comment rĂ©cupĂ©rer de la donnĂ©es autrement quâen scannant toute la table !
Merci de votre lecture â€ïž