

- Description
- Peinture indienne de tailleurs de pierre travaillant au ciseau et au maillet sur leurs blocs.
- Source
- auteur non indiqué (Wellcome Collection) · page d’origine
- Licence
- PD Domaine public
La démarche : préparer, puis frapper une fois
Utiliser le moteur ne suffit pas : il faut organiser le traitement pour que le moteur ait tout ce qu'il lui faut quand on lui pose la question.
En bref
- Avant : périmètre, grain, clés, index, fonctions pures et matérialisées, règles en données, dérivées, entrées figées, contrôles.
- Pendant : très peu de très grosses requêtes, aucune itération sur des lignes.
- Partout déterministe : un maillon séquentiel ou non déterministe annule la chaîne ; un bloc excellent dans un curseur ne sert plus à rien.
- Volontaire : les décisions se prennent avant d'écrire.
Sur cette page
- 1. Deux postures
- 2. Deux phases : préparer, puis frapper une fois
- 3. Ce qui se prépare avant
- 4. Exécuter : très peu de très grosses requêtes
- 5. Tout le traitement doit être déterministe
- 6. Une démarche volontaire : les questions avant la première requête
- 7. Tables de travail, variables de table et tables en mémoire
- 8. Coût et limites de la démarche
- Sources et vérification
Le moteur ne fait que la moitié du travail. La première moitié de la programmation ensembliste, c'est de bien utiliser ses outils (jointures, fonctions de fenêtre, APPLY…) : c'est l'objet des pages précédentes. La seconde est une démarche : organiser le traitement pour que le moteur dispose, au moment où l'on lui pose enfin la question, de tout ce qu'il lui faut. On prépare le modèle avant ; on pose ensuite très peu de très grosses requêtes.
Les mesures de la section (pages « Cas catastrophiques ») ont été prises sur SQL Server 2019 (LocalDB). Le nombre d'instructions exécutées, le nombre d'appels de fonction et les lectures logiques sont reproductibles à l'identique. Les durées et le temps CPU, eux, dépendent de la machine et varient d'une exécution à l'autre (jusqu'à un facteur 2 sur ma machine) : lisez-les comme des ordres de grandeur.
1. Deux postures#
| Question | Posture « requête » | Posture « traitement » |
|---|---|---|
| Où se trouve le coût ? | À chaque exécution, réparti dans de nombreuses instructions | Payé en préparation, une fois ; l'exécution est banale |
| Qu'optimise-t-on ? | Chaque requête, isolément | Le traitement entier, de l'entrée au résultat |
| Combien d'instructions ? | Des dizaines, des milliers, parfois une par entité | Quelques-unes, très grosses, sans itération |
| Où vit la logique ? | Dans le code : CASE, fonctions, boucles | Dans le modèle : tables de règles, clés, index, objets déterministes |
| Que teste-t-on ? | Un cas, un client, une date | L'ensemble, et l'équivalence avec l'ancienne version |
| Quand décide-t-on ? | En écrivant | Avant d'écrire : grain, périmètre, dérivées, contrôles |
Un développeur qui connaît le moteur à la perfection peut rester dans la première posture : chaque requête qu'il écrit est excellente. La performance se joue pourtant dans l'assemblage.
2. Deux phases : préparer, puis frapper une fois#
La phase de préparation est longue et volontaire : on y passe du temps, sans y chercher de résultat. La phase d'exécution est courte et banale : si elle est difficile, c'est que la préparation est incomplète.
3. Ce qui se prépare avant#
| À préparer | Pourquoi | Méthode |
|---|---|---|
| Le périmètre | Une seule définition du « quoi » ; plus de filtres répétés et divergents | Table de périmètre (clés des entités à traiter) ou paramètre table ; jointure partout |
| Le grain et les clés | Presque toutes les erreurs viennent d'un grain mal défini | Clés primaires et étrangères ; unicité par construction plutôt que DISTINCT de précaution |
| Le chemin d'accès global | L'ordre physique doit servir le traitement entier, pas une lecture unitaire | Clés de clustering alignées sur l'ordre de traitement, index couvrants, statistiques à jour après chargement |
| Les fonctions pures | Des blocs déterministes, indexables, inlinables, rejouables | Fonctions table en ligne, SCHEMABINDING, aucun appel non déterministe (voir Déterminisme) |
| Les fonctions matérialisées | Remplacer un appel par ligne par une jointure | La fonction devient une table de vérité sur son domaine utile (jours × segments, codes × libellés…) |
| Les règles, en données | Modifiables sans redéployer, jointes comme tout le reste | Barèmes et règles dans des tables, validés par contraintes, éventuellement expansés (intervalles → une ligne par jour) |
| Les dérivées structurelles | Hiérarchies, droits, calendriers recalculés partout sinon | Table de fermeture, table d'accès, calendrier, colonnes persistées, vues indexées |
| Les tables de travail | Des espaces intermédiaires typés, indexés, de bonne taille | Tables de travail créées et indexées en préparation ; tables à mémoire optimisée non durables pour le staging ; voir §7 |
| Les entrées figées | Un traitement reproductible a des entrées qui ne bougent pas pendant qu'il s'exécute | Date d'exploitation en paramètre, ensemble d'entrée en paramètre table, lecture sous isolation cohérente |
| Le plan de contrôle | Savoir à quoi ressemble un résultat juste, avant de le produire | Comptes, sommes, invariants, EXCEPT contre l'ancienne version |
4. Exécuter : très peu de très grosses requêtes#
- Aucune itération sur des lignes de données. Une boucle ne se justifie que sur des objets (voir les limites) ou des lots d'ensembles.
- Chaque requête traite tout le périmètre, pas un élément à la fois : le paramètre d'hier est la colonne d'aujourd'hui.
- Une écriture par table cible et par étape, avec
OUTPUTpour tracer ce qui change, plutôt qu'un déclencheur qui re-parcourt. - Un ordre explicite entre étapes (un graphe de dépendances, pas un empilement historique) ; chaque étape déclare ce qu'elle lit et ce qu'elle écrit.
- Des lots seulement par contrainte physique (journal, verrous, durée), bornés par plages de clés alignées sur l'index, jamais « une ligne à la fois ».
- Rejouable : relancer le traitement sur les mêmes entrées redonne exactement le même résultat.
5. Tout le traitement doit être déterministe#
« Déterministe » prend ici son sens le plus large : mêmes entrées, mêmes sorties, à tout moment, sans dépendance cachée (heure, aléa, ordre implicite, état d'autres transactions, plan d'exécution). C'est ce qui rend un traitement préparable (ce qu'on matérialise reste valable), rejouable, testable et comparable à sa version précédente.
5.1 La chaîne vaut son maillon le plus faible#
Il suffit d'un seul maillon séquentiel ou non déterministe pour que l'ensemble en pâtisse. La loi d'Amdahl (1967) en donne l'ordre de grandeur : si une part s du travail reste séquentielle, l'accélération est bornée par 1/s, quel que soit le nombre de cœurs. Avec p = 1 − s la part parallélisable et n cœurs : S = 1 / ((1 − p) + p/n).
| Part séquentielle du traitement | Accélération maximale (cœurs illimités) | avec 8 cœurs |
|---|---|---|
| 50 % | × 2 | × 1,8 |
| 10 % | × 10 | × 4,7 |
| 5 % | × 20 | × 5,9 |
| 1 % | × 100 | × 7,5 |
Et la boucle est pire que la loi : la part « séquentielle » n'est pas constante, elle croît avec le nombre d'itérations, car chaque tour paie un surcoût fixe.
5.2 Un bloc déterministe dans un curseur, et tous les gains sont perdus#
Un bloc parfaitement écrit (une fonction table en ligne, une requête indexée, une fonction pure) reste excellent à l'unité. Invoqué N fois par un curseur, il subit ce que coûte l'itération :
| Ce que coûte un bloc dans une boucle | Pourquoi cela annule l'optimisation du bloc |
|---|---|
| L'appel lui-même, à chaque tour | Recherche du plan, ouverture du contexte d'exécution, passage des paramètres : un surcoût fixe, multiplié par N |
| Une validation par tour (autocommit) | Chaque instruction valide séparément et attend l'écriture du journal |
| Des ensembles minuscules | Plus de jointure par hachage ni de parcours unique : N recherches dispersées, ou N parcours si le chemin d'accès n'est pas fait pour cela |
| Plus de parallélisme, plus de mode batch | Chaque exécution est trop petite, et certains constructs (fonctions scalaires non inlinées) interdisent le parallélisme |
| Des statistiques et des estimations « à l'unité » | Chaque plan est bon pour une entité ; personne n'optimise la somme |
L'optimisation du bloc est locale ; le coût de la boucle est global. Les mesures ci-dessous comparent, pour trois cas, la boucle « optimisée au mieux » (index ajoutés, requêtes propres) avec la démarche ensembliste :
| Cas (page) | Boucle « optimisée au mieux » | Version ensembliste | Écart structurel |
|---|---|---|---|
| Valorisation (2 000 clients) | 188 002 instructions · 180 000 appels de fonction · 3,6 M lectures logiques (index ajouté pour la boucle) | 4 instructions · 0 appel · 0,76 M lectures logiques | × 47 000 instructions · plus aucun appel |
| Hiérarchie (20 000 nœuds) | 40 002 instructions · 1,8 M lectures logiques (index présent) | 2 instructions · 1,7 M lectures — puis 1 instruction pour chaque recalcul (environ 13 fois moins de lectures) | × 20 000 instructions |
| Habilitations (40 utilisateurs) | 200 082 instructions · 200 000 appels · 59 M lectures logiques | 3 instructions · 0 appel · 14 000 lectures | × 4 000 en lectures logiques |
Instructions, appels de fonction et lectures logiques sont des mesures structurelles : elles ne dépendent pas de la machine. L'écart de temps varie, lui, avec le volume, l'environnement et la concurrence ; sur les cas mesurés, il passe de quelques unités à plusieurs centaines selon la taille, parce que la boucle dépense en proportion du nombre d'itérations, et parfois du carré (voir les pages de cas).
6. Une démarche volontaire : les questions avant la première requête#
- Quel est le grain du résultat ? Une ligne, c'est quoi ?
- Quel est le périmètre ? Peut-il se matérialiser en une table de clés ?
- Quelles entrées doivent être figées ? Quelle date d'exploitation, quel instantané ?
- Qu'est-ce qui est stable, qu'est-ce qui change souvent ? Le stable se matérialise ; ce qui change à chaque exécution se joint.
- Quelles dérivées vont être utilisées plus d'une fois ? Hiérarchies, droits, calendriers, correspondances.
- Où se trouve la boucle cachée ? Un appelant, un orchestrateur, un déclencheur, une fonction scalaire ?
- Comment saura-t-on que le résultat est juste ? Le plan de contrôle s'écrit avant le traitement.
- À quoi ressemble la requête finale ? Si l'on ne sait pas l'écrire, la préparation n'est pas terminée.
7. Tables de travail, variables de table et tables en mémoire#
| Objet | Ce que dit la documentation | Usage dans la démarche |
|---|---|---|
| Variable de table | Pas de statistiques de distribution ; l'optimiseur suppose souvent aucune ligne ; les requêtes qui la modifient ne sont pas parallélisées ; pas de recompilation déclenchée ; compilation différée avec le niveau de compatibilité 150 ; elle n'est pas une structure « en mémoire seule » (elle vit dans tempdb) | Petits ensembles seulement (quelques dizaines de lignes) ; pas pour de gros volumes de travail |
| Table temporaire | Index complets et statistiques ; recompilations possibles | Table de travail de volume significatif, indexée avant usage |
Table à mémoire optimisée non durable (DURABILITY = SCHEMA_ONLY) | Données transitoires sans E/S disque ni journal ; adaptée au staging ETL et au remplacement des structures tempdb ; SQL Server 2014 et ultérieur ; pas un « bouton magique » (peu utile pour des agrégats sur de grandes plages, où l'on préfère le columnstore) | Staging rapide entre étapes, sans coût de journal |
| Type table à mémoire optimisée | Paramètres table et variables de table qui héritent des avantages des tables non durables | Passer un ensemble à une routine sans E/S |
| Module compilé en mode natif | Procédures, déclencheurs et fonctions scalaires peuvent être compilés en code natif | Bloc déterministe à très faible coût unitaire, quand une logique séquentielle est inévitable |
8. Coût et limites de la démarche#
| Coût de la démarche | Parade |
|---|---|
| Temps de préparation (conception, chargement, indexation) | C'est le coût assumé ; il est amorti sur chaque exécution et chaque nouvel usage de la même préparation |
| Stockage des dérivées matérialisées | Ne matérialiser que ce qui est réutilisé ; types étroits ; compression |
| Fraîcheur : la préparation peut devenir périmée | Invalider et régénérer explicitement quand la source change (voir E10) |
| Complexité accrue du déploiement | Préparation = scripts versionnés, rejouables, testés ; générateurs déterministes (génération de code) |
| Cas où elle ne s'applique pas : accès unitaire à faible latence, volumes minuscules, effets externes, référentiel qui change à chaque exécution | Rester simple : l'ensembliste est un outil, pas un dogme (voir les limites) |
Les erreurs concrètes que cette démarche permet d'éviter sont cataloguées dans Les erreurs de conception d'un expert, et mesurées dans Les cas catastrophiques.
Sources et vérification#
- Amdahl, G. M. (1967), « Validity of the single processor approach to achieving large scale computing capabilities », AFIPS Conference Proceedings, vol. 30, p. 483–485 — loi d'Amdahl
- Microsoft Learn — table (Transact-SQL) : variables de table — statistiques, parallélisme, compilation différée, tempdb
- Microsoft Learn — In-Memory OLTP : vue d'ensemble et scénarios — tables non durables, types table à mémoire optimisée, modules natifs
- Microsoft Learn — Scalar UDF Inlining — exécution itérative et absence de parallélisme des fonctions scalaires