
La collecte, le stockage, la conversion et la présentation des données sont les principaux défis auxquels sont confrontés les ingénieurs de données. Le département Business Intelligence Badoo reçoit et traite plus de 20 milliards d'événements envoyés depuis les appareils des utilisateurs par jour, soit 2 To de données entrantes.
L'étude et l'interprétation de toutes ces données ne sont pas toujours une tùche triviale, il devient parfois nécessaire d'aller au-delà des capacités des bases de données toutes faites. Et si vous avez le courage et décidé de faire quelque chose de nouveau, vous devez d'abord vous familiariser avec les principes de fonctionnement des solutions existantes.
En un mot, dĂ©veloppeurs curieux et forts d'esprit, cet article est adressĂ©. Vous y trouverez une description du modĂšle traditionnel d'exĂ©cution de requĂȘtes dans des bases de donnĂ©es relationnelles en utilisant le langage de dĂ©monstration PigletQL comme exemple.
Table des matiĂšres
Contexte
Notre groupe d'ingénieurs est engagé dans les backends et les interfaces, offrant des opportunités d'analyse et de recherche de données au sein de l'entreprise (en passant, nous nous développons ). Nos outils standard sont une base de données distribuée de dizaines de serveurs (Exasol) et un cluster Hadoop pour des centaines de machines (Hive et Presto).
La plupart des requĂȘtes vers ces bases de donnĂ©es sont analytiques, c'est-Ă -dire affectant des centaines de milliers Ă des milliards d'enregistrements. Leur exĂ©cution prend des minutes, des dizaines de minutes voire des heures, selon la solution utilisĂ©e et la complexitĂ© de la demande. Avec le travail manuel de l'utilisateur-analyste, ce temps est considĂ©rĂ© comme acceptable, mais ne convient pas Ă la recherche interactive via l'interface utilisateur.
Au fil du temps, nous avons mis en Ă©vidence les requĂȘtes analytiques populaires et les requĂȘtes, qui sont difficiles Ă dĂ©finir en termes de SQL, et avons dĂ©veloppĂ© pour elles de petites bases de donnĂ©es spĂ©cialisĂ©es. Ils stockent un sous-ensemble de donnĂ©es dans un format adaptĂ© aux algorithmes de compression lĂ©gers (par exemple, streamvbyte), ce qui vous permet de stocker des donnĂ©es sur une seule machine pendant plusieurs jours et d'exĂ©cuter des requĂȘtes en quelques secondes.
Les premiĂšres langues de requĂȘte pour ces donnĂ©es et leurs interprĂštes ont Ă©tĂ© implĂ©mentĂ©es sur une intuition, nous avons dĂ» les affiner constamment, et chaque fois cela prenait un temps inacceptable.
Les langages de requĂȘte n'Ă©taient pas suffisamment flexibles, bien qu'il n'y ait aucune raison Ă©vidente de limiter leurs capacitĂ©s. En consĂ©quence, nous nous sommes tournĂ©s vers l'expĂ©rience des dĂ©veloppeurs d'interprĂštes SQL, grĂące auxquels nous avons pu rĂ©soudre partiellement les problĂšmes qui se sont posĂ©s.
Ci-dessous, je parlerai du modĂšle d'exĂ©cution de requĂȘte le plus courant dans les bases de donnĂ©es relationnelles - Volcano. Le code source de l'interprĂ©teur du dialecte SQL primitif, PigletQL , est joint Ă l'article , de sorte que toutes les personnes intĂ©ressĂ©es peuvent facilement se familiariser avec les dĂ©tails du rĂ©fĂ©rentiel.
Structure d'interpréteur SQL

