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

@ -0,0 +1,63 @@
<?php
use App\Models\Classe;
use App\Models\Correction;
use App\Models\Eleve;
use App\Models\Lab;
use App\Models\LabSession;
use App\Models\Observation;
use App\Models\Seance;
use App\Models\User;
use Illuminate\Support\Facades\DB;
/**
* Bloc B, optimisation MariaDB (2026-10-14) : « Résultats » faisait une
* requête de corrections et une d'observations PAR classe — un N+1
* classique. Vérifié en comptant les vraies requêtes SQL, pas en devinant :
* le nombre doit rester le même, que le prof ait 1 classe ou 5.
*/
function resultatsPourProfAvecNClasses($test, int $nombreClasses): int
{
$prof = User::factory()->create();
for ($i = 0; $i < $nombreClasses; $i++) {
$classe = Classe::factory()->create(['user_id' => $prof->id]);
$eleve = Eleve::factory()->create(['classe_id' => $classe->id]);
$seance = Seance::create(['classe_id' => $classe->id, 'lab_id' => Lab::factory()->create()->id, 'statut' => Seance::TERMINEE]);
$labSession = LabSession::create(['seance_id' => $seance->id, 'eleve_id' => $eleve->id, 'lab_id' => $seance->lab_id, 'etat' => LabSession::TERMINE]);
Correction::create(['lab_session_id' => $labSession->id, 'reussi' => true, 'note' => 8, 'note_max' => 10, 'details' => []]);
Observation::create(['classe_id' => $classe->id, 'eleve_id' => $eleve->id, 'user_id' => $prof->id, 'contenu' => 'Observation de test']);
}
$requetes = 0;
DB::listen(function () use (&$requetes) {
$requetes++;
});
prof($test, $prof)->get(route('resultats'))->assertOk();
return $requetes;
}
test('le nombre de requêtes de /resultats ne dépend pas du nombre de classes (pas de N+1)', function () {
$requetesUneClasse = resultatsPourProfAvecNClasses($this, 1);
$requetesCinqClasses = resultatsPourProfAvecNClasses($this, 5);
expect($requetesCinqClasses)->toBe($requetesUneClasse);
});
test('les corrections et observations affichées restent correctes après le regroupement en lot', function () {
$prof = User::factory()->create();
$classeA = Classe::factory()->create(['user_id' => $prof->id, 'nom' => 'Classe A']);
$classeB = Classe::factory()->create(['user_id' => $prof->id, 'nom' => 'Classe B']);
$seanceA = Seance::create(['classe_id' => $classeA->id, 'lab_id' => Lab::factory()->create()->id, 'statut' => Seance::TERMINEE]);
$eleveA = Eleve::factory()->create(['classe_id' => $classeA->id, 'prenom' => 'Alice']);
$labSessionA = LabSession::create(['seance_id' => $seanceA->id, 'eleve_id' => $eleveA->id, 'lab_id' => $seanceA->lab_id, 'etat' => LabSession::TERMINE]);
Correction::create(['lab_session_id' => $labSessionA->id, 'reussi' => true, 'note' => 9, 'note_max' => 10, 'details' => []]);
Observation::create(['classe_id' => $classeA->id, 'eleve_id' => $eleveA->id, 'user_id' => $prof->id, 'contenu' => 'Bonne participation']);
// Classe B : aucune activité, ne doit jamais récupérer les données de la classe A.
$page = prof($this, $prof)->get(route('resultats'));
$page->assertOk()->assertSeeInOrder(['Classe A', '1 correction(s)', 'Bonne participation', 'Classe B', '0 correction(s)']);
});