Éliminer les Requêtes N+1 et Optimiser l'Indexation Multi-Locataires sous Forte Charge SaaS

Comment nous avons diagnostiqué les requêtes lentes, implémenté des index composés sur les tables multi-tenant et éradiqué les goulots d'étranglement N+1 en production.

Akram Hafaiedh
Akram Hafaiedh
Développeur Full Stack
2026-03-306 min de lecture
// BACKEND & PIPELINE ENGINEERING
PRODUCTION VERIFIED
TECHNICAL CASE STUDY

Éliminer les Requêtes N+1 et Optimiser l'Indexation Multi-Locataires sous Forte Charge SaaS

PHP 8.3QUEUESPARALLEL WORKERS

Optimisation des Requêtes et Indexation Multi-Tenant

À mesure que les bases de données SaaS multi-tenant atteignent des millions d'enregistrements, les requêtes non indexées et les problèmes de requêtes répétitives (N+1) dégradent fortement les performances.

Lors de la récupération de données filtrées par company_id (factures, mouvements, articles), l'absence d'index composés provoque un balayage complet de la table (full table scan). Voici comment nous avons optimisé l'exécution des requêtes SQL.


1. Concevoir des Index Composés pour la Multi-Location

Dans un schéma multi-tenant, les requêtes associent quasi systématiquement le champ company_id à d'autres filtres comme status ou created_at. Les index à colonne unique s'avèrent inefficaces.

php
class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// database/migrations/2026_03_30_000000_add_composite_indexes_to_invoices_table.php
use Illuminate\Database\Migrations\Migration;
use Illuminate\Database\Schema\Blueprint;
use Illuminate\Support\Facades\Schema;

return new class extends Migration
{
    public function up(): void
    {
        Schema::table('invoices', function (Blueprint $table) {
            class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// Index composé ordonné par (company_id, status, created_at)
            $table->index(['company_id', 'status', 'created_at'], 'idx_invoices_tenant_status_date');
            
            class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// Index composé pour les recherches par client
            $table->index(['company_id', 'customer_id'], 'idx_invoices_tenant_customer');
        });
    }

    public function down(): void
    {
        Schema::table('invoices', function (Blueprint $table) {
            $table->dropIndex('idx_invoices_tenant_status_date');
            $table->dropIndex('idx_invoices_tenant_customer');
        });
    }
};

2. Détecter et Éliminer les Requêtes N+1

Le problème N+1 survient lorsqu'une boucle parcourt une liste de modèles sans charger au préalable leurs relations (eager loading).

Code Problématique (N+1) :

php
class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// Exécute 1 requête pour les factures + N requêtes individuelles pour chaque client !
$invoices = Invoice::where('company_id', $companyId)->get();

foreach ($invoices as $invoice) {
    echo $invoice->customer->name; class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// Déclenche une requête SQL séparée par ligne !
}

Code Optimisé avec Chargement Préalable (Eager Loading) :

php
class=class="text-emerald-500 dark:text-emerald-400 font-medium">"text-slate-500 italic">// Exécute exactement 2 requêtes SQL optimisées quel que soit le nombre de factures !
$invoices = Invoice::where('company_id', $companyId)
    ->with(['customer:id,name,email', 'items:id,invoice_id,quantity,unit_price'])
    ->select(['id', 'company_id', 'customer_id', 'total_amount', 'status', 'created_at'])
    ->paginate(25);

3. Analyser les Plans d'Exécution avec EXPLAIN

En utilisant l'analyse EXPLAIN sous MySQL, vérifiez que vos index composés évitent le parcours complet de la table :

sql
EXPLAIN SELECT id, total_amount, status 
FROM invoices 
WHERE company_id = 42 AND status = 'PAID' 
ORDER BY created_at DESC;
  • Index utilisé : idx_invoices_tenant_status_date
  • Lignes balayées : Réduites de 450 000+ lignes à seulement 12 lignes.
  • Temps d'exécution : Chute spectaculaire de 1 450 ms à 4 ms !

L'intégration d'index composés et du chargement préalable des relations a éliminé les pics de latence de la base de données et garanti des temps de réponse sous les 50 ms.

Engineering Collaboration

Building a High-Scale SaaS or Complex System?

Let’s discuss your architecture, full-stack development, or system modernization needs.

Articles Similaires

// BACKEND & PIPELINE ENGINEERING
PRODUCTION VERIFIED
TECHNICAL CASE STUDY

Exportation de Données Massives : Éviter les Surcharges Mémoire avec Laravel & Vue 3

PHP 8.3QUEUESPARALLEL WORKERS
Backend
2026-03-25•Backend

Exportation de Données Massives : Éviter les Surcharges Mémoire avec Laravel & Vue 3

Read Article
// FRONTEND & UI RENDERING
PRODUCTION VERIFIED
TECHNICAL CASE STUDY

Moderniser un Moteur de Contenu : Migration d'un Blog Laravel Blade vers Vue 3 avec Recherche & Recommandations

VUE 3NEXT.JS 15TAILWIND
Frontend
2026-03-15•Frontend

Moderniser un Moteur de Contenu : Migration d'un Blog Laravel Blade vers Vue 3 avec Recherche & Recommandations

Read Article