Les bases de donnĂ©es les plus populaires fournissent une interface aux donnĂ©es sous la forme d'un langage de requĂȘte SQL dĂ©claratif. Une requĂȘte sous la forme d'une chaĂźne est convertie par l'analyseur en une description de la requĂȘte, semblable Ă une arborescence de syntaxe abstraite. Il est possible d'exĂ©cuter des requĂȘtes simples dĂ©jĂ Ă ce stade, cependant, pour optimiser les transformations et l'exĂ©cution ultĂ©rieure, cette reprĂ©sentation n'est pas pratique. Dans les bases de donnĂ©es que je connais, des reprĂ©sentations intermĂ©diaires sont introduites Ă ces fins.
L'algĂšbre relationnelle est devenue un modĂšle de reprĂ©sentations intermĂ©diaires. Il s'agit d'un langage dans lequel les transformations ( opĂ©rateurs ) effectuĂ©es sur les donnĂ©es sont explicitement dĂ©crites: sĂ©lection d'un sous-ensemble de donnĂ©es selon un prĂ©dicat, combinaison de donnĂ©es provenant de diffĂ©rentes sources, etc. De plus, l'algĂšbre relationnelle est une algĂšbre au sens mathĂ©matique, c'est-Ă -dire un grand nombre d'Ă©quivalents transformations. Par consĂ©quent, il est pratique d'effectuer des transformations d'optimisation sur une requĂȘte sous la forme d'un arbre d'opĂ©rateurs d'algĂšbre relationnelle.
Il existe des différences importantes entre les représentations internes dans les bases de données et l'algÚbre relationnelle d'origine, il est donc plus correct de l'appeler algÚbre logique .
La vĂ©rification de la validitĂ© d'une requĂȘte est gĂ©nĂ©ralement effectuĂ©e lors de la compilation de la reprĂ©sentation initiale de la requĂȘte en opĂ©rateurs d'algĂšbre logique et correspond au stade de l'analyse sĂ©mantique dans les compilateurs conventionnels. Le rĂŽle de la table des symboles dans les bases de donnĂ©es est jouĂ© par le rĂ©pertoire de base de donnĂ©es , qui stocke des informations sur le schĂ©ma et les mĂ©tadonnĂ©es de la base de donnĂ©es: tables, colonnes de table, index, droits d'utilisateur, etc.
Par rapport aux interprĂštes Ă usage gĂ©nĂ©ral, les interprĂštes de base de donnĂ©es ont une particularitĂ© de plus: les diffĂ©rences de volume de donnĂ©es et les mĂ©ta-informations sur les donnĂ©es auxquelles les requĂȘtes sont censĂ©es ĂȘtre adressĂ©es. Dans les tables ou les relations en termes d'algĂšbre relationnelle, il peut y avoir une quantitĂ© diffĂ©rente de donnĂ©es, sur certaines colonnes ( attributs de relation) des index peuvent ĂȘtre construits, etc. Autrement dit, selon le schĂ©ma de la base de donnĂ©es et la quantitĂ© de donnĂ©es dans les tables, la requĂȘte doit ĂȘtre effectuĂ©e par diffĂ©rents algorithmes et utilisez-les dans un ordre diffĂ©rent.
Pour rĂ©soudre ce problĂšme, une autre reprĂ©sentation intermĂ©diaire est introduite - l'algĂšbre physique . Selon la disponibilitĂ© des index sur les colonnes, la quantitĂ© de donnĂ©es dans les tables et la structure de l'arbre d'algĂšbre logique, diffĂ©rentes formes de l'arbre d'algĂšbre physique sont proposĂ©es, parmi lesquelles la meilleure option est choisie. C'est cet arbre qui est affichĂ© dans la base de donnĂ©es sous forme de plan de requĂȘte. Dans les compilateurs conventionnels, cette Ă©tape correspond conditionnellement aux Ă©tapes d'allocation de registre, de planification et de sĂ©lection d'instructions.
La derniÚre étape du travail de l'interprÚte est directement l'exécution de l'arbre d'opérateurs d'algÚbre physique.
ModĂšle de volcan et exĂ©cution de requĂȘte
Les interprÚtes physiques d'algÚbre ont toujours été utilisés dans des bases de données commerciales fermées, mais la littérature académique se réfÚre généralement à l'optimiseur expérimental Volcano, développé au début des années 90.
Dans le modÚle du volcan, chaque opérateur d'un arbre d'algÚbre physique se transforme en une structure à trois fonctions: ouvrir, ensuite, fermer. En plus des fonctions, l'opérateur contient un état de fonctionnement - état. La fonction open initie l'état de l'instruction, la fonction suivante retourne soit le tuple suivant (tuple anglais), soit NULL, s'il n'y a plus de tuples, la fonction close termine l'instruction:

