Ma Base de Données Était "Correcte". Elle Était Aussi 296 Fois Trop Lente.
J'ai découvert que ma base de données Postgres avait 89 clés étrangères mais zéro index dessus — transformant des requêtes en millisecondes en cauchemars de 843 ms qui ont failli tuer mon lancement en alpha.
25 octobre 2025. Mon application SaaS était enfin feature-complete. Neuf fonctionnalités majeures, architecture multi-tenant, apprentissage progressif — tout fonctionnait. Mais c'était lent. Vraiment lent.
Les requêtes du tableau de bord ramaient. Des listes qui auraient dû se charger instantanément prenaient plusieurs secondes. Les testeurs alpha commençaient à demander "Le site est cassé ?"
Ce n'était pas qu'une question de performance. Je me demandais si j'allais survivre.
J'ai passé deux jours à optimiser des requêtes, à réécrire des politiques RLS, à ajouter du cache. Rien ne faisait de différence. La frustration montait — 5 jours avant mon lancement alpha prévu et je ne comprenais pas pourquoi une base de données techniquement correcte était aussi lente.
Puis j'ai exécuté une requête de diagnostic qui m'a glacé le sang.
89 clés étrangères. Zéro index.
Voici l'histoire d'un "détail" Postgres que personne ne mentionne — les 2 semaines durant lesquelles ça a failli tuer mon lancement alpha — et le correctif en 4 minutes qui a tout sauvé.
Les enjeux : quand lent signifie mort
La recherche montre que chaque 100 ms de latence coûte 1 % de conversions. Avec des temps de chargement de 2 à 3 secondes, je ne livrais pas qu'une mauvaise expérience — je détruisais les premières impressions qui feraient ou briseraient mon alpha.
Les enjeux immédiats :
- Des testeurs alpha qui questionnaient si la plateforme était assez stable pour la recommander
- Une crédibilité perdue avec les premiers utilisateurs qui m'avaient accordé leur confiance
- Si ça persistait jusqu'au lancement complet : la recherche du secteur suggère un taux de rebond de 40 % à 3 secondes de chargement
- La différence entre "c'est prometteur" et "ça semble cassé"
- Le CPU de la base de données à 80-90 % (coûteux et ça empirait)
- 5 jours avant le lancement prévu, complètement bloqué
Ce que ces 2 semaines ont vraiment coûté :
- 40+ heures à déboguer des requêtes lentes, à profiler du code, à blâmer les politiques RLS
- 1 semaine de retard au lancement (manqué ma deadline interne)
- Opportunité perdue de construire des fonctionnalités
- Dommages de crédibilité avec les premiers testeurs
La dette technique devenait une dette commerciale. Et je ne savais pas pourquoi.
---
L'investigation : suivre les requêtes lentes
Fin octobre, ma plateforme avait une vraie complexité : 30+ tables, 80+ politiques RLS, des centaines de relations de clés étrangères. Des opérations simples qui auraient dû être instantanées étaient douloureusement lentes — chargement de listes, récupération des données du tableau de bord, filtrage par `org_id`.
J'ai commencé avec `pg_stat_statements` pour trouver les coupables :
```sql
SELECT query, mean_exec_time, calls
FROM pg_stat_statements
ORDER BY mean_exec_time DESC
LIMIT 10;
```
Chaque requête lente avait le même pattern — filtrage par clés étrangères :
```sql
SELECT * FROM user_data
WHERE org_id = '...' AND resource_id = '...';
```
Ça devrait être instantané. J'avais ces colonnes. J'avais des politiques RLS. Pourquoi c'était lent ?
J'ai vérifié le plan d'exécution :
```sql
EXPLAIN ANALYZE
SELECT * FROM user_data
WHERE org_id = 'abc-123' AND resource_id = 'xyz-789';
```
Le résultat m'a glacé le sang :
```
Seq Scan on user_data (cost=0.00..2847.23 rows=15 width=1024)
Filter: ((org_id = 'abc-123') AND (resource_id = 'xyz-789'))
Rows Removed by Filter: 12,834
Execution Time: 843.271 ms
```
Scan séquentiel. Postgres lisait chaque ligne de la table, puis filtrait. Sur une requête qui devrait prendre des millisecondes.
Mais j'avais des clés étrangères sur `org_id` et `resource_id`. Est-ce qu'elles ne devraient pas être indexées ?
---
La révélation : les clés étrangères ne créent pas d'index automatiquement
Voici ce que j'ai appris ce jour-là, à la dure :
Postgres crée automatiquement des index pour les PRIMARY KEY et les contraintes UNIQUE.
Postgres ne crée PAS automatiquement d'index pour les clés étrangères.
Laisse-moi répéter ça, parce que ce seul malentendu m'a coûté 2 semaines de ma vie :
CLÉS ÉTRANGÈRES ≠ INDEX
Quand tu écris ça :
```sql
CREATE TABLE user_data (
id UUID PRIMARY KEY, -- ✅ Indexé automatiquement
org_id UUID REFERENCES organizations(id), -- ❌ PAS indexé !
resource_id UUID REFERENCES resources(id), -- ❌ PAS indexé !
created_at TIMESTAMPTZ
);
```
Postgres crée la contrainte de clé étrangère (intégrité référentielle), mais il ne crée pas d'index sur `org_id` ou `resource_id`.
Pourquoi ? Parce que Postgres ne peut pas supposer comment tu vas interroger les données. Peut-être que tu ne filtres jamais par clés étrangères. Peut-être que tu fais toujours des jointures dans une direction précise. Il te laisse donc prendre la décision.
Le problème ? Je ne savais pas que je devais prendre cette décision. Je supposais que "clé étrangère" signifiait "indexé pour les requêtes". Ce n'est pas le cas.
---
Le diagnostic : à quel point c'était grave ?
J'ai écrit une requête pour trouver chaque clé étrangère sans index :
```sql
SELECT
c.conrelid::regclass AS table_name,
a.attname AS column_name
FROM pg_constraint c
JOIN pg_attribute a ON a.attnum = ANY(c.conkey) AND a.attrelid = c.conrelid
WHERE c.contype = 'f'
AND NOT EXISTS (
SELECT 1 FROM pg_index i
WHERE i.indrelid = c.conrelid AND a.attnum = ANY(i.indkey)
);
```
Le résultat m'a percuté comme un train de marchandises :
89 lignes.
89 clés étrangères sur 32 tables. Zéro index.
Chaque jointure, chaque filtre, chaque vérification de politique RLS faisait un scan complet de table. Pas étonnant que tout soit lent.
---
Le correctif : l'Apocalypse des Index
25 octobre 2025. Je savais ce que je devais faire.
Une migration. 89 index. Je l'ai appelée "L'Apocalypse des Index."
```sql
-- La migration Apocalypse des Index
-- Objectif : Corriger les performances en indexant toutes les colonnes de clés étrangères
-- Tables scopées organisation
CREATE INDEX idx_users_org_id ON users(org_id);
CREATE INDEX idx_resources_org_id ON resources(org_id);
CREATE INDEX idx_documents_org_id ON documents(org_id);
-- Tables scopées ressource
CREATE INDEX idx_user_data_resource_id ON user_data(resource_id);
CREATE INDEX idx_tasks_resource_id ON tasks(resource_id);
-- Index multi-colonnes pour les patterns de requête courants
CREATE INDEX idx_user_data_org_resource ON user_data(org_id, resource_id);
-- Requêtes soft-delete
CREATE INDEX idx_resources_org_archived ON resources(org_id, archived_at);
-- Total : 89 index sur 32 tables
```
J'ai lancé la migration. Ma main a littéralement hésité au-dessus de la touche Entrée pendant un moment, en me demandant si j'allais aggraver les choses. T.T
Temps de migration : 4 minutes.
---
Les résultats : de la catastrophe au lancement prêt
Avant (sans index)
```sql
Seq Scan on user_data
Execution Time: 843.271 ms
Rows Removed by Filter: 12,834
```
Après (avec index)
```sql
Index Scan using idx_user_data_org_resource
Execution Time: 2.847 ms
```
843 ms → 2,8 ms
C'est 296 fois plus rapide pour une seule requête.
Impact réel :
- Chargement du tableau de bord : 2-3 secondes → 120 ms
- Listes de ressources : 1+ seconde → 45 ms
- Requêtes de données : 850 ms → 12 ms
Amélioration moyenne : 20 à 40 fois plus rapide. Certaines jointures complexes avec plusieurs clés étrangères ? 100 fois plus rapides.
La différence était de nuit et de jour. La plateforme est passée de "C'est cassé ?" à "Waouh, c'est rapide." :D
---
Pourquoi c'est critique pour les SaaS multi-tenant
Si tu construis un SaaS multi-tenant avec Row-Level Security, c'est absolument critique.
Les politiques RLS s'exécutent sur chaque requête :
```sql
CREATE POLICY resources_org_isolation ON resources
USING (org_id = get_user_org_id());
```
Sans index sur `org_id`, cette politique force un scan séquentiel sur chaque requête. Même des SELECT simples deviennent douloureusement lents.
Le coût : Isolation multi-tenant sans index = infrastructure coûteuse + mauvaise expérience utilisateur = churn.
La leçon commerciale : Des fonctionnalités de sécurité sans optimisation des performances ne sont pas réellement sécurisées — parce que les utilisateurs partiront avant d'avoir le temps d'apprécier ta sécurité.
---
Le pattern : quoi indexer
Après cette leçon coûteuse, voici ma checklist pour chaque nouvelle table :
Toujours indexer :
1. Les colonnes de clés étrangères - Chacune d'entre elles
2. Les colonnes dans les politiques RLS - Surtout `org_id` dans les apps multi-tenant
3. Les colonnes dans les clauses WHERE - Si tu filtres fréquemment dessus, indexe-la
4. Les colonnes dans ORDER BY - Trier sans index tue les performances
Envisager des index multi-colonnes :
```sql
-- Pour : WHERE org_id = X AND resource_id = Y
CREATE INDEX idx_table_org_resource ON table(org_id, resource_id);
```
Ne pas sur-indexer : Chaque index coûte du stockage, des performances en écriture et de la maintenance. Règle de base : si ça apparaît dans WHERE/JOIN/ORDER BY fréquemment, indexe-le. Sinon, ne le fais pas.
---
Comment vérifier ta base de données maintenant
Lance cette requête de diagnostic :
```sql
SELECT
c.conrelid::regclass AS table_name,
a.attname AS column_name
FROM pg_constraint c
JOIN pg_attribute a ON a.attnum = ANY(c.conkey) AND a.attrelid = c.conrelid
WHERE c.contype = 'f'
AND NOT EXISTS (
SELECT 1 FROM pg_index i
WHERE i.indrelid = c.conrelid AND a.attnum = ANY(i.indkey)
);
```
Si tu obtiens des résultats, tu as des index manquants.
Ensuite trouve tes requêtes lentes :
```sql
SELECT query, mean_exec_time, calls
FROM pg_stat_statements
WHERE mean_exec_time > 100
ORDER BY mean_exec_time DESC;
```
Utilise `EXPLAIN ANALYZE` sur les plus lentes pour vérifier que tu vois bien des scans séquentiels.
---
Les leçons apprises
1. Les suppositions avec Postgres sont dangereuses
Je supposais que les clés étrangères étaient indexées. Elles ne le sont pas. Toujours vérifier.
2. RLS a besoin d'index
La Row-Level Security est inutile — pire qu'inutile — sans index corrects. Elle ajoute une surcharge à chaque requête.
3. Multi-tenant = indexer org_id partout
Dans une architecture multi-tenant, `org_id` apparaît dans presque chaque requête. Indexe-le sur chaque table. Sans exception.
4. La performance est un problème commercial
Les utilisateurs se fichent que ton SQL soit techniquement correct si la page met 3 secondes à se charger. Des requêtes rapides = meilleure conversion = revenus.
5. Ajouter les index tôt
Ajouter des index sur des tables vides est instantané. Les ajouter sur des tables avec des millions de lignes prend des heures et verrouille la table. Fais-le pendant le développement initial.
6. Tout mesurer
Utilise `EXPLAIN ANALYZE` avant et après. Prouve l'amélioration avec des données.
---
Ta checklist
Pour chaque nouvelle table que tu crées :
```sql
CREATE TABLE new_table (
id UUID PRIMARY KEY,
org_id UUID REFERENCES organizations(id),
resource_id UUID REFERENCES resources(id),
created_at TIMESTAMPTZ,
archived_at TIMESTAMPTZ
);
-- ✅ Indexer les clés étrangères
CREATE INDEX idx_new_table_org_id ON new_table(org_id);
CREATE INDEX idx_new_table_resource_id ON new_table(resource_id);
-- ✅ Indexer les colonnes RLS + filtres courants
CREATE INDEX idx_new_table_org_archived ON new_table(org_id, archived_at);
-- ✅ Indexer les colonnes de tri
CREATE INDEX idx_new_table_created ON new_table(created_at DESC);
-- ✅ Créer les politiques RLS
CREATE POLICY new_table_org_isolation ON new_table
FOR ALL TO authenticated
USING (org_id = get_user_org_id());
```
---
Réflexions finales
89 index manquants. 2 semaines de débogage. Une requête de diagnostic pour les trouver tous. 4 minutes pour corriger.
L'ironie ? Postgres te donne tous les outils pour diagnostiquer ça. Je ne savais juste pas où chercher. Maintenant chaque table que je crée est indexée immédiatement — clés étrangères, colonnes RLS, champs de tri, tout.
La migration en 4 minutes qui a ajouté 89 index a économisé des semaines de future optimisation et a empêché une catastrophe au lancement.
Pour les fondateurs : La performance n'est pas qu'un problème d'ingénierie. C'est un problème de conversion, un problème de rétention, et un problème de crédibilité. Cette base de données "techniquement correcte" qui est trop lente à utiliser ? Elle te coûte des clients. Investis dans les performances tôt.
Pour les ingénieurs : Postgres n'auto-indexe pas tes clés étrangères. Mais toi, tu devrais le faire. Tes utilisateurs (et le CPU de ta base de données) te remercieront.
Tu as déjà eu un moment "techniquement correct mais douloureusement lent" avec ta base de données ? Quel était le correctif — et combien de temps as-tu mis à le trouver ?
Cordialement, Chandler
La série architecture STRAŦUM : Cette crise de performance était la dernière pièce d'un puzzle multi-tenant qui a commencé avec la construction de la multi-location au Jour 2, s'est poursuivi avec une reconstruction complète du schéma au Jour 67, et comprenait la correction de 31 écrans blancs causés par un contexte de navigation perdu.
---
Tu construis un SaaS multi-tenant avec Postgres ? J'ai appris cette leçon à la dure pour que tu n'aies pas à le faire. Vérifie ta base de données maintenant avant que tes utilisateurs ne le remarquent.
Demande un accès alpha sur https://stratum.chandlernguyen.com/request-invitation
---
Toujours en train d'apprendre que "ça fonctionne" et "ça fonctionne vite" sont deux objectifs différents — et qu'un seul d'entre eux se livre en production.
---





