Aller au contenu
Réussir mon BTS SIO

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

Par exemple

Chapitre 516 minDébutant

Écoconception logicielle : fonctionnalités, données et API

Par Josselyn C., prof de BTS SIO. Mis à jour le .

À la fin de ce chapitre, tu sauras

  • écrire des spécifications sobres et des terminaux cibles testables ;
  • éviter SELECT *, les requêtes N+1 et les conditions qui ignorent l'index ;
  • paginer et filtrer une API côté serveur ;
  • mettre en cache un calcul coûteux et limiter l'interrogation répétée ;
  • fixer et appliquer des durées de conservation des données.

4 exercices corrigés

L'écoconception logicielle applique la démarche à toute l'application, côté serveur compris : ne développer que les fonctionnalités utiles, ne transférer et ne stocker que les données nécessaires, et faire travailler le serveur et la base de données le moins possible pour le même service rendu. Ce chapitre s'adresse d'abord aux développeurs : spécifications, requêtes SQL, API, cache serveur, tâches de fond, durées de conservation et compatibilité avec les appareils anciens.

Spécifier sobrement

L'écoconception commence dans le cahier des charges, bien avant le code. Trois décisions y ont le plus d'effet.

Distinguer l'essentiel du confort. La méthode MoSCoW (must, should, could, won't) range chaque exigence par priorité (expression des besoins et cahier des charges). Une fonctionnalité « could » qui n'est pas développée ne coûte rien : ni serveur, ni maintenance, ni faille de sécurité.

Définir les terminaux cibles. Le RGESN demande de lister les profils de matériels qui doivent pouvoir utiliser le service (critère 2.1), puis de vérifier qu'il fonctionne sur d'anciens modèles (2.2) et d'anciennes versions de systèmes et de navigateurs (2.4). Écris-le comme une exigence testable : « L'application fonctionne sur un smartphone Android de six ans et sur les postes fixes du parc achetés en 2019, avec un navigateur à jour. » Sans cette exigence, chacun développe et teste sur sa machine récente.

Prévoir la connexion lente et le hors connexion. Un technicien d'entrepôt dans une zone mal couverte, un commercial dans le train : l'application doit rester utilisable en bas débit, et si possible enregistrer une saisie pour l'envoyer plus tard (critère 2.3).

Des requêtes SQL qui en font moins

La base de données est souvent le composant qui travaille le plus. Trois erreurs courantes multiplient son travail sans rien apporter. Les exemples utilisent la base parc du cours SQL.

1. Lire des colonnes inutiles. SELECT * renvoie toutes les colonnes, y compris celles que l'écran n'affiche pas. Nomme les colonnes dont tu as besoin : moins de données lues, transférées et gardées en mémoire, et une requête plus lisible.

2. Le problème N+1. Afficher la liste des postes avec le nom de leur utilisateur, en faisant une requête par poste, c'est 1 requête pour la liste plus N requêtes, une par ligne :

php
// À éviter : 1 requête pour les postes, puis 1 requête par poste affecté
$postes = $pdo->query('SELECT num_inventaire, id_employe FROM poste')->fetchAll();
$requete = $pdo->prepare('SELECT prenom, nom FROM employe WHERE id_employe = ?');
foreach ($postes as $poste) {
    if ($poste['id_employe'] === null) {
        continue;
    }
    $requete->execute([$poste['id_employe']]);
    $employe = $requete->fetch();
    echo $poste['num_inventaire'], ' : ', $employe['prenom'], ' ', $employe['nom'], PHP_EOL;
}

La base parc contient 15 postes, dont 13 affectés : ce code envoie 1 + 13 = 14 requêtes. Avec 2 000 postes, il en enverrait plus de 2 000, avec chaque fois un aller-retour réseau entre l'application et la base. Une jointure fait le même travail en une seule requête (jointures) :

php
// Une seule requête : la base fait la jointure
$sql = 'SELECT p.num_inventaire, e.prenom, e.nom
        FROM poste p
        LEFT JOIN employe e ON e.id_employe = p.id_employe
        ORDER BY p.num_inventaire';
foreach ($pdo->query($sql) as $ligne) {
    echo $ligne['num_inventaire'], ' : ', $ligne['prenom'] ?? '(en stock)', ' ', $ligne['nom'] ?? '', PHP_EOL;
}

