Partie 17: Logical Plan

Lecture 9 min. ‱

Table des matiĂšres

Les articles de la série

Bonjour à toutes et tous 😃

Lors de la partie prĂ©cĂ©dente, on dĂ©finit une directive EXPLAIN et propagĂ© un flag dans le reste de l’application.

Mais une question vous brûle les lÚvres :

Question

Pourquoi ???

Rendre le QueryEngine “intelligent” et verbeux

Et bien, nous allons complexifier de plus en plus la brique QueryEngine au fil des articles qui vont arriver, mais pour Ă©viter de se retrouver devant une “boĂźte noire” totalement incomprĂ©hensible, nous avons besoin de savoir les choix que le query engine rĂ©alise.

Quels sont-ils ?

Réponse courte : comment récupérer efficacement de la donnée dans la base de données.

Réponse plus élaborée.

Nous avons deux choix pour récupérer de la donnée :

Des index nous en avons de deux types:

Les index secondaires sont eux mĂȘme dĂ©composĂ© en deux catĂ©gories:

Notre EXPLAIN va avoir deux effets :

Prenons une table telle que celle-ci

CREATE TABLE Client (
    id INTEGER PRIMARY KEY, 
    nom TEXT(50),
    prénom Text(50), 
    genre TEXT(2),
    ville Text(100)
);
CREATE TABLE Client (id INTEGER PRIMARY KEY, nom TEXT(50),prénom Text(50), genre TEXT(2), ville Text(100) );

Muni des index suivants :

