Interroger et optimiser
Ce que ce chapitre apporte
- Traduire une demande formulée en français en une requête sur un schéma en étoile.
- Écrire une requête joignant plusieurs tables, avec agrégation et filtres au bon endroit.
- Nommer les opérations d'analyse multidimensionnelle et les reconnaître dans une demande.
- Expliquer ce qui rend une requête lente sur de gros volumes.
- Choisir une clé de partitionnement et un regroupement par hachage, et dire ce que chacun apporte.
Une demande métier arrive en français : « le chiffre d'affaires par famille de produits et par trimestre, hors retours, sur les magasins de plus de mille mètres carrés ». La traduire en requête est mécanique dès que le modèle est dimensionnel, et c'est le premier objet de ce chapitre. Le second est ce qui décide du temps de réponse, et qui n'est presque jamais l'écriture de la requête : c'est la quantité de données qu'elle est obligée de lire.
Un entrepôt bien modélisé rend les requêtes prévisibles : elles ont toutes la même forme, une table de faits jointe à quelques dimensions, filtrée puis regroupée. Cette régularité est ce qui permet à un métier d'écrire ses propres analyses, et à un moteur de les optimiser.
Elle ne suffit pas. Une requête impeccable sur cinq milliards de lignes reste lente si rien n'a été prévu pour lui éviter de les lire toutes.
Traduire une demande en requête
Quelle mesure ? Ce qu'on additionne, compte ou moyenne. Elle vient de la table de faits.
À quel grain de restitution ? Les mots qui suivent « par ». Ils deviennent le GROUP BY et les colonnes de dimension à joindre.
Quels filtres, et sur quoi portent-ils ? Un filtre sur une dimension va dans le WHERE. Un filtre sur la mesure agrégée va dans le HAVING, et nulle part ailleurs.
Quel tri, quelle limite ? Souvent implicites dans la demande, et toujours utiles à expliciter.
Le bloc suivant construit un petit entrepôt en étoile et pose la question de l'introduction. Il s'exécute dans la page, et se modifie.
La forme est toujours la même : la table de faits, autant de jointures que de dimensions citées, un WHERE qui filtre sur les dimensions, un GROUP BY qui reprend exactement les colonnes affichées non agrégées.
WHERE s'applique aux lignes avant le regroupement. HAVING s'applique aux groupes après.
Filtrer les retours dans le HAVING reviendrait à les inclure dans les sommes, puis à écarter des groupes entiers. Filtrer un montant agrégé dans le WHERE est en revanche impossible, puisque l'agrégat n'existe pas encore.
La règle est mécanique : filtre sur une valeur de ligne, donc WHERE ; filtre sur un résultat d'agrégation, donc HAVING.
Le schéma en étoile garantit qu'une clé de fait désigne une et une seule ligne de dimension. Cette garantie tombe dès qu'une dimension contient plusieurs versions d'une même entité, comme la dimension historisée du chapitre 7.
Si la jointure ne restreint pas à la bonne version, chaque fait se duplique autant de fois qu'il existe de versions, et les sommes sont multipliées d'autant. Le résultat reste plausible, seulement trop grand.
Le contrôle qui l'attrape en dix secondes : comparer COUNT(*) avant et après la jointure. Si le nombre de lignes a augmenté, la jointure n'est pas de un vers un, et tout agrégat qui en découle est faux.
Les opérations d'analyse
Le métier ne demande presque jamais une requête isolée : il explore, en partant d'un chiffre global et en descendant vers le détail. Ces mouvements portent des noms.
Agrégation : monter dans une hiérarchie, du mois vers le trimestre, de la ville vers la région.
Forage : descendre, du trimestre vers le mois, de la famille vers le produit.
Tranche : fixer une valeur sur une dimension, pour ne regarder que 2024.
Découpe : restreindre plusieurs dimensions à la fois, pour ne regarder que le premier trimestre dans le Nord.
Rotation : échanger les axes de lecture, passer d'un tableau produits en lignes et mois en colonnes à son transposé.
Ces cinq mouvements se traduisent tous par des variations minimes de la même requête : changer une colonne du GROUP BY, ajouter une condition au WHERE, permuter deux colonnes d'affichage. C'est cette régularité qui permet aux outils de restitution de les offrir par un clic, et c'est un bon test du modèle : si une exploration exige de réécrire la requête entièrement, le modèle est mal découpé.
Ce qui rend une requête lente
Sur de gros volumes, une requête passe l'essentiel de son temps à lire des données depuis le disque ou le réseau. Le nombre de jointures et la longueur du texte ne comptent presque pas ; ce qui compte est le nombre d'octets que le moteur est obligé de parcourir.
Trois leviers réduisent ce volume, et un seul est habituellement en place :
Ne lire que les colonnes utiles, ce que permet un stockage en colonnes.
Ne lire que les partitions utiles, ce que permet un partitionnement aligné sur les filtres.
Ne lire que les fichiers utiles, ce que permettent les statistiques minimum et maximum conservées par fichier.
Partitionner
Partitionner consiste à ranger physiquement les données dans des répertoires ou des fichiers séparés, selon la valeur d'une colonne : une partition par jour, par mois, par région.
Un filtre portant sur la colonne de partitionnement permet au moteur d'ignorer les autres partitions sans les ouvrir. C'est l'élagage de partitions, et c'est de très loin l'optimisation la plus rentable d'un entrepôt.
Descendre au jour semble mieux que le mois, puisque l'élagage est plus fin. Sur trois ans, cela fait pourtant 1 095 partitions, et si chacune est encore découpée par région, plusieurs dizaines de milliers de répertoires contenant chacun quelques fichiers minuscules.
On retrouve alors le problème des petits fichiers du chapitre 4 : le temps passe à ouvrir des fichiers plutôt qu'à lire des données, et le catalogue sature.
Le bloc ci-dessus le montre par un autre biais : sur une requête portant sur un trimestre, partitionner au jour ne lit pas moins de lignes que partitionner au mois. L'élagage plus fin n'apporte rien dès que le filtre couvre plusieurs partitions entières, et il ajoute mille répertoires.
Deux règles empiriques encadrent le choix. Une partition doit peser au moins quelques centaines de mégaoctets, et le nombre total de partitions doit rester de l'ordre du millier, pas de la dizaine de milliers. Un partitionnement au mois, éventuellement au jour sur des volumes très importants, couvre presque tous les cas.
Partitionner par région alors que toutes les requêtes filtrent sur le temps n'apporte rien : chaque requête lit toutes les partitions.
Pire, cela peut nuire. Une clé de partitionnement dont les valeurs sont très inégales, comme une région qui concentre la moitié de l'activité, reproduit le déséquilibre du chapitre 3 : une partition énorme et des dizaines de partitions vides.
La clé se choisit donc en regardant les filtres réellement écrits par les utilisateurs, pas la structure du métier.
Regrouper par hachage
Un regroupement par hachage, souvent appelé bucketing, répartit les lignes d'une partition dans un nombre fixe de fichiers, selon l'empreinte d'une colonne.
Toutes les lignes de même valeur atterrissent dans le même fichier, et deux tables regroupées sur la même colonne avec le même nombre de fichiers ont leurs valeurs alignées fichier par fichier.
C'est là qu'est le gain. Une jointure entre deux grandes tables oblige normalement à un mélange complet, celui du chapitre 3, avec tout le trafic réseau que cela représente. Si les deux tables sont regroupées sur la colonne de jointure avec le même nombre de fichiers, chaque fichier de l'une ne peut se joindre qu'au fichier correspondant de l'autre : la jointure se fait localement, sans mélange.
| Partitionnement | Regroupement par hachage | |
|---|---|---|
| Découpe selon | la valeur d'une colonne | l'empreinte d'une colonne |
| Nombre de morceaux | variable, une par valeur | fixe, choisi à l'avance |
| Bon pour | les filtres, par élagage | les jointures et l'échantillonnage |
| Colonne adaptée | peu de valeurs, souvent filtrée | beaucoup de valeurs, souvent jointe |
| Risque | trop de partitions, ou déséquilibre | fichiers déséquilibrés si une valeur domine |
L'usage courant partitionne par date et regroupe par identifiant : ventes/annee=2024/mois=03/ contient 64 fichiers regroupés sur l'identifiant client.
Une requête filtrée sur mars lit une seule partition, et une jointure avec la table des clients, regroupée de la même façon, se fait fichier par fichier sans mélange.
La combinaison est efficace parce que les deux mécanismes répondent à deux questions différentes : le partitionnement à « quelles lignes puis-je ignorer », le regroupement à « comment éviter de tout déplacer ».
Exercices type
Exercice 1 : traduire « les dix produits les plus vendus en volume au premier trimestre 2024, dans les magasins du Nord ».
Afficher la solution
Les quatre questions donnent tout.
Mesure : la quantité, donc SUM(f.quantite). Grain de restitution : le produit, donc un GROUP BY sur son libellé. Filtres : sur le temps et sur la région, deux dimensions, donc dans le WHERE. Tri et limite : décroissant sur la mesure, dix lignes.
À coller dans le bloc exécutable plus haut, à la place de sa dernière requête :
SELECT p.libelle, SUM(f.quantite) AS volume
FROM Fait_Vente f
JOIN Dim_Produit p ON p.idProduit = f.idProduit
JOIN Dim_Temps t ON t.idTemps = f.idTemps
JOIN Dim_Magasin m ON m.idMagasin = f.idMagasin
WHERE t.annee = 2024 AND t.trimestre = 1
AND m.region = 'Nord'
AND f.estRetour = 0
GROUP BY p.libelle
ORDER BY volume DESC
LIMIT 10;
Le filtre sur les retours n'était pas dans la demande, et il faut le poser explicitement : « les plus vendus » signifie presque toujours hors retours, et c'est le genre de convention qu'il faut faire confirmer plutôt que supposer.
Exercice 2 : une requête renvoie un chiffre d'affaires trois fois trop élevé depuis qu'une dimension a été historisée. Pourquoi ?
Afficher la solution
La jointure n'est plus de un vers un.
Historiser crée plusieurs lignes par entité, une par période de validité. Une jointure écrite sur la clé naturelle, sans restreindre à la version pertinente, rattache chaque fait à toutes les versions de son entité, et le duplique d'autant. Un client ayant trois versions triple ses ventes.
Deux corrections. Si les faits portent la clé de substitution, comme ils le doivent, joindre dessus et non sur la clé naturelle. Sinon, ajouter la condition de période, ou restreindre à la version courante par un indicateur.
Le contrôle qui l'attrape : comparer le nombre de lignes de la table de faits seule à celui obtenu après jointure. Il doit être identique.
Exercice 3 : une table de 4 téraoctets est partitionnée par jour sur cinq ans, et chaque partition est encore découpée par région, avec vingt régions. Que se passe-t-il ?
Afficher la solution
1 825 jours multipliés par 20 régions donnent 36 500 partitions, pour 4 téraoctets, soit environ 110 mégaoctets par partition, souvent répartis en plusieurs fichiers plus petits encore.
Le résultat est le problème des petits fichiers : le temps de réponse est dominé par l'ouverture des fichiers et la consultation du catalogue, pas par la lecture des données. Une requête sur un mois ouvre 600 partitions au lieu d'en lire une grosse.
La correction consiste à remonter d'un cran : partitionner par mois uniquement, ce qui donne 60 partitions d'environ 68 gigaoctets, et remplacer le découpage par région par un regroupement par hachage à l'intérieur de chaque partition, si les jointures le justifient.
Exercice 4 : deux tables de plusieurs milliards de lignes sont jointes chaque nuit sur l'identifiant client, et le traitement passe l'essentiel de son temps en transfert réseau. Que proposer ?
Afficher la solution
Regrouper les deux tables par hachage sur l'identifiant client, avec le même nombre de fichiers.
Le mélange complet disparaît : chaque fichier de l'une ne peut correspondre qu'au fichier de même rang dans l'autre, et la jointure se fait localement, sans déplacer les données.
Deux points de vigilance. Le nombre de fichiers doit être identique des deux côtés, sans quoi l'alignement n'existe pas et le mélange revient. Et un identifiant très déséquilibré, comme un client technique qui porterait la moitié des lignes, produira un fichier énorme, exactement le déséquilibre du chapitre 3, à traiter par les mêmes remèdes.
1.Un filtre sur le montant total d'un groupe se place…
2.Comment repérer qu'une jointure vers une dimension fausse les sommes ?
3.Qu'est-ce qui dicte le temps de réponse d'une requête sur de gros volumes ?
4.À quoi sert le partitionnement ?
5.Partitionner par jour sur cinq ans, avec vingt régions par jour…
6.Que permet le regroupement par hachage que le partitionnement ne permet pas ?
La méthode
- Poser les quatre questions : quelle mesure, à quel grain, quels filtres, quel tri.
- Placer chaque filtre selon qu'il porte sur une ligne ou sur un agrégat.
- Vérifier le nombre de lignes avant et après chaque jointure vers une dimension.
- Joindre sur la clé de substitution, jamais sur la clé naturelle d'une dimension historisée.
- Regarder les filtres réellement écrits avant de choisir une clé de partitionnement.
- Viser des partitions de quelques centaines de mégaoctets, et de l'ordre du millier au total.
- Regrouper par hachage les tables jointes en permanence, avec le même nombre de fichiers des deux côtés.
- Mesurer le volume lu, pas la durée seule : c'est lui qui explique la durée.
Synthèse
- Quatre questions suffisent à traduire une demande : mesure, grain, filtres, tri.
- Une requête en étoile a toujours la même forme : faits, jointures,
WHERE,GROUP BY. WHEREfiltre les lignes avant regroupement,HAVINGfiltre les groupes après.- Une jointure vers une dimension historisée duplique les faits si elle ne restreint pas à la bonne version.
- Le contrôle qui l'attrape : comparer le nombre de lignes avant et après.
- Cinq mouvements d'analyse : agrégation, forage, tranche, découpe, rotation.
- Si une exploration exige de réécrire la requête, le modèle est mal découpé.
- Le temps de réponse est dicté par le volume lu, pas par la complexité écrite.
- Le partitionnement permet d'ignorer des données sans les ouvrir : c'est l'optimisation la plus rentable.
- Une partition ne sert que si les requêtes filtrent dessus.
- Trop partitionner recrée le problème des petits fichiers.
- Le regroupement par hachage aligne deux tables et supprime le mélange d'une jointure.
- Les deux se combinent : partitionner par date, regrouper par identifiant.