Les ORM (Doctrine, Eloquent, Prisma) et les ODM comme Mongoose provoquent facilement ce problème sans que tu le voies : active le journal des requêtes en développement et compte-les.

3. Empêcher l'usage d'un index. Une condition qui applique une fonction à la colonne (WHERE YEAR(date_achat) = 2019) oblige la base à calculer la fonction pour chaque ligne, sans pouvoir utiliser l'index de la colonne. Écris plutôt une plage : WHERE date_achat >= '2019-01-01' AND date_achat < '2020-01-01'. Vérifie avec EXPLAIN (index et EXPLAIN).

Des API qui renvoient juste ce qu'il faut

Une API qui renvoie trop de données gaspille à chaque appel : sérialisation côté serveur, transfert, analyse côté client.

  • Paginer. Une route GET /api/tickets qui renvoie les 2 400 tickets de la base, à environ 1,2 Ko chacun en JSON, transfère près de 2,9 Mo pour un écran qui en affiche 20. Avec ?page=1&limite=20, la réponse pèse 24 Ko, 120 fois moins (pagination, filtres et versionnement).
  • Filtrer côté serveur. Le client ne télécharge pas tous les tickets pour garder ceux de son service : il demande ?service=COMPTA.
  • Choisir les champs. Une liste n'a pas besoin du compte rendu complet de chaque intervention ; le détail se charge à l'ouverture d'un ticket.
  • Compresser les réponses JSON (gzip ou Brotli), comme les fichiers statiques.

Le cache côté serveur

Le critère 7.1 du RGESN demande de mettre en cache côté serveur les données les plus demandées, pour ne pas les recalculer à chaque appel. C'est utile quand le calcul est coûteux et que le résultat change peu : un tableau de bord, des statistiques, une liste de référence.

js
// Statistiques du parc, recalculées au plus une fois toutes les dix minutes
const cache = new Map(); // clé -> { valeur, expire }

async function statistiquesParc() {
  const entree = cache.get('stats-parc');
  if (entree && entree.expire > Date.now()) {
    return entree.valeur; // pas de requête à la base
  }
  const valeur = await calculerStatistiques(); // plusieurs requêtes d'agrégat
  cache.set('stats-parc', { valeur, expire: Date.now() + 10 * 60 * 1000 });
  return valeur;
}

Si 200 personnes ouvrent le tableau de bord dans la même heure, la base fait au plus 6 calculs au lieu de 200. En production, on utilise plutôt un cache partagé entre les processus, comme Redis ou Valkey, mais le principe est le même. La difficulté est l'invalidation : il faut vider l'entrée quand les données changent vraiment, ou accepter un léger décalage, et le dire.

Les tâches de fond et l'interrogation répétée

L'interrogation répétée (polling) consiste à redemander au serveur, à intervalle fixe, si quelque chose a changé. Un tableau de bord ouvert par 50 personnes qui se rafraîchit toutes les 5 secondes envoie 50 × 720 = 36 000 requêtes par heure, presque toutes pour apprendre que rien n'a changé. Trois solutions :

  • allonger l'intervalle à ce qui est utile : une minute suffit pour des statuts de livraison ;
  • suspendre l'interrogation quand l'onglet n'est pas visible (l'événement visibilitychange du navigateur) ;
  • pousser l'information depuis le serveur quand elle change (WebSocket, server-sent events), ou recevoir un webhook d'un autre service (webhooks et WebSocket).

Les traitements lourds qui n'ont pas besoin d'être immédiats (exports, rapports, recalculs, sauvegardes) se programment en dehors des heures de pointe et regroupés, plutôt qu'à chaque action d'un utilisateur. Le critère 8.10 du RGESN va plus loin : tenir compte de contraintes externes, comme l'intensité carbone de l'électricité au moment du calcul.

Les données : collecter moins, garder moins longtemps

Les données ont un coût environnemental permanent : chaque octet stocké l'est sur un disque qui consomme, puis dans chaque sauvegarde, souvent en plusieurs copies. Deux principes, qui sont aussi des obligations du RGPD, réduisent ce coût.

La minimisation. Ne collecte que les données nécessaires à la finalité (critère 1.6 du RGESN, article 5 du RGPD). Un formulaire de suivi de colis n'a besoin ni de la date de naissance ni du numéro de téléphone du visiteur.