CREATE UNIQUE INDEX idx_identité ON Client(nom, prénom);
CREATE INDEX idx_ville ON Client(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');

Si nous demandons toutes les entrĂ©es, aucune intelligence ne va ĂȘtre dĂ©veloppĂ©e, nous rĂ©alisons un Full Scan et c’est tout.

SELECT * FROM Client;

Cet Ă©tat va ĂȘtre dĂ©tectĂ©, car il n’y a pas de clause_where Ă  la suite de la sĂ©lection de table, le Query Engine viendra opĂ©rer l’opĂ©ration la plus coĂ»teuse : lire chaque entrĂ©e de la table “Client”, dĂ©sĂ©rialiser les donnĂ©es.

Par contre, si on demande toutes les femmes du registre, cette fois-ci, il y a une expression qui sert de clause_where, donc potentiellement de l’intelligence Ă  avoir sur la rĂ©cupĂ©ration des entrĂ©es correspondantes.

Chaque table possĂšde une collection d’index sur divers tuples de champs, le sport intellectuel du Query Engine va ĂȘtre de dĂ©tecter si oui ou non l’expression de la requĂȘte peut coller avec un index.

Si la requĂȘte est :

SELECT * FROM Client WHERE genre = 'F';

Le query engine va demander à la table “Client”

Question

C’est quoi tes index ?

Et la table va lui répondre:

primary => (id,)
idx_identité => (nom, prénom)
idx_ville => (ville,)

Le Query Engine va alors dĂ©construire l’expression genre = 'F' en (genre,).

Il va ensuite boucler sur les index à la recherche d’une correspondance.

Ici aucune correspondance ne peut ĂȘtre trouvĂ©e, on fallback sur du FullScan que l’on filtrera par la suite.

Par contre, si la requĂȘte est:

SELECT * FROM Client WHERE ville = 'Paris';

De la mĂȘme maniĂšre, le QueryEngine dĂ©construit l’expression ville = 'Paris' en (ville,).

Cette fois-ci, il existe une correspondance d’index : idx_ville => (ville,).

Le QueryEngine va alors utiliser l’index idx_ville pour rĂ©cupĂ©rer la donnĂ©e.

Index non-unique qui se présente ainsi:

('Paris',)    => [0, 5],
('New York',) => [1, 8],
('Tokyo',)    => [2, 9],
('Beyrouth',) => [3, 4],
('Osaka',)    => [6]
('Lyon',)     => [7]

Des tuples associé à des liste de row_id.

Lorsque l’on demande “les personnes qui habitent Paris”, ce que l’on fait, c’est demander la liste des row_id des entrĂ©es qui correspondent Ă  cette ville.

Une fois que l’on a rĂ©cupĂ©rĂ© cette liste, on par row_id, rechercher les tuples de donnĂ©es correspondantes.

Cette fois-ci le mode d’accĂšs a Ă©tĂ© rĂ©alisĂ© au travers de l’index secondaire idx_ville. On obtient alors la liste de row_id suivante [0, 5].

Attention

Passer par les index n’est pas toujours le plus efficace, si votre table est petite, il est parfois prĂ©fĂ©rable de full scan que d’abord de rĂ©cupĂ©rer les row_id puis les tuples.

Tout cela dĂ©pend des statistiques de la table, mais nous, nous n’avons pas bĂąti ces statistiques, et donc n’avons pas de moyens de deviner la bonne marche Ă  suivre.

Si l’expression est plus consĂ©quente comme dans:

SELECT * FROM Client WHERE nom = 'Dubois' AND prénom = 'Sophie';

Alors la dĂ©construction doit prendre garde Ă  la construction logique de l’expression.

Il faut analyser l’entiĂšretĂ© de l’expression et ses relation logiques. Si l’index est composite comme (nom, prĂ©nom) alors ça doit se matĂ©rialiser au niveau de l’expression par une intersection reprĂ©sentĂ© par l’opĂ©rateur logique “AND”.

On recherche donc une expression qui possÚde à la nom = '...' ET prénom = '...'.

Attention

Les champs d’index peuvent trĂšs bien ĂȘtre sĂ©parĂ©s dans l’expression par une autre expression qui n’a pas de rapport avec l’index actuel.

SELECT * FROM Client WHERE nom = 'Dubois' AND  id > 10  AND prénom = 'Sophie';

Ici le id n’a pas de rĂŽle Ă  jouer dans la dĂ©finition de la compatibilitĂ© d’index.

Trouver l’index composite s’il existe va donc nĂ©cessiter deux Ă©tapes:

Et cela, il faut le réaliser pour tous les index.

Implémentation

Maintenant que l’on a tout bien dĂ©fini et expliquĂ©, nous allons pouvoir nous attaquer Ă  la partie intĂ©ressante de l’implĂ©mentation de tout cela.

Plan

La premiĂšre chose que nous allons dĂ©clarer est appelĂ©e le Plan, c’est toutes les Ă©tapes que le QueryEngine pense rĂ©aliser si on lui demande de rechercher dans la base de donnĂ©es en utilisant telle ou telle Expression.

pub struct Plan {
    steps: Vec<PlanStep>,
}

impl Plan {
    pub fn new() -> Self {
        Self { steps: vec![] }
    }

    // Ajoute une nouvelle étapes dans l'exécution du plan
    pub fn add_step(&mut self, step: PlanStep) {
        self.steps.push(step);
    }
}

Cette structure est un wrapper autours d’un Vec<PlanStep>.

Le PlanStep est lui-mĂȘme une Ă©numĂ©ration.

enum PlanStep {
    /// Represents a scan operation in a query plan.
    ///
    /// # Fields
    /// - `kind`: The kind of scan to perform (e.g., full table scan, index scan).
    /// - `table_name`: The name of the table to scan.
    Scan { kind: ScanKind, table_name: String },

    /// Represents a filter operation in a query plan.
    ///
    /// # Fields
    /// - `expression`: The logical expression used to filter rows.
    Filter { expression: Expression },
}

Comme nous l’avons vu prĂ©cĂ©demment, il existe une grande variĂ©tĂ© de façons de lire de la donnĂ©e dans la base de donnĂ©es, ces maniĂšres sont matĂ©rialisĂ©es par l’énumĂ©ration.

enum ScanKind {
    /// A scan that reads all rows in the table (Full Table Scan).
    FullScan(FullScan),
    /// A scan that retrieves rows by their primary key values.
    ByPk(ScanByPk),
    /// A scan that retrieves rows using a secondary index.
    ByIndex(ScanByIndex),
}

Ces variantes ont elle-mĂȘme des structures comme valeur, car nous viendrons y accrocher des comportements dans de prochains articles.

/// Represents a scan operation that retrieves rows by their primary key values.
///
/// # Fields
/// - `primary_key`: The primary key columns used for the scan.
/// - `values`: The set of primary key values used to fetch the matching rows.
#[derive(Debug, PartialEq)]
pub struct ScanByPk {
    pub(crate) primary_key: PrimaryKey,
    pub(crate) values: Vec<Value>,
}

/// Represents a scan operation using a secondary index.
///
/// # Fields
/// - `index`: The columns that are part of the index.
/// - `name`: The name of the index being used.
/// - `values`: The values used to search the index.
/// - `uniqueness`: Indicates if the index enforces uniqueness (`Unique` or `NonUnique`).
struct ScanByIndex {
    index: Vec<String>,
    name: String,
    values: Vec<Value>,
    uniqueness: Uniqueness,
}

Détermination de la méthode de scan

Pour cela, nous allons devoir faire le mĂȘme chemin mental que lors de la premiĂšre partie de cet article.

La question que l’on se pose est : est-ce que l’expression associĂ©e Ă  la sĂ©lection correspond Ă  la dĂ©finition d’un index ou pas?

Le boulot de la mĂ©thode expression_from_index_definition_to_tuple est de rĂ©aliser la premiĂšre Ă©tape qui consiste Ă  transformer par rapport Ă  une expression, une potentielle dĂ©finition d’index.

Si cette dĂ©finition d’index est compatible alors il est possible de construire sur cette expression une clef d’index.

/// From index column names and an [Expression]
/// creates a tuple only if the expression is
/// compatible to the index definition
pub fn expression_from_index_definition_to_tuple(
    index: &[String],
    expression: &Expression,
) -> Result<Option<Vec<Value>>, QueryError> {
    let index = index.iter().map(|s| s.as_str()).collect::<Vec<&str>>();
    // Create the map that will accumulate the association between the
    // index column name and the value in the expression
    // for expression : col1 = val1 AND col2 > val2 AND col3 = val3 AND col4 = val4
    // for index : (col1, col3)
    // gives the map:
    //      col1 => val1
    //      col3 => val3
    //      col4 => val4
    let mut columns = HashMap::new();
    deconstruct_to_index_fields(expression, &mut columns);

    let columns_names: HashSet<&&str> = HashSet::from_iter(columns.keys());
    // Build a set out of the index definition
    let index_fields = HashSet::from_iter(&index);

    // if column_names is a superset of index_fields
    // then all index definition can be found in the expression
    if columns_names.is_superset(&index_fields) {
        let mut values = vec![];
        for column_name in index {
            if let Some(ColumnExpression { value, .. }) = columns.get(column_name) {
                // accumulate values in the right order
                values.push(value.clone());
            }
        }
        // giving in our example the tuple (val1, val3)
        Ok(Some(values))
    } else {
        // if it's not a superset then the index isn't compatible with the expression
        Ok(None)
    }
}

Attention

Cette méthode est naïve et comporte des bugs connus, mais est une base suffisante pour débuter.

Pour l’aider dans sa tñche, elle peut compter sur

fn deconstruct_to_index_fields<'a>(
    expression: &'a Expression,
    acc: &mut HashMap<&'a str, &'a ColumnExpression>,
) {
    match expression {
        // nous nous limitons aux expressions en AND
        Expression::Logical(LogicalExpression { lhs, operator, rhs }) => {
            if operator == &LogicalOperator::And {
                deconstruct_to_index_fields(lhs, acc);
                deconstruct_to_index_fields(rhs, acc);
            }
        }
        // si c'est une colonne, on peut extraire le nom
        Expression::Column(column) => {
            let ColumnExpression {
                column: column_name,
                operator,
                ..
            } = column;
            // pour ĂȘtre une dĂ©finition d'index, la valeur doit ĂȘtre Ă©gale
            if operator == &BinaryOperator::Equal {
                acc.insert(column_name.0.as_str(), column);
            }
        }
    }
}

