clement13.net
Photo d’origine : Peinture indienne de tailleurs de pierre travaillant au ciseau et au maillet sur leurs blocs.
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
Photo d’origine, non retouchée. L’arrière-plan en est une version stylisée par le site.

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.

≈ 9 min de lectureLecture
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

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.

Mesures citées dans cette section

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#

QuestionPosture « requête »Posture « traitement »
Où se trouve le coût ?À chaque exécution, réparti dans de nombreuses instructionsPayé en préparation, une fois ; l'exécution est banale
Qu'optimise-t-on ?Chaque requête, isolémentLe 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, bouclesDans le modèle : tables de règles, clés, index, objets déterministes
Que teste-t-on ?Un cas, un client, une dateL'ensemble, et l'équivalence avec l'ancienne version
Quand décide-t-on ?En écrivantAvant 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#

Les phases d'un traitement ensembliste Préparation longue et volontaire, exécution en très peu de très grosses requêtes sans itération, puis contrôle d'équivalence. 1 · PRÉPARATION — longue, volontaire, rejouable périmètre · grain · clés · index · statistiques fonctions pures · fonctions matérialisées en tables règles en données · dérivées matérialisées entrées figées · tables de travail · plan de contrôle 2 · EXÉCUTION très peu de requêtes, très grosses, sur tout le périmètre ; aucune itération sur des lignes 3 · CONTRÔLE équivalence invariants comptes rejouabilité
Le coût se paie en phase 1, une fois ; la phase 2 est volontairement banale.

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éparerPourquoiMéthode
Le périmètreUne seule définition du « quoi » ; plus de filtres répétés et divergentsTable de périmètre (clés des entités à traiter) ou paramètre table ; jointure partout
Le grain et les clésPresque toutes les erreurs viennent d'un grain mal définiClés primaires et étrangères ; unicité par construction plutôt que DISTINCT de précaution
Le chemin d'accès globalL'ordre physique doit servir le traitement entier, pas une lecture unitaireClés de clustering alignées sur l'ordre de traitement, index couvrants, statistiques à jour après chargement
Les fonctions puresDes blocs déterministes, indexables, inlinables, rejouablesFonctions table en ligne, SCHEMABINDING, aucun appel non déterministe (voir Déterminisme)
Les fonctions matérialiséesRemplacer un appel par ligne par une jointureLa fonction devient une table de vérité sur son domaine utile (jours × segments, codes × libellés…)
Les règles, en donnéesModifiables sans redéployer, jointes comme tout le resteBarèmes et règles dans des tables, validés par contraintes, éventuellement expansés (intervalles → une ligne par jour)
Les dérivées structurellesHiérarchies, droits, calendriers recalculés partout sinonTable de fermeture, table d'accès, calendrier, colonnes persistées, vues indexées
Les tables de travailDes espaces intermédiaires typés, indexés, de bonne tailleTables 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éesUn traitement reproductible a des entrées qui ne bougent pas pendant qu'il s'exécuteDate d'exploitation en paramètre, ensemble d'entrée en paramètre table, lecture sous isolation cohérente
Le plan de contrôleSavoir à quoi ressemble un résultat juste, avant de le produireComptes, sommes, invariants, EXCEPT contre l'ancienne version

4. Exécuter : très peu de très grosses requêtes#

  1. 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.
  2. 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.
  3. Une écriture par table cible et par étape, avec OUTPUT pour tracer ce qui change, plutôt qu'un déclencheur qui re-parcourt.
  4. 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.
  5. 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 ».
  6. 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 traitementAccé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 bouclePourquoi cela annule l'optimisation du bloc
L'appel lui-même, à chaque tourRecherche 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 minusculesPlus 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 batchChaque 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 logiques3 instructions · 0 appel · 14 000 lectures× 4 000 en lectures logiques
Lecture des chiffres

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#

  1. Quel est le grain du résultat ? Une ligne, c'est quoi ?
  2. Quel est le périmètre ? Peut-il se matérialiser en une table de clés ?
  3. Quelles entrées doivent être figées ? Quelle date d'exploitation, quel instantané ?
  4. 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.
  5. Quelles dérivées vont être utilisées plus d'une fois ? Hiérarchies, droits, calendriers, correspondances.
  6. Où se trouve la boucle cachée ? Un appelant, un orchestrateur, un déclencheur, une fonction scalaire ?
  7. Comment saura-t-on que le résultat est juste ? Le plan de contrôle s'écrit avant le traitement.
  8. À 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#

ObjetCe que dit la documentationUsage dans la démarche
Variable de tablePas 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 temporaireIndex complets et statistiques ; recompilations possiblesTable 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éeParamètres table et variables de table qui héritent des avantages des tables non durablesPasser un ensemble à une routine sans E/S
Module compilé en mode natifProcédures, déclencheurs et fonctions scalaires peuvent être compilés en code natifBloc 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émarcheParade
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éesNe matérialiser que ce qui est réutilisé ; types étroits ; compression
Fraîcheur : la préparation peut devenir périméeInvalider et régénérer explicitement quand la source change (voir E10)
Complexité accrue du déploiementPré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écutionRester 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#