Partie 15: Index secondaires
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
Bonjour à toutes et tous 😃
Nous avons commencé à indexer de la données via sa clef primaire, cela permet de nous passer du scan et de la désérialisation des tuples des pages. Cette indexation améliore grandement la vitesse de récupération des entrées.
Le seul petit problème c'est que une fois que l'on a défini une clef primaire, il n'est plus possible d'indexer quoique ce soit d'autre.
Enfin, pour le moment. 😅
Nous allons rajouter une commande qui aura pour rôle de crér un nouveau type d'index que nous allons désigner comme secondaires.
Il y a pas mal de boulot encore une fois, mais comme d'habitude on va tout découper et gérer les choses dans l'ordre.
A l'attaque !
Clef primaire composite
Cela semble étrange de re-parler de clef primaire, mais nous allons en avoir besoin pour quelque chose de très spécial que je ne vais pas vous spoilé, tellement innatendu que ça m'a moi-même amusé. 😍
Aujourd'hui, une clef primaire est définie de manière unique par schéma.
(
id INTEGER NOT NULL PRIMARY KEY, // 👈 ici
nom TEXT(50) NOT NULL,
âge INTEGER
);
Mais imaginons que nous n'ayons pas d'id.
(
nom TEXT(50) NOT NULL,
prénom TEXT(50) NOT NULL,
âge INTEGER
);
Sauf qu'il faut une clef primaire. On peut choisir de mettre le "nom" comme index.
(
nom TEXT(50) NOT NULL PRIMARY KEY,
prénom TEXT(50) NOT NULL,
âge INTEGER
);
Mais le problème, c'est qu'il y a pas mal de Martin en france ^^'
Jean Martin
Marie Martin
Pierre Martin
Claire Martin
Jacques Martin
Sophie Martin
Paul Martin
Anne Martin
Louis Martin
Camille Martin
On peut facilement avoir des doublons. On peut minimiser cela en créant un couple (nom, prénom) comme clef primaire.
Nous allons introduire une nouvelle notation.
(
nom TEXT(50) NOT NULL,
prénom TEXT(50) NOT NULL,
âge INTEGER,
PRIMARY KEY(nom, prénom) // 👈 ici
);
Celle-ci impose de gérer une variation dans la définition du schéma. Jusqu'à présent notre schéma était un emsemble de ColumnDefinition
séparé par des virgules.
Nous devons lui rajouter une PrimaryKeyDefinition
qui aura pour rôle de parser le groupe qui possédera la définition de notre clef primaire composite.
PrimaryKeyDefinition
Nous allons définir notre élément comme un tableau de colonnes.
Je vous fourni le code commenté du parser
On peut alors reconnaître notre groupe.
Schéma à clef primaire composite
Maintenant que nous sommes capables de reconnaître notre PrimaryKeyDefinition
, nous pouvons le réintégrer au sein de notre schéma.
Pour cela on défini un ColumnDefinitionResult
, qui va nous permettre de réaliser l'union de reconnaissance entre ColumnDefinition
et PrimaryKeyDefinition
On peut alors modifier le parser de Schema
pour prendre en compte cette union.
La méthode parse_primary_key
prend également quelque changements
Notre clef primaire peut désormais prendre en compte deux champs.
Fin du crochet sur les clefs primaires composites. 😎
Les index dans une base de données
Vous allez voir c'est très semblables dans sa construction aux clefs primaires composites.
Il s'agit à nouveau de "marquer" un groupe de colonne pour qu'il ait un sens particulier.
On pourrait également utiliser le schéma pour le faire, mais la création d'index peut survenir après la création de la table et le début de l'insertion de tuples.
Cela obligerait à recréer une nouvelle table avec ces index. Impliquant de copier tous les tuples de la table 1 vers la table 2.
Ce n'est pas pratique ni efficace.
A la place nous allons créer une nouvelle commande qui aura pour rôle d'ajouter un ou plusieurs index sur une table déjà créer.
Qu'est-ce qu'un index?
Un index est un raccourci sur de la donnée.
On associe un tuple à un row_id.
Prenons la table défini par
(
nom TEXT(50) NOT NULL,
prénom TEXT(50) NOT NULL,
ville TEXT(50),
PRIMARY KEY(nom, prénom)
);
Avec les données suivantes
row_id | nom | prénom | ville
-------|--------|---------|-----------
1 | Martin | Jean | Paris
2 | Martin | Marie | Lyon
3 | Martin | Pierre | Marseille
4 | Martin | Claire | Bordeaux
5 | Martin | Jacques | Nantes
6 | Martin | Sophie | Toulouse
7 | Martin | Paul | Lille
8 | Martin | Anne | Strasbourg
9 | Martin | Louis | Nice
10 | Martin | Camille | Rennes
Le row_id
est invisible à l'utilisateur.
Lorsque l'on a créé nos index de clef primaire. On a simuler l'index au travers d'une map.
Cet table produit alors cet index primaire.
(Martin, Jean) => 1
(Martin, Marie) => 2
(Martin, Pierre) => 3
(Martin, Claire) => 4
(Martin, Jacques) => 5
(Martin, Sophie) => 6
(Martin, Paul) => 7
(Martin, Anne) => 8
(Martin, Louis) => 9
(Martin, Camille) => 10
Mais on peut également avoir cette typologie.
(
id INTEGER NOT NULL PRIMARY KEY,
nom TEXT(50),
prénom TEXT(50),
ville TEXT(50),
);
Avec les données.
row_id | id | nom | prénom | ville
-------|--------|--------|---------|-----------
1 | 1 | Martin | Jean | Paris
2 | 2 | Martin | Marie | Lyon
3 | 3 | Martin | Pierre | Nantes
4 | 4 | Martin | Claire | Lyon
5 | 5 | Martin | Jacques | Nantes
6 | 6 | Martin | Sophie | Paris
7 | 7 | Martin | Paul | Lille
8 | 8 | Martin | Anne | Nantes
9 | 9 | Martin | Louis | Nice
10 | 10 | Martin | Camille | Paris
Ce qui provoque l'index suivante.
(1,) => 1
(2,) => 2
(3,) => 3
(4,) => 4
(5,) => 5
(6,) => 6
(7,) => 7
(8,) => 8
(9,) => 9
(10,) => 10
Nous allons définir deux types d'index différents:
- unique : la table ne peut avoir qu'une valeur identique
- non unique : la table peut exister à de multiples occasions
Un index unique serait (nom, prénom)
.
(Martin, Jean) => 1
(Martin, Marie) => 2
(Martin, Pierre) => 3
(Martin, Claire) => 4
(Martin, Jacques) => 5
(Martin, Sophie) => 6
(Martin, Paul) => 7
(Martin, Anne) => 8
(Martin, Louis) => 9
(Martin, Camille) => 10
Et un index non-unique (ville,)
.
(Paris,) => [ 1, 6, 10 ]
(Lyon,) => [ 2, 4 ]
(Nantes,) => [ 3, 5, 8 ]
(Lille,) => [ 7 ]
(Nice,) => [ 9 ]
Commande CREATE INDEX
On introduit deux nouvelles commandes.
Une pour les index unique.
(nom, prénom);
Le idx_ville
est le nom de cet index, il se doit d'être unique par table.
On peut également créé sur cette table un index non-unique.
(ville);
Parser CREATE INDEX
Il nous faut 3 nouveaux tokens
On défini une énumération sur le type d'index: "unique" ou "non-unique".
On matérialise la commande.
Que l'on peut visiter
Ce qui permet de parser ces différentes commandes
Index de table
Nous allons généraliser les index primaires et secondaire au sein d'un même container Index
.
IndexError
On défini une erreur spécialisé dans la gestion des index.
Indexable
Comme nous voulons gérer des index uniques et non uniques. Nous devons trouver un moyen de réaliser son union.
Généralement on passerait par une énumération, mais on va essayer une nouvelle manière au travers des traits.
Transformer un tuple en clef d'index
Il est nécessaire de transformer le tuple de données à insérer sous la forme d'un autre tuple uniquement composé des colonnes de l'index.
/// Convert the tuple row as a tuple index
/// with an index like (lastname, firstname)
/// on a schema (lastname, firstname, city)
/// a row tuple (Doe, John, Paris)
/// the index tuple becomes (Doe, John)
Index
Un index est un ensemble est une map de tuple et de T
.
Ce T
va permettre de matérialiser l'union des index uniques et non-uniques.
On implémente DerefMut
sur l'Index
pour exposer la map.
Index Unique et NonUnique
On matérialise alors nos index.
pub type UniqueIndex = ;
pub type NonUniqueIndex = ;
Et en implémenté le trait Indexable
.
Les index unique force une unicité des valeurs.
Contrairement à un index non-unique, qui permet d'accumuler autant de valeurs identiques que l'on veut.
IndexRegistry
Chaque table peut posséder plusieurs index à la fois unique ou non unique.
Mais il faut que le nom de l'index reste unique, il ne peut pas y avoir plus d'un index avec le même nom.
On a alors set de noms qui gère l'unicité des noms et deux map d'index: unique et non-unique.
On implémente l'ajout d'index dans la registry.
Pour tous les index, on vient pousser une nouvelle entré.
On peut alors l'utiliser dans notre IndexRegistry
Nous avons désormais un moyen de définir des index secondaire.
Création des index de table et indexation des données
Nous allons rattacher notre IndexRegistry
à la Table
Pour permettre l'ajout des index secondaire, on expose une nouvelle méthode au sein de la table.
Lors de l'insertion de nouvelle de données, nous avons désormais la possibilité de les indexer.
Notre table est désormais capable d'indexer de la données autre que des clefs primaires. 🤩
Table Master
Il y a plusieurs centaines de lignes, je vous avais dit que les clef primaires composites allaient servir.
C'est le moment ! 😃
Nous allons définir une table qui va nous permettre de stocker nos index.
Plus précisément la commande sql CREATE INDEX
qui a conduit à la création de l'index.
(
type TEXT(10), /* type d'entité ici "index" */
name TEXT(50), /* nom de l'entité, ici le nom de l'index */
tbl_name TEXT(50), /* nom de la table associé à l'entité */
sql TEXT(300), /* la commande sql de création de l'entité */
PRIMARY KEY (type, name, tbl_name)
);
On s'assure qu'au boot de la base de données la clef "db_master" existe bien.
pub const MASTER_TABLE_NAME: &str = "db_master";
Comme la table "db_master" prend en valeur le "sql" de création de l'index, nous allons définir le Display
de la commande.
Implémentation de la commande CREATE INDEX
Allez! Un petit effort, c'est la dernière ligne droite! 😇
Que l'on propage vers la base de données.
A partir de maintenant la commande d'ajout d'index secondaire est entièrement fonctionnelle. 😍
Testons!
Créations des tables
On se créé deux tables.
TEXT(50) NOT NULL, prénom TEXT(50) NOT NULL, data TEXT(50), PRIMARY KEY(nom, prénom));
(nom TEXT(50) NOT NULL, prénom TEXT(50) NOT NULL, data TEXT(50), PRIMARY KEY(nom, prénom));
(nom
Vérification de la cohérence des index
On essaie de définir un index sur une colonne "name" qui n'existe pas dans le schéma.
(name);
Ce qui provoque bel et bien une erreur.
CreateIndex(IndexError(ColumnNotExist { column: "name", table_name: "identité", index_name: "idx_identité" }))
On créé deux index sur la même table.
(nom);
(prénom);
(prénom);
Le premier index est bien créé, par contre le second est refusé car le nom de l'index est le même.
A contrario le troisième index est accepté même si le nom est identique, la table associé n'est pas la même.
CreateIndex(IndexError(DuplicateIndex { index_name: "idx_identité", table_name: "identité" }))
Insertion de données
On peut alors commencé à insérer de la donnée.
INSERT INTO identité (nom, prénom) VALUES ('Dupond', 'Gilbert');
INSERT INTO identité (nom, prénom) VALUES ('Doe', 'Jhon');
On voit que le système réagit correctement et vient rajouter les entrés dans l'index idx_identité
.
Insert [Text("Dupond")] into non unique index idx_identité => row_id 0
Insert [Text("Doe")] into non unique index idx_identité => row_id 1
Index unique
On peut également se créer un index unique et on insère de la donnée.
(nom, prénom);
INSERT INTO identité (nom, prénom) VALUES ('Doe', 'Jane');
On voit que la donnée se fait doublement indexée, à la fois sur l'index unique et non-unique.
Insert [Text("Doe"), Text("Jane")] into unique index idx_identité_2 => row_id 2
Insert [Text("Doe")] into non unique index idx_identité => row_id 2
Par contre, comme l'index est unique, il va interdire l'insertion de donnée dupliquée.
INSERT INTO identité (nom, prénom, data) VALUES ('Doe', 'Jane', 'ma super data de la mort qui tue');
On se fait bien rejeter pour duplication de données.
Insertion(IndexError(DuplicateIndexValue { index_name: "idx_identité_2", table_name: "identité", values: [Text("Doe"), Text("Jane")] }))
J'ai pris la décision arbitraire de refuser l'insertion, j'aurai pu choisir d'uniquement ignorer l'insertion ou de l'écraser.
Mais je préfère conserver l'index synchronisé à la donnée.
Scan des données
On vérifie que les données sont bien en base.
SELECT * FROM identité;
Ce qui est bien le cas, les donnée qui ont été refusées, ne sont réellement pas là!
[Text("Dupond"), Text("Gilbert"), Null]
[Text("Doe"), Text("Jhon"), Null]
[Text("Doe"), Text("Jane"), Null]
Table de méta-data
On peut alors vérifier l'existence des index dans la table master.
SELECT * FROM db_master;
Et oui nous avons bien 2 index sur la première table et 1 sur la seconde.
[Text("index"), Text("idx_identité"), Text("identité"), Text("CREATE INDEX idx_identité ON identité (nom);")]
[Text("index"), Text("idx_identité"), Text("identité2"), Text("CREATE INDEX idx_identité ON identité2 (prénom);")]
[Text("index"), Text("idx_identité_2"), Text("identité"), Text("CREATE UNIQUE INDEX idx_identité_2 ON identité (nom, prénom);")]
Conclusion
Notre pokédex de fonctionnalité se remplit petit à petit. 😎
Dans la prochaine partie nous verront comment supprimer de la données.
Merci de votre lecture ❤️
Vous pouvez trouver le code la partie ici et le diff là.
Ce travail est sous licence CC BY-NC-SA 4.0.