Les opĂ©rateurs peuvent ĂȘtre imbriquĂ©s pour former un arbre d'opĂ©rateurs d'algĂšbre physique. Ainsi, chaque opĂ©rateur itĂšre sur les tuples d'une relation existant sur un support rĂ©el ou d'une relation virtuelle formĂ©e en Ă©numĂ©rant les tuples d'opĂ©rateurs imbriquĂ©s:

En termes de langages modernes de haut niveau, l'arborescence de ces opérateurs est une cascade d'itérateurs.
MĂȘme les interprĂ©teurs de requĂȘtes industriels dans les SGBD relationnels sont repoussĂ©s du modĂšle Volcano, c'est pourquoi je l'ai pris comme base de l'interprĂ©teur PigletQL.
PigletQL

Pour dĂ©montrer le modĂšle, j'ai dĂ©veloppĂ© l'interprĂ©teur du langage de requĂȘte limitĂ© PigletQL . Il est Ă©crit en C, prend en charge la crĂ©ation de tables dans le style de SQL, mais est limitĂ© Ă un seul type - entiers positifs 32 bits. Toutes les tables sont en mĂ©moire. Le systĂšme fonctionne dans un seul thread et n'a pas de mĂ©canisme de transaction.
Il n'y a pas d'optimiseur dans PigletQL et les requĂȘtes SELECT sont compilĂ©es directement dans l'arborescence d'opĂ©rateurs d'algĂšbre physique. Les requĂȘtes restantes (CREATE TABLE et INSERT) fonctionnent directement Ă partir des vues internes principales.
Exemple de session utilisateur dans PigletQL:
> ./pigletql > CREATE TABLE tab1 (col1,col2,col3); > INSERT INTO tab1 VALUES (1,2,3); > INSERT INTO tab1 VALUES (4,5,6); > SELECT col1,col2,col3 FROM tab1; col1 col2 col3 1 2 3 4 5 6 rows: 2 > SELECT col1 FROM tab1 ORDER BY col1 DESC; col1 4 1 rows: 2
Lexique et analyseur
PigletQL est un langage trĂšs simple, et sa mise en Ćuvre n'Ă©tait pas requise aux Ă©tapes de l'analyse lexicale et d'analyse.
L'analyseur lexical est Ă©crit Ă la main. Un objet analyseur ( scanner_t ) est créé Ă partir de la chaĂźne de requĂȘte, qui distribue les jetons un par un:
scanner_t *scanner_create(const char *string); void scanner_destroy(scanner_t *scanner); token_t scanner_next(scanner_t *scanner);
L'analyse est effectuĂ©e Ă l'aide de la mĂ©thode de descente rĂ©cursive. Tout d'abord, l'objet parser_t est créé qui, aprĂšs avoir reçu l'analyseur lexical (scanner_t), remplit l'objet query_t avec des informations sur la requĂȘte:
query_t *query_create(void); void query_destroy(query_t *query); parser_t *parser_create(void); void parser_destroy(parser_t *parser); bool parser_parse(parser_t *parser, scanner_t *scanner, query_t *query);
Le rĂ©sultat de l'analyse dans query_t est l'un des trois types de requĂȘte pris en charge par PigletQL:
typedef enum query_tag { QUERY_SELECT, QUERY_CREATE_TABLE, QUERY_INSERT, } query_tag; typedef struct query_t { query_tag tag; union { query_select_t select; query_create_table_t create_table; query_insert_t insert; } as; } query_t;
Le type de requĂȘte le plus complexe dans PigletQL est SELECT. Il correspond Ă la structure de donnĂ©es query_select_t :
typedef struct query_select_t { attr_name_t attr_names[MAX_ATTR_NUM]; uint16_t attr_num; rel_name_t rel_names[MAX_REL_NUM]; uint16_t rel_num; query_predicate_t predicates[MAX_PRED_NUM]; uint16_t pred_num; bool has_order; attr_name_t order_by_attr; sort_order_t order_type; } query_select_t;
La structure contient une description de la requĂȘte (un tableau d'attributs demandĂ©s par l'utilisateur), une liste de sources de donnĂ©es - relations, un tableau de prĂ©dicats filtrant les tuples et des informations sur l'attribut utilisĂ© pour trier les rĂ©sultats.
Analyseur sémantique
La phase d'analyse sĂ©mantique en SQL standard implique la vĂ©rification de l'existence des tables et des colonnes rĂ©pertoriĂ©es dans les tables et la vĂ©rification des types dans les expressions de requĂȘte. Pour les vĂ©rifications liĂ©es aux tables et aux colonnes, le rĂ©pertoire de la base de donnĂ©es est utilisĂ©, oĂč toutes les informations sur la structure des donnĂ©es sont stockĂ©es.
Il n'y a pas d'expressions complexes dans PigletQL, donc la vĂ©rification des requĂȘtes se rĂ©duit Ă la vĂ©rification des mĂ©tadonnĂ©es de catalogue des tables et des colonnes. Les requĂȘtes SELECT, par exemple, sont validĂ©es par la fonction validate_select . Je vais l'apporter sous forme abrĂ©gĂ©e:
static bool validate_select(catalogue_t *cat, const query_select_t *query) { for (size_t rel_i = 0; rel_i < query->rel_num; rel_i++) { if (catalogue_get_relation(cat, query->rel_names[rel_i])) continue; fprintf(stderr, "Error: relation '%s' does not exist\n", query->rel_names[rel_i]); return false; } if (!rel_names_unique(query->rel_names, query->rel_num)) return false; if (!attr_names_unique(query->attr_names, query->attr_num)) return false; return true; }
Si la demande est valide, l'étape suivante consiste à compiler l'arborescence d'analyse en une arborescence d'opérateurs.
Compilation de requĂȘtes dans une vue intermĂ©diaire

