Aller au contenu
Réussir mon BTS SIO

Tape une notion, une commande ou le nom d'une épreuve.

Par exemple

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 NULL par une valeur avec COALESCE.

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

FonctionRôleExempleRésultat
CONCAT(a, b, ...)ConcatèneCONCAT('PC', '-', '0001')PC-0001
CONCAT_WS(sep, a, b, ...)Concatène avec un séparateur, en ignorant les NULLCONCAT_WS(' ', 'Karim', NULL, 'Morel')Karim Morel
UPPER(s), LOWER(s)Majuscules, minusculesUPPER('Lefèvre')LEFÈVRE
LEFT(s, n), RIGHT(s, n)Les n premiers ou derniers caractèresRIGHT('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èresCHAR_LENGTH('Hélène')6
LENGTH(s)Nombre d'octetsLENGTH('Hélène')8
TRIM(s), LTRIM, RTRIMRetire les espaces au début et à la finTRIM(' Odoo ')Odoo
REPLACE(s, cherche, remplace)Remplace toutes les occurrencesREPLACE('1099,00', ',', '.')1099.00
LPAD(s, n, c)Complète à gauche jusqu'à n caractèresLPAD('7', 4, '0')0007
LOCATE(sous_chaine, s)Position de la première occurrence, 0 si absenteLOCATE('@', 'a.b@c.fr')4
sql
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_completsignatureidentifiant
LEFÈVRE JulieJ. Lefèvrejlefèvre
MARTIN PaulP. Martinpmartin
MOREL KarimK. Morelkmorel

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

FonctionRôleExempleRésultat
ROUND(x, d)Arrondit à d décimalesROUND(49.1667, 1)49.2
TRUNCATE(x, d)Tronque à d décimalesTRUNCATE(49.1667, 1)49.1
CEIL(x), FLOOR(x)Entier supérieur, entier inférieurCEIL(4.1), FLOOR(4.9)5, 4
MOD(a, b) ou a % bReste de la division entièreMOD(130, 60)10
a DIV bQuotient entier130 DIV 602
ABS(x)Valeur absolueABS(-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.

sql
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_interventionduree_minduree
112404 h 00
121803 h 00
131502 h 30

Dates et heures

FonctionRôleExempleRésultat
NOW()Date et heure courantesNOW()2026-10-28 14:30:00
CURDATE()Date du jourCURDATE()2026-10-28
DATE(dt)Partie date d'un DATETIMEDATE('2026-10-05 08:05:00')2026-10-05
YEAR(d), MONTH(d), DAY(d), HOUR(dt)Une composanteMONTH('2026-10-05')10
EXTRACT(unité FROM d)Une composante, forme normaliséeEXTRACT(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éroTIMESTAMPDIFF(MINUTE, '2026-09-02 08:47', '2026-09-02 11:15')148
DATE_ADD(d, INTERVAL n unité)Ajoute une duréeDATE_ADD('2026-10-28', INTERVAL 30 DAY)2026-11-27
DATE_SUB(d, INTERVAL n unité)Retire une duréeDATE_SUB('2026-10-28', INTERVAL 5 YEAR)2021-10-28
DATE_FORMAT(d, format)Met en formeDATE_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 autrementSTR_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 :

sql
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_ticketminutesheures
11482
2183030
3250
4474079
5298049
8280
9206534
10751

Les postes de plus de 5 ans au 28 octobre 2026, c'est-à-dire achetés avant le 28 octobre 2021 :

sql
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.

sql
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_dateun_entierun_decimal
2026-10-28431099.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.

sql
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 :

sql
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_inventairedate_achatageetat
PC-00032019-02-147à renouveler
PC-00042021-06-035à renouveler
PC-00152022-11-083à surveiller
PC-00022023-01-103à surveiller
PC-00062024-03-182ré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 :

sql
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_tickettitrepriorite
13Lenteurs réseau à l'entrepôtcritique
11Accès VPN impossiblehaute
7Installer Sage pour Paul Martinnormale
14Batterie à remplacernormale
6Ventilateur bruyantbasse
12Clavier cassébasse

Un ORDER BY priorite aurait trié par ordre alphabétique : basse, critique, haute, normale.

Les autres fonctions conditionnelles :

FonctionRôleExemple
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 atotal / 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

BesoinMySQL et MariaDBPostgreSQLSQL Server
ConcaténerCONCAT(a, b)a || b (ou concat(), qui ignore les NULL)a + b ou CONCAT()
Longueur en caractèresCHAR_LENGTH(s)char_length(s) ou length(s)LEN(s)
Date du jourCURDATE()CURRENT_DATECAST(GETDATE() AS date)
Date et heureNOW()now()GETDATE() ou SYSDATETIME()
Ajouter 7 joursDATE_ADD(d, INTERVAL 7 DAY)d + INTERVAL '7 days'DATEADD(day, 7, d)
Écart en joursDATEDIFF(fin, debut)fin::date - debut::dateDATEDIFF(day, debut, fin)
Formater une dateDATE_FORMAT(d, '%d/%m/%Y')to_char(d, 'DD/MM/YYYY')FORMAT(d, 'dd/MM/yyyy')
Remplacer NULLIFNULL(x, 0) ou COALESCECOALESCE(x, 0)ISNULL(x, 0) ou COALESCE
Condition courteIF(c, a, b)CASEIIF(c, a, b)
Premières lignesLIMIT nLIMIT n ou FETCH FIRST n ROWS ONLYTOP (n) ou OFFSET … FETCH
Liste de valeurs d'un groupeGROUP_CONCAT(x SEPARATOR ', ')string_agg(x, ', ')STRING_AGG(x, ', ')
Identifiant auto-incrémentéAUTO_INCREMENTGENERATED ALWAYS AS IDENTITYIDENTITY(1,1)
5 / 22.500022
Comparaison sans cassePar 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
sql
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
sql
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;
nomembaucheanciennete
Faure03/06/201313
Girard20/02/20179
Bonnet03/10/20224
Rousseau10/06/20242

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
sql
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_interventionheuredelai_min
109:1023
210:50123

É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
sql
SELECT nom,
       CASE
         WHEN nb_licences IS NULL THEN 'libre'
         ELSE CONCAT(nb_licences, ' licences')
       END AS licence
FROM logiciel
ORDER BY nom;
nomlicence
Acrobat Pro2 licences
Firefoxlibre
KeePassXClibre
LibreOfficelibre
Microsoft 36512 licences
Odoo8 licences
Sage 100 Comptabilité3 licences
Wiresharklibre

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é ?

Chapitre terminé !

Fais les 4 exercices avant de passer à la suite : c'est en t'entraînant que tout se fixe.

chapitres
–
de série
–
du cours
–
Chapitre suivant : COUNT, GROUP BY et HAVINGRevoir le sommaire du cours