La durée de conservation. Chaque donnée a une durée de vie, fixée à l'avance, au terme de laquelle elle est supprimée ou archivée (critère 7.2 du RGESN). Une tâche planifiée applique la règle. Si la base parc avait une table journal_acces des connexions, conservée douze mois selon la politique de l'organisation, MySQL ferait la purge chaque nuit :

sql
-- Purge quotidienne des connexions de plus de douze mois
CREATE EVENT purger_journal_acces
  ON SCHEDULE EVERY 1 DAY STARTS '2026-11-09 03:00:00'
  DO DELETE FROM journal_acces
     WHERE date_acces < NOW() - INTERVAL 12 MONTH;

Le planificateur d'événements est actif par défaut dans MySQL 8.4 ; dans MariaDB, il faut l'activer (event_scheduler = ON). Le détail des événements et des procédures est dans le cours Procédures stockées et triggers.

Les données froides. Les données rarement consultées (tickets clos depuis trois ans, anciennes photos) peuvent quitter le stockage rapide pour un stockage d'archive, moins coûteux en énergie (critère 8.8). Et chaque copie doit avoir une raison : les sauvegardes, oui, avec une rétention définie (rétention des sauvegardes) ; dix exports oubliés sur un partage réseau, non (critère 8.9).

Les fichiers envoyés par les utilisateurs. Une photo de colis abîmé prise avec un smartphone fait souvent plusieurs mégaoctets. Indique le poids et le format attendus avant l'envoi (critère 4.11), redimensionne côté navigateur ou côté serveur à la taille utile, et fixe sa durée de conservation.

Durer : mises à jour, dépendances, compatibilité

Un logiciel sobre est aussi un logiciel qui dure, et qui laisse durer les appareils.

  • Séparer les mises à jour correctives des évolutions (critère 3.5) : l'utilisateur doit pouvoir installer un correctif de sécurité sans subir une nouvelle version plus lourde.
  • Livrer des mises à jour incrémentielles (critère 3.6) plutôt que de faire retélécharger toute l'application.
  • Surveiller les dépendances. Chaque bibliothèque ajoutée pèse, se met à jour et peut porter une faille. Avant npm install, demande-toi si quelques lignes de code natif ne suffisent pas ; après, retire celles qui ne servent plus.
  • Maintenir la compatibilité avec les versions de systèmes encore répandues, au lieu d'exiger la dernière.
  • Mesurer en continu. Un test automatique qui échoue quand le poids d'une page ou le nombre de requêtes dépasse un budget fixé empêche les régressions (tests automatisés dans la CI). Le RGESN prévoit aussi une revue de conception et de code qui vérifie l'impact environnemental de chaque fonctionnalité (critère 2.6).

Exercices

Exercice 1 sur 4 Compter les requêtes N+1 Réussi À revoir

  1. Dans la base parc, combien de requêtes envoie la première version du code PHP (la boucle) ? Et la version avec jointure ?
  2. Après une fusion, le parc compte 400 postes, dont 370 affectés. Reprends le calcul.
  3. Chaque requête coûte environ 2 ms d'aller-retour entre l'application et la base. Quel temps d'attente la boucle ajoute-t-elle à l'affichage, par rapport à la jointure ?
Voir la réponseMasquer la réponse de l'exercice 1
  1. Boucle : 1 requête pour la liste + 13 pour les 13 postes affectés = 14 requêtes. Jointure : 1 requête.
  2. Boucle : 1 + 370 = 371 requêtes. Jointure : toujours 1 requête.
  3. 371 × 2 ms = 742 ms pour la boucle, contre environ 2 ms pour la jointure : la boucle ajoute environ 0,74 seconde à chaque affichage, et autant de travail au serveur de base de données.

Tu avais trouvé ?

Exercice 2 sur 4 Interroger moins souvent Réussi À revoir

Le tableau de bord des expéditions est ouvert toute la journée (8 heures) sur 8 écrans de Sorbier : les postes des quatre salariés de la logistique et des trois commerciaux, et un écran au quai de chargement. Il interroge l'API toutes les 5 secondes.

  1. Combien de requêtes reçoit l'API par jour ?
  2. Le responsable logistique confirme qu'une mise à jour par minute suffit. Combien de requêtes reste-t-il, et quel pourcentage est économisé ?
  3. On suspend en plus l'interrogation quand l'onglet n'est pas visible. En moyenne, l'onglet est à l'écran 25 % du temps. Combien de requêtes par jour ?