Dans les interpréteurs SQL à part entiÚre, il existe généralement deux représentations intermédiaires: l'algÚbre logique et physique.
Un interprĂ©teur PigletQL simple exĂ©cute les requĂȘtes CREATE TABLE et INSERT directement Ă partir de ses arbres d'analyse, c'est-Ă - dire les structures query_create_table_t et query_insert_t . Les requĂȘtes SELECT plus complexes sont compilĂ©es en une seule reprĂ©sentation intermĂ©diaire, qui sera exĂ©cutĂ©e par l'interprĂ©teur.
L'arbre des opérateurs est construit à partir des feuilles jusqu'à la racine dans la séquence suivante:
Dans la partie droite de la requĂȘte ("... FROM relation1, relation2, ..."), les noms des relations souhaitĂ©es sont obtenus, pour chacun desquels une instruction scan est créée.
En extrayant les tuples des relations, les opérateurs de scan sont combinés en un arbre binaire à gauche via l'opérateur de jointure.
Les attributs demandés par l'utilisateur ("SELECT attr1, attr2, ...") sont sélectionnés par l'énoncé de projet.
Si des prédicats sont spécifiés ("... WHERE a = 1 AND b> 10 ..."), alors l'instruction select est ajoutée à l'arborescence ci-dessus.
Si la méthode de tri du résultat est spécifiée ("... ORDER BY attr1 DESC"), alors l'opérateur de tri est ajouté en haut de l'arborescence.
Compilation en code PigletQL:
operator_t *compile_select(catalogue_t *cat, const query_select_t *query) { operator_t *root_op = NULL; { size_t rel_i = 0; relation_t *rel = catalogue_get_relation(cat, query->rel_names[rel_i]); root_op = scan_op_create(rel); rel_i += 1; for (; rel_i < query->rel_num; rel_i++) { rel = catalogue_get_relation(cat, query->rel_names[rel_i]); operator_t *scan_op = scan_op_create(rel); root_op = join_op_create(root_op, scan_op); } } root_op = proj_op_create(root_op, query->attr_names, query->attr_num); if (query->pred_num > 0) { operator_t *select_op = select_op_create(root_op); for (size_t pred_i = 0; pred_i < query->pred_num; pred_i++) { query_predicate_t predicate = query->predicates[pred_i]; } root_op = select_op; } if (query->has_order) root_op = sort_op_create(root_op, query->order_by_attr, query->order_type); return root_op; }
Une fois l'arbre formé, des transformations d'optimisation sont généralement effectuées, mais PigletQL passe immédiatement à l'étape d'exécution de la représentation intermédiaire.
Réalisation d'une présentation intermédiaire

