Un tableau croisé dynamique (pivot) transforme des lignes en colonnes : une ligne par groupe, et une colonne pour chaque valeur d’un autre champ, par exemple une colonne par trimestre. En SQL, tu le construis avec CASE à l’intérieur d’un agrégat comme SUM, groupé selon ce que chaque ligne doit représenter. Les exemples fonctionnent dans PostgreSQL, MySQL, SQLite et SQL Server.
Comment faire un tableau croisé dynamique en SQL
Écris un agrégat par colonne de sortie. Le CASE à l’intérieur ne laisse passer que les lignes qui appartiennent à cette colonne, et GROUP BY donne une ligne par groupe :
SELECT
row_column,
SUM(CASE WHEN pivot_column = 'value 1' THEN amount ELSE 0 END) AS value_1,
SUM(CASE WHEN pivot_column = 'value 2' THEN amount ELSE 0 END) AS value_2
FROM table_name
GROUP BY row_column;
Ici, chaque produit obtient une ligne, avec ses unités pour chaque trimestre côte à côte :
-- Input: sales
-- product | quarter | units
-- ---------+---------+------
-- Keyboard | Q1 | 120
-- Keyboard | Q2 | 95
-- Monitor | Q1 | 40
-- Monitor | Q2 | 55
-- Mouse | Q2 | 210
SELECT
product,
SUM(CASE WHEN quarter = 'Q1' THEN units ELSE 0 END) AS q1,
SUM(CASE WHEN quarter = 'Q2' THEN units ELSE 0 END) AS q2
FROM sales
GROUP BY product
ORDER BY product;
-- Output:
-- product | q1 | q2
-- ---------+-----+----
-- Keyboard | 120 | 95
-- Monitor | 40 | 55
-- Mouse | 0 | 210
Les règles à connaître
-
Tu nommes chaque colonne toi-même. SQL ne peut pas créer de colonnes à partir des données, donc un troisième trimestre demande une troisième ligne
CASE. Si la liste des valeurs change sans cesse, construis la requête dans le code de ton application. -
ELSE 0ne sert que pourSUMet les comptages. Il transforme une combinaison absente en 0 au lieu de NULL. AvecAVG,MINouMAX, ometsELSE, sinon les zéros comptent comme de vraies valeurs :
-- Input: the sales table above
SELECT
product,
ROUND(AVG(CASE WHEN quarter = 'Q1' THEN units ELSE 0 END), 1) AS q1_else_0,
ROUND(AVG(CASE WHEN quarter = 'Q1' THEN units END), 1) AS q1_no_else
FROM sales
GROUP BY product
ORDER BY product;
-- Output:
-- product | q1_else_0 | q1_no_else
-- ---------+-----------+-----------
-- Keyboard | 60.0 | 120.0
-- Monitor | 20.0 | 40.0
-- Mouse | 0.0 | NULL
Syntaxe alternative
Tu croiseras aussi FILTER dans PostgreSQL et SQLite, et l’opérateur PIVOT dans SQL Server. Les deux laissent une combinaison absente à NULL plutôt qu’à 0 :
-- Input: the sales table above
-- PostgreSQL and SQLite
SELECT
product,
SUM(units) FILTER (WHERE quarter = 'Q1') AS q1,
SUM(units) FILTER (WHERE quarter = 'Q2') AS q2
FROM sales
GROUP BY product
ORDER BY product;
-- SQL Server
SELECT product, [Q1] AS q1, [Q2] AS q2
FROM (SELECT product, quarter, units FROM sales) AS src
PIVOT (SUM(units) FOR quarter IN ([Q1], [Q2])) AS p
ORDER BY product;
-- Output:
-- product | q1 | q2
-- ---------+------+----
-- Keyboard | 120 | 95
-- Monitor | 40 | 55
-- Mouse | NULL | 210
Dans du code MySQL, tu verras souvent SUM(IF(quarter = 'Q1', units, 0)), une façon plus courte d’écrire la version avec CASE. L’extension tablefunc de PostgreSQL propose aussi une fonction crosstab() ; consulte créer des tableaux croisés dynamiques dans PostgreSQL avec crosstab().
Quand utiliser un tableau croisé dynamique ?
Fais un pivot quand une liste de lignes serait plus lisible sous forme de grille. Voici les raisons les plus courantes.
Comparer une mesure côte à côte
Place les valeurs à comparer (jours, trimestres, régions) les unes à côté des autres sur une seule ligne, au lieu de les éparpiller dans une longue liste.
Afficher plusieurs mesures par colonne
Chaque colonne de sortie est un agrégat à part, tu peux donc les associer : sessions et pages vues par appareil, ou commandes et chiffre d’affaires par canal.
Compter les lignes de chaque catégorie
Pour compter au lieu d’additionner, fais la somme de 1 pour chaque ligne correspondante, ou utilise COUNT(CASE WHEN ... THEN 1 END) : COUNT ignore les NULL produits par les autres lignes. C’est ainsi que tu obtiens les tâches par statut ou les tickets par priorité.
Transformer des lignes clé-valeur en colonnes
Les paramètres et les attributs produit sont souvent stockés sous forme d’une ligne par nom et valeur. Un pivot avec MAX(CASE ...) donne une ligne par élément avec une colonne par attribut. MAX fonctionne sur du texte et récupère l’unique valeur de chaque groupe.
Exemples concrets de tableaux croisés dynamiques en SQL
Note moyenne d’une app par plateforme
Omets ELSE quand tu calcules une moyenne, pour qu’une app sans avis sur une plateforme affiche NULL au lieu d’une note trompeuse :
-- Input: reviews
-- app | platform | rating
-- ---------+----------+-------
-- Budgetly | iOS | 5
-- Budgetly | iOS | 4
-- Budgetly | Android | 3
-- FitTrack | Android | 4
-- FitTrack | Android | 5
-- In SQL Server, AVG of an integer column drops the decimals: use rating * 1.0
SELECT
app,
ROUND(AVG(CASE WHEN platform = 'iOS' THEN rating END), 1) AS ios,
ROUND(AVG(CASE WHEN platform = 'Android' THEN rating END), 1) AS android
FROM reviews
GROUP BY app
ORDER BY app;
-- Output:
-- app | ios | android
-- ---------+------+--------
-- Budgetly | 4.5 | 3.0
-- FitTrack | NULL | 4.5
Commandes et ventes par canal pour chaque région
Deux mesures pour chaque canal, un nombre de commandes et un total :
-- Input: orders
-- id | region | channel | amount
-- ---+--------+---------+-------
-- 1 | North | web | 120.00
-- 2 | North | store | 80.00
-- 3 | North | web | 45.50
-- 4 | South | store | 210.00
-- 5 | South | store | 64.00
SELECT
region,
COUNT(CASE WHEN channel = 'web' THEN 1 END) AS web_orders,
SUM(CASE WHEN channel = 'web' THEN amount ELSE 0 END) AS web_sales,
COUNT(CASE WHEN channel = 'store' THEN 1 END) AS store_orders,
SUM(CASE WHEN channel = 'store' THEN amount ELSE 0 END) AS store_sales
FROM orders
GROUP BY region
ORDER BY region;
-- Output:
-- region | web_orders | web_sales | store_orders | store_sales
-- -------+------------+-----------+--------------+------------
-- North | 2 | 165.50 | 1 | 80.00
-- South | 0 | 0 | 2 | 274.00
Tickets de support par agent et par priorité
Un comptage pour chaque priorité, plus un total. COUNT(*) compte toujours toutes les lignes du groupe :
-- Input: tickets
-- id | agent | priority
-- ---+-------+---------
-- 1 | Priya | urgent
-- 2 | Priya | normal
-- 3 | Marco | normal
-- 4 | Priya | normal
-- 5 | Marco | low
SELECT
agent,
COUNT(CASE WHEN priority = 'low' THEN 1 END) AS low,
COUNT(CASE WHEN priority = 'normal' THEN 1 END) AS normal,
COUNT(CASE WHEN priority = 'urgent' THEN 1 END) AS urgent,
COUNT(*) AS total
FROM tickets
GROUP BY agent
ORDER BY agent;
-- Output:
-- agent | low | normal | urgent | total
-- ------+-----+--------+--------+------
-- Marco | 1 | 1 | 0 | 2
-- Priya | 0 | 2 | 1 | 3
Une ligne de paramètres par utilisateur
Chaque paramètre est stocké sous forme de nom et de valeur. Un utilisateur qui ne l’a jamais défini obtient NULL dans cette colonne :
-- Input: user_settings
-- user_id | setting | value
-- --------+----------+--------------
-- 1 | theme | dark
-- 1 | language | en
-- 2 | language | de
-- 2 | timezone | Europe/Berlin
-- 3 | theme | light
SELECT
user_id,
MAX(CASE WHEN setting = 'theme' THEN value END) AS theme,
MAX(CASE WHEN setting = 'language' THEN value END) AS language,
MAX(CASE WHEN setting = 'timezone' THEN value END) AS timezone
FROM user_settings
GROUP BY user_id
ORDER BY user_id;
-- Output:
-- user_id | theme | language | timezone
-- --------+-------+----------+--------------
-- 1 | dark | en | NULL
-- 2 | NULL | de | Europe/Berlin
-- 3 | light | NULL | NULL
Pour aller plus loin sur les tableaux croisés dynamiques
Sur ce blog :
-
Comment faire un pivot dans PostgreSQL :
CASEetcrosstab()dans PostgreSQL -
Créer des tableaux croisés dynamiques dans PostgreSQL avec crosstab() : l’extension
tablefuncen détail - Guide de l’instruction CASE en SQL : les conditions dans chaque colonne du pivot
- Guide de GROUP BY en SQL : regrouper les lignes avant de les pivoter
Documentation officielle :
- PostgreSQL : expressions d’agrégat et FILTER et crosstab()
- MySQL : opérateur CASE
- SQLite : fonctions d’agrégat et FILTER
- SQL Server : PIVOT et UNPIVOT
Beekeeper Studio est une interface de base de données gratuite & open source
Le meilleur outil de requêtes & éditeur SQL que j’aie jamais utilisé. Il m’offre tout ce dont j’ai besoin pour gérer ma base de données. - ⭐⭐⭐⭐⭐ Mit
Beekeeper Studio est rapide, intuitif et facile à utiliser. Beekeeper prend en charge plein de bases de données et fonctionne parfaitement sous Windows, Mac et Linux.
Ce que les utilisateurs disent de Beekeeper Studio
« Beekeeper Studio a complètement remplacé mon ancien workflow SQL. C’est rapide, intuitif, et ça rend le travail sur les bases de données à nouveau agréable. »
« J’ai essayé beaucoup d’interfaces de bases de données, mais Beekeeper trouve l’équilibre parfait entre fonctionnalités et simplicité. Ça marche, tout simplement. »