Partie 18: Exécution du plan logique
Lecture 16 min. âą
Table des matiĂšres
- Index itérables
- IndexIterator
- Index Registry
- Full scan
- Execution du plan logique
- Plan Executor
- Adaptation de la méthode select de la base de données
- Adaptation de la déclaration de table
- On a fini !
- Conclusion
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 đ
Lors de la partie prĂ©cĂ©dente, nous avons dĂ©fini une directive EXPLAIN et avons propagĂ© un flag dans le reste de lâapplication.
Celui-ci nous permet de demander Ă la base de donnĂ©es ce quâelle compte faire comme opĂ©rations pour rĂ©cupĂ©rer des entrĂ©es selon des critĂšres.
On a vu que si lâon possĂ©dait des index sur les donnĂ©es, il Ă©tait possible de limiter les opĂ©rations de lectures complĂštes de la table, que lâon appelle plus communĂ©ment un âfull scanâ.
Aujourdâhui, nous allons modifier la base de code pour rĂ©aliser concrĂštement les opĂ©rations de scan dâindex et de tables.
Et pour cela, nous allons devoir revoir pas mal de choses sur les index et leur définition.
Mais Ă©galement tout le processus de lecture de base de donnĂ©es qui, pour le moment, est basĂ© sur un systĂšme dâallocation mĂ©moire et de copie de donnĂ©es, systĂšme qui fonctionne sur de petits volumes de donnĂ©es, mais sur des tables avec des milliers de lignes, ce nâest juste pas jouable.
Nous allons donc passer tout ce beau monde dans une logique dâitĂ©rateur.
Nous avions Ă©galement abordĂ© dans la conclusion de la partie 12 la notion de âVolcano Modelâ, nous allons le mettre en place Ă©galement. Cela signifie que des itĂ©rateurs consomment dâautres itĂ©rateurs jusquâĂ produire un rĂ©sultat streamable.
On verra les dĂ©tails dans la partie de lâarticle qui lui est consacrĂ©e.
Bref, on a du pain sur la planche !
Index itérables
Théorie
Comme je vous lâai dit dans lâintroduction, les index actuels ne nous permettent pas de scanner les donnĂ©es, de plus la maniĂšre mĂȘme dont ils sont dĂ©finis ne les rend pas portables dans les itĂ©rateurs.
Or, nous voulons streamer le contenu de nos index.
Prenons les données suivantes:
id | nom | prénom | genre | ville |
---|---|---|---|---|
1 | Smith | John | M | Paris |
2 | Martin | Marie | F | New York |
3 | Haddad | Karim (Ù۱ÙÙ ) | M | Tokyo |
4 | Dubois | Sophie | F | Beyrouth |
5 | Tanaka | Hiroshi (ăČăă) | M | Beyrouth |
6 | Yamamoto | Sakura (ăăă) | F | Paris |
7 | Smith | Emily | F | Osaka |
8 | Martin | Jean | M | Lyon |
9 | Haddad | Layla (ÙÙÙÙ) | F | New York |
10 | Dubois | Paul | M | Tokyo |
Muni des index suivants :
(nom, prénom);
(ville);
Pour lâidentitĂ©, pas de surprise, il y a un ID qui correspond Ă un couple unique (nom, prĂ©nom).
Donc pour chaque entrĂ©e dans lâindex, nous avons un tableau âidsâ dâun seul Ă©lĂ©ment qui reprĂ©sente la ârow_idâ de lâentrĂ©e correspondant Ă la valeur de lâindex.
En bases, les index vont ressembler Ă ceci.
nom | prénom | ids |
---|---|---|
Dubois | Paul | [10] |
Dubois | Sophie | [4] |
Haddad | Karim (Ù۱ÙÙ ) | [3] |
Haddad | Layla (ÙÙÙÙ) | [9] |
Martin | Jean | [8] |
Martin | Marie | [2] |
Smith | Emily | [7] |
Smith | John | [1] |
Tanaka | Hiroshi (ăČăă) | [5] |
Yamamoto | Sakura (ăăă) | [6] |
Lâindex âidx_villeâ est plus intĂ©ressant, car il peut y avoir plus dâune personne qui habite la mĂȘme ville.
ville | ids |
---|---|
Beyrouth | [4, 5] |
Lyon | [8] |
New York | [2, 9] |
Osaka | [7] |
Paris | [1, 6] |
Tokyo | [3, 10] |
Et câest bien ce qui est stockĂ© en mĂ©moire, non pas un ârow_idâ, mais une collection de ârow_idâ.
Chaque entrée représente une entrée qui possÚde la ville indexée comme valeur.
Prenons par exemple âParisâ. Nous avons [1, 6]
comme valeur.
Et si on regarde nos donnĂ©es, nous avons effectivement deux entrĂ©es, la 1 et la 6, qui ont comme valeur ville=âParisâ.
id | nom | prénom | genre | ville |
---|---|---|---|---|
1 | Smith | John | M | Paris |
6 | Yamamoto | Sakura (ăăă) | F | Paris |
Maintenant, imaginez que cette table est le registre mondial dâune grande entreprise.
Ce ne sont pas des dizaines dâentrĂ©es, mais des millions quâil faudra gĂ©rer.
Et parmi ces millions de personnes, la probabilitĂ© que des dizaines de milliers de personnes habitent au mĂȘme endroit est plus que probable.
Câest pour cela quâil nous faut un itĂ©rateur. Pour rappel, un itĂ©rateur est un mĂ©canisme qui permet de charger des donnĂ©es uniquement lorsque câest nĂ©cessaire.
Si on reprend notre exemple, il y a 15556 personnes habitant Ă Paris.
Cela donnerai une structure du type:
Paris => 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ..., 15554, 15555, 15556
Il nâest pas exclu de tout charger en mĂ©moire, mais ce nâest pas trĂšs efficace.
Ă la place, nous allons plutĂŽt parcourir le tableau de donnĂ©es de lâindex au moyen dâun curseur, capable de âse rappeler de la position oĂč il estâ.
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ..., 15554, 15555, 15556
â
Lorsque lâon vient lire lâindex, le curseur se dĂ©place dâun cran.
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ..., 15554, 15555, 15556
â
Et ainsi de suite jusquâĂ atteindre la fin de lâindex.
0, 1, 2, 3, 4, 5, 6, 7, 8, 9, ..., 15554, 15555, 15556
â
De cette maniĂšre, on peut consommer au fur et Ă mesure de la donnĂ©e, sans avoir Ă charger lâintĂ©gralitĂ© de lâindex en mĂ©moire.
CâĂ©tait la thĂ©orie, maintenant place Ă la pratique ! đ
Trait Indexable
La premiÚre chose que nous allons faire est de déclarer un trait qui nous permettra de travailler plus confortablement avec nos index.
Ătant donnĂ© que nous allons utiliser trĂšs rĂ©guliĂšrement lâitĂ©rateur, nous allons crĂ©er un alias de type pour le trait.
pub type IndexIteratorData<'a> = ;
Maintenant nous allons pouvoir définir le trait.
Ce trait contient 5 méthodes:
- push qui permet de rajouter un row_id Ă une entrĂ©e de lâindex
- check qui valide que la donnĂ©e Ă indexĂ©e est conforme Ă la dĂ©finition de lâindex notamment lâunicitĂ© de la valeur indexĂ©e
- get_value qui renvoie le fameux itĂ©rateur que lâon va dĂ©finir.
- get_index_name, get_table_name et get_definition, les getters des champs associés
Pour rappel, les Value
sont les valeur de nos tuples.
Voici un exemple de lâutilisation du trait.
// indexation
index.push;
index.push;
index.push;
index.push;
index.push;
let index_iterator = index.get_value;
// On vérifie que l'index existe bien pour la valeur demandée
let index_iterator = index_iterator.except;
// Il peut alors ĂȘtre consommĂ©
for row_id in index_iterator
ValueIndex
Nous allons en définir une implémentation qui a pour rÎle de venir indexer des tuples de valeurs.
Nous allons lâappeler ValueIndex
.
Et voici son implémentation du trait Indexable
.
La méthode get_value
renvoie un itĂ©rateur sur les row_ids associĂ©es Ă lâentrĂ©e de lâindex si celle-ci existe.
Il faut remarquer quâil nây a aucune allocation mĂ©moire lors de la crĂ©ation de lâitĂ©rateur. Cela signifie que lâon peut lire les donnĂ©es sans avoir besoin de copier les donnĂ©es de lâindex.
On utilise Ă©galement une fonctionnalitĂ© de Rust pour convertir notre Box dâitĂ©rateur en IndexIteratorData
.
Primary Index
Nous avions prĂ©cĂ©demment dĂ©fini les index primaires sĂ©parĂ©ment car nous devions nous lâavouer, la modĂ©lisation de nos index nâĂ©tait pas vraiment optimale. đ
Désormais, nous possédons le trait Indexable
qui définit les comportements des index.
Nous pouvons donc définir une seconde implémentation de Indexable
pour nos index primaires.
pub const PRIMARY_INDEX_NAME: &str = "PRIMARY";
Les index primaires sont unique Ă une table de donnĂ©e, son nom est Ă©galement unique et se nomme âPRIMARYâ.
IndexIterator
Nous avons un itĂ©rateur de row IDs, mais ce nâest pas ce qui nous intĂ©resse vraiment.
Nous voulons les lignes associées à ces row IDs.
Nous avons donc besoin dâun itĂ©rateur sur les lignes.
Pour cela, nous allons définir une structure IndexIterator
.
Cette structure a deux champs:
query_engine
: Le moteur de requĂȘte.row_ids
: Un itérateur sur les row IDs.
Pour créer cet itérateur nous allons utiliser la fonction get_row
de QueryEngine
qui renvoie un itérateur sur les rows.
Câest Ă ce moment-lĂ que nous allons rĂ©ellement lire lâindex et obtenir les row IDs.
Dans notre implémentation actuelle, nos index sont des BTreeMap en mémoire, donc le gain de performance est minime. Par contre, les futurs index seront des fichiers sur disque.
Lire les donnĂ©es de maniĂšre âlazyâ signifie que les donnĂ©es seront lues lorsque lâon va les utiliser, ce qui nâest pas nĂ©gligeable pour des accĂšs disque. đ
Reprenons nos données précédentes:
Sur cette table, il y a 3 index:
PRIMARY id
index primaire.idx_ville ville
index non-unique.idx_identité (nom, prénom)
index unique.
id | nom | prénom | genre | ville |
---|---|---|---|---|
1 | Smith | John | M | Paris |
2 | Martin | Marie | F | New York |
3 | Haddad | Karim (Ù۱ÙÙ ) | M | Tokyo |
6 | Yamamoto | Sakura (ăăă) | F | Paris |
10 | Dubois | Paul | M | Tokyo |
On commence par obtenir depuis lâindex idx_ville
lâitĂ©rateur sur les row IDs qui correspondent Ă la ville âParisâ:
let index_rows = index.get_value;.except;
De cet itérateur nous obtenons ensuite un itérateur sur les rows:
let index_iterator = IndexIterator ;
Celui-ci peut alors ĂȘtre consommĂ© de cette maniĂšre:
for row in index_iterator
On induit alors une deuxiĂšme couche de laziness: lâindex itĂ©rateur nâappelle lâitĂ©rateur interne que lorsque les donnĂ©es sont utilisĂ©es, et lâitĂ©rateur interne nâappelle lâindex que lorsque lâindex itĂ©rateur lâappelle.
De ce fait, notre index peut avoir des milliers de lignes, et nous ne lirons les pages de la table que lorsque cela est réellement nécessaire.
On progresse ! đ
Index Registry
Maintenant que nous avons des index non seulement itérables mais également scannables, nous avons besoin de les stocker afin de pouvoir les retrouver lorsque cela sera necessaire.
Tout dâabord modĂ©lisons notre collection dâindex.
/// A map of column definitions to index implementations.
type Index = ;
Chaque index possÚde un nom unique qui nous permettra de le retrouver facilement lorsque cela sera nécessaire.
On pourrait sâarrĂȘter lĂ , mais nous allons lĂ©gĂšrement complexifier le modĂšle pour optimiser les recherches.
Dans une base de données, rechercher par index primaire est toujours plus avantageux que de rechercher dans des index.
Nous voulons donc que les requĂȘtes se fassent sur les index primaires autant que possible.
Si la requĂȘte ne permet pas de trouver des rows par index primaire, nous allons chercher par index non-unique ou unique.
Mais dans ces deux index il y a également une notion de performance qui entre en ligne de compte.
En effet, par dĂ©finition, un index unique associe une seule row Ă une entrĂ©e dans lâindex. Au contraire, un index non-unique peut associer plusieurs rows Ă une seule entrĂ©e.
Cela signifie que scanner un index unique est la certitude de nâavoir au plus quâune seule row, tandis que scanner un index non-unique peut avoir plusieurs rows, parfois des milliers.
La modĂ©lisation de notre stockage dâindex va matĂ©rialiser cette distinction de type dâindex, via une map entre le type dâindex et son contenu.
Nous allons utiliser une BtreeMap pour cela. Car elle possÚde la particularité de fournir un itérateur ordonné.
Cela signifie que si nous dĂ©finissons un ordre de prioritĂ© sur nos types dâindex, alors nous aurons la certitude que le type dâindex scanner sera dans lâordre dâimportance que nous voulons.
PRIMARY > UNIQUE > NON_UNIQUE
Pour cela nous pouvons utiliser une propriété des énumération qui permet de donner explicitement une valeur à une variante.
Ensuite en dérivant Ord et PartialOrd sur notre enum, nous aurons une ordonnancement naturelle.
On définit ensuite notre struct IndexRegistry
qui utilise une BTreeMap associant un IndexType
et un Index
.
On stocke Ă©galement le nom des index pour ne pas avoir de duplication de nom dâindex sur la table.
On peut alors implementer IndexRegistry
:
On commence par lâajout dâindex.
On vĂ©rifie que lâindex nâa pas de doublon avant de lâajouter.
On ajoute ensuite le nom de lâindex au registre. Nous sommes obligĂ© de dĂ©coupler les cas dâindex unique et non-unique.
Note
LâAPI entry de BtreeMap permet de crĂ©er une entrĂ©e si elle nâexiste pas alors or_default permet de la crĂ©er et de renvoyer une rĂ©fĂ©rence mutable vers lâentrĂ©e.
let map = ; let value = map.entry.or_default;
Dans le cas oĂč celle-ci nâexiste pas,
value
est une référence mutable vers une String vide qui sera créé dans la map à la clef 1.Sinon
value
est une réference mutable vers la valeur de la clef1.
Ensuite, nous implĂ©mentons les mĂ©thodes permettant dâinsĂ©rer des valeurs dans les index, ainsi que celle permettant de trouver un index par sa dĂ©finition.
On implĂ©mente Ă©galement la mĂ©thode permettant de vĂ©rifier si un index peut ĂȘtre ajoutĂ©.
Ici on va utiliser la fonctionnalitĂ© de flat_map de Iterator pour itĂ©rer sur tous les types dâindex et les indexes eux-mĂȘmes.
Mais également le find_map
de la option de Option qui permet de prendre la premiĂšre valeur de lâoption qui satisfait une condition.
Ces deux APIs permettent de traiter de maniÚre indifférentielle les index uniques et non-uniques.
Note
Ce nâest pas rĂ©ellement nĂ©cessaire, mais je voulais mâamuser un peu avec les APIs de Rust. ^^
On implémente la méthode permettant de trouver un index par expression.
Pour chaque table de la base de données. La structure des index est la suivante:
{
Primary: {
1 => [1],
2 => [2],
8 => [8],
9 => [9],
10 => [10],
}
Unique: {
"idx_identité": {
("Smith", "John") => [1],
("Martin", "Marie") => [2],
("Martin", "Jean") => [8],
("Haddad", "Layla") => [9],
("Dubois", "Paul") => [10],
},
},
NonUnique: {
"idx_ville": {
"Beyrouth" => [4, 5],
"Lyon" => [2, 4],
"New York" => [2, 9],
"Osaka" => [7],
},
"idx_nom": {
"Smith" => [1],
"Martin" => [2, 8],
"Haddad" => [9],
"Dubois" => [10],
}
}
}
Comme les index sont stockés par type et que les index uniques sont prioritaires, alors les index uniques seront parcourus en premier.
for index in self.indexes.keys
Donc si un index unique correspondant Ă lâexpression est trouvĂ©, alors on sort de la boucle et on retourne lâindex sans passer par les indexes non uniques.
Bon, ça commence Ă prendre forme. đ
Full scan
Nous allons adapter le scanner que nous avions fait en partie 12.
Pas grand-chose à ajouter, à part que le scanner utilise désormais le QueryEngine pour réaliser la récupération des données.
Cela permet dâhomogĂ©nĂ©iser les deux API entre le scan dâindex et le scan de table.
Le premier via lâIndexIterator et le deuxiĂšme via le Scanner.
Les deux API sont des itérateurs de Vec<Value>
.
Execution du plan logique
Maintenant que tous nos composants de rĂ©cupĂ©ration de donnĂ©es sont sous la forme dâitĂ©rateurs, nous pouvons passer Ă lâexĂ©cution du plan logique.
Pour rappel, un plan est un ensemble dâĂ©tapes de traitement de la donnĂ©e, qui va de la rĂ©cupĂ©ration de celles-ci dans la base de donnĂ©es jusquâĂ leur affichage final.
Trait ExecutePlan
Nous allons maintenant rajouter une trait ExecutePlan
qui va permettre de traiter un plan de maniere dynamique.
Nous allons définir deux type alias pour faciliter la lecture.
/// The input of the step of the plan to execute
type PlanExecInput<'a> = ;
/// The output of the step of the plan executed
pub type PlanExecOutput<'a> =
;
Le boxing est nécessaire car les itérateurs ne sont pas des structures mais des traits.
PlanExecInput
est un itérateur de Vec<Value>
il sera utilisé comme entree de la prochaine étape du plan.
PlanExecOutput
est un itérateur de Vec<Value>
il sera utilisé comme sortie de la prochaine étape du plan.
Chaque étape du plan prendra deux arguments:
database
: la base de donnéesinputs
: la sortie de lâetape precedente
Note
inputs
peut ĂȘtre un seul itĂ©rateur ou plusieurs itĂ©rateurs ou mĂȘme aucun.
Plan
Voici sa modélisation en Rust.
```
#### Etapes de scans
Les opérations de scans elles se décomposent en plusieurs types:
- ` ByIndex`
- ` FullScan`
```rust
Un full scan est la maniÚre la plus simple mais aussi généralement la plus lente et peu efficace de récupérer des données.
Le full scan va lire toutes les lignes de la table et les retourner. Cette simplicité se retrouve dans modélisation: seul le nom de la table est requis.
Pour cela on utilise le Scanner
qui va nous retourner un itérateur sur les lignes de la table.
Un scan par index est légÚrement plus complexe.
On implémente ExecutePlan
pour ScanByIndex
:
Etape de filtres
Un filtre est une opĂ©ration qui permet de conserver uniquement certaines lignes dâune table.
Le filtre prend en argument une expression et un nom de table.
Nous allons réutiliser la fonction filter_row de la partie 12.
On vĂ©rifie que lâĂ©tape possĂšde une seule entrĂ©e.
Ensuite, on applique notre filtrage sur cet itérateur.
LâitĂ©rateur rĂ©sultant sera le filtrage du rĂ©sultat de lâĂ©tape prĂ©cĂ©dente.
flowchart Scan--->|Iterator Rows|Filter Filter --->|Iterator Rows filtred|Output
Plan Executor
Maintenant que chaque étape est exécutable, il est temps de mettre en musique tout cela.
Pour cela, nous allons dĂ©finir un plan executor qui va permettre dâordonner les Ă©tapes et de les exĂ©cuter successivement.
Celui-ci prend la base de données et le plan en argument.
Nous allons définir une fonction dummy
qui nous permettra de renvoyer un iterator vide.
Nous pouvons maintenant exécuter notre plan.
Le fonctionnement est le suivant :
- On initialise un itérateur vide.
- On itÚre sur chaque étape du plan.
- Pour chaque étape, on exécutera :
- Si lâĂ©tape est une scan, on lâexĂ©cutera et on mettra Ă jour lâitĂ©rateur (
out
). - Si lâĂ©tape est un filtre, on lâexĂ©cutera avec lâitĂ©rateur (
out
) comme entrĂ©e et on mettra Ă jour lâitĂ©rateur (out
).
- Si lâĂ©tape est une scan, on lâexĂ©cutera et on mettra Ă jour lâitĂ©rateur (
Et on renverra lâitĂ©rateur contenant les rĂ©sultats de la derniĂšre Ă©tape.
Note
A noter que mĂȘme Ă lâissue de la derniĂšre Ă©tape, aucune lecture nâa encore eu lieu.
Nous avons simplement défini des itérateurs imbriqués les uns des autres.
Mais tant que lâitĂ©rateur nâest pas consommĂ©, aucun accĂšs Ă la base de donnĂ©es ne sera fait.
Adaptation de la méthode select de la base de données
Nous devons également adapter la méthode select
de la base de données pour la faire utiliser notre plan executor.
Le fonctionnement est le suivant :
- On crée un plan executor avec la base de données et le plan.
- On exĂ©cute le plan et on renvoie lâitĂ©rateur contenant les rĂ©sultats.
Adaptation de la déclaration de table
Nous avons transformĂ© nos index primaires en index âclassiqueâ, il faut donc faire une petite adaptation.
Précédemment nous avons défini la structure de la table comme suit.
Les index primaires étaient séparés des autres index.
Nous allons les stocker de maniĂšre naturelle dans lâIndexRegistry
.
On adapte le constructeur en conséquence.
Et on modifie le processus dâinsertion.
La diffĂ©rence Ă©tant quâil nây a plus dâindexation primaire explicite, lâindexation par clĂ© primaire est directement gĂ©rĂ©e par lâIndexRegistry
. đ€©
On a fini !
Tout est en place, il est maintenant temps de tester.
Toute ressemblance avec la partie précedente est purement fortuite.
Voici notre déclaration de table.
(
id INTEGER PRIMARY KEY,
nom TEXT(50),
prénom Text(50),
genre TEXT(2),
ville Text(100)
);
Muni des index suivants :
(nom, prénom);
(ville);
On fournit le set de données suivant :
INSERT INTO Client (id, nom, prénom, genre, ville) VALUES (1, 'Smith', 'John', 'M', 'Paris');
INSERT INTO Client (id, nom, prénom, genre, ville) VALUES (2, 'Martin', 'Marie', 'F', 'New York');
INSERT INTO Client (id, nom, prĂ©nom, genre, ville) VALUES (3, 'Haddad', 'Karim (Ù۱ÙÙ
)', 'M', 'Tokyo');
INSERT INTO Client (id, nom, prénom, genre, ville) VALUES (4, 'Dubois', 'Sophie', 'F', 'Beyrouth');
INSERT INTO Client (id, nom, prĂ©nom, genre, ville) VALUES (5, 'Tanaka', 'Hiroshi (ăČăă)', 'M', 'Beyrouth');
INSERT INTO Client (id, nom, prĂ©nom, genre, ville) VALUES (6, 'Yamamoto', 'Sakura (ăăă)', 'F', 'Paris');
INSERT INTO Client (id, nom, prénom, genre, ville) VALUES (7, 'Smith', 'Emily', 'F', 'Osaka');
INSERT INTO Client (id, nom, prénom, genre, ville) VALUES (8, 'Martin', 'Jean', 'M', 'Lyon');
INSERT INTO Client (id, nom, prĂ©nom, genre, ville) VALUES (9, 'Haddad', 'Layla (ÙÙÙÙ)', 'F', 'New York');
INSERT INTO Client (id, nom, prénom, genre, ville) VALUES (10, 'Dubois', 'Paul', 'M', 'Tokyo');
Le plan logique va ĂȘtre le suivant:
db > EXPLAIN SELECT * FROM Client WHERE ville = 'Tokyo';
Scan index idx_ville : ville = 'Tokyo' for table Client
Filter: ville = 'Tokyo'
Et lorsque lâon exĂ©cute sans le EXPLAIN
, on obitient.
db > SELECT * FROM Client WHERE ville = 'Tokyo';
Ok([Integer(3), Text("Haddad"), Text("Karim (Ù۱ÙÙ
)"), Text("M"), Text("Tokyo")])
Ok([Integer(10), Text("Dubois"), Text("Paul"), Text("M"), Text("Tokyo")])
Ce qui est le résultat attendu !
Nous sommes donc capable dâutiliser les index secondaoires non-uniques pour rĂ©cupĂ©rer nos entrĂ©s.
Voyons par accĂšs par clef primaires.
db > EXPLAIN SELECT * FROM Client WHERE id = 7 and ville = 'Paris';
Scan index PRIMARY : id = 7 for table Client
Filter: ville = 'Paris' AND id = 7
Sans EXPLAIN
, on obtient.
db > EXPLAIN SELECT * FROM Client WHERE id = 7 and ville = 'Paris';
RienâŻ! Et câest normal.
id | nom | prénom | genre | ville |
---|---|---|---|---|
7 | Smith | Emily | F | Osaka |
Personne nâhabite Ă Paris et nâa lâid 7.
Par contre si on enlĂšve le fitre de ville, on obtient.
db > EXPLAIN SELECT * FROM Client WHERE id = 7;
Scan index PRIMARY : id = 7 for table Client
Filter: id = 7
db > SELECT * FROM Client WHERE id = 7;
Ok([Integer(7), Text("Smith"), Text("Emily"), Text("F"), Text("Osaka")])
Et lĂ nous sommes contents! đ
Si la donnĂ©es nâest indexĂ© nulle part, on va lire chaque entrĂ©e de la table.
db > EXPLAIN SELECT * FROM Client WHERE genre = 'F';
Full scan Client
Filter: genre = 'F'
db > SELECT * FROM Client WHERE genre = 'F';
Ok([Integer(2), Text("Martin"), Text("Marie"), Text("F"), Text("New York")])
Ok([Integer(4), Text("Dubois"), Text("Sophie"), Text("F"), Text("Beyrouth")])
Ok([Integer(6), Text("Yamamoto"), Text("Sakura (ăăă)"), Text("F"), Text("Paris")])
Ok([Integer(7), Text("Smith"), Text("Emily"), Text("F"), Text("Osaka")])
Ok([Integer(9), Text("Haddad"), Text("Layla (ÙÙÙÙ)"), Text("F"), Text("New York")])
Si aucune expression nâest dĂ©fini, alors on nâa mĂȘme pas de filtrage.
db > EXPLAIN SELECT * FROM Client;
Full scan for table Client
db > SELECT * FROM Client;
Ok([Integer(1), Text("Smith"), Text("John"), Text("M"), Text("Paris")])
Ok([Integer(2), Text("Martin"), Text("Marie"), Text("F"), Text("New York")])
Ok([Integer(3), Text("Haddad"), Text("Karim (Ù۱ÙÙ
)"), Text("M"), Text("Tokyo")])
Ok([Integer(4), Text("Dubois"), Text("Sophie"), Text("F"), Text("Beyrouth")])
Ok([Integer(5), Text("Tanaka"), Text("Hiroshi (ăČăă)"), Text("M"), Text("Beyrouth")])
Ok([Integer(6), Text("Yamamoto"), Text("Sakura (ăăă)"), Text("F"), Text("Paris")])
Ok([Integer(7), Text("Smith"), Text("Emily"), Text("F"), Text("Osaka")])
Ok([Integer(8), Text("Martin"), Text("Jean"), Text("M"), Text("Lyon")])
Ok([Integer(9), Text("Haddad"), Text("Layla (ÙÙÙÙ)"), Text("F"), Text("New York")])
Ok([Integer(10), Text("Dubois"), Text("Paul"), Text("M"), Text("Tokyo")])
Et voilĂ , nous avons un systĂšme de recherche de donnĂ©es optimisĂ© par index !! đ
Conclusion
Et bien, câĂ©tait un gros morceau ! On a passĂ© un temps non nĂ©gligeable sur les index, alors que le titre de lâarticle Ă©tait âExĂ©cution du plan logiqueâ, mais câĂ©tait essentiel.
Une base de données sans index performant est totalement inutilisable.
Or notre plan se base totalement sur les index pour éviter les full-scans.
Le deuxiĂšme axe qui nous a occupĂ© une belle partie est la transformation de notre systĂšme de rĂ©cupĂ©ration de donnĂ©es par recopie et allocation vers un modĂšle âlazyâ via lâutilisation dâitĂ©rateurs.
Cette optimisation permet Ă notre base de donnĂ©es de ne plus ĂȘtre dĂ©pendante de la taille de la table qui est lue !
Le systĂšme dâitĂ©rateurs imbriquĂ©s permet de donner la responsabilitĂ© Ă une Ă©tape du plan de lire ou non de la donnĂ©e. Ce systĂšme va ĂȘtre essentiel lorsque lâon abordera la jointure de tables.
Dans la prochaine partie, nous verrons comment supprimer de la donnée.
Merci de votre lecture â€ïž