Qui va récursivement décomposer notre Expression afin de déterminer si une expression logique existe et si oui la décomposer en map de colonne_name <=> colonne.

Comme une table peut avoir autant d’index secondaire que l’on veut et que ceux-ci peuvent avoir la dĂ©finition qu’ils dĂ©sirent. Il faut que l’IndexRegistry associĂ© Ă  la table soit en mesure de dĂ©terminer si l’expression actuelle est compatible avec un de ses index ou non.

Pour cela, celui-ci va boucler sur ses index à la recherche du premier qui possÚde les critÚres adéquats

impl IndexRegistry {
    fn search_index<I: Indexable>(
        &self,
        expression: &Expression,
        indexes: &BTreeMap<Vec<String>, I>,
        uniqueness: Uniqueness,
    ) -> Result<Option<ScanKind>, QueryError> {
        for (index_columns, index) in indexes.iter() {
            if let Some(index_values) =
                expression_from_index_definition_to_tuple(index_columns, expression)?
            {
                return Ok(Some(ScanKind::ByIndex(ScanByIndex::new(
                    index_columns.clone(),
                    index.name().to_string(),
                    index_values,
                    uniqueness,
                ))));
            }
        }

        Ok(None)
    }
}

Comme il existe deux types d’index, on se créé une fonction qui va rechercher dans les deux groupes d’index.

impl IndexRegistry {
    pub fn get_index(&self, expression: &Expression) -> Result<Option<ScanKind>, QueryError> {
        if let Some(index) =
            self.search_index(expression, &self.unique_indexes, Uniqueness::Unique)?
        {
            return Ok(Some(index));
        }

        if let Some(index) = self.search_index(expression, &self.indexes, Uniqueness::NonUnique)? {
            return Ok(Some(index));
        }

        Ok(None)
    }
}

