SQL Server ne ralentit pas juste parce que la base a grossi
Pourquoi une requête instantanée avec cinquante clients devient-elle pénible après quelques années? « SQL Server est rendu lent » ne décrit rien. Le moteur reçoit du SQL déclaratif — le résultat voulu — puis son optimiseur choisit un plan physique selon les index, les statistiques et un coût estimé.
Prenons une table Facture de cinq millions de lignes :
SELECT NoFacture, DateFacture, Total
FROM Facture
WHERE IdClient = @IdClient
AND DateFacture >= @Depuis
ORDER BY DateFacture DESC
Sans index utile, SQL Server lit toutes les pages de la table, teste chaque ligne, puis trie le résultat. Un index non cluster sur (IdClient, DateFacture) range les clés dans un arbre B et pointe vers les lignes. Le moteur peut descendre de la racine jusqu’aux premières pages du client, lire seulement la plage de dates et obtenir déjà le bon ordre. Si Total et NoFacture ne sont pas dans l’index, il doit ensuite retrouver chaque ligne dans la table ou l’index cluster; beaucoup de key lookups peuvent rendre un scan moins cher.
Les statistiques résument la distribution des valeurs. L’optimiseur estime combien de factures correspondent à @IdClient et choisit une jointure ou un accès. Des statistiques périmées, un client qui possède 40 % des lignes ou un paramètre très différent de celui utilisé lors de la compilation peuvent produire une mauvaise estimation. Le plan réel montre les lignes estimées et réellement lues; un grand écart indique où chercher.
Une condition peut aussi cacher l’index :
-- oblige souvent à calculer YEAR pour chaque ligne
WHERE YEAR(DateFacture) = 2005
-- donne une plage directement cherchable
WHERE DateFacture >= '2005-01-01'
AND DateFacture < '2006-01-01'
On appelle la deuxième forme sargable : elle peut devenir un argument de recherche dans l’arbre. Une conversion implicite entre une colonne texte et un paramètre numérique peut causer le même problème. Les paramètres gardent les types, évitent l’injection et favorisent la réutilisation du plan, mais il faut les déclarer avec le type exact de la colonne.
L’index n’est pas gratuit. Chaque INSERT, DELETE ou changement de clé doit modifier sa structure et écrire dans le journal. Un index large consomme cache et disque. J’ajouterais celui qui sert une requête mesurée, puis je vérifierais son coût sur l’écriture, pas un index sur chaque colonne « au cas où ».
Les verrous protègent la cohérence. Une mise à jour prend des verrous exclusifs; une lecture normale peut attendre. Une transaction qui ouvre, modifie une ligne, affiche une boîte de dialogue puis attend la personne garde ses verrous beaucoup trop longtemps. Le code devrait rassembler les données, commencer la transaction au dernier moment, effectuer le travail et commettre ou annuler immédiatement.
Deux transactions peuvent créer un deadlock :
- A verrouille
Client 1, puis demandeFacture 9; - B verrouille
Facture 9, puis demandeClient 1; - chacune attend l’autre;
- SQL Server choisit une victime et annule sa transaction.
L’application doit reconnaître l’erreur, faire un court délai et rejouer toute la transaction si l’opération est sûre. Accéder aux ressources dans le même ordre réduit le risque. Un retry partiel pourrait doubler une étape déjà commise hors transaction.
Mon test de performance aura une taille et une concurrence crédibles : distribution réelle des clients, dates sur plusieurs années, cache froid puis chaud, deux écritures croisées. Je capture le plan, les lectures logiques, le temps CPU, la durée et les attentes. Si une correction réduit la durée mais augmente énormément les écritures, je garde le compromis visible.
La pratique la plus utile est de partir de la requête lente et de son plan réel, pas d’un soupçon général. Je veux savoir quelles pages SQL Server lit, combien de lignes il pensait trouver et quel verrou il attend. La base n’est pas « rendue lente » : elle exécute un chemin qu’on peut observer et améliorer.