Chapitre 616 minDébutant
Fonctions SQL : dates, chaînes et CASE
Par Josselyn C., prof de BTS SIO. Mis à jour le .
À la fin de ce chapitre, tu sauras
- mettre en forme des chaînes et des nombres ;
- calculer des durées et formater des dates ;
- écrire une condition dans une requête avec
CASE; - remplacer un
NULLpar une valeur avecCOALESCE.
4 exercices corrigés
Une fonction SQL prend des valeurs et renvoie une valeur : UPPER('abc') renvoie ABC, DATEDIFF l'écart en jours entre deux dates. Tu l'utilises dans le SELECT, le WHERE, l'ORDER BY, ou dans un UPDATE. Ce chapitre présente les fonctions de MySQL pour les chaînes, les nombres, les dates, les conversions et les conditions (CASE, IF, COALESCE), sur la base d'exemple parc. Les fonctions sont la partie de SQL qui varie le plus d'un SGBD à l'autre : le tableau de fin de chapitre donne les équivalences.
Chaînes de caractères
| Fonction | Rôle | Exemple | Résultat |
|---|---|---|---|
CONCAT(a, b, ...) | Concatène | CONCAT('PC', '-', '0001') | PC-0001 |
CONCAT_WS(sep, a, b, ...) | Concatène avec un séparateur, en ignorant les NULL | CONCAT_WS(' ', 'Karim', NULL, 'Morel') | Karim Morel |
UPPER(s), LOWER(s) | Majuscules, minuscules | UPPER('Lefèvre') | LEFÈVRE |
LEFT(s, n), RIGHT(s, n) | Les n premiers ou derniers caractères | RIGHT('PC-0012', 4) | 0012 |
SUBSTRING(s, debut, longueur) | Extrait une partie (le premier caractère est en position 1) | SUBSTRING('PC-0012', 4, 2) | 00 |
CHAR_LENGTH(s) | Nombre de caractères | CHAR_LENGTH('Hélène') | 6 |
LENGTH(s) | Nombre d'octets | LENGTH('Hélène') | 8 |
TRIM(s), LTRIM, RTRIM | Retire les espaces au début et à la fin | TRIM(' Odoo ') | Odoo |
REPLACE(s, cherche, remplace) | Remplace toutes les occurrences | REPLACE('1099,00', ',', '.') | 1099.00 |
LPAD(s, n, c) | Complète à gauche jusqu'à n caractères | LPAD('7', 4, '0') | 0007 |
LOCATE(sous_chaine, s) | Position de la première occurrence, 0 si absente | LOCATE('@', 'a.b@c.fr') | 4 |
SELECT CONCAT(UPPER(nom), ' ', prenom) AS nom_complet,
CONCAT(LEFT(prenom, 1), '. ', nom) AS signature,
LOWER(CONCAT(LEFT(prenom, 1), nom)) AS identifiant
FROM employe
WHERE id_service = 2
ORDER BY nom;
| nom_complet | signature | identifiant |
|---|---|---|
| LEFÈVRE Julie | J. Lefèvre | jlefèvre |
| MARTIN Paul | P. Martin | pmartin |
| MOREL Karim | K. Morel | kmorel |
jlefèvre contient un accent, ce qu'un identifiant de connexion évite souvent : il faudrait un REPLACE par lettre accentuée, ou laisser l'application s'en charger.
LENGTH compte les octets : en UTF-8, é et è occupent deux octets chacun, d'où 8 pour « Hélène ». Pour contrôler la longueur d'une saisie, utilise CHAR_LENGTH.
Nombres
| Fonction | Rôle | Exemple | Résultat |
|---|---|---|---|
ROUND(x, d) | Arrondit à d décimales | ROUND(49.1667, 1) | 49.2 |
TRUNCATE(x, d) | Tronque à d décimales | TRUNCATE(49.1667, 1) | 49.1 |
CEIL(x), FLOOR(x) | Entier supérieur, entier inférieur | CEIL(4.1), FLOOR(4.9) | 5, 4 |
MOD(a, b) ou a % b | Reste de la division entière | MOD(130, 60) | 10 |
a DIV b | Quotient entier | 130 DIV 60 | 2 |
ABS(x) | Valeur absolue | ABS(-3) | 3 |
Avec MySQL, l'opérateur / renvoie toujours un décimal : 5 / 2 vaut 2.5000. Une division par zéro renvoie NULL avec un avertissement dans un SELECT.
SELECT id_intervention, duree_min,
CONCAT(duree_min DIV 60, ' h ', LPAD(duree_min MOD 60, 2, '0')) AS duree
FROM intervention
WHERE id_ticket = 9;
| id_intervention | duree_min | duree |
|---|---|---|
| 11 | 240 | 4 h 00 |
| 12 | 180 | 3 h 00 |
| 13 | 150 | 2 h 30 |
Dates et heures
| Fonction | Rôle | Exemple | Résultat |
|---|---|---|---|
NOW() | Date et heure courantes | NOW() | 2026-10-28 14:30:00 |
CURDATE() | Date du jour | CURDATE() | 2026-10-28 |
DATE(dt) | Partie date d'un DATETIME | DATE('2026-10-05 08:05:00') | 2026-10-05 |
YEAR(d), MONTH(d), DAY(d), HOUR(dt) | Une composante | MONTH('2026-10-05') | 10 |
EXTRACT(unité FROM d) | Une composante, forme normalisée | EXTRACT(YEAR FROM '2026-10-05') | 2026 |
DATEDIFF(fin, debut) | Écart en jours (les heures sont ignorées) | DATEDIFF('2026-10-28', '2026-10-01') | 27 |
TIMESTAMPDIFF(unité, debut, fin) | Écart en unités entières, arrondi vers zéro | TIMESTAMPDIFF(MINUTE, '2026-09-02 08:47', '2026-09-02 11:15') | 148 |
DATE_ADD(d, INTERVAL n unité) | Ajoute une durée | DATE_ADD('2026-10-28', INTERVAL 30 DAY) | 2026-11-27 |
DATE_SUB(d, INTERVAL n unité) | Retire une durée | DATE_SUB('2026-10-28', INTERVAL 5 YEAR) | 2021-10-28 |
DATE_FORMAT(d, format) | Met en forme | DATE_FORMAT('2026-10-05 08:05:00', '%d/%m/%Y %H:%i') | 05/10/2026 08:05 |
STR_TO_DATE(s, format) | Lit une date écrite autrement | STR_TO_DATE('14/02/2019', '%d/%m/%Y') | 2019-02-14 |
Les unités s'écrivent MINUTE, HOUR, DAY, WEEK, MONTH, YEAR. Dans DATE_FORMAT, %d est le jour, %m le mois, %Y l'année sur 4 chiffres, %H l'heure sur 24 h et %i les minutes. Pour des noms de jours et de mois en français, règle d'abord la langue de la session : SET lc_time_names = 'fr_FR'; puis DATE_FORMAT('2026-09-02', '%W %e %M %Y') donne mercredi 2 septembre 2026.
Le délai de résolution des tickets résolus, en minutes et en heures entières :
SELECT id_ticket,
TIMESTAMPDIFF(MINUTE, date_ouverture, date_cloture) AS minutes,
TIMESTAMPDIFF(HOUR, date_ouverture, date_cloture) AS heures
FROM ticket
WHERE statut = 'resolu'
ORDER BY id_ticket;
| id_ticket | minutes | heures |
|---|---|---|
| 1 | 148 | 2 |
| 2 | 1830 | 30 |
| 3 | 25 | 0 |
| 4 | 4740 | 79 |
| 5 | 2980 | 49 |
| 8 | 28 | 0 |
| 9 | 2065 | 34 |
| 10 | 75 | 1 |
Les postes de plus de 5 ans au 28 octobre 2026, c'est-à-dire achetés avant le 28 octobre 2021 :
SELECT num_inventaire, date_achat
FROM poste
WHERE date_achat < DATE_SUB('2026-10-28', INTERVAL 5 YEAR)
ORDER BY date_achat, num_inventaire;
Elle renvoie 8 postes : PC-0001, PC-0003, PC-0013 (2019), PC-0007, PC-0008, PC-0009 (2020), PC-0004 et PC-0005 (juin 2021).
Conversions
CAST(valeur AS type) et CONVERT(valeur, type) convertissent une valeur. Les types cibles de MySQL sont CHAR, SIGNED (entier), UNSIGNED, DECIMAL(p, s), DATE, DATETIME, TIME, DOUBLE et JSON.
SELECT CAST('2026-10-28' AS DATE) AS une_date,
CAST('42' AS SIGNED) + 1 AS un_entier,
CAST(1099 AS DECIMAL(8,2)) AS un_decimal;
| une_date | un_entier | un_decimal |
|---|---|---|
| 2026-10-28 | 43 | 1099.00 |
MySQL convertit aussi tout seul quand tu compares des types différents, et c'est souvent un piège. Comparer une chaîne à un nombre convertit la chaîne en nombre ; une chaîne qui ne commence pas par un chiffre vaut 0.
SELECT COUNT(*) FROM poste WHERE num_inventaire = 0;
| COUNT(*) |
|---|
| 15 |
Les 15 numéros d'inventaire commencent par PC- : convertis en nombre, ils valent tous 0. MySQL ne lève pas d'erreur, seulement des avertissements (SHOW WARNINGS; affiche Truncated incorrect DOUBLE value: 'PC-0001'). PostgreSQL refuse cette comparaison. Compare toujours une colonne à une valeur de son type : num_inventaire = 'PC-0001'.
Conditions : CASE, IF, COALESCE et NULLIF
CASE est la structure conditionnelle normalisée. Sa forme générale teste des conditions dans l'ordre et renvoie le résultat de la première vraie :
SELECT num_inventaire, date_achat,
TIMESTAMPDIFF(YEAR, date_achat, '2026-10-28') AS age,
CASE
WHEN TIMESTAMPDIFF(YEAR, date_achat, '2026-10-28') >= 5 THEN 'à renouveler'
WHEN TIMESTAMPDIFF(YEAR, date_achat, '2026-10-28') >= 3 THEN 'à surveiller'
ELSE 'récent'
END AS etat
FROM poste
WHERE id_employe IN (1, 2, 4, 6)
ORDER BY date_achat;
| num_inventaire | date_achat | age | etat |
|---|---|---|---|
| PC-0003 | 2019-02-14 | 7 | à renouveler |
| PC-0004 | 2021-06-03 | 5 | à renouveler |
| PC-0015 | 2022-11-08 | 3 | à surveiller |
| PC-0002 | 2023-01-10 | 3 | à surveiller |
| PC-0006 | 2024-03-18 | 2 | récent |
Sans ELSE, une ligne qui ne vérifie aucune condition reçoit NULL. La forme simple compare une expression à des valeurs : CASE priorite WHEN 'critique' THEN 1 WHEN 'haute' THEN 2 ... END. Elle sert notamment à trier selon un ordre métier :
SELECT id_ticket, titre, priorite
FROM ticket
WHERE statut <> 'resolu'
ORDER BY CASE priorite
WHEN 'critique' THEN 1
WHEN 'haute' THEN 2
WHEN 'normale' THEN 3
ELSE 4
END,
date_ouverture;
| id_ticket | titre | priorite |
|---|---|---|
| 13 | Lenteurs réseau à l'entrepôt | critique |
| 11 | Accès VPN impossible | haute |
| 7 | Installer Sage pour Paul Martin | normale |
| 14 | Batterie à remplacer | normale |
| 6 | Ventilateur bruyant | basse |
| 12 | Clavier cassé | basse |
Un ORDER BY priorite aurait trié par ordre alphabétique : basse, critique, haute, normale.
Les autres fonctions conditionnelles :
| Fonction | Rôle | Exemple |
|---|---|---|
COALESCE(a, b, ...) | Première valeur non NULL (norme SQL) | COALESCE(telephone, 'non renseigné') |
IFNULL(a, b) | b si a est NULL (MySQL et MariaDB) | IFNULL(nb_licences, 0) |
NULLIF(a, b) | NULL si a = b, sinon a | total / NULLIF(nb, 0) évite la division par zéro |
IF(condition, si_vrai, si_faux) | Condition courte (MySQL et MariaDB) | IF(date_cloture IS NULL, 'en attente', 'clos') |
Équivalences entre SGBD
| Besoin | MySQL et MariaDB | PostgreSQL | SQL Server |
|---|---|---|---|
| Concaténer | CONCAT(a, b) | a || b (ou concat(), qui ignore les NULL) | a + b ou CONCAT() |
| Longueur en caractères | CHAR_LENGTH(s) | char_length(s) ou length(s) | LEN(s) |
| Date du jour | CURDATE() | CURRENT_DATE | CAST(GETDATE() AS date) |
| Date et heure | NOW() | now() | GETDATE() ou SYSDATETIME() |
| Ajouter 7 jours | DATE_ADD(d, INTERVAL 7 DAY) | d + INTERVAL '7 days' | DATEADD(day, 7, d) |
| Écart en jours | DATEDIFF(fin, debut) | fin::date - debut::date | DATEDIFF(day, debut, fin) |
| Formater une date | DATE_FORMAT(d, '%d/%m/%Y') | to_char(d, 'DD/MM/YYYY') | FORMAT(d, 'dd/MM/yyyy') |
Remplacer NULL | IFNULL(x, 0) ou COALESCE | COALESCE(x, 0) | ISNULL(x, 0) ou COALESCE |
| Condition courte | IF(c, a, b) | CASE | IIF(c, a, b) |
| Premières lignes | LIMIT n | LIMIT n ou FETCH FIRST n ROWS ONLY | TOP (n) ou OFFSET … FETCH |
| Liste de valeurs d'un groupe | GROUP_CONCAT(x SEPARATOR ', ') | string_agg(x, ', ') | STRING_AGG(x, ', ') |
| Identifiant auto-incrémenté | AUTO_INCREMENT | GENERATED ALWAYS AS IDENTITY | IDENTITY(1,1) |
5 / 2 | 2.5000 | 2 | 2 |
| Comparaison sans casse | Par défaut (interclassement _ci) | ILIKE ou lower() | Selon l'interclassement, souvent insensible |
DATEDIFF existe en MySQL et en SQL Server, avec des arguments dans un ordre différent : DATEDIFF(fin, debut) d'un côté, DATEDIFF(day, debut, fin) de l'autre.
Exercices
Exercice 1 sur 4 Un annuaire présentable Réussi À revoir
Affiche, pour les employés du service commercial (3) et de la logistique (4), une colonne contact de la forme BENALI Sofiane - 01 99 00 30 01, ou PETIT Thomas - pas de numéro quand le téléphone manque. Trie par nom.
Voir la réponseMasquer la réponse de l'exercice 1
SELECT CONCAT(UPPER(nom), ' ', prenom, ' - ', COALESCE(telephone, 'pas de numéro')) AS contact
FROM employe
WHERE id_service IN (3, 4)
ORDER BY nom;
| contact |
|---|
| BENALI Sofiane - 01 99 00 30 01 |
| BONNET Inès - pas de numéro |
| FAURE Nadia - 01 99 00 40 01 |
| GIRARD Lucas - 01 99 00 40 02 |
| PETIT Thomas - pas de numéro |
| ROUSSEAU Léa - 01 99 00 40 03 |
| ROUX Camille - 01 99 00 30 02 |
Sans COALESCE, CONCAT renverrait NULL pour Inès Bonnet et Thomas Petit.
Tu avais trouvé ?
Exercice 2 sur 4 Ancienneté Réussi À revoir
Pour chaque employé de la logistique (service 4), affiche le nom, la date d'embauche au format JJ/MM/AAAA et l'ancienneté en années complètes au 28 octobre 2026, du plus ancien au plus récent.
Voir la réponseMasquer la réponse de l'exercice 2
SELECT nom,
DATE_FORMAT(date_embauche, '%d/%m/%Y') AS embauche,
TIMESTAMPDIFF(YEAR, date_embauche, '2026-10-28') AS anciennete
FROM employe
WHERE id_service = 4
ORDER BY date_embauche;
| nom | embauche | anciennete |
|---|---|---|
| Faure | 03/06/2013 | 13 |
| Girard | 20/02/2017 | 9 |
| Bonnet | 03/10/2022 | 4 |
| Rousseau | 10/06/2024 | 2 |
Trie sur la colonne date_embauche, pas sur l'alias embauche : la chaîne '03/06/2013' se trierait par jour, pas par date.
Tu avais trouvé ?
Exercice 3 sur 4 Délai de prise en charge Réussi À revoir
Pour les interventions du ticket 1, affiche l'heure de l'intervention au format HH:MM et le nombre de minutes écoulées depuis l'ouverture du ticket (le 2 septembre 2026 à 8 h 47).
Voir la réponseMasquer la réponse de l'exercice 3
SELECT id_intervention,
DATE_FORMAT(date_intervention, '%H:%i') AS heure,
TIMESTAMPDIFF(MINUTE, '2026-09-02 08:47:00', date_intervention) AS delai_min
FROM intervention
WHERE id_ticket = 1
ORDER BY date_intervention;
| id_intervention | heure | delai_min |
|---|---|---|
| 1 | 09:10 | 23 |
| 2 | 10:50 | 123 |
Écrire la date d'ouverture en dur est un pis-aller : une jointure avec ticket ira la chercher.
Tu avais trouvé ?
Exercice 4 sur 4 Logiciels libres ou payants Réussi À revoir
Affiche chaque logiciel avec une colonne licence qui vaut libre si nb_licences est NULL, sinon le nombre de licences suivi du mot « licences » (par exemple 12 licences). Trie par nom.
Voir la réponseMasquer la réponse de l'exercice 4
SELECT nom,
CASE
WHEN nb_licences IS NULL THEN 'libre'
ELSE CONCAT(nb_licences, ' licences')
END AS licence
FROM logiciel
ORDER BY nom;
| nom | licence |
|---|---|
| Acrobat Pro | 2 licences |
| Firefox | libre |
| KeePassXC | libre |
| LibreOffice | libre |
| Microsoft 365 | 12 licences |
| Odoo | 8 licences |
| Sage 100 Comptabilité | 3 licences |
| Wireshark | libre |
COALESCE(CONCAT(nb_licences, ' licences'), 'libre') donne le même résultat en exploitant le fait que CONCAT renvoie NULL. Le CASE est plus lisible.
Tu avais trouvé ?