Partie 15: Index secondaires
Lecture 8 min. âą
Table des matiĂšres
- Clef primaire composite
- Les index dans une base de données
- Commande CREATE INDEX
- Index de table
- Table Master
- Implémentation de la commande CREATE INDEX
- Testons!
- 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 đ
Nous avons commencé à indexer de la donnée 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 quâune fois que lâon a dĂ©fini une clef primaire, il nâest plus possible dâindexer quoi que ce soit dâautre.
Enfin, pour le moment. đ
Nous allons rajouter une commande qui aura pour rĂŽle de crĂ©er 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 spoiler, 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.
CREATE TABLE Users (
id INTEGER NOT NULL PRIMARY KEY, // đ ici
nom TEXT(50) NOT NULL,
Ăąge INTEGER
);Mais imaginons que nous nâayons pas dâid.
CREATE TABLE Identité (
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.
CREATE TABLE Identité (
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 MartinOn 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.
CREATE TABLE Identité (
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.
struct PrimaryKeyDefinition {
fields: Vec<String>,
}Je vous fournis le code commenté du parser
impl<'a> Visitable<'a, u8> for PrimaryKeyDefinition {
fn accept(scanner: &mut Scanner<'a, u8>) -> crate::parser::Result<Self> {
// on nettoie de potentiels blancs
scanner.visit::<OptionalWhitespaces>()?;
// on reconnait le token PRIMARY
recognize(Token::Primary, scanner)?;
// on reconnait au moins 1 blanc
scanner.visit::<Whitespaces>()?;
// on reconnait le token KEY
recognize(Token::Key, scanner)?;
// on nettoie de potentiels blancs
scanner.visit::<OptionalWhitespaces>()?;
// visite des colonnes
let columns = scanner.visit::<Columns>()?;
// on nettoie de potentiels blancs
scanner.visit::<OptionalWhitespaces>()?;
Ok(PrimaryKeyDefinition { fields: columns.0 })
}
}On peut alors reconnaĂźtre notre groupe.
#[test]
fn test_parse_primary_key() {
let data = b"PRIMARY KEY (id)";
let mut tokenizer = Tokenizer::new(data);
let primary_key = tokenizer.visit::<PrimaryKeyDefinition>();
assert_eq!(
primary_key,
Ok(PrimaryKeyDefinition {
fields: vec!["id".to_string()],
})
);
let data = b"PRIMARY KEY (id, name)";
let mut tokenizer = Tokenizer::new(data);
let primary_key = tokenizer.visit::<PrimaryKeyDefinition>();
assert_eq!(
primary_key,
Ok(PrimaryKeyDefinition {
fields: vec!["id".to_string(), "name".to_string()],
})
);
}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Ă©finit un ColumnDefinitionResult, qui va nous permettre de rĂ©aliser lâunion de reconnaissance entre ColumnDefinition et PrimaryKeyDefinition
enum ColumnDefinitionResult {
Column(ColumnDefinition),
PrimaryKey(PrimaryKeyDefinition),
}
impl<'a> Visitable<'a, u8> for ColumnDefinitionResult {
fn accept(scanner: &mut Scanner<'a, u8>) -> crate::parser::Result<Self> {
Acceptor::new(scanner)
.try_or(|column| Ok(ColumnDefinitionResult::Column(column)))?
.try_or(|primary_key| Ok(ColumnDefinitionResult::PrimaryKey(primary_key)))?
.finish()
.ok_or(ParseError::UnexpectedToken)
}
}On peut alors modifier le parser de Schema pour prendre en compte cette union.
impl<'a> Schema {
fn parse(scanner: &mut Scanner<'a, u8>) -> crate::parser::Result<Self> {
// snip ...
// que l'on visite comme une liste de définitions de colonne séparées par des virgules
let columns_definitions = fields_group_tokenizer
// â ïž c'est dĂ©sormais un ColumnDefinitionResult
.visit::<SeparatedList<ColumnDefinitionResult, SeparatorComma>>()?;
// on récupÚre la primary key
let primary_key = parse_primary_key(&columns_definitions.data)?;
// que l'on transforme en notre map de définition de champs
let columns =
columns_definitions
.into_iter()
.fold(Vec::new(), |mut fields, column_definition| {
// on fitre sur les définition de colonnes, la primary key
// ne nous intéresse pas
if let ColumnDefinitionResult::Column(column_definition) = column_definition {
fields.push((
column_definition.name,
crate::data::ColumnDefinition::new(
column_definition.field,
// avec les contraintes associées
column_definition.constraints,
),
));
}
fields
});
// on n'oublie pas de déplacer le curseur du scanner externe de la taille du groupe
scanner.bump_by(fields_group.data.len());
Ok(Schema::new(columns, primary_key))
}
}La méthode parse_primary_key prend également quelques changements
fn parse_primary_key(fields: &Vec<ColumnDefinitionResult>) -> crate::parser::Result<Vec<String>> {
let mut primary_keys = Vec::new();
for definition in fields {
match definition {
// si c'est une colonne
ColumnDefinitionResult::Column(column_definition) => {
// récupérer les contraintes
if !column_definition
.constraints
.iter()
.filter(|constraint| constraint == &&Constraint::PrimaryKey)
.collect::<Vec<_>>()
.is_empty()
{
// et accumuler la colonne aux résultat si c'est
// une primary key
primary_keys.push(vec![column_definition.name.clone()]);
}
}
// si le groupe est une primary key, l'accumuler aux résultats
ColumnDefinitionResult::PrimaryKey(primary_key_definition) => {
primary_keys.push(primary_key_definition.fields.clone());
}
}
}
// il faut au moins une primary key par schéma
if primary_keys.is_empty() {
return Err(ParseError::PrimaryKeyNotExists);
}
// la primary key ne peut ĂȘtre qu'unique
if primary_keys.len() > 1 {
return Err(ParseError::NonUniquePrimaryKey(primary_keys));
}
Ok(primary_keys.remove(0))
}Notre clef primaire peut désormais prendre en compte deux champs.
#[test]
fn test_primary_key_composite() {
let data = b"(id INTEGER NOT NULL, name TEXT(50) NOT NULL, data INTEGER, PRIMARY KEY (id, name))";
let mut tokenizer = Tokenizer::new(data);
let schema = tokenizer.visit::<Schema>();
assert_eq!(
schema,
Ok(Schema::new(
vec![
(
"id".to_string(),
ColumnDefinition::new(ColumnType::Integer, vec![Constraint::NotNull,])
),
(
"name".to_string(),
ColumnDefinition::new(ColumnType::Text(50), vec![Constraint::NotNull,])
),
(
"data".to_string(),
ColumnDefinition::new(ColumnType::Integer, vec![])
),
],
vec!["id".to_string(), "name".to_string()]
))
)
}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 colonnes 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.
Ă 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éée.
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éfinie par
CREATE TABLE Identité (
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 simulĂ© lâindex au travers dâune map.
BTreeMap<Vec<Value>, usize>Cette 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) => 10Mais on peut également avoir cette typologie.
CREATE TABLE Identité (
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 suivant.
(1,) => 1
(2,) => 2
(3,) => 3
(4,) => 4
(5,) => 5
(6,) => 6
(7,) => 7
(8,) => 8
(9,) => 9
(10,) => 10Nous 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) => 10Et 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 uniques.
CREATE UNIQUE INDEX idx_ville ON IdentitĂ© (nom, prĂ©nom);Le idx_ville est le nom de cet index, il se doit dâĂȘtre unique par table.
On peut également créer sur cette table un index non-unique.
CREATE INDEX idx_ville ON Identité (ville);Parser CREATE INDEX
Il nous faut 3 nouveaux tokens
enum Token {
// snip ...
/// INDEX token
Index,
/// UNIQUE token
Unique,
/// ON token
On,
}On dĂ©finit une Ă©numĂ©ration sur le type dâindex: âuniqueâ ou ânon-uniqueâ.
enum Uniqueness {
Unique,
NonUnique,
}On matérialise la commande.
struct CreateIndexCommand {
// nom de l'index
pub name: String,
// table associée à l'index
pub table: String,
// colonnes associées à l'index
pub columns: Vec<String>,
pub uniqueness: Uniqueness,
}Que lâon peut visiter
impl<'a> Visitable<'a, u8> for CreateIndexCommand {
fn accept(scanner: &mut Scanner<'a, u8>) -> crate::parser::Result<Self> {
// on nettoie les potentiels blancs
scanner.visit::<OptionalWhitespaces>()?;
// on reconnait le token CREATE
recognize(Token::Create, scanner)?;
// on reconnait au moins 1 blanc
scanner.visit::<Whitespaces>()?;
// reconnaissance du token UNIQUE
let uniqueness = Token::Unique
.recognize(scanner)?
.map(|_| Uniqueness::Unique)
.unwrap_or(Uniqueness::NonUnique);
// on nettoie les potentiels blancs
scanner.visit::<OptionalWhitespaces>()?;
// on reconnait le token Index
recognize(Token::Index, scanner)?;
// on reconnait au moins 1 blanc
scanner.visit::<Whitespaces>()?;
// nom de l'index
let index_name_tokens = forecast(UntilToken(Token::Whitespace), scanner)?;
let index_name =
String::from_utf8(index_name_tokens.data.to_vec()).map_err(ParseError::Utf8Error)?;
scanner.bump_by(index_name_tokens.data.len());
// on reconnait au moins 1 blanc
scanner.visit::<Whitespaces>()?;
// ON token
recognize(Token::On, scanner)?;
// on reconnait au moins 1 blanc
scanner.visit::<Whitespaces>()?;
// table name
let table_name_tokens = Forecaster::new(scanner)
.add_forecastable(UntilToken(Token::Whitespace))
.add_forecastable(UntilToken(Token::OpenParen))
.forecast()?
.ok_or(ParseError::UnexpectedToken)?;
let table_name =
String::from_utf8(table_name_tokens.data.to_vec()).map_err(ParseError::Utf8Error)?;
scanner.bump_by(table_name_tokens.data.len());
// on nettoie les potentiels blancs
scanner.visit::<OptionalWhitespaces>()?;
// columns
let columns_tokens = forecast(GroupKind::Parenthesis, scanner)?;
let mut columns_tokenizer =
Scanner::new(&columns_tokens.data[1..columns_tokens.data.len() - 1]);
// on visite une liste de colonnes
let columns_list = columns_tokenizer.visit::<SeparatedList<Column, SeparatorComma>>()?;
let columns = columns_list
.into_iter()
.map(|x| x.0)
.collect::<Vec<String>>();
scanner.bump_by(columns_tokens.data.len());
// on nettoie les potentiels blancs
scanner.visit::<OptionalWhitespaces>()?;
// doit terminer par un point-virgule
recognize(Token::Semicolon, scanner)?;
Ok(CreateIndexCommand {
name: index_name,
table: table_name,
columns,
uniqueness,
})
}
}Ce qui permet de parser ces différentes commandes
#[test]
fn parse_create_index_non_unique_multiple_columns() {
let data = b"CREATE UNIQUE INDEX idx_identity ON Users (name, lastname);";
let mut tokenizer = Scanner::new(data);
let command = CreateIndexCommand::accept(&mut tokenizer);
assert_eq!(
command,
Ok(CreateIndexCommand {
name: "idx_identity".to_string(),
table: "Users".to_string(),
columns: vec!["name".to_string(), "lastname".to_string()],
uniqueness: Uniqueness::Unique
})
);
}
#[test]
fn parse_create_index_unique() {
let data = b"CREATE INDEX idx_name ON Users (name);";
let mut tokenizer = Scanner::new(data);
let command = CreateIndexCommand::accept(&mut tokenizer);
assert_eq!(
command,
Ok(CreateIndexCommand {
name: "idx_name".to_string(),
table: "Users".to_string(),
columns: vec!["name".to_string()],
uniqueness: Uniqueness::NonUnique
})
);
}Index de table
Nous allons gĂ©nĂ©raliser les index primaires et secondaires au sein dâun mĂȘme container Index.
IndexError
On définit une erreur spécialisée dans la gestion des index.
enum IndexError {
DuplicateIndexValue {
index_name: String,
table_name: String,
values: Vec<Value>,
},
DuplicateIndex {
index_name: String,
table_name: String,
},
ColumnNotExist {
column: String,
table_name: String,
index_name: String,
},
}Indexable
Comme nous voulons gérer des index uniques et non uniques, nous devons trouver un moyen de réaliser leur union.
Généralement on passerait par une énumération, mais on va essayer une nouvelle maniÚre au travers des traits.
pub trait Indexable: DerefMut<Target = BTreeMap<Vec<Value>, Self::Value>> {
type Value;
/// Push a new value to the index
fn push(&mut self, row_id: usize, value: Vec<Value>);
/// Check value is compatible to the index
fn check(&self, columns: &[String], row: &HashMap<String, Value>) -> Result<(), IndexError>;
}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)
fn as_index_tuple(index_columns: &[String], row: &HashMap<String, Value>) -> Vec<Value> {
index_columns.iter().fold(vec![], |mut acc, column| {
if let Some(v) = row.get(column) {
acc.push(v.clone())
}
acc
})
}Index
Un index est un ensemble de tuple et de T.
Ce T va permettre de matĂ©rialiser lâunion des index uniques et non-uniques.
struct Index<T: Sized> {
name: String,
table_name: String,
inner: BTreeMap<Vec<Value>, T>,
}
impl<T> Index<T> {
pub fn new(name: String, table_name: String) -> Self {
Self {
name,
table_name,
inner: BTreeMap::new(),
}
}
}On implĂ©mente DerefMut sur lâIndex pour exposer la map.
impl<T> Deref for Index<T> {
type Target = BTreeMap<Vec<Value>, T>;
fn deref(&self) -> &Self::Target {
&self.inner
}
}
impl<T> DerefMut for Index<T> {
fn deref_mut(&mut self) -> &mut Self::Target {
&mut self.inner
}
}Index Unique et NonUnique
On matérialise alors nos index.
pub type UniqueIndex = Index<usize>;
pub type NonUniqueIndex = Index<Vec<usize>>;Et en implémente le trait Indexable.
Les index uniques forcent une unicité des valeurs.
impl Indexable for UniqueIndex {
type Value = usize;
fn push(&mut self, row_id: usize, value: Vec<Value>) {
println!(
"Insert {:?} into unique index {} => row_id {row_id}",
value, self.name
);
self.insert(value, row_id);
}
fn check(&self, columns: &[String], row: &HashMap<String, Value>) -> Result<(), IndexError> {
let key = as_index_tuple(columns, row);
// on vérifie que le tuple n'est pas déjà présent
if self.contains_key(&key) {
// sinon, on lĂšve une exception
return Err(IndexError::DuplicateIndexValue {
index_name: self.name.to_string(),
table_name: self.table_name.to_string(),
values: key,
});
}
Ok(())
}
}Contrairement Ă un index non-unique, qui permet dâaccumuler autant de valeurs identiques que lâon veut.
impl Indexable for NonUniqueIndex {
type Value = Vec<usize>;
fn push(&mut self, row_id: usize, value: Vec<Value>) {
println!(
"Insert {:?} into non unique index {} => row_id {row_id}",
value, self.name
);
self.entry(value).or_default().push(row_id);
}
fn check(&self, _columns: &[String], _row: &HashMap<String, Value>) -> Result<(), IndexError> {
Ok(())
}
}IndexRegistry
Chaque table peut posséder plusieurs index à la fois, uniques ou non uniques.
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 un set de noms qui gĂšre lâunicitĂ© des noms et deux map dâindex: unique et non-unique.
struct IndexRegistry {
indexes: BTreeMap<Vec<String>, NonUniqueIndex>,
unique_indexes: BTreeMap<Vec<String>, UniqueIndex>,
names: BTreeSet<String>,
}On implĂ©mente lâajout dâindex dans la registry.
impl IndexRegistry {
pub fn add_index(
&mut self,
columns: Vec<String>,
name: &str,
table_name: &str,
unique: Uniqueness,
) -> Result<(), IndexError> {
// vérification de l'unicité du nom d'index
if self.names.contains(name) {
return Err(IndexError::DuplicateIndex {
index_name: name.to_string(),
table_name: table_name.to_string(),
});
}
// création et insertion de l'index
match unique {
Uniqueness::Unique => {
let index = UniqueIndex::new(name.to_string(), table_name.to_string());
self.unique_indexes.insert(columns, index);
}
Uniqueness::NonUnique => {
let index = NonUniqueIndex::new(name.to_string(), table_name.to_string());
self.indexes.insert(columns, index);
}
}
// insertion de son nom
self.names.insert(name.to_string());
Ok(())
}
}Pour tous les index, on vient pousser une nouvelle entrée.
fn insert_into_index<I: Indexable>(
index: &mut BTreeMap<Vec<String>, I>,
row: &HashMap<String, Value>,
row_id: usize,
) -> Result<(), IndexError> {
// pour tous les index
for (index_key, index) in index.iter_mut() {
// pour toutes les valeurs de la row
let key = as_index_tuple(index_key, row);
// on pousse le tuple dans l'index
index.push(row_id, key)
}
Ok(())
}On peut alors lâutiliser dans notre IndexRegistry
impl IndexRegistry {
pub fn insert_into_index(
&mut self,
value: &HashMap<String, Value>,
row_id: usize,
) -> Result<(), IndexError> {
// sur les index uniques
insert_into_index(&mut self.unique_indexes, value, row_id)?;
// non unique
insert_into_index(&mut self.indexes, value, row_id)?;
Ok(())
}
}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
struct Table {
table_name: String,
pub schema: Schema,
pub row_number: usize,
// đ l'index primaire devient un Index
primary_indexes: Index<usize>,
pub pager: Pager,
// đ on rajoute les index secondaires
indexes: IndexRegistry,
}Pour permettre lâajout des index secondaire, on expose une nouvelle mĂ©thode au sein de la table.
impl Table {
fn create_index(
&mut self,
name: String,
columns: Vec<String>,
uniqueness: Uniqueness,
) -> Result<(), CreateIndexError> {
// avant de permettre l'ajout de l'index
// on vérifie que la définition de l'index est conforme
// au schéma
for column in columns.iter() {
// si une colonne n'existe pas on lĂšve une erreur
if !self.schema.column_indexes.contains_key(column) {
return Err(CreateIndexError::IndexError(IndexError::ColumnNotExist {
column: column.to_string(),
table_name: self.table_name.clone(),
index_name: name.clone(),
}));
}
}
// sinon on rajoute un index, si le nom n'est pas déjà utilisé sur la table
self.indexes
.add_index(columns, &name, &self.table_name, uniqueness)
.map_err(CreateIndexError::IndexError)?;
Ok(())
}
}Lors de lâinsertion de nouvelles donnĂ©es, nous avons dĂ©sormais la possibilitĂ© de les indexer.
impl Table {
pub fn insert(&mut self, row: HashMap<String, Value>) -> Result<(), InsertionError> {
//snip ...
// on vérifie que les données à indexer sont cohérentes
self.indexes
.check(&row)
.map_err(InsertionError::IndexError)?;
// insertion des données
self.schema
.serialize(&mut writer, &row)
.map_err(InsertionError::Serialization)?;
// insertion des index secondaires
self.indexes
.insert_into_index(&row, self.row_number)
.map_err(InsertionError::IndexError)?;
// insertion dans l'index primaire de notre tuple
// et de l'ID courant d'insertion
self.primary_indexes.insert(pk, self.row_number);
// incrémentation de l'ID interne
self.row_number += 1;
Ok(())
}
}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.
CREATE TABLE db_master (
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 table âdb_masterâ existe bien.
pub const MASTER_TABLE_NAME: &str = "db_master";
impl Database {
pub fn run(&mut self, command: &str) -> Result<ExecuteResult, ExecutionError> {
// si la table master n'existe pas, la créer
if !self.tables.contains_key(MASTER_TABLE_NAME) {
let schema = "(type TEXT(10), name TEXT(50), tbl_name TEXT(50), sql TEXT(300), PRIMARY KEY (type, name, tbl_name));";
// récupÚre le schéma
let schema = schema_from_str(schema).map_err(ExecutionError::Parse)?;
// création de la table master
let master_table = Table::new(schema, MASTER_TABLE_NAME.to_string());
// insertion de la table master à la base de données
self.tables
.insert(MASTER_TABLE_NAME.to_string(), master_table);
}
parse(command)
.map_err(ExecutionError::Command)?
.execute(self)
}
}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 Display for CreateIndexCommand {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self.uniqueness {
Uniqueness::Unique => {
write!(
f,
"CREATE UNIQUE INDEX {} ON {} ({});",
self.name,
self.table,
self.columns.join(", ")
)
}
Uniqueness::NonUnique => {
write!(
f,
"CREATE INDEX {} ON {} ({});",
self.name,
self.table,
self.columns.join(", ")
)
}
}
}
}Implémentation de la commande CREATE INDEX
Allez! Un petit effort, câest la derniĂšre ligne droite! đ
impl Execute for CreateIndexCommand {
fn execute(self, database: &mut Database) -> Result<ExecuteResult, ExecutionError> {
let sql = format!("{}", &self);
let CreateIndexCommand {
name,
table,
columns,
uniqueness,
} = self;
// création de la commande d'insertion de la commande de création de l'index
// dans la table master
let master_insert = format!(
"INSERT INTO {MASTER_TABLE_NAME}(type, name, tbl_name, sql) VALUES('index', '{name}', '{table}', '{sql}');"
);
// création de l'index
database
.create_index(table, name, columns, uniqueness)
.map_err(ExecutionError::CreateIndex)
.map(|_| ExecuteResult::Nil)?;
// insertion de l'index dans la table master
database.run(&master_insert)
}
}Que lâon propage vers la base de donnĂ©es.
impl Database {
pub fn create_index(
&mut self,
table_name: String,
name: String,
fields: Vec<String>,
uniqueness: Uniqueness,
) -> Result<(), CreateIndexError> {
match self.tables.get_mut(&table_name) {
Some(table) => table.create_index(name, fields, uniqueness),
None => Err(CreateIndexError::TableNotExist(table_name))?,
}
}
}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.
CREATE TABLE identité (nom TEXT(50) NOT NULL, prénom TEXT(50) NOT NULL, data TEXT(50), PRIMARY KEY(nom, prénom));
CREATE TABLE identité2 (nom TEXT(50) NOT NULL, prénom TEXT(50) NOT NULL, data TEXT(50), PRIMARY KEY(nom, pré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.
CREATE INDEX idx_identité ON identité(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.
CREATE INDEX idx_identité ON identité(nom);
CREATE INDEX idx_identité ON identité(prénom);
CREATE INDEX idx_identitĂ© ON identitĂ©2(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 commencer à 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 1Index unique
On peut également se créer un index unique et on insÚre de la donnée.
CREATE UNIQUE INDEX idx_identité_2 ON identité(nom, prénom);
INSERT INTO identitĂ© (nom, prĂ©nom) VALUES ('Doe', 'Jane');On voit que la donnĂ©e se fait doublement indexer, Ă 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 2Par 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 verrons de quelle maniĂšre lâon peut efficacement rechercher de la donnĂ©e.
Merci de votre lecture â€ïž