On s’assure de taper les index uniques en premier, car ce sont ceux qui renvoient le moins de rĂ©sultats, 1 seul pour ĂȘtre plus prĂ©cis.

Une fois cela fait on peut alors se construire une mĂ©thode qui va ĂȘtre en musure de trouver la mĂ©thode de lecture de la DB la plus efficace, compte tenu de l’expression.

/// Determines the most efficient scan method for a query based on the provided table
/// and expression. It evaluates whether the query can utilize primary keys or indexes
/// for optimized access to the table.
///
/// # Arguments
///
/// * `table` - A reference to the `Table` for which the scan method needs to be determined.
/// * `expression` - The query `Expression` that defines conditions for accessing table data.
///
/// # Returns
///
/// A result containing the `ScanKind` variant that represents the scan method:
///
/// - `ScanKind::ByPk` if the query can make use of the table's primary key.
/// - An indexed scan method if the query matches one of the table's defined indexes.
/// - `ScanKind::FullScan` if no specific scan method is applicable.
///
/// Returns a `QueryError` if any issue arises during the determination of the scan kind,
/// such as invalid expressions or index lookup failures.
fn get_scan_method<'a>(
    table: &'a Table,
    expression: &'a Expression,
) -> Result<ScanKind, QueryError> {
    if let Some(pk) =
        expression_from_index_definition_to_tuple(&table.schema.primary_key, expression)?
    {
        return Ok(ScanKind::ByPk(ScanByPk::new(expression.clone(), pk)));
    }

    if let Some(scan) = table.indexes.get_index(expression)? {
        return Ok(scan);
    }

    Ok(ScanKind::FullScan(FullScan))
}

Construction du Plan

Nous allons définir un QueryPlanner son travail est de venir ajouter les étapes au plan qui permettent de rechercher de la données dans une table.

/// The `QueryPlanner` struct is responsible for generating query execution plans
/// based on the database, table name, and an optional filter expression.
///
/// # Fields
/// - `database`: A reference to the `Database` that contains the tables being queried.
/// - `table_name`: The name of the table to execute the query against.
/// - `expression`: An optional filter expression to use for query optimization
///   or filtering the results.
pub struct QueryPlanner<'a> {
    /// The database object containing all tables and their metadata.
    database: &'a Database,
    /// The name of the table on which the query operation is being planned.
    table_name: &'a str,
    /// An optional filter expression that determines which rows are included in the query.
    expression: Option<&'a Expression>,
}

Sa méthode plan à pour travail de faire ces opérations

impl QueryPlanner<'_> {
    pub fn plan(&self, plan: &mut Plan) -> Result<(), QueryError> {
        // on détermine s'il y a besoin de filtrer les résultats
        let expression = if let Some(expression) = self.expression {
            expression
        } else {
            // si ce n'est pas le cas, on réalise un fullscan
            plan.add_step(PlanStep::Scan {
                kind: ScanKind::FullScan(FullScan),
                table_name: self.table_name.to_string(),
            });
            return Ok(());
        };

        let table = self
            .database
            .get_table(self.table_name)
            .ok_or(QueryError::TableNotExist(self.table_name.to_string()))?;

        // en fonction de l'expression, on choisit la méthode de scan
        // la plus appropriée, les index ou le fullscan
        let scan_kind = get_scan_method(table, expression)?;

        // on ajoute une étape de scan
        plan.add_step(PlanStep::Scan {
            kind: scan_kind,
            table_name: self.table_name.to_string(),
        });
        // on ajoute l'étape de filtration des résultats
        plan.add_step(PlanStep::Filter {
            expression: expression.clone(),
        });

        Ok(())
    }
}

Modification du ExecuteResult

On modifie le ExecuteResult pour qu’il soit en mesure d’afficher le Plan que l’on avait mis en attente sous la forme d’un Vec<String> dans l’article prĂ©cĂ©dent.

enum ExecuteResult {
    Nil,
    Tuples(Vec<Vec<Value>>),
    Explain(Plan),
}

Comme Plan implémente Display, il est possible lui faire afficher les étapes au travers de la méthode run principale.

pub fn run() -> Result<(), Box<dyn Error>> {
    let mut database = database::Database::new();
    loop {
        print!("db > ");
        std::io::stdout().flush()?;
        let mut command = String::new();
        std::io::stdin().read_line(&mut command)?;
        let command = command.trim();

        match database.run(command) {
            Ok(ExecuteResult::Tuples(rows)) => {
                for row in rows {
                    println!("{:?}", row);
                }
            }
            Ok(ExecuteResult::Explain(plan)) => {
                println!("{}", plan);
            }
            Ok(ExecuteResult::Nil) => {}
            Err(err) => {
                println!("{}", err);
            }
        }
    }
}