Voir la réponseMasquer la réponse de l'exercice 2
  1. Toutes les 5 secondes, c'est 3 600 / 5 = 720 requêtes par heure et par écran : 8 × 720 × 8 = 46 080 requêtes par jour.
  2. Une par minute : 8 × 60 × 8 = 3 840 requêtes. Économie : 1 − 3 840 / 46 080 ≈ 0,917, soit 91,7 %.
  3. 3 840 × 0,25 = 960 requêtes par jour, 48 fois moins qu'au départ, pour le même service rendu.

Tu avais trouvé ?

Exercice 3 sur 4 Trouver les postes à surveiller Réussi À revoir

Le service informatique de Sorbier veut la liste des postes achetés il y a six ans ou plus, au 8 novembre 2026, avec le nom de leur utilisateur, pour décider lesquels prolonger (chapitre 6).

  1. Écris la requête sur la base parc, en gardant les postes en stock.
  2. Donne le résultat.
  3. Réécris la condition pour qu'elle puisse utiliser un index sur date_achat.
Voir la réponseMasquer la réponse de l'exercice 3
  1. Une jointure externe garde les postes sans utilisateur :
sql
SELECT p.num_inventaire, p.modele, p.date_achat,
       TIMESTAMPDIFF(YEAR, p.date_achat, '2026-11-08') AS age_ans,
       e.prenom, e.nom
FROM poste p
LEFT JOIN employe e ON e.id_employe = p.id_employe
WHERE TIMESTAMPDIFF(YEAR, p.date_achat, '2026-11-08') >= 6
ORDER BY p.date_achat, p.num_inventaire;
  1. Six postes, tous des postes fixes :
num_inventairemodeledate_achatage_ansprenomnom
PC-0001OptiPlex 70102019-02-147JulieLefèvre
PC-0003OptiPlex 70102019-02-147KarimMorel
PC-0013OptiPlex 70102019-02-147NULLNULL
PC-0007ProDesk 4002020-09-226NadiaFaure
PC-0008ProDesk 4002020-09-226LucasGirard
PC-0009ProDesk 4002020-09-226LéaRousseau
  1. WHERE p.date_achat <= '2026-11-08' - INTERVAL 6 YEAR, c'est-à-dire date_achat <= '2020-11-08' : la colonne reste nue, la base peut parcourir l'index. Le résultat est le même. En production, remplace la date fixe par CURDATE().

Tu avais trouvé ?

Exercice 4 sur 4 Revue de code écoconçue Réussi À revoir

Une collègue propose cette route Express pour l'écran « mes tickets » :

js
app.get('/api/tickets', async (req, res) => {
  const tickets = await db.query('SELECT * FROM ticket');
  res.json(tickets.filter(t => t.id_demandeur === req.user.id));
});

Relève trois problèmes et propose une version corrigée.

Voir la réponseMasquer la réponse de l'exercice 4
  1. Toutes les colonnes sont lues (SELECT *) alors que la liste n'en affiche que quelques-unes.
  2. Tous les tickets de l'entreprise sont lus puis filtrés en JavaScript : la base fait un travail inutile, et l'application charge en mémoire les tickets des autres, ce qui pose aussi un problème de confidentialité si le filtre est oublié un jour.
  3. Pas de pagination : un utilisateur ancien reçoit tout son historique à chaque affichage.
js
app.get('/api/tickets', async (req, res) => {
  const limite = Math.min(Number(req.query.limite) || 20, 100);
  const page = Math.max(Number(req.query.page) || 1, 1);
  const tickets = await db.query(
    `SELECT id_ticket, titre, date_ouverture, statut, priorite
     FROM ticket
     WHERE id_demandeur = ?
     ORDER BY date_ouverture DESC
     LIMIT ? OFFSET ?`,
    [req.user.id, limite, (page - 1) * limite]
  );
  res.json(tickets);
});

Le filtre est dans la requête préparée, la base ne renvoie que les colonnes et la page utiles, et la limite est plafonnée à 100 pour qu'un client ne puisse pas tout demander d'un coup.

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 : Sobriété des infrastructures : virtualisation, PUE, durée de vie et DEEERevoir le sommaire du cours