optimise MariaDB : deux index confirmés par EXPLAIN, N+1 supprimé

Jeu de données de charge (500 classes, 20 000 élèves, 30 000 sessions
de lab) généré localement contre une MariaDB jetable, jamais en
production, via la nouvelle commande charge:generer (refuse de
tourner si APP_ENV=production).

EXPLAIN confirme deux manques réels : lab_sessions.etat (30 056 lignes
parcourues sans index, lu chaque minute par l'expiration des sessions
et le self-healing) et classes.archivee_a (lu chaque jour par la purge
RGPD). devoirs.echeance n'avait besoin de rien : l'index existant sur
classe_id suffisait déjà, EXPLAIN l'a confirmé plutôt que d'ajouter un
index inutile.

ResultatsController faisait une requête de corrections et une
d'observations par classe du prof (N+1) : remplacé par une requête de
chaque pour toutes les classes, regroupées en mémoire.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
Adam Belkacemi 2026-10-14 09:31:11 +02:00
parent ca544ac620
commit e35e0e3c0b
5 changed files with 305 additions and 6 deletions

View file

@ -1125,3 +1125,26 @@ La leçon : un test local qui passe ne prouve rien sur un comportement qui dépe
**Suite**
Prochain chantier du bloc B : optimisation MariaDB (index, `EXPLAIN`, requêtes N+1, jeu de données de charge).
## 2026-10-14 — Optimisation MariaDB (bloc B)
Dernier chantier annoncé du bloc B pour l'instant : index vérifiés par `EXPLAIN`, requêtes N+1 supprimées, sur un vrai jeu de données de charge — jamais deviné, jamais sur la base de production.
**Fait**
- `php artisan charge:generer` : nouvelle commande, refuse de tourner si `APP_ENV=production`. Remplit une base avec des classes, élèves, séances, sessions de lab, devoirs et rendus, en insertions par lot (pas un `Hash::make()` par élève — 20 000 hachages individuels auraient pris des minutes). Lancée en local contre un conteneur MariaDB 11 jetable (même version qu'en production), jamais contre le serveur réel : 20 000 lignes de test géneraient les vrais profs pendant la mesure, pour un bénéfice qui ne demande pas d'y toucher.
- Jeu généré : 500 classes, 20 000 élèves, 1 500 séances, 30 000 sessions de lab, 1 000 devoirs, 13 000 rendus — l'échelle de référence du CLAUDE.md.
- `EXPLAIN` sur les requêtes qui filtrent par statut/état/archivage a confirmé deux vrais manques (et écarté une fausse piste) :
- `lab_sessions.etat` (lu chaque minute par `labs:expirer-sessions` et la surveillance du self-healing) : parcours complet de la table, 30 056 lignes examinées. Avec l'index : 1 161 lignes (-96 %).
- `classes.archivee_a` (lu chaque jour par la purge RGPD) : 500 lignes examinées sans index, 1 avec.
- `devoirs.echeance` : pas touché. L'index existant sur `classe_id` suffisait déjà (2 lignes examinées, la requête filtre d'abord par classe) — `EXPLAIN` a évité d'ajouter un index inutile.
- N+1 supprimé dans `ResultatsController` (« Résultats », un prof avec plusieurs classes) : une requête de corrections et une d'observations étaient lancées par classe. Remplacé par une requête de chaque pour **toutes** les classes du prof, regroupées en mémoire — 4 requêtes au total, qu'il y ait 1 classe ou 50.
- 2 nouveaux tests Pest (117 au total) : le nombre de requêtes de `/resultats` ne bouge pas avec 1 ou 5 classes (compté via `DB::listen()`, pas deviné) ; les données restent correctement associées à la bonne classe après le regroupement en mémoire.
**Comprendre**
Deux fois dans ce chantier, mesurer plutôt que deviner a changé la décision : `devoirs.echeance` avait l'air d'un candidat évident à un index composite, `EXPLAIN` a montré que non. À l'inverse, `lab_sessions.etat` n'avait rien d'alarmant en lisant juste le code — c'est le volume réel (30 000 lignes, après des mois d'usage) qui rend le manque d'index coûteux, invisible sur les quelques lignes qu'on a aujourd'hui en production.
**Suite**
Déployer la migration d'index et le correctif N+1, vérifier en réel, puis reprendre le dernier point du bloc B : stockage S3 des travaux d'élèves.