Intégration du QueryPlanner

Nous allons enfin pouvoir utiliser le flag venant de la directive EXPLAIN.

impl Database {
    pub fn select(
        &mut self,
        table_name: String,
        where_clause: Option<WhereClause>,
        explain: bool,
    ) -> Result<ExecuteResult, SelectError> {
        // si on est en dry run
        if explain {
            let expression = where_clause
                .as_ref()
                .map(|where_clause| &where_clause.expression);
            let query_planner = QueryPlanner::new(self, &table_name, expression);
            // on créé un plan vide
            let mut plan = Plan::new();
            query_planner.plan(&mut plan).map_err(SelectError::Query)?;
            // on renvoie un explain
            Ok(ExecuteResult::Explain(plan))
        } else {
            // le code précédent
            match self.tables.get(&table_name) {
                Some(table) => table.select(where_clause),
                None => Err(SelectError::TableNotExist(table_name))?,
            }
        }
    }
}

Je ne vais pas les recopier ici, mais ci-joint, voici les divers tests qui s’assure du bon fonctionnement de l’ensemble.

On peut enfin jouer avec !

Reprenons notre exemple du début.

Au travers de la table:

CREATE TABLE Client (
    id INTEGER PRIMARY KEY, 
    nom TEXT(50),
    prénom Text(50), 
    genre TEXT(2),
    ville Text(100)
);

Muni des index suivants :

CREATE UNIQUE INDEX idx_identité ON Client(nom, prénom);
CREATE INDEX idx_ville ON Client(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');

Il est alors possible de lui demander ce qu’il compte faire.

EXPLAIN SELECT * FROM Client  WHERE ville = 'Tokyo';

Il va alors vous dire qu’il connait index secondaire idx_ville qui est apte Ă  donner la bonne rĂ©ponse sans tout scanner.

Scan secondary index idx_ville : ville = 'Tokyo' for table Client
Filter: ville = 'Tokyo'

Mais rajoute tout de mĂȘme l’étape de filtration, car il ne sait pas si c’est exactement la donnĂ©e attendue, en tout cas les clients sont assurĂ©s d’habiter Tokyo.

On pourrait par exemple avoir:

SELECT * FROM Client  WHERE ville = 'Paris' AND gender = 'F';

Sans filtrage, on aurait alors 2 entrĂ©es provenant directement de l’index

[Integer(1), Text("Smith"), Text("John"), Text("M"), Text("Paris")]
[Integer(6), Text("Yamamoto"), Text("Sakura (さくら)"), Text("F"), Text("Paris")]

Or nous ne voulons que les femmes, c’est pour cela que le filter vient rajouter la contrainte supplĂ©mentaire.

db > EXPLAIN SELECT * FROM Client  WHERE ville = 'Paris' AND gender = 'F';
Scan secondary index idx_ville : ville = 'Paris' for table Client
Filter: gender = 'F' AND ville = 'Paris'

De sorte Ă  ce que le rĂ©sultat soit celui que l’on attend bien.

Pour les clefs primaires, c’est le mĂȘme fonctionnement.

db > EXPLAIN SELECT * FROM Client  WHERE id = 7 and ville = 'Paris'; 
PK direct access : id = 7 for table Client
Filter: ville = 'Paris' AND id = 7

Et si la donnĂ©es n’est indexĂ©e nulle part, alors on se rabat sur le coĂ»teux fullscan.

db > EXPLAIN SELECT * FROM Client WHERE genre = 'F';                
Full scan for table Client
Filter: genre = 'F'

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

Nous venons de dĂ©finir un gros morceau de ce qui fait l’ingĂ©niositĂ© d’une base de donnĂ©es, son LogicalPlan.

Conclusion

Cette fois-ci nous sommes en mesure de savoir ce que le QueryEngine pense faire de notre requĂȘte.

Il n’est pas parfait et possĂšde nombreuses failles algorithmiques, mais c’est une bonne base de rĂ©flexion pour itĂ©rer par-dessus.

Dans la prochaine partie, nous allons utiliser ce LogicalPlan pour en faire un plan d’éxĂ©cution qui va rĂ©ellement aller rĂ©aliser les opĂ©rations de recherches dans les index et de scan de tables.

Merci de votre lecture ❀

Vous pouvez trouver le code la partie ici et le diff lĂ .