Aller au contenu
Développement medium

Fonctions SQL et agrégations : COUNT, SUM, GROUP BY et HAVING

16 min de lecture

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.

  • Compter, sommer, moyenner avec COUNT, SUM, AVG, MIN, MAX
  • Regrouper les résultats avec GROUP BY
  • Filtrer les groupes avec HAVING (distinct de WHERE)
  • 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

Les fonctions SQL sont utilisées constamment 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
  • 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
  • Savoir écrire un SELECT … WHERE (guide Syntaxe de base)
  • La base fil rouge infra.db chargée dans ~/Projets/cours-sql
  • Comprendre les jointures (guide précédent) pour les exemples multi-tables

Les fonctions d’agrégation calculent une seule valeur à partir de plusieurs lignes.

-- Nombre total de serveurs
SELECT COUNT(*) FROM servers;
-- Nombre de serveurs actifs
SELECT COUNT(*) FROM servers WHERE active = 1;
ExpressionComportement
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_roles
FROM users;
-- RAM totale de tous les serveurs de production
SELECT SUM(ram_gb) AS ram_totale
FROM servers
WHERE environment = 'production' AND active = 1;
-- Durée moyenne de déploiement (en secondes)
SELECT AVG(duration_seconds) AS duree_moyenne
FROM deployments
WHERE status = 'success';
-- Premier et dernier serveur créé
SELECT MIN(created_at) AS premier, MAX(created_at) AS dernier
FROM servers;
-- Disque le plus grand
SELECT MAX(disk_gb) AS plus_gros_disque FROM servers;

MIN et MAX fonctionnent aussi sur les chaînes (ordre alphabétique) et les dates.

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 environnement
SELECT environment, COUNT(*) AS nb_serveurs
FROM servers
WHERE active = 1
GROUP BY environment;

Résultat : une ligne par environment, avec le nombre de serveurs associé.

-- Nombre d'alertes par serveur et par sévérité
SELECT server_id, severity, COUNT(*) AS nb
FROM alerts
GROUP BY server_id, severity
ORDER BY server_id, nb DESC;

Pour afficher le hostname au lieu de l’ID, combinez GROUP BY et JOIN :

SELECT sv.hostname, a.severity, COUNT(*) AS nb_alertes
FROM alerts a
INNER JOIN servers sv ON sv.id = a.server_id
GROUP BY sv.hostname, a.severity
ORDER 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 »
ClauseFiltreExécuté
WHERELignes individuelles avant le regroupementAvant GROUP BY
HAVINGGroupes après le regroupementAprès GROUP BY
-- WHERE filtre les lignes AVANT le regroupement
SELECT server_id, COUNT(*) AS nb
FROM alerts
WHERE severity = 'critical'
GROUP BY server_id;
-- HAVING filtre les groupes APRÈS le regroupement
SELECT server_id, COUNT(*) AS nb
FROM alerts
GROUP BY server_id
HAVING COUNT(*) > 3;
-- Utilisateurs qui ont effectué plus de 3 déploiements
SELECT u.full_name, COUNT(*) AS nb_deploy
FROM deployments d
INNER JOIN users u ON u.id = d.deployed_by
GROUP BY u.full_name
HAVING COUNT(*) > 3
ORDER BY nb_deploy DESC;
SELECT LENGTH('bonjour'); -- 7
SELECT UPPER('bonjour'); -- BONJOUR
SELECT LOWER('BONJOUR'); -- bonjour
SELECT TRIM(' bonjour '); -- bonjour
SELECT LTRIM(' bonjour'); -- bonjour
SELECT RTRIM('bonjour '); -- bonjour
-- Extraire les 3 premiers caractères du hostname
SELECT SUBSTR(hostname, 1, 3) FROM servers;
-- Remplacer un domaine dans les emails
SELECT 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)

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'ASCII
SELECT * FROM users WHERE email LIKE '%@infra.local';

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.

SELECT date('now'); -- 2026-04-13
SELECT time('now'); -- 08:30:00
SELECT datetime('now'); -- 2026-04-13 08:30:00
SELECT strftime('%Y', created_at) AS annee,
strftime('%m', created_at) AS mois,
strftime('%w', created_at) AS jour_semaine
FROM servers;

Formats courants : %Y (année), %m (mois), %d (jour), %H (heure), %M (minute), %S (seconde), %w (jour de la semaine 0=dimanche).

SQLite utilise des modificateurs au lieu de INTERVAL :