Le modÚle Volcano implique une interface pour travailler avec les opérateurs à travers trois opérations communes d'ouverture / de fermeture / de fermeture. Essentiellement, chaque instruction Volcano est un itérateur à partir duquel les tuples sont «tirés» un par un, donc cette approche de l'exécution est également appelée modÚle pull.
Chacun de ces itĂ©rateurs peut lui-mĂȘme appeler les mĂȘmes fonctions que les itĂ©rateurs imbriquĂ©s, crĂ©er des tables temporaires avec des rĂ©sultats intermĂ©diaires et convertir les tuples entrants.
ExĂ©cution de requĂȘtes SELECT dans PigletQL:
bool eval_select(catalogue_t *cat, const query_select_t *query) { operator_t *root_op = compile_select(cat, query); { root_op->open(root_op->state); size_t tuples_received = 0; tuple_t *tuple = NULL; while((tuple = root_op->next(root_op->state))) { if (tuples_received == 0) dump_tuple_header(tuple); dump_tuple(tuple); tuples_received++; } printf("rows: %zu\n", tuples_received); root_op->close(root_op->state); } root_op->destroy(root_op); return true; }
La requĂȘte est d'abord compilĂ©e par la fonction compile_select, qui renvoie la racine de l'arborescence des opĂ©rateurs, aprĂšs quoi les mĂȘmes fonctions d'ouverture / suivante / de fermeture sont appelĂ©es sur l'opĂ©rateur racine. Chaque appel Ă next renvoie soit le tuple suivant soit NULL. Dans ce dernier cas, cela signifie que tous les tuples ont Ă©tĂ© extraits et que la fonction d'itĂ©rateur de fermeture doit ĂȘtre appelĂ©e.
Les tuples résultants sont recalculés et sortis par la table dans le flux de sortie standard.
Les opérateurs
La chose la plus intéressante à propos de PigletQL est l'arborescence des opérateurs. Je vais montrer l'appareil de certains d'entre eux.
Les opérateurs ont une interface commune et se composent de pointeurs vers la fonction d'ouverture / suivante / de fermeture et d'une fonction de destruction supplémentaire, qui libÚre les ressources de l'arborescence entiÚre de l'opérateur à la fois:
typedef void (*op_open)(void *state); typedef tuple_t *(*op_next)(void *state); typedef void (*op_close)(void *state); typedef void (*op_destroy)(operator_t *op); struct operator_t { op_open open; op_next next; op_close close; op_destroy destroy; void *state; } ;
En plus des fonctions, l'opérateur peut contenir un état interne arbitraire (pointeur d'état).
Ci-dessous j'analyserai le dispositif de deux opérateurs intéressants: le scan le plus simple et la création d'un tri de relation intermédiaire.
Instruction de scan
L'instruction qui dĂ©marre une requĂȘte est scan. Il passe en revue tous les tuples de la relation. L'Ă©tat interne de l'analyse est un pointeur sur la relation d'oĂč les tuples seront rĂ©cupĂ©rĂ©s, l'index du tuple suivant dans la relation et une structure de lien vers le tuple actuel transmis Ă l'utilisateur:
typedef struct scan_op_state_t { const relation_t *relation; uint32_t next_tuple_i; tuple_t current_tuple; } scan_op_state_t;
Pour créer l'état d'une instruction d'analyse, vous avez besoin d'une relation source; tout le reste (pointeurs vers les fonctions correspondantes) est déjà connu:
operator_t *scan_op_create(const relation_t *relation) { operator_t *op = calloc(1, sizeof(*op)); assert(op); *op = (operator_t) { .open = scan_op_open, .next = scan_op_next, .close = scan_op_close, .destroy = scan_op_destroy, }; scan_op_state_t *state = calloc(1, sizeof(*state)); assert(state); *state = (scan_op_state_t) { .relation = relation, .next_tuple_i = 0, .current_tuple.tag = TUPLE_SOURCE, .current_tuple.as.source.tuple_i = 0, .current_tuple.as.source.relation = relation, }; op->state = state; return op; }
Opérations d'ouverture / fermeture dans le cas d'une réinitialisation de l'analyse renvoie au premier élément de la relation:
void scan_op_open(void *state) { scan_op_state_t *op_state = (typeof(op_state)) state; op_state->next_tuple_i = 0; tuple_t *current_tuple = &op_state->current_tuple; current_tuple->as.source.tuple_i = 0; } void scan_op_close(void *state) { scan_op_state_t *op_state = (typeof(op_state)) state; op_state->next_tuple_i = 0; tuple_t *current_tuple = &op_state->current_tuple; current_tuple->as.source.tuple_i = 0; }
L'appel suivant retourne soit le tuple suivant, soit NULL s'il n'y a plus de tuples dans la relation:
tuple_t *scan_op_next(void *state) { scan_op_state_t *op_state = (typeof(op_state)) state; if (op_state->next_tuple_i >= op_state->relation->tuple_num) return NULL; tuple_source_t *source_tuple = &op_state->current_tuple.as.source; source_tuple->tuple_i = op_state->next_tuple_i; op_state->next_tuple_i++; return &op_state->current_tuple; }
Instruction de tri
L'instruction sort produit des tuples dans l'ordre spécifié par l'utilisateur. Pour ce faire, créez une relation temporaire avec des tuples obtenus à partir d'opérateurs imbriqués et triez-la.
L'état interne de l' opérateur:
typedef struct sort_op_state_t { operator_t *source; attr_name_t sort_attr_name; sort_order_t sort_order; relation_t *tmp_relation; operator_t *tmp_relation_scan_op; } sort_op_state_t;
Le tri est effectué selon les attributs spécifiés dans la demande (sort_attr_name et sort_order) sur le rapport temporel (tmp_relation). Tout cela se produit lorsque la fonction open est appelée:
void sort_op_open(void *state) { sort_op_state_t *op_state = (typeof(op_state)) state; operator_t *source = op_state->source; source->open(source->state); tuple_t *tuple = NULL; while((tuple = source->next(source->state))) { if (!op_state->tmp_relation) { op_state->tmp_relation = relation_create_for_tuple(tuple); assert(op_state->tmp_relation); op_state->tmp_relation_scan_op = scan_op_create(op_state->tmp_relation); } relation_append_tuple(op_state->tmp_relation, tuple); } source->close(source->state); relation_order_by(op_state->tmp_relation, op_state->sort_attr_name, op_state->sort_order); op_state->tmp_relation_scan_op->open(op_state->tmp_relation_scan_op->state); }
L'énumération des éléments de la relation temporaire est effectuée par l'opérateur temporaire tmp_relation_scan_op:
tuple_t *sort_op_next(void *state) { sort_op_state_t *op_state = (typeof(op_state)) state; return op_state->tmp_relation_scan_op->next(op_state->tmp_relation_scan_op->state);; }
La relation temporaire est désallouée dans la fonction close:
void sort_op_close(void *state) { sort_op_state_t *op_state = (typeof(op_state)) state; if (op_state->tmp_relation) { op_state->tmp_relation_scan_op->close(op_state->tmp_relation_scan_op->state); scan_op_destroy(op_state->tmp_relation_scan_op); relation_destroy(op_state->tmp_relation); op_state->tmp_relation = NULL; } }
Ici, vous pouvez voir clairement pourquoi les opérations de tri sur des colonnes sans index peuvent prendre beaucoup de temps.
Exemples de travaux
Je vais donner quelques exemples de requĂȘtes PigletQL et les arbres d'algĂšbre physique correspondants.
L'exemple le plus simple oĂč tous les tuples d'une relation sont sĂ©lectionnĂ©s:
> ./pigletql > create table rel1 (a1,a2,a3); > insert into rel1 values (1,2,3); > insert into rel1 values (4,5,6); > select a1 from rel1; a1 1 4 rows: 2 >
Pour les requĂȘtes les plus simples, seules les tuples de rĂ©cupĂ©ration de la relation de scan sont utilisĂ©es et la sĂ©lection du seul attribut de projet Ă partir des tuples:

