La théorie des bases de données de Jeffrey Ullman derrière des requêtes rapides et évolutives
Comment les idées centrales de Jeffrey Ullman alimentent les bases modernes : algèbre relationnelle, règles d'optimisation, jointures et planification façon compilateur qui aident les systèmes à monter en charge.

Pourquoi Ullman compte pour le travail moderne sur les données
La plupart des gens qui écrivent du SQL, construisent des tableaux de bord ou optimisent une requête lente ont profité du travail de Jeffrey Ullman — même s'ils n'ont jamais entendu son nom. Ullman est un informaticien et pédagogue dont les recherches et les manuels ont aidé à définir comment les bases de données décrivent les données, raisonnent sur les requêtes et les exécutent efficacement.
L'influence discrète derrière les outils du quotidien
Quand un moteur de base de données transforme votre SQL en quelque chose qu'il peut exécuter rapidement, il s'appuie sur des idées à la fois précises et adaptables. Ullman a aidé à formaliser le sens des requêtes (pour que le système puisse les réécrire en toute sécurité) et à relier la pensée base de données à la pensée compilateur (pour qu'une requête soit analysée, optimisée et traduite en étapes exécutables).
Cette influence est discrète parce qu'elle n'apparaît pas comme un bouton dans votre outil BI ni comme une fonctionnalité visible dans votre console cloud. Elle se manifeste par :
- des requêtes qui s'exécutent plus vite après l'ajout d'un index ou la réécriture d'un
JOIN - des optimiseurs qui choisissent des plans différents à mesure que les données croissent
- des systèmes qui montent en charge sans changer le résultat renvoyé par votre requête
Ce que vous apprendrez dans cet article (sans surcharge mathématique)
Cet article utilise les idées centrales d'Ullman comme visite guidée des internals de base de données qui comptent le plus en pratique : comment l'algèbre relationnelle se cache sous le SQL, comment les réécritures de requêtes préservent le sens, pourquoi les optimiseurs fondés sur les coûts font les choix qu'ils font, et comment les algorithmes de jointure décident souvent si un travail se termine en secondes ou en heures.
Nous intégrerons aussi quelques concepts proches du compilateur — parsing, réécriture et planification — parce que les moteurs de base de données ressemblent davantage à des compilateurs sophistiqués que beaucoup ne le réalisent.
Une promesse rapide : nous garderons la discussion précise, mais éviterons les preuves mathématiques lourdes. L'objectif est de vous donner des modèles mentaux utilisables au travail la prochaine fois que la performance, la mise à l'échelle ou un comportement de requête déroutant apparaîtra.
Fondamentaux des bases de données qu'Ullman a aidé à cimenter
Si vous avez déjà écrit une requête SQL en supposant qu'elle « ne peut signifier qu'une chose », vous vous appuyez sur des idées qu'Ullman a contribué à populariser et formaliser : un modèle propre pour les données, plus des façons précises de décrire ce qu'une requête demande.
Le modèle relationnel en termes simples
Au cœur, le modèle relationnel traite les données comme des tables (relations). Chaque table a des lignes (tuples) et des colonnes (attributs). Cela semble maintenant évident, mais l'aspect important est la discipline que cela crée :
- Clés qui identifient les lignes. Une clé primaire est l'étiquette d'identification de chaque enregistrement.
- Relations qui relient les tables via des clés étrangères, vous permettant de garder les faits au bon endroit et de les référencer ailleurs.
Ce cadrage permet de raisonner sur la correction et la performance sans approximations. Quand vous savez ce qu'une table représente et comment les lignes sont identifiées, vous pouvez prédire ce que les jointures doivent faire, ce que signifient les doublons et pourquoi certains filtres modifient les résultats.
Algèbre relationnelle : une calculatrice pour les requêtes
L'enseignement d'Ullman utilise souvent l'algèbre relationnelle comme une sorte de calculatrice de requêtes : un petit ensemble d'opérateurs (select, project, join, union, difference) que l'on combine pour exprimer ce que l'on veut.
Pourquoi c'est utile avec le SQL : les bases de données traduisent le SQL en une forme algébrique puis le réécrivent dans une forme équivalente. Deux requêtes qui semblent différentes peuvent être algébriquement identiques — c'est ainsi que les optimiseurs peuvent réordonner des jointures, pousser des filtres ou supprimer du travail redondant tout en conservant le sens.
Algèbre vs calcul (niveau élevé)
- L'algèbre relationnelle est plus un « comment » : une séquence d'opérations pour calculer le résultat.
- Le calcul relationnel est plus un « quoi » : une description du résultat souhaité.
Le SQL est en grande partie du « quoi », mais les moteurs optimisent souvent en utilisant l'algèbre comme « comment ».
Les fondations valent mieux que la mémorisation d'un dialecte
Les dialectes SQL varient (Postgres vs Snowflake vs MySQL), mais les fondamentaux tiennent. Comprendre les clés, les relations et l'équivalence algébrique vous aide à repérer quand une requête est logiquement incorrecte, quand elle est simplement lente, et quelles modifications préservent le sens entre plateformes.
Algèbre relationnelle : le langage caché sous SQL
L'algèbre relationnelle est la « mathématique sous-jacente » du SQL : un petit ensemble d'opérateurs qui décrivent le résultat souhaité. Le travail d'Ullman a contribué à rendre cette vision opératoire claire et enseignable — et c'est toujours le modèle mental que la plupart des optimiseurs utilisent.
Les opérateurs de base (et ce qu'ils signifient)
Une requête peut s'exprimer comme un pipeline de quelques blocs de construction :
- Select (σ) : filtrer les lignes (l'idée du
WHEREen SQL) - Project (π) : conserver des colonnes spécifiques (l'idée de
SELECT col1, col2en SQL) - Join (⋈) : combiner des tables selon une condition (
JOIN ... ON ...) - Union (∪) : empiler des résultats ayant la même forme (
UNION) - Difference (−) : lignes dans A mais pas dans B (comme
EXCEPTdans plusieurs dialectes SQL)
Parce que l'ensemble est petit, il devient plus facile de raisonner sur la correction : si deux expressions algébriques sont équivalentes, elles retournent la même table pour tout état de base de données valide.
Comment le SQL se mappe à l'algèbre (conceptuellement)
Prenons une requête familière :
SELECT c.name
FROM customers c
JOIN orders o ON o.customer_id = c.id
WHERE o.total > 100;
Conceptuellement, c'est :
-
commencer par une jointure de customers et orders :
customers ⋈ orders -
sélectionner uniquement les orders supérieurs à 100 :
σ(o.total > 100)(...) -
projeter la colonne voulue :
π(c.name)(...)
Ce n'est pas la notation interne exacte utilisée par chaque moteur, mais c'est l'idée : le SQL devient un arbre d'opérateurs.
Équivalence : la porte vers l'optimisation
Beaucoup d'arbres différents peuvent signifier le même résultat. Par exemple, les filtres peuvent souvent être poussés plus tôt (appliquer σ avant une grosse jointure), et les projections peuvent souvent éliminer des colonnes inutilisées plus tôt (appliquer π plus tôt).
Ces règles d'équivalence permettent à une base de données de réécrire votre requête en un plan moins coûteux sans changer la signification. Une fois que vous voyez les requêtes comme de l'algèbre, « l'optimisation » cesse d'être de la magie et devient une reformulation sûre guidée par des règles.
Du SQL aux plans de requête : réécritures qui préservent le sens
Quand vous écrivez du SQL, la base de données ne l'exécute pas « tel que tapé ». Elle traduit votre instruction en un plan de requête : une représentation structurée du travail à effectuer.
Un bon modèle mental est un arbre d'opérateurs. Les feuilles lisent des tables ou des index ; les nœuds internes transforment et combinent des lignes. Les opérateurs courants incluent scan, filter (sélection), project (choix de colonnes), join, group/aggregate et sort.
Plan logique vs plan physique (quoi vs comment)
Les bases séparent typiquement la planification en deux couches :
- Plan logique : quoi calculer, exprimé avec des opérateurs abstraits (filtre, jointure, agrégation) et leurs relations.
- Plan physique : comment l'exécuter sur le stockage réel et le matériel (scan d'index vs scan complet, hash join vs nested-loop join, parallèle vs mono‑thread).
L'influence d'Ullman apparait dans l'accent mis sur les transformations préservant le sens : réarranger le plan logique de nombreuses façons sans changer la réponse, puis choisir une stratégie physique efficace.
Réécritures basées sur des règles qui réduisent le travail
Avant de choisir l'approche finale d'exécution, les optimiseurs appliquent des règles algébriques de « nettoyage ». Ces réécritures ne changent pas les résultats ; elles réduisent le travail inutile.
Exemples fréquents :
- Pushdown de sélection : appliquer les filtres le plus tôt possible pour que moins de lignes circulent dans les étapes suivantes.
- Élagage des projections : ne conserver que les colonnes nécessaires, réduisant les E/S et la mémoire.
- Réordonnancement des jointures : joindre d'abord des résultats plus petits/intermédiaires quand c'est sûr, plutôt que de suivre l'ordre apparent dans le SQL.
Un exemple simple de réécriture
Supposons que vous vouliez les commandes d'utilisateurs dans un pays :
SELECT o.order_id, o.total
FROM users u
JOIN orders o ON o.user_id = u.id
WHERE u.country = 'CA';
Une interprétation naïve pourrait joindre tous les users à toutes les orders puis filtrer pour le Canada. Une réécriture qui préserve le sens pousse le filtre plus tôt pour réduire le nombre de lignes impliquées :
- filtrer les users sur
country = 'CA' - joindre ces users aux orders
- projeter seulement
order_idettotal
En termes de plan, l'optimiseur tente de transformer :
Join(Users, Orders) → Filter(country='CA') → Project(order_id,total)
en quelque chose qui ressemble plutôt à :
Filter(country='CA') on Users → Join(with Orders) → Project(order_id,total)
Même réponse. Moins de travail.
Ces réécritures sont faciles à négliger parce que vous ne les tapez jamais — et pourtant elles expliquent en grande partie pourquoi le même SQL peut être rapide sur une base et lent sur une autre.
Optimisation fondée sur les coûts sans le jargon
Quand vous exécutez une requête SQL, la base de données considère plusieurs manières valides d'obtenir le même résultat, puis choisit celle qu'elle s'attend à être la moins coûteuse. Ce processus s'appelle optimisation fondée sur les coûts — et c'est un lieu pratique où la théorie à la Ullman apparaît dans la performance quotidienne.
Ce qu'est vraiment un « modèle de coûts »
Un modèle de coûts est un système de notation que l'optimiseur utilise pour comparer des plans alternatifs. La plupart des moteurs estiment le coût en se basant sur quelques ressources clés :
- Lignes traitées (le travail tend à croître avec la quantité de données qui traverse chaque étape)
- E/S (lecture de pages depuis le disque ou le SSD, plus les effets de cache)
- CPU (filtrage, hash, tri, agrégation)
- Mémoire (si une opération tient en RAM ou déverse sur disque)
Le modèle n'a pas besoin d'être parfait ; il doit être juste dans la bonne direction assez souvent pour choisir de bons plans.
Estimation de cardinalité, en termes simples
Avant de pouvoir évaluer des plans, l'optimiseur se pose la question à chaque étape : combien de lignes cela produira-t-il ? C'est l'estimation de cardinalité.
Si vous filtrez WHERE country = 'CA', le moteur estime quelle fraction de la table correspond. Si vous joignez customers et orders, il estime combien de paires correspondront sur la clé de jointure. Ces prédictions déterminent s'il préfère un index scan plutôt qu'un scan complet, un hash join plutôt qu'un nested loop, ou si un tri sera petit ou énorme.
Pourquoi les statistiques comptent (et ce qui tourne mal sans elles)
Les estimations de l'optimiseur sont guidées par des statistiques : comptes, distributions de valeurs, taux de null, et parfois corrélations entre colonnes.
Quand les stats sont obsolètes ou manquantes, le moteur peut se tromper de plusieurs ordres de grandeur sur le nombre de lignes. Un plan qui semble bon sur le papier peut devenir coûteux en réalité — symptômes classiques : ralentissements soudains après croissance des données, changements « aléatoires » de plan, ou jointures qui déversent des données sur disque.
Le compromis inévitable : précision vs temps de planification
De meilleures estimations demandent souvent plus de travail : statistiques plus détaillées, échantillonnage ou exploration d'un plus grand nombre de plans candidats. Mais la planification coûte elle-même du temps, surtout pour des requêtes complexes.
Ainsi, les optimiseurs équilibrent deux objectifs :
- Planifier assez vite pour les charges interactives
- Planifier assez bien pour éviter des choix catastrophiques
Comprendre ce compromis vous aide à interpréter la sortie d'un EXPLAIN : l'optimiseur n'essaie pas d'être brillant — il essaie d'être prévisiblement correct avec des informations limitées.
Algorithmes de jointure et le cœur de la performance
Ullman a aidé à populariser une idée simple mais puissante : le SQL n'est pas tant « exécuté » que traduit en un plan d'exécution. Nulle part cela n'est plus visible que dans les jointures. Deux requêtes qui retournent les mêmes lignes peuvent avoir des temps d'exécution très différents selon l'algorithme de jointure choisi et l'ordre des jointures.
Nested loop, hash join, merge join — quand chacun a du sens
Nested loop join : pour chaque ligne du côté gauche, trouver les lignes correspondantes à droite. Il peut être rapide quand le côté gauche est petit et que le côté droit dispose d'un index utile.
Hash join : construire une table de hachage à partir d'une des entrées (souvent la plus petite) et la sonder avec l'autre. Il excelle pour de grandes entrées non triées avec des conditions d'égalité (par ex. A.id = B.id), mais nécessite de la mémoire ; un déversement sur disque peut effacer l'avantage.
Merge join : parcourir deux entrées en ordre trié. C'est idéal quand les deux côtés sont déjà ordonnés (ou triables à moindre coût), par exemple quand des index fournissent les lignes dans l'ordre de la clé de jointure.
Pourquoi l'ordre des jointures peut dominer la performance
Avec trois tables ou plus, le nombre d'ordres de jointure possibles explose. Joindre deux grandes tables en premier peut créer un intermédiaire gigantesque qui ralentit tout le reste. Un meilleur ordre commence souvent par le filtre le plus sélectif (le moins de lignes) et étend depuis là, en maintenant les intermédiaires petits.
Les index changent le menu des bons plans
Les index ne se contentent pas d'accélérer des recherches — ils rendent certaines stratégies de jointure viables. Un index sur la clé de jointure peut transformer un nested loop coûteux en une série de « seeks » rapides. À l'inverse, l'absence ou l'inutilisabilité d'index peut pousser le moteur vers des hash joins ou de grands tris pour des merge joins.
Checklist pratique : symptômes d'un mauvais plan de jointure
- Le temps d'exécution augmente largement avec un peu plus de données (l'ordre de jointure amplifie les intermédiaires).
- Le plan montre d'énormes écarts « rows estimated vs rows actual » (mauvaises estimations de cardinalité conduisent à de mauvais choix de jointure).
- On observe de gros tris ou des déversements de hash sur disque (pression mémoire ou index manquants).
- Une petite table filtrée est jointe tardivement au lieu d'être utilisée dès le départ (les filtres ne sont pas suffisamment poussés).
- Le prédicat de jointure n'est pas une égalité propre entre types compatibles (empêche des hash/merge efficients).
Idées de compilateur à l'intérieur des moteurs de bases de données
Les bases de données ne se contentent pas d'« exécuter » du SQL. Elles le compilent. L'influence d'Ullman couvre la théorie des bases de données et la pensée compilateur, et cette connexion explique pourquoi les moteurs de requête ressemblent à des chaînes d'outils de langages : ils traduisent, réécrivent et optimisent avant de faire le moindre travail.
Parsing et arbres de syntaxe : comment le SQL est lu
Quand vous envoyez une requête, la première étape ressemble à l'avant‑propos d'un compilateur. Le moteur tokenise mots-clés et identifiants, vérifie la grammaire et construit un parse tree (souvent simplifié en AST). C'est là que les erreurs de base sont détectées : virgules manquantes, noms de colonne ambigus, règles de groupement invalides.
Un modèle mental utile : le SQL est un langage de programmation dont le « programme » décrit des relations de données plutôt que des boucles.
Du parse tree aux opérateurs logiques
Les compilateurs convertissent la syntaxe en une représentation intermédiaire (IR). Les bases font de même : elles traduisent le SQL en opérateurs logiques tels que :
- sélection (filtrage de lignes)
- projection (choix de colonnes)
- jointure (combinaison de tables)
- agrégation (
GROUP BY)
Cette forme logique se rapproche davantage de l'algèbre relationnelle que du texte SQL, ce qui facilite le raisonnement sur le sens et l'équivalence.
Pourquoi les optimiseurs ressemblent à des optimisations de compilateur
Les optimisations de compilateur conservent le résultat identique tout en rendant l'exécution moins coûteuse. Les optimiseurs de bases de données font de même, en utilisant des systèmes de règles tels que :
- pousser les filtres plus tôt (réduire le travail immédiatement)
- réordonner les jointures (même résultat, coût différent)
- supprimer les calculs redondants
C'est la version base de données de « dead code elimination » : pas exactement les mêmes techniques, mais la même philosophie — préserver la sémantique, réduire le coût.
Débogage : lire les plans comme du code compilé
Si votre requête est lente, ne vous contentez pas d'inspecter le SQL. Regardez le plan de requête comme vous liriez la sortie d'un compilateur. Un plan vous dit ce que le moteur a effectivement choisi : ordre des jointures, utilisation d'index et où le temps est passé.
Conclusion pratique : apprenez à lire la sortie d'un EXPLAIN comme une « liste d'assemblage » de performance. Cela transforme l'optimisation en débogage fondé sur des preuves. Pour développer cette habitude, voir /blog/practical-query-optimization-habits.
Théorie du schéma qui impacte la performance réelle
Une bonne performance de requête commence souvent avant d'écrire le SQL. La théorie de conception de schéma d'Ullman (en particulier la normalisation) vise à structurer les données pour que la base puisse les maintenir correctes, prévisibles et efficaces à mesure qu'elles grandissent.
Objectifs de la normalisation (pourquoi elle existe)
La normalisation vise à :
- Réduire les anomalies (par ex. mettre à jour une adresse client à cinq endroits et en oublier un)
- Améliorer la cohérence en faisant vivre chaque fait dans un « foyer » unique
- Rendre les contraintes exprimables (clés, clés étrangères) pour que le moteur fasse respecter les règles plutôt que le code applicatif
Ces gains de correction se traduisent souvent par des gains de performance : moins de champs dupliqués, des index plus petits et moins de mises à jour coûteuses.
Formes normales en langage courant
Pas besoin d'apprendre des preuves pour utiliser les idées :
- 1NF : stocker des valeurs atomiques dans les colonnes (pas de listes séparées par des virgules). Cela facilite le filtrage et l'indexation.
- 2NF : dans les tables à clé composite, chaque colonne non-clé doit dépendre de la clé entière (et non d'une partie). Cela évite la répétition d'attributs.
- 3NF : les colonnes non-clés doivent dépendre uniquement de la clé, pas d'autres colonnes non-clés. Cela empêche la duplication cachée.
- BCNF : version stricte de la 3NF où chaque déterminant est une clé candidate — utile quand des colonnes « presque uniques » créent des duplications subtiles.
Quand la dénormalisation est raisonnable
La dénormalisation peut être un choix judicieux quand :
- vous construisez des tables analytiques lourdes (wide fact tables, reporting)
- les jointures deviennent le goulot et vous pouvez accepter une redondance contrôlée
- vous optimisez la vitesse de lecture avec des règles de rafraîchissement claires (par ex. rebuild nightly)
L'important est de dénormaliser délibérément, avec un processus pour garder les duplications synchronisées.
Comment les choix de schéma affectent l'optimiseur et la montée en charge
La conception du schéma façonne ce que l'optimiseur peut faire. Des clés et des clés étrangères claires permettent de meilleures stratégies de jointure, des réécritures plus sûres et des estimations de cardinalité plus précises. À l'inverse, une duplication excessive peut gonfler les index et ralentir les écritures, et les colonnes multi‑valeurs bloquent des prédicats efficaces. À mesure que le volume de données croît, ces décisions de modélisation initiales valent souvent plus que l'optimisation micro d'une seule requête.
Comment la théorie apparaît quand les systèmes montent en charge
Quand un système « scale », il ne s'agit rarement d'ajouter simplement des machines plus puissantes. Le défi tient souvent à préserver le même sens de requête alors que le moteur choisit une stratégie physique très différente pour garder les temps d'exécution prévisibles. L'accent d'Ullman sur les équivalences formelles est précisément ce qui permet de changer ces stratégies sans modifier les résultats.
L'échelle est souvent une question d'agencement physique + choix de plan
À petite échelle, beaucoup de plans « fonctionnent ». À grande échelle, la différence entre scanner une table, utiliser un index ou utiliser un résultat précalculé peut faire passer d'une seconde à des heures. La partie théorique compte parce que l'optimiseur a besoin d'un ensemble sûr de règles de réécriture (pousser les filtres, réordonner les jointures) qui n'altèrent pas la réponse — même si elles modifient radicalement le travail effectué.
Le partitionnement change la requête que vous exécutez, même si le SQL est identique
Le partitionnement (par date, client, région, etc.) transforme une table logique en plusieurs morceaux physiques. Cela affecte la planification :
- quelles partitions peuvent être sautées (partition pruning)
- si les jointures se font au sein des partitions ou nécessitent un reshuffle de données entre nœuds
- si l'agrégation peut se faire localement avant de combiner les résultats
Le texte SQL peut rester inchangé, mais le meilleur plan dépendra désormais de l'endroit où vivent les lignes.
Vues matérialisées : la précomputation comme raccourci algébrique
Les vues matérialisées sont essentiellement des « sous‑expressions sauvegardées ». Si le moteur peut prouver que votre requête correspond (ou peut être réécrite pour correspondre) à un résultat stocké, il peut remplacer un travail coûteux — comme des jointures et agrégations répétées — par une simple lecture. C'est de la pensée algébrique relationnelle en pratique : reconnaître l'équivalence, puis réutiliser.
Le caching : utile, mais il ne sauvera pas une mauvaise forme de travail
Le cache accélère les lectures répétées, mais il ne sauvera pas une requête qui doit scanner trop de données, échanger d'énormes intermédiaires ou calculer une jointure géante. Quand des problèmes d'échelle apparaissent, la solution est souvent : réduire la quantité de données touchées (agencement/partitionnement), réduire le calcul répété (vues matérialisées) ou changer le plan — pas seulement « ajouter du cache ».
Habitudes pratiques d'optimisation inspirées par Ullman
L'influence d'Ullman se manifeste dans un état d'esprit simple : traiter une requête lente comme une intention que la base est libre de réécrire, puis vérifier ce qu'elle a réellement décidé de faire. Pas besoin d'être théoricien pour en tirer parti — il suffit d'une routine reproductible.
1) Lire un plan EXPLAIN : quoi regarder en premier
Commencez par les éléments qui dominent généralement le temps d'exécution :
- Méthode d'accès : le moteur scanne-t-il toute la table alors que vous attendiez un lookup par index ?
- Estimations vs réels (si votre SGBD les affiche) : de gros écarts expliquent souvent la lenteur mystérieuse.
- Ordre des jointures : quelle table pilote la jointure, et commence‑t‑elle par le filtre le plus sélectif ?
- Opérateurs coûteux : tris, constructions de hash, nested loops sur de gros inputs — ces éléments révèlent souvent où se fait le travail.
Si vous ne faites qu'une chose, identifiez le premier opérateur où le nombre de lignes explose. C'est généralement la cause racine.
2) Anti‑patterns fréquents qui piègent les optimiseurs
Faciles à écrire et étonnamment coûteux :
- Fonctions sur colonnes indexées :
WHERE LOWER(email) = ...peut empêcher l'utilisation d'un index (préférez une colonne normalisée ou un index fonctionnel si pris en charge). - Filtres manquants : oublier une plage de dates ou un filtre de tenant transforme une requête ciblée en un large scan.
- Cross joins accidentels : une condition de jointure manquante peut multiplier les lignes et forcer des intermédiaires gigantesques.
3) Formuler une hypothèse en pensant algèbre relationnelle
L'algèbre relationnelle encourage deux mouvements pratiques :
- Pousser les filtres tôt : appliquer les
WHEREavant les jointures quand c'est possible pour réduire les entrées. - Réduire les colonnes tôt : ne sélectionner que les colonnes nécessaires (surtout avant des jointures) pour réduire mémoire et E/S.
Une bonne hypothèse ressemble à : « Cette jointure est coûteuse parce qu'on joint trop de lignes ; si on filtre orders aux 30 derniers jours d'abord, l'entrée de la jointure diminue. »
4) Index, réécriture ou modification du schéma ?
Règle de décision simple :
- Ajouter un index quand la requête est correcte, sélective et exécutée régulièrement.
- Réécrire la requête quand
EXPLAINmontre du travail évitable (jointures inutiles, filtrage tardif, prédicats non sargables). - Changer le schéma quand le motif de charge est stable et que vous vous battez toujours contre le même goulot (agrégats précomputés, champs de lookup dénormalisés, partitionnement par temps/tenant).
Le but n'est pas d'écrire du SQL « malin », mais d'obtenir des résultats intermédiaires prévisibles et plus petits — exactement le type d'amélioration qui devient plus visible grâce aux idées d'Ullman.
Appliquer ces idées quand vous bâtissez des produits réels
Ces concepts ne sont pas réservés aux administrateurs de bases. Si vous expédiez une application, vous prenez des décisions de planification et de base de données, que vous en ayez conscience ou non : forme du schéma, choix de clés, motifs de requêtes et couche d'accès aux données influencent tout ce que peut faire l'optimiseur.
Si vous utilisez un workflow « vibe‑coding » (par exemple générer une app React + Go + PostgreSQL depuis une interface conversationnelle avec Koder.ai), les modèles mentaux à la Ullman sont un filet de sécurité pratique : vous pouvez examiner le schéma généré pour vérifier des clés et relations propres, inspecter les requêtes de votre application et valider les performances avec EXPLAIN avant que les problèmes n'atteignent la production. Plus vite vous itérez sur « intention de requête → plan → correction », plus vous tirez de valeur d'un développement accéléré.
Où en apprendre davantage et comment l'appliquer au travail
Vous n'avez pas besoin d'« étudier la théorie » comme loisir séparé. Le moyen le plus rapide de profiter des fondamentaux à la Ullman est d'apprendre juste assez pour lire des plans de requête en confiance — puis de pratiquer sur votre propre base.
Ressources accessibles pour débuter
Recherchez ces livres et thèmes de cours (aucune affiliation — simplement des points de départ largement cités) :
- « A First Course in Database Systems » (Ullman & Widom) — bases de données accessibles avec un cadrage pratique.
- « Principles of Database and Knowledge-Base Systems » (Ullman) — théorie plus approfondie si vous voulez plus de rigueur.
- « Compilers: Principles, Techniques, and Tools » (Aho, Lam, Sethi, Ullman) — pour comprendre le lien « pourquoi les optimiseurs ressemblent à des compilateurs ? »
- Thèmes de cours à rechercher : algèbre relationnelle, réécriture de requêtes, ordonnancement de jointures, optimisation fondée sur les coûts, index et sélectivité, parsing et langages de requête.
Un parcours d'apprentissage léger
Commencez petit et ancrez chaque étape à quelque chose d'observable :
- Algèbre relationnelle : maîtriser sélection, projection, jointure et règles d'équivalence.
- Plans : apprendre à lire les nœuds de plan (types de scan, filtres, jointures, tris, agrégats).
- Jointures : comprendre nested loop vs hash join vs merge join et quand chacun gagne.
- Modèles de coûts : connaître les quelques entrées qui guident les décisions (comptes de lignes, sélectivité, E/S vs CPU).
Petits exercices qui rapportent vite
Choisissez 2–3 requêtes réelles et itérez :
- Réécrivez : changer
INenEXISTS, pousser des prédicats, enlever des colonnes inutiles, comparer les résultats. - Comparez des plans : capturez les plans « avant/après » et notez ce qui a changé (ordre/type de jointure, type de scan).
- Variez les index : ajoutez/enlevez un index à la fois et observez les comptes estimés vs réels.
Communiquer vos constats à des collègues
Utilisez un langage clair, centré sur le plan :
- « Le plan est passé d'un scan séquentiel à un index scan parce que le filtre est devenu sélectif. »
- « Les estimations de lignes étaient erronées de 100×, donc l'optimiseur a choisi un mauvais ordre de jointure. »
- « Cette réécriture est équivalente (même résultat), mais elle permet le pushdown de prédicat et fait entrer moins de lignes dans la jointure. »
Voilà le gain pratique des fondations d'Ullman : un vocabulaire partagé pour expliquer la performance — sans supposition.
FAQ
Qui est Jeffrey Ullman, et pourquoi son travail importe-t-il si je ne fais que rédiger du SQL ?
Jeffrey Ullman a contribué à formaliser la façon dont les bases de données « représentent le sens des requêtes » et comment elles peuvent transformer des requêtes pour les exécuter plus rapidement tout en garantissant l'équivalence des résultats. Cette base théorique se manifeste chaque fois qu'un moteur réécrit une requête, réordonne des jointures ou choisit un plan d'exécution différent sans modifier le jeu de résultats.
Qu'est-ce que l'algèbre relationnelle, et comment est-elle reliée au SQL ?
L'algèbre relationnelle est un petit ensemble d'opérateurs (select, project, join, union, difference) qui décrivent précisément le résultat d'une requête. Les moteurs traduisent couramment le SQL en un arbre d'opérateurs proche de l'algèbre pour appliquer des règles d'équivalence (comme pousser des filtres plus tôt) avant de choisir une stratégie d'exécution.
Pourquoi les réécritures de requêtes « préservant le sens » sont-elles importantes en pratique ?
Parce que l'optimisation repose sur la preuve qu'une requête réécrite renvoie les mêmes résultats. Les règles d'équivalence permettent à l'optimiseur de :
- pousser des filtres
WHEREavant une jointure - élaguer les colonnes inutilisées tôt
- réordonner des jointures lorsque c'est sûr sur le plan logique
Ces modifications peuvent réduire considérablement le travail sans changer le sens.
Quelle est la différence entre un plan logique et un plan physique ?
Un plan logique décrit quoi calculer (filtre, jointure, agrégation) indépendamment des détails de stockage. Un plan physique choisit comment l'exécuter (scan d'index vs scan complet, hash join vs nested loop, parallélisme, stratégies de tri). La plupart des différences de performance viennent des choix physiques, rendus possibles par des réécritures logiques.
Qu'est-ce que l'optimisation basée sur les coûts en termes simples ?
L'optimisation fondée sur les coûts évalue plusieurs plans valides et choisit celui qui a le coût estimé le plus bas. Les coûts sont généralement déterminés par des facteurs pratiques tels que le nombre de lignes traitées, les E/S, le CPU et la mémoire (y compris le risque de déversement disque pour hash ou tri).
Qu'est-ce que l'estimation de cardinalité, et pourquoi cause-t-elle des performances imprévisibles ?
L'estimation de cardinalité est la prédiction du nombre de lignes qu'une étape produira. Ces estimations déterminent l'ordre des jointures, le type de jointure et si un scan d'index est rentable. Quand elles sont erronées (souvent à cause de statistiques absentes ou obsolètes), on observe des ralentissements soudains, des déversements massifs ou des changements de plan surprenants.
Quand dois‑je m'attendre à ce que nested loop, hash join ou merge join soient les plus rapides ?
- Nested loop join : idéal quand le côté gauche est petit et que le côté droit peut être sondé efficacement (souvent via un index).
- Hash join : performant pour de grandes jointures d'égalité sur des données non triées, mais nécessite suffisamment de mémoire pour éviter le déversement.
- Merge join : efficace lorsque les deux entrées sont déjà triées (ou peuvent l'être à faible coût), souvent aidé par des index fournissant l'ordre.
Comment lire un plan EXPLAIN sans être submergé ?
Concentrez‑vous sur quelques indices forts :
- où le nombre de lignes explose (le premier grand bond indique souvent la cause racine)
- écarts « estimé vs réel » (statistiques incorrectes)
- opérateurs coûteux (tri volumineux, construction de hash, nested loops sur des entrées importantes)
- choix de scan (scan complet alors que vous attendiez un index)
Considérez le plan comme du code compilé : il montre ce que le moteur a réellement choisi de faire.
Comment la normalisation influence-t-elle les performances des requêtes, et quand la dénormalisation est-elle acceptable ?
La normalisation réduit la duplication des faits et les anomalies de mise à jour, ce qui conduit souvent à des tables et index plus petits et à des jointures plus fiables. La dénormalisation peut être appropriée pour l'analytics ou des motifs de lecture intense, mais elle doit être délibérée (règles de rafraîchissement claires, redondance maîtrisée) pour ne pas dégrader la cohérence.
Quelles techniques aident les requêtes à rester rapides lorsque les données montent en charge sans changer les résultats ?
Garder des performances à l'échelle implique souvent de changer la stratégie physique tout en conservant le sens logique. Outils courants :
- partitionnement pour le pruning et la localité
- vues matérialisées pour réutiliser des sous-résultats équivalents
- mises à jour de statistiques pour guider de meilleurs plans
Le cache accélère les lectures répétées, mais ne corrigera pas une requête qui doit toucher trop de données ou produire d'énormes jointures intermédiaires.