-- Alertes de moins de 24 heures
SELECT * FROM alerts
WHERE created_at > datetime('now', '-24 hours');
-- Serveurs créés dans les 30 derniers jours
SELECT hostname, created_at FROM servers
WHERE created_at > date('now', '-30 days');
-- Dans 7 jours
SELECT date('now', '+7 days');
-- SQLite : strftime avec le format souhaité
SELECT strftime('%d/%m/%Y %H:%M', created_at) AS date_fr
FROM servers;
-- Résultat : 15/03/2025 09:00
SELECT ROUND(3.14159, 2); -- 3.14
SELECT ROUND(ram_gb * 1.0 / cpu_cores, 1) AS ram_par_core FROM servers;
SELECT ABS(-42); -- 42
SELECT MAX(disk_gb) % 100 FROM servers; -- modulo
-- SQL standard (fonctionne partout)
SELECT CAST('42' AS INTEGER);
SELECT CAST(ram_gb AS REAL) / cpu_cores FROM servers;

COALESCE renvoie la première valeur non NULL parmi ses arguments :

-- Afficher "non renseigné" si l'email est NULL
SELECT username, COALESCE(email, 'non renseigné') AS email FROM users;
-- Afficher "Aucune note" pour les serveurs sans note
SELECT hostname, COALESCE(notes, 'Aucune note') AS notes FROM servers;
-- Traiter les NULL comme 0 dans un calcul
SELECT SUM(COALESCE(duration_seconds, 0)) FROM deployments;

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_succes
FROM deployments d
INNER JOIN services s ON s.id = d.service_id
GROUP BY s.name;

Ouvrez sqlite3 infra.db et pratiquez :

  1. Comptez le nombre d’alertes par sévérité.

    SELECT severity, COUNT(*) AS nb
    FROM alerts
    GROUP BY severity
    ORDER BY nb DESC;
  2. 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 nb
    FROM deployments d
    INNER JOIN users u ON u.id = d.deployed_by
    WHERE d.status = 'success'
    GROUP BY u.full_name
    HAVING COUNT(*) > 3
    ORDER BY duree_moyenne;
  3. 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_creation
    FROM servers
    WHERE created_at > date('now', '-12 months')
    ORDER BY created_at DESC;
  4. Remplacez les emails NULL par « non renseigné » et les notes NULL par « Aucune note ».

    SELECT username,
    COALESCE(email, 'non renseigné') AS email
    FROM users;
    SELECT hostname,
    COALESCE(notes, 'Aucune note') AS notes
    FROM servers;
  5. 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_totaux
    FROM servers
    WHERE active = 1
    GROUP BY datacenter;
SymptômeCause probableSolution
column must appear in GROUP BYColonne dans le SELECT sans agrégation ni GROUP BYAjoutez la colonne au GROUP BY ou entourez-la d’une fonction d’agrégation
AVG donne un résultat inattenduLes NULL sont ignorés par AVGUtilisez COALESCE(col, 0) avant l’agrégation si les NULL doivent compter comme 0
Division par zéroLe dénominateur est 0Utilisez NULLIF(denominateur, 0)
HAVING ne filtre pasVous avez mis la condition dans WHERE au lieu de HAVINGDéplacez les conditions sur des agrégats dans HAVING
strftime renvoie NULLLa date n’est pas au format ISO 8601Vérifiez que les données sont au format YYYY-MM-DD HH:MM:SS
  • COUNT, SUM, AVG, MIN, MAX calculent une valeur par groupe de lignes
  • GROUP BY définit les groupes — toute colonne du SELECT doit y figurer ou être dans une agrégation
  • WHERE filtre avant le regroupement, HAVING filtre après
  • COALESCE remplace les NULL par une valeur par défaut — indispensable pour les calculs
  • NULLIF protège contre les divisions par zéro
  • La concaténation utilise || en SQLite/PostgreSQL et CONCAT() en MySQL
  • Les fonctions de dates SQLite utilisent strftime(), date(), datetime() avec des modificateurs ('-30 days', '+1 hour')

Ce site vous est utile ?

Sachez que moins de 1% des lecteurs soutiennent ce site.

Je maintiens +700 guides gratuits, sans pub ni tracing. Aujourd'hui, ce site ne couvre même pas mes frais d'hébergement, d'électricité, de matériel, de logiciels, mais surtout de cafés.

Un soutien régulier, même symbolique, m'aide à garder ces ressources gratuites et à continuer de produire des guides de qualité. Merci pour votre appui.

Abonnez-vous et suivez mon actualité DevSecOps sur LinkedIn