Choisir des tuples avec un prédicat:
> ./pigletql > create table rel1 (a1,a2,a3); > insert into rel1 values (1,2,3); > insert into rel1 values (4,5,6); > select a1 from rel1 where a1 > 3; a1 4 rows: 1 >
Les prédicats sont exprimés par l'instruction select:

Sélection de tuples avec tri:
> ./pigletql > create table rel1 (a1,a2,a3); > insert into rel1 values (1,2,3); > insert into rel1 values (4,5,6); > select a1 from rel1 order by a1 desc; a1 4 1 rows: 2
L'opérateur de tri de numérisation dans l'appel ouvert crée ( matérialise ) une relation temporaire, y place tous les tuples entrants et trie le tout. AprÚs cela, dans les prochains appels, il déduit les tuples de la relation temporaire dans l'ordre spécifié par l'utilisateur:

Combinaison de tuples de deux relations avec un prédicat:
> ./pigletql > create table rel1 (a1,a2,a3); > insert into rel1 values (1,2,3); > insert into rel1 values (4,5,6); > create table rel2 (a4,a5,a6); > insert into rel2 values (7,8,6); > insert into rel2 values (9,10,6); > select a1,a2,a3,a4,a5,a6 from rel1, rel2 where a3=a6; a1 a2 a3 a4 a5 a6 4 5 6 7 8 6 4 5 6 9 10 6 rows: 2
L'opérateur de jointure dans PigletQL n'utilise aucun algorithme complexe, mais forme simplement un produit cartésien à partir des ensembles de tuples des sous-arbres gauche et droit. C'est trÚs inefficace, mais pour un interprÚte de démonstration, cela fera:

Conclusions
En conclusion, je note que si vous faites un interprĂšte d'un langage similaire Ă SQL, alors vous devriez probablement prendre l'une des nombreuses bases de donnĂ©es relationnelles disponibles. Des milliers d'annĂ©es-personnes ont Ă©tĂ© investies dans des optimiseurs modernes et des interprĂštes de requĂȘtes de bases de donnĂ©es populaires, et il faut des annĂ©es pour dĂ©velopper mĂȘme les bases de donnĂ©es Ă usage gĂ©nĂ©ral les plus simples.
Le langage de dĂ©monstration PigletQL imite le travail de l'interprĂ©teur SQL, mais en rĂ©alitĂ© nous n'utilisons que des Ă©lĂ©ments individuels de l'architecture Volcano et uniquement pour ces (rares!) Types de requĂȘtes difficiles Ă exprimer dans le cadre du modĂšle relationnel.
NĂ©anmoins, je le rĂ©pĂšte: mĂȘme une connaissance superficielle de l'architecture de tels interprĂštes est utile dans les cas oĂč il est nĂ©cessaire de travailler de maniĂšre flexible avec les flux de donnĂ©es.
Littérature
Si vous ĂȘtes intĂ©ressĂ© par les problĂšmes de base du dĂ©veloppement de bases de donnĂ©es, alors les livres valent mieux que «ImplĂ©mentation du systĂšme de base de donnĂ©es» (Garcia-Molina H., Ullman JD, Widom J., 2000), vous ne trouverez pas.
Son seul inconvĂ©nient est une orientation thĂ©orique. Personnellement, j'aime quand des exemples concrets de code ou mĂȘme un projet de dĂ©monstration sont attachĂ©s au matĂ©riel. Pour cela, vous pouvez vous rĂ©fĂ©rer au livre «Conception et implĂ©mentation de base de donnĂ©es» (Sciore E., 2008), qui fournit le code complet d'une base de donnĂ©es relationnelle en Java.
Les bases de donnĂ©es relationnelles les plus populaires utilisent encore des variations sur le thĂšme du volcan. La publication originale est Ă©crite dans une langue trĂšs accessible et peut ĂȘtre facilement trouvĂ©e sur Google Scholar: "Volcano - un systĂšme d'Ă©valuation de requĂȘte extensible et parallĂšle" (Graefe G., 1994).
Bien que les interprĂštes SQL aient beaucoup changĂ© en dĂ©tail au cours des derniĂšres dĂ©cennies, la structure trĂšs gĂ©nĂ©rale de ces systĂšmes n'a pas changĂ© depuis trĂšs longtemps. Vous pouvez vous en faire une idĂ©e dans un article de synthĂšse du mĂȘme auteur, «Techniques d'Ă©valuation des requĂȘtes pour les grandes bases de donnĂ©es» (Graefe G. 1993).