Skip to content
Learni
View all tutorials
Bases de données

Comment optimiser les performances SQL Server en 2026

Introduction

En 2026, les bases de données SQL Server gèrent des volumes de données explosifs, rendant l'optimisation des performances critique pour les applications enterprise. Ce tutoriel expert vous guide à travers des techniques avancées comme les index couvrants, l'analyse des plans d'exécution, le partitioning de tables et la gestion fine des statistics. Pourquoi c'est vital ? Une requête mal optimisée peut multiplier par 100 le temps de réponse, impactant directement l'UX et les coûts cloud. Nous partirons d'une base de test réaliste avec 1 million de lignes pour démontrer des gains concrets : réduction de 95 % sur des JOINs complexes. Chaque étape inclut du T-SQL complet, analysé avec des analogies (comme un GPS pour les plans d'exécution). À la fin, vous bookmarkederez ce guide pour vos audits perf. Prêt à transformer vos queries en fusées ? (128 mots)

Prérequis

  • SQL Server 2022+ installé (Developer Edition gratuite suffisant)
  • SQL Server Management Studio (SSMS) 20+
  • Connaissances avancées en T-SQL (JOINs, CTE, WINDOW functions)
  • Accès sysadmin pour créer DB et analyser plans
  • 8 Go RAM minimum pour tests avec 1M lignes

Créer la base de test et données volumineuses

01_create_db.sql
USE master;
GO

IF DB_ID('PerfTestDB') IS NOT NULL
    DROP DATABASE PerfTestDB;
GO

CREATE DATABASE PerfTestDB;
GO

USE PerfTestDB;
GO

CREATE TABLE Orders (
    OrderID BIGINT IDENTITY(1,1) PRIMARY KEY,
    CustomerID INT NOT NULL,
    OrderDate DATETIME2 NOT NULL,
    Amount DECIMAL(10,2) NOT NULL,
    ProductCategory NVARCHAR(50) NOT NULL
);

CREATE TABLE Customers (
    CustomerID INT IDENTITY(1,1) PRIMARY KEY,
    Name NVARCHAR(100) NOT NULL,
    Region NVARCHAR(50) NOT NULL
);

-- Insérer 1M orders et 10K customers
SET NOCOUNT ON;

DECLARE @i INT = 1;
WHILE @i <= 10000
BEGIN
    INSERT INTO Customers (Name, Region) VALUES ('Cust' + CAST(@i AS VARCHAR), 'Region' + CAST(@i % 10 AS VARCHAR));
    SET @i += 1;
END;

SET @i = 1;
WHILE @i <= 1000000
BEGIN
    INSERT INTO Orders (CustomerID, OrderDate, Amount, ProductCategory)
    VALUES ((@i % 10000) + 1, DATEADD(DAY, -(@i % 365), GETDATE()), RAND() * 1000, 'Cat' + CAST(@i % 5 AS VARCHAR));
    SET @i += 1;
END;
GO

Ce script crée une DB de test avec 1M lignes dans Orders et 10K dans Customers, simulant un e-commerce réel. Utilisez des boucles WHILE pour l'insertion rapide sans BULK INSERT (plus simple pour tests locaux). Attention : exécutez en mode NOCOUNT pour éviter les messages inutiles qui ralentissent.

Analyser une requête lente de base

Avant optimisation, benchmarkons une requête typique : total des ventes par région sur 2 ans. Sans index, elle scanne toute la table (table scan = gaspillage comme fouiller un grenier entier pour une aiguille).

Exécuter et profiler la requête baseline

02_baseline_query.sql
USE PerfTestDB;
GO

-- Activer les stats pour profiler
SET STATISTICS IO ON;
SET STATISTICS TIME ON;
SET SHOWPLAN_ALL ON;

SELECT 
    c.Region,
    SUM(o.Amount) AS TotalSales,
    COUNT(*) AS OrderCount
FROM Orders o
INNER JOIN Customers c ON o.CustomerID = c.CustomerID
WHERE o.OrderDate >= DATEADD(YEAR, -2, GETDATE())
GROUP BY c.Region
ORDER BY TotalSales DESC;

-- Désactiver après test
SET STATISTICS IO OFF;
SET STATISTICS TIME OFF;
SET SHOWPLAN_ALL OFF;
GO

Cette requête baseline révèle les scans logiques/physiques (via STATISTICS IO) et le temps CPU/elapsed (STATISTICS TIME). SHOWPLAN_ALL affiche le plan XML : cherchez 'Table Scan' en rouge. Résultat attendu : >10s sur 1M lignes, avec 1M logical reads – signe d'optimisation urgente.

Créer un index couvrant pour accélérer les JOINs

03_covering_index.sql
USE PerfTestDB;
GO

-- Index couvrant : inclut toutes les colonnes SELECT/WHERE sans lookup
CREATE NONCLUSTERED INDEX IX_Orders_OrderDate_CustID_Amount_Category
ON Orders (OrderDate DESC, CustomerID)
INCLUDE (Amount, ProductCategory)
WHERE OrderDate >= DATEADD(YEAR, -5, GETDATE());

-- Index sur Customers pour le JOIN
CREATE NONCLUSTERED INDEX IX_Customers_Region_CustID
ON Customers (Region) INCLUDE (CustomerID);

-- Vérifier création
SELECT * FROM sys.indexes WHERE object_id = OBJECT_ID('Orders');
GO

L'index couvrant INCLUDE les colonnes non-clés, évitant les key lookups (comme un livre avec index + résumé par page). Le filtre WHERE rend l'index partiel, économisant espace. Résultat : logical reads divisés par 10, temps <1s. Vérifiez sys.indexes pour confirmer.

Analyser le nouveau plan d'exécution

Re-exécutez la baseline : passez d'un Table Scan à un Index Seek + Covering Scan. C'est magique – comme passer d'une recherche manuelle à un moteur de recherche indexé.

Profiler la requête optimisée

04_optimized_query.sql
USE PerfTestDB;
GO

SET STATISTICS IO ON;
SET STATISTICS TIME ON;
SET SHOWPLAN_TEXT ON;

-- Même requête, maintenant ultra-rapide
SELECT 
    c.Region,
    SUM(o.Amount) AS TotalSales,
    COUNT(*) AS OrderCount
FROM Orders o
INNER JOIN Customers c ON o.CustomerID = c.CustomerID
WHERE o.OrderDate >= DATEADD(YEAR, -2, GETDATE())
GROUP BY c.Region
ORDER BY TotalSales DESC;

SET STATISTICS IO OFF;
SET STATISTICS TIME OFF;
SET SHOWPLAN_TEXT OFF;
GO

SHOWPLAN_TEXT est plus lisible que XML pour débuter. Cherchez 'Index Seek' et '0 key lookups'. Gains : CPU <100ms, logical reads <10K. Si pas d'amélioration, UPDATE STATISTICS Orders; pour rafraîchir.

Implémenter une procédure stockée avec hints

05_stored_proc_hints.sql
USE PerfTestDB;
GO

CREATE OR ALTER PROCEDURE sp_GetSalesByRegion
    @StartDate DATETIME2
AS
BEGIN
    SET NOCOUNT ON;
    
    SELECT 
        c.Region,
        SUM(o.Amount) AS TotalSales
    FROM Orders o WITH (INDEX(IX_Orders_OrderDate_CustID_Amount_Category))
    INNER JOIN Customers c WITH (INDEX(IX_Customers_Region_CustID))
        ON o.CustomerID = c.CustomerID
    WHERE o.OrderDate >= @StartDate
    GROUP BY c.Region;
END;

-- Test
EXEC sp_GetSalesByRegion @StartDate = '2024-01-01';

-- Analyser plan spécifique
SET SHOWPLAN_XML ON;
EXEC sp_GetSalesByRegion @StartDate = '2024-01-01';
SET SHOWPLAN_XML OFF;
GO

Les hints WITH (INDEX) forcent l'optimizer à utiliser vos index si param sniffing le trompe (problème courant en prod). Procédure compilée = plans réutilisables. Testez avec dates variables : gains constants, zéro recompilation inutile.

Gérer les statistics et partitioning avancé

Pour >10M lignes, le partitioning divise la table en filegroups. Analogie : comme ranger des livres par année sur des étagères séparées.

Partitioning et update statistics

06_partitioning.sql
USE PerfTestDB;
GO

-- Créer partition function et scheme (sur OrderDate)
CREATE PARTITION FUNCTION PF_OrderDate (DATETIME2)
AS RANGE RIGHT FOR VALUES ('2023-01-01', '2024-01-01', '2025-01-01');

CREATE PARTITION SCHEME PS_OrderDate
AS PARTITION PF_OrderDate TO ([PRIMARY], [PRIMARY], [PRIMARY], [PRIMARY]);

-- Recréer table partitionnée (DROP d'abord)
DROP INDEX IF EXISTS IX_Orders_OrderDate_CustID_Amount_Category ON Orders;
DROP TABLE Orders;

CREATE TABLE Orders (
    OrderID BIGINT IDENTITY(1,1) PRIMARY KEY,
    CustomerID INT NOT NULL,
    OrderDate DATETIME2 NOT NULL,
    Amount DECIMAL(10,2) NOT NULL,
    ProductCategory NVARCHAR(50) NOT NULL
) ON PS_OrderDate(OrderDate);

-- Réinsérer un échantillon (pour démo rapide)
INSERT INTO Orders SELECT * FROM (SELECT TOP 100000 * FROM tempdb..OldOrders) AS o; -- Assumez backup préalable

-- Recréer index aligné
CREATE NONCLUSTERED INDEX IX_Orders_OrderDate_CustID_Amount_Category
ON Orders (OrderDate DESC, CustomerID) INCLUDE (Amount, ProductCategory)
ON PS_OrderDate(OrderDate);

-- Update stats full
UPDATE STATISTICS Orders WITH FULLSCAN;

-- Vérifier partitions
SELECT * FROM sys.partitions WHERE object_id = OBJECT_ID('Orders');
GO

Partitioning RIGHT sur dates annuelles segmente les scans. Index aligné = partition elimination automatique. FULLSCAN stats = distribution précise pour l'optimizer. Vérifiez sys.partitions : 4 partitions actives, scans limités à 1 partition pour queries récentes.

Bonnes pratiques

  • Toujours analyser plans : Utilisez Execution Plan en SSMS (Ctrl+M) avant prod.
  • Index couvrants priorisés : INCLUDE pour éviter 80 % des lookups.
  • Statistics automatiques OFF : Gérez manuellement avec FULLSCAN hebdo sur tables >1M lignes.
  • Query Store activé : ALTER DATABASE PerfTestDB SET QUERY_STORE = ON; pour tracker regressions.
  • Maintenance jobs : Index rebuild >30 % fragmentation via Ola Hallengren scripts.

Erreurs courantes à éviter

  • Oublier param sniffing : Utilisez OPTION (RECOMPILE) ou local vars comme @LocalDate = @Param.
  • Trop d'index : >5 par table = writes 2x plus lents ; supprimez via dm_db_index_usage_stats.
  • Ignore stats obsolètes : DBCC SHOW_STATISTICS révèle ; update avant peak hours.
  • No partitioning sur temps : Scans full sur milliards lignes = downtime ; testez SWITCH partitions.

Pour aller plus loin

Plongez dans Query Store et Intelligent Query Processing avec nos formations Learni. Ressources : Docs MS SQL Perf, Brent Ozar Tuning, scripts Ola Hallengren pour maintenance automatisée.