Combien d'alertes critiques non acquittées ? Quel est le temps moyen de déploiement par service ? Quel serveur a le plus gros disque ? Toutes ces questions se résolvent en une seule requête SQL grâce aux fonctions d'agrégation (COUNT, SUM, AVG, MIN, MAX) combinées avec GROUP BY.
Au-delà de l'agrégation, SQL propose des dizaines de fonctions intégrées pour manipuler les chaînes, les dates et les nombres, directement dans vos requêtes, sans passer par un langage de programmation. Ce guide couvre les fonctions les plus utiles au quotidien, avec les particularités de SQLite et les différences avec PostgreSQL/MySQL.
Ce que vous allez apprendre
Section intitulée « Ce que vous allez apprendre »- Compter, sommer, moyenner avec
COUNT,SUM,AVG,MIN,MAX - Regrouper les résultats avec
GROUP BY - Filtrer les groupes avec
HAVING(distinct deWHERE) - Manipuler les chaînes :
UPPER,LOWER,TRIM,SUBSTR, concaténation avec|| - Travailler avec les dates :
date(),datetime(),strftime()en SQLite - Convertir et gérer les
NULL:CAST,COALESCE,NULLIF
Dans quel contexte ?
Section intitulée « Dans quel contexte ? »Interroger une base d'exploitation revient presque toujours à produire un chiffre ou un classement, pas une liste brute de lignes. Les fonctions d'agrégation évitent d'exporter les données pour les traiter ensuite dans un tableur ou un script. Voici les usages qui reviennent le plus souvent en administration :
- Compter les alertes par sévérité et par serveur
- Calculer le temps moyen de déploiement par service
- Trouver la date du dernier déploiement réussi pour chaque service
- Nettoyer des données (majuscules, espaces parasites) avant une migration
- Formater des dates pour un export CSV lisible par l'équipe
Ce guide ne couvre pas…
Section intitulée « Ce guide ne couvre pas… »Trois sujets voisins des agrégations sont traités ailleurs pour ne pas alourdir cette page. Le fenêtrage en particulier ressemble à une agrégation mais conserve toutes les lignes du résultat, une différence de fond qui mérite son propre guide.
- Les fonctions de fenêtrage (
ROW_NUMBER,RANK,LAG,LEAD) → voir le guide SQL avancé - Les jointures (
JOIN) → voir le guide Jointures et sous-requêtes - La création de fonctions personnalisées (
CREATE FUNCTION) → consultez la documentation de votre SGBD
Prérequis
Section intitulée « Prérequis »- Savoir écrire un
SELECT … WHERE(guide Syntaxe de base) - La base fil rouge
infra.dbchargée dans~/Projets/cours-sql - Comprendre les jointures (guide précédent) pour les exemples multi-tables
Les fonctions d'agrégation
Section intitulée « Les fonctions d'agrégation »Les fonctions d'agrégation calculent une seule valeur à partir de plusieurs lignes. Sans GROUP BY, elles considèrent tout le résultat de la requête comme un unique groupe et renvoient donc une seule ligne, quel que soit le nombre de lignes lues. Autre point commun décisif : à l'exception de COUNT(*), elles ignorent les valeurs NULL au lieu de les compter comme zéro, ce qui explique la plupart des résultats surprenants.
COUNT : compter les lignes
Section intitulée « COUNT : compter les lignes »COUNT est la seule fonction d'agrégation qui accepte l'étoile en argument, avec un sens particulier : compter les lignes elles-mêmes plutôt que les valeurs d'une colonne. Combinée à WHERE, elle compte les lignes retenues par le filtre.
-- Nombre total de serveursSELECT COUNT(*) FROM servers;
-- Nombre de serveurs actifsSELECT COUNT(*) FROM servers WHERE active = 1;COUNT(*) vs COUNT(colonne) vs COUNT(DISTINCT)
Section intitulée « COUNT(*) vs COUNT(colonne) vs COUNT(DISTINCT) »Ces trois écritures se ressemblent mais ne comptent pas la même chose, et l'écart entre elles est justement ce qui rend le résultat intéressant. Comparer COUNT(*) et COUNT(email) dans une même requête donne immédiatement le nombre de valeurs manquantes dans la colonne.
| Expression | Comportement |
|---|---|
COUNT(*) | Compte toutes les lignes (y compris celles avec des NULL) |
COUNT(email) | Compte les lignes où email n'est pas NULL |
COUNT(DISTINCT os) | Compte le nombre de valeurs uniques de os |
SELECT COUNT(*) AS total_users, COUNT(email) AS avec_email, COUNT(DISTINCT role) AS nb_rolesFROM users;SUM et AVG : somme et moyenne
Section intitulée « SUM et AVG : somme et moyenne »SUM additionne les valeurs, AVG en calcule la moyenne arithmétique. Sur une table vide ou dont toutes les valeurs sont NULL, les deux renvoient NULL et non 0, ce qui se propage ensuite dans tout calcul les utilisant.
-- RAM totale de tous les serveurs de productionSELECT SUM(ram_gb) AS ram_totaleFROM serversWHERE environment = 'production' AND active = 1;
-- Durée moyenne de déploiement (en secondes)SELECT AVG(duration_seconds) AS duree_moyenneFROM deploymentsWHERE status = 'success';MIN et MAX : valeurs extrêmes
Section intitulée « MIN et MAX : valeurs extrêmes »Ces deux fonctions renvoient la valeur la plus petite et la plus grande de la colonne. Attention à ce qu'elles ne disent pas : elles donnent la valeur extrême, pas la ligne qui la porte, il faut une sous-requête ou un tri pour récupérer le reste de l'enregistrement.
-- Premier et dernier serveur crééSELECT MIN(created_at) AS premier, MAX(created_at) AS dernierFROM servers;
-- Disque le plus grandSELECT MAX(disk_gb) AS plus_gros_disque FROM servers;MIN et MAX fonctionnent aussi sur les chaînes (ordre alphabétique) et les dates.
Regrouper avec GROUP BY
Section intitulée « Regrouper avec GROUP BY »Une agrégation sur toute la table donne un chiffre global, rarement suffisant pour décider quoi que ce soit. GROUP BY découpe les lignes en paquets partageant la même valeur, puis calcule l'agrégat paquet par paquet. C'est la clause qui transforme une requête de comptage en tableau de bord, et elle impose en retour une règle stricte sur le contenu du SELECT.
Principe : une ligne par groupe
Section intitulée « Principe : une ligne par groupe »GROUP BY divise les lignes en groupes selon une ou plusieurs colonnes, puis applique les fonctions d'agrégation sur chaque groupe :
-- Nombre de serveurs par environnementSELECT environment, COUNT(*) AS nb_serveursFROM serversWHERE active = 1GROUP BY environment;Résultat : une ligne par environment, avec le nombre de serveurs associé.
GROUP BY sur plusieurs colonnes
Section intitulée « GROUP BY sur plusieurs colonnes »Lister plusieurs colonnes crée un groupe par combinaison de valeurs rencontrée dans les données. Les combinaisons absentes n'apparaissent pas dans le résultat : un serveur sans alerte critique n'aura simplement aucune ligne pour cette sévérité.
-- Nombre d'alertes par serveur et par sévéritéSELECT server_id, severity, COUNT(*) AS nbFROM alertsGROUP BY server_id, severityORDER BY server_id, nb DESC;GROUP BY avec jointure
Section intitulée « GROUP BY avec jointure »Pour afficher le hostname au lieu de l'ID, combinez GROUP BY et JOIN :
SELECT sv.hostname, a.severity, COUNT(*) AS nb_alertesFROM alerts aINNER JOIN servers sv ON sv.id = a.server_idGROUP BY sv.hostname, a.severityORDER BY nb_alertes DESC;Pièges courants : colonnes non agrégées dans le SELECT
Section intitulée « Pièges courants : colonnes non agrégées dans le SELECT »C'est l'erreur la plus fréquente sur les requêtes groupées, et sa gravité dépend du moteur utilisé. Le raisonnement à tenir est simple : après regroupement, chaque groupe contient potentiellement plusieurs valeurs différentes pour une colonne non listée, le moteur ne peut donc pas en choisir une sans arbitraire.
Filtrer les groupes avec HAVING
Section intitulée « Filtrer les groupes avec HAVING »Une fois les groupes constitués, vous voudrez souvent n'en garder qu'une partie : les serveurs à plus de trois alertes, les services dont la durée moyenne dépasse un seuil. WHERE ne peut pas le faire, car il s'exécute avant que les agrégats n'existent. HAVING est la clause prévue pour cela, et c'est la seule qui accepte une fonction d'agrégation dans sa condition.
HAVING vs WHERE
Section intitulée « HAVING vs WHERE »Les deux clauses filtrent, mais à des moments différents du traitement de la requête. Cette différence a aussi un effet sur les performances : WHERE réduit le volume avant le regroupement, il est donc préférable d'y placer tout ce qui peut l'être.
| Clause | Filtre | Exécuté |
|---|---|---|
WHERE | Lignes individuelles avant le regroupement | Avant GROUP BY |
HAVING | Groupes après le regroupement | Après GROUP BY |
-- WHERE filtre les lignes AVANT le regroupementSELECT server_id, COUNT(*) AS nbFROM alertsWHERE severity = 'critical'GROUP BY server_id;
-- HAVING filtre les groupes APRÈS le regroupementSELECT server_id, COUNT(*) AS nbFROM alertsGROUP BY server_idHAVING COUNT(*) > 3;Exemple : groupes au-dessus d'un seuil
Section intitulée « Exemple : groupes au-dessus d'un seuil »La requête suivante combine les trois mécanismes vus jusqu'ici : une jointure pour récupérer le nom complet, un regroupement par utilisateur, puis un filtre sur le compte obtenu.
-- Utilisateurs qui ont effectué plus de 3 déploiementsSELECT u.full_name, COUNT(*) AS nb_deployFROM deployments dINNER JOIN users u ON u.id = d.deployed_byGROUP BY u.full_nameHAVING COUNT(*) > 3ORDER BY nb_deploy DESC;Fonctions de manipulation de chaînes
Section intitulée « Fonctions de manipulation de chaînes »À la différence des agrégations, ces fonctions travaillent ligne par ligne : elles transforment une valeur en une autre sans réduire le nombre de résultats. Elles servent surtout à normaliser des données avant une comparaison ou une migration, ou à construire un libellé lisible directement dans la requête. Les noms sont largement communs aux moteurs, la concaténation faisant exception.
LENGTH, UPPER, LOWER, TRIM
Section intitulée « LENGTH, UPPER, LOWER, TRIM »TRIM ne retire que les espaces situés aux extrémités, jamais ceux du milieu ; LTRIM et RTRIM limitent l'opération à la gauche ou à la droite. C'est ce qu'il faut pour rattraper une saisie approximative avant de comparer deux valeurs.
SELECT LENGTH('bonjour'); -- 7SELECT UPPER('bonjour'); -- BONJOURSELECT LOWER('BONJOUR'); -- bonjourSELECT TRIM(' bonjour '); -- bonjourSELECT LTRIM(' bonjour'); -- bonjourSELECT RTRIM('bonjour '); -- bonjourSUBSTR, REPLACE, concaténation
Section intitulée « SUBSTR, REPLACE, concaténation »En SQL, les positions de caractères commencent à 1 et non à 0 : SUBSTR(hostname, 1, 3) renvoie bien les trois premiers caractères. REPLACE remplace toutes les occurrences trouvées, pas seulement la première, et ne modifie rien dans la table tant que la requête reste un SELECT.
-- Extraire les 3 premiers caractères du hostnameSELECT SUBSTR(hostname, 1, 3) FROM servers;
-- Remplacer un domaine dans les emailsSELECT REPLACE(email, '@infra.local', '@newdomain.fr') FROM users WHERE email IS NOT NULL;En SQLite (et PostgreSQL), la concaténation utilise || :
SELECT username || ' (' || role || ')' AS display FROM users;-- Résultat : jdupont (admin)Pattern matching au-delà de LIKE
Section intitulée « Pattern matching au-delà de LIKE »SQLite ne dispose pas de ILIKE (PostgreSQL) ni de REGEXP natif. LIKE est insensible à la casse pour les lettres ASCII (A-Z) par défaut en SQLite :
-- Fonctionne en SQLite : LIKE est case-insensitive pour l'ASCIISELECT * FROM users WHERE email LIKE '%@infra.local';Fonctions de dates et heures
Section intitulée « Fonctions de dates et heures »SQLite stocke les dates comme du texte au format ISO 8601 ('2026-04-13 08:30:00'). Les comparaisons avec <, >, BETWEEN fonctionnent correctement sur ce format. Les fonctions de date de SQLite sont différentes de celles de PostgreSQL et MySQL.
Date et heure courantes
Section intitulée « Date et heure courantes »Le mot-clé 'now' renvoie l'instant courant en UTC, pas dans le fuseau du serveur. Pour obtenir l'heure locale, ajoutez le modificateur 'localtime', par exemple datetime('now', 'localtime') : sans lui, un rapport horaire peut être décalé d'une ou deux heures selon la saison.
SELECT date('now'); -- 2026-04-13SELECT time('now'); -- 08:30:00SELECT datetime('now'); -- 2026-04-13 08:30:00Extraire une partie de la date avec strftime
Section intitulée « Extraire une partie de la date avec strftime »strftime renvoie toujours du texte, y compris pour une année ou un mois, et avec les zéros de tête : le mois de mars vaut '03'. Extraire une partie de date est particulièrement utile en amont d'un GROUP BY, pour regrouper des évènements par année ou par mois.
SELECT strftime('%Y', created_at) AS annee, strftime('%m', created_at) AS mois, strftime('%w', created_at) AS jour_semaineFROM servers;Formats courants : %Y (année), %m (mois), %d (jour), %H (heure), %M (minute), %S (seconde), %w (jour de la semaine 0=dimanche).
Arithmétique de dates (modificateurs)
Section intitulée « Arithmétique de dates (modificateurs) »SQLite utilise des modificateurs au lieu de INTERVAL :
-- Alertes de moins de 24 heuresSELECT * FROM alertsWHERE created_at > datetime('now', '-24 hours');
-- Serveurs créés dans les 30 derniers joursSELECT hostname, created_at FROM serversWHERE created_at > date('now', '-30 days');
-- Dans 7 joursSELECT date('now', '+7 days');Formater une date
Section intitulée « Formater une date »La même fonction strftime sert à produire un affichage lisible, en assemblant plusieurs codes de format dans une seule chaîne. Réservez ce formatage à l'affichage final : une date au format JJ/MM/AAAA ne se trie plus correctement et ne se compare plus avec BETWEEN.
-- SQLite : strftime avec le format souhaitéSELECT strftime('%d/%m/%Y %H:%M', created_at) AS date_frFROM servers;-- Résultat : 15/03/2025 09:00Fonctions numériques
Section intitulée « Fonctions numériques »Les fonctions numériques servent surtout à rendre un résultat présentable, en arrondissant une moyenne ou un ratio. Un détail change tout dans ces calculs : la division de deux entiers produit un entier, la partie décimale étant perdue. C'est la raison de la multiplication par 1.0 dans l'exemple ci-dessous, qui force le calcul en nombre réel avant l'arrondi.
SELECT ROUND(3.14159, 2); -- 3.14SELECT ROUND(ram_gb * 1.0 / cpu_cores, 1) AS ram_par_core FROM servers;
SELECT ABS(-42); -- 42SELECT MAX(disk_gb) % 100 FROM servers; -- moduloFonctions de conversion
Section intitulée « Fonctions de conversion »Trois fonctions règlent la quasi-totalité des problèmes de type et de valeur manquante : CAST change le type d'une valeur, COALESCE fournit une valeur de repli quand la donnée est absente, et NULLIF transforme une valeur gênante en NULL. Elles sont normalisées et s'écrivent de la même façon sur SQLite, PostgreSQL et MySQL.
CAST convertit explicitement une valeur d'un type vers un autre. Vers INTEGER, la conversion tronque la partie décimale au lieu d'arrondir, et une chaîne non numérique est convertie en 0 par SQLite plutôt que de produire une erreur.
-- SQL standard (fonctionne partout)SELECT CAST('42' AS INTEGER);SELECT CAST(ram_gb AS REAL) / cpu_cores FROM servers;COALESCE : remplacer les NULL
Section intitulée « COALESCE : remplacer les NULL »COALESCE renvoie la première valeur non NULL parmi ses arguments :
-- Afficher "non renseigné" si l'email est NULLSELECT username, COALESCE(email, 'non renseigné') AS email FROM users;
-- Afficher "Aucune note" pour les serveurs sans noteSELECT hostname, COALESCE(notes, 'Aucune note') AS notes FROM servers;
-- Traiter les NULL comme 0 dans un calculSELECT SUM(COALESCE(duration_seconds, 0)) FROM deployments;NULLIF : comparer deux valeurs
Section intitulée « NULLIF : comparer deux valeurs »NULLIF(a, b) renvoie NULL si a = b, sinon renvoie a. Utile pour éviter les divisions par zéro :
-- Taux de succès par service (évite la division par zéro)SELECT s.name, COUNT(CASE WHEN d.status = 'success' THEN 1 END) * 100.0 / NULLIF(COUNT(*), 0) AS taux_succesFROM deployments dINNER JOIN services s ON s.id = d.service_idGROUP BY s.name;Exercice pratique
Section intitulée « Exercice pratique »Les cinq requêtes ci-dessous reprennent chaque notion du guide sur la base fil rouge. Écrivez-les d'abord sans regarder la correction, puis comparez : l'objectif est de repérer par vous-même à quel moment WHERE doit céder la place à HAVING. Ouvrez sqlite3 infra.db et pratiquez :
-
Comptez le nombre d'alertes par sévérité.
SELECT severity, COUNT(*) AS nbFROM alertsGROUP BY severityORDER BY nb DESC; -
Calculez la durée moyenne de déploiement par utilisateur, uniquement pour ceux qui ont plus de 3 déploiements réussis.
SELECT u.full_name, AVG(d.duration_seconds) AS duree_moyenne, COUNT(*) AS nbFROM deployments dINNER JOIN users u ON u.id = d.deployed_byWHERE d.status = 'success'GROUP BY u.full_nameHAVING COUNT(*) > 3ORDER BY duree_moyenne; -
Listez les serveurs créés ces 12 derniers mois avec la date formatée en français.
SELECT hostname, os,strftime('%d/%m/%Y', created_at) AS date_creationFROM serversWHERE created_at > date('now', '-12 months')ORDER BY created_at DESC; -
Remplacez les emails NULL par « non renseigné » et les notes NULL par « Aucune note ».
SELECT username,COALESCE(email, 'non renseigné') AS emailFROM users;SELECT hostname,COALESCE(notes, 'Aucune note') AS notesFROM servers; -
Calculez la RAM totale et le nombre de cœurs par datacenter pour les serveurs actifs.
SELECT datacenter,COUNT(*) AS nb_serveurs,SUM(ram_gb) AS ram_totale,SUM(cpu_cores) AS cores_totauxFROM serversWHERE active = 1GROUP BY datacenter;
Dépannage
Section intitulée « Dépannage »Les requêtes agrégées échouent rarement de façon spectaculaire : le plus souvent elles s'exécutent et renvoient un résultat faux. Les cinq situations ci-dessous couvrent l'essentiel des cas rencontrés, la moitié d'entre elles ayant pour origine le traitement des valeurs NULL.
| Symptôme | Cause probable | Solution |
|---|---|---|
column must appear in GROUP BY | Colonne dans le SELECT sans agrégation ni GROUP BY | Ajoutez la colonne au GROUP BY ou entourez-la d'une fonction d'agrégation |
AVG donne un résultat inattendu | Les NULL sont ignorés par AVG | Utilisez COALESCE(col, 0) avant l'agrégation si les NULL doivent compter comme 0 |
| Division par zéro | Le dénominateur est 0 | Utilisez NULLIF(denominateur, 0) |
HAVING ne filtre pas | Vous avez mis la condition dans WHERE au lieu de HAVING | Déplacez les conditions sur des agrégats dans HAVING |
strftime renvoie NULL | La date n'est pas au format ISO 8601 | Vérifiez que les données sont au format YYYY-MM-DD HH:MM:SS |
À retenir
Section intitulée « À retenir »COUNT,SUM,AVG,MIN,MAXcalculent une valeur par groupe de lignesGROUP BYdéfinit les groupes, toute colonne duSELECTdoit y figurer ou être dans une agrégationWHEREfiltre avant le regroupement,HAVINGfiltre aprèsCOALESCEremplace lesNULLpar une valeur par défaut, indispensable pour les calculsNULLIFprotège contre les divisions par zéro- La concaténation utilise
||en SQLite/PostgreSQL etCONCAT()en MySQL - Les fonctions de dates SQLite utilisent
strftime(),date(),datetime()avec des modificateurs ('-30 days','+1 hour')