Les premiers mois sont faciles. Votre application est lancée, les clients cliquent, des données atterrissent quelque part et tout le monde se sent productif.
Puis le vrai travail commence : un tableau de bord est lent, un incident vous appelle à 02:17, et le modèle de données “temporaire” devient une obligation contractuelle.
PostgreSQL et MongoDB peuvent tous deux tenir en production sérieuse. Mais ils échouent différemment et exigent des disciplines différentes.
La question n’est pas « lequel est meilleur », mais « lequel fera moins mal plus tard pour les habitudes, contraintes et tolérance au risque de votre équipe ? »
La thèse : la flexibilité n’est pas gratuite
Le pitch de MongoDB — modèle de documents, schéma flexible, vélocité développeur facile — peut être vrai. Le pitch de PostgreSQL — modèle relationnel,
forte cohérence, boîte à outils opérationnelle mature — peut aussi être vrai. Le piège est de supposer que les bénéfices sont gratuits.
En production, votre « choix de base de données » est principalement un « choix de comportement opérationnel ». Postgres tend à récompenser la structure :
schéma explicite, contraintes, transactions et opérations ennuyeuses répétables. MongoDB tend à récompenser la discipline que vous devez apporter vous-même :
cohérence des formes de documents, indexation soignée et hygiène opérationnelle rigoureuse autour des replica sets et des write concerns.
Si vous avez une équipe qui aime les frontières formelles — migrations, contraintes, types stricts — Postgres fera moins mal plus tard.
Si vous avez une équipe capable de maintenir une structure documentaire cohérente sans que la base de données l’impose, et que vous bénéficiez réellement
des documents imbriqués, MongoDB peut faire moins mal plus tard. Si vous choisissez MongoDB parce que vous ne voulez pas penser au schéma, vous y penserez
absolument plus tard, mais vous le ferez en situation d’incident.
Une citation à garder sur un post-it : L’espoir n’est pas une stratégie.
— Général Gordon R. Sullivan.
Les bases de données transforment l’espoir en bruit de pager.
Faits intéressants et contexte (pourquoi les valeurs par défaut sont ainsi)
- PostgreSQL a commencé comme POSTGRES à l’UC Berkeley dans les années 1980, avec pour objectif l’extensibilité ; cet ADN explique les types personnalisés, les extensions et une mentalité de « boîte à outils ».
- MongoDB a émergé à la fin des années 2000 comme un magasin de documents convivial pour les développeurs, quand les équipes web en avaient assez de forcer des données JSON dans des modèles ORM rigides.
- Le modèle MVCC de PostgreSQL (contrôle de concurrence multiversion) explique pourquoi les lectures ne bloquent pas les écritures — mais aussi pourquoi le vacuuming devient un devoir opérationnel réel.
- La popularité initiale de MongoDB a surfé sur l’ère du « web scale » où le sharding semblait inévitable, et non pas un choix d’architecture avec des conséquences nettes.
- Postgres a obtenu JSONB (stockage binaire JSON et indexation) pour répondre aux besoins des applications modernes sans renoncer aux forces relationnelles ; cela a modifié de nombreuses décisions « Mongo par défaut ».
- MongoDB a ajouté les transactions multi-documents plus tard, réduisant l’écart — mais introduisant aussi plus de nuances de performance et de réglage pour les charges transactionnelles.
- La réplication logique de Postgres a mûri en un outil pratique pour migrations, réplication partielle et mises à niveau — utile quand « l’arrêt est inacceptable » devient une exigence soudaine.
- L’histoire opérationnelle de MongoDB privilégie les replica-sets : élections, préférences de lecture et write concerns sont des concepts centraux, pas de simples boutons de réglage optionnels.
Ce ne sont pas des anecdotes. Elles expliquent pourquoi chaque système vous « pousse » vers certaines architectures — et pourquoi résister à ces poussées peut coûter cher.
Modélisation des données : documents, lignes et les mensonges qu’on se raconte
Le plus grand avantage de MongoDB : la localité et les agrégats naturels
Quand une « chose » de votre domaine est naturellement hiérarchique — une commande avec des lignes, adresses d’expédition et transitions d’état — les documents
peuvent bien correspondre. Une lecture obtient tout. Une écriture met à jour l’ensemble. Ce n’est pas seulement pratique ; c’est moins d’aller-retours et
moins de logique de jointure.
Mais les documents posent une question permanente : imbriquer ou référencer ? Imbriquer pour la localité ; référencer pour les entités partagées et les limites de croissance.
Si vous imbriquez trop, les documents gonflent et les mises à jour réécrivent beaucoup d’octets. Si vous référencez trop, vous ré-inventez les jointures dans le code applicatif,
généralement sans sécurité transactionnelle entre collections à moins de payer le coût des transactions.
Le plus grand avantage de PostgreSQL : les invariants appliqués
Postgres brille quand la justesse compte et que les relations comptent. Les clés étrangères, contraintes uniques, contraintes CHECK et triggers ne sont pas
de la « bureaucratie entreprise ». Ce sont des moyens d’empêcher vos données de se transformer silencieusement en grenier hanté rempli d’objets à moitié finis.
Postgres vous donne aussi JSONB, qui permet de stocker des attributs flexibles sans renoncer à la possibilité de les indexer et de les requêter. C’est
le compromis pratique : garder le cœur relationnel et autoriser un « sac d’attributs » pour la longue traîne. La plupart des systèmes ont une longue traîne.
Le mensonge : « On normalisera plus tard » / « On nettoiera les documents plus tard »
« Plus tard » c’est quand vous avez plus de données, plus de dépendances, plus d’attentes clients et moins de tolérance aux interruptions. « Plus tard » c’est
quand chaque nettoyage devient une migration en direct avec un risque réel. Le travail sur le schéma, c’est comme le fil dentaire : l’ignorer fait gagner du temps jusqu’à ce que ça n’en fasse plus.
Blague #1 : Votre schéma est comme vos reçus fiscaux — l’ignorer est très agréable jusqu’à ce que quelqu’un vienne vous auditer.
Transactions et cohérence : sur quoi votre appli mise implicitement
Cette section est le lieu où naissent les incidents de production. Pas parce que les gens ignorent ACID, mais parce qu’ils supposent que leur système
se comporte comme la dernière base de données qu’ils ont utilisée.
PostgreSQL : valeurs par défaut fortes, outils précis
Postgres par défaut offre de fortes garanties transactionnelles. Si vous mettez à jour deux tables dans une transaction, vous validez les deux ou aucun des deux.
Les contraintes s’appliquent à la frontière de la base. Vous pouvez choisir des niveaux d’isolation ; vous pouvez aussi vous tirer une balle dans le pied avec
des transactions longues qui gonflent l’historique MVCC et bloquent le vacuum. Postgres vous laisse être malin. Parfois vous ne devriez pas l’être.
MongoDB : choisissez explicitement votre modèle de cohérence
MongoDB peut être fortement cohérent en pratique si vous utilisez les bons réglages : majority write concern, read concern approprié et préférences de lecture raisonnables.
Il peut aussi être « rapide mais surprenant » si vous lisez depuis des secondaires, acceptez des lectures obsolètes ou des écritures non répliquées. Ce n’est pas une faute morale ; c’est un choix.
Le problème arrive quand c’est un choix accidentel.
Si votre logique métier exige « argent déplacé exactement une fois », vous voulez un système qui rende la violation d’invariants difficile.
Postgres le fait par défaut. MongoDB peut le faire, mais vous devez en concevoir l’usage : clés d’idempotence, sessions transactionnelles là où il le faut,
et réglages opérationnels qui correspondent à vos exigences de correction.
« Formes » de performance : ce qui devient rapide, ce qui devient étrange
MongoDB : lectures rapides tant que vos index correspondent à la réalité
MongoDB peut être extrêmement rapide quand les requêtes s’alignent sur les index et que les documents sont bien façonnés. La douleur arrive quand les équipes
ajoutent de nouveaux patterns de requête chaque semaine et que la stratégie d’indexation devient réactive. Pire : le schéma flexible signifie qu’une requête
peut devoir gérer plusieurs formes et champs manquants, ce qui peut mener à des index sélectifs qui ne se comportent pas comme vous le pensez.
Vous rencontrerez aussi la « taxe de croissance du document ». Si les documents grossissent avec le temps — ajout d’array, de champs imbriqués — les mises à jour
peuvent devenir plus lourdes. La fragmentation du stockage et l’amplification d’écriture apparaissent. Ce n’est pas théorique ; c’est ce qui arrive quand « timeline de profil »
devient « timeline de profil plus fil d’activité plus paramètres plus tout ».
PostgreSQL : les jointures vont bien ; les mauvais plans non
Postgres peut gérer des jointures à grande échelle, mais seulement si les statistiques sont saines et les requêtes raisonnables. Quand la performance s’effondre,
c’est souvent à cause de : index manquants, mauvais ordre de jointure dû à des stats obsolètes, ou requêtes paramétrées qui produisent des plans génériques catastrophiques
pour certaines valeurs. La correction est généralement visible dans EXPLAIN (ANALYZE, BUFFERS). Postgres est honnête si vous lui demandez correctement.
La différence de “forme” qui mord les ops
Les problèmes MongoDB ressemblent souvent à « CPU saturé sur le primary + cache misses + latence de réplication ». Les problèmes Postgres ressemblent souvent à
« I/O saturé + autovacuum en retard + une requête faisant quelque chose de profondément malheureux ». Les deux peuvent être diagnostiqués rapidement,
mais les modèles mentaux diffèrent.
Indexation : la ligne budgétaire silencieuse
Les index sont la façon d’acheter de la performance avec stockage et coût d’écriture. Les deux bases vous punissent pour trop d’index. Les deux vous punissent davantage pour
trop peu d’index. La différence est la facilité avec laquelle vous pouvez vous retrouver accidentellement avec des problèmes opérationnels liés aux index.
Pièges d’indexation PostgreSQL
- Ajouter des index « parce que la lecture est lente » sans vérifier le surcoût en écriture ou le bloat.
- Ne pas utiliser d’index partiels lorsque c’est approprié, conduisant à d’énormes index qui ne servent qu’une fraction des requêtes.
- Ignorer le fillfactor et les mises à jour HOT, augmentant le bloat et la pression sur le vacuum.
- Oublier qu’un index est aussi quelque chose qui doit être vacuumé et maintenu.
Pièges d’indexation MongoDB
- Indexes composés qui ne correspondent pas à l’ordre tri + filtre, provoquant des scans.
- Indexer des champs absents dans de nombreux documents, créant des indexes à faible sélectivité.
- Laisser des index TTL agir comme un « job de suppression gratuit », puis découvrir qu’ils créent une pression d’écriture et un retard de réplication pendant les fenêtres de nettoyage.
- Construire de gros index sur un primary occupé sans planification, puis être surpris quand la latence explose.
Réplication et basculement : douleur prévisible vs douleur surprise
PostgreSQL : la réplication est simple ; le basculement est votre responsabilité
La réplication physique de Postgres est éprouvée. Mais le basculement automatique n’est pas une fonctionnalité unique intégrée ; c’est un choix d’écosystème
(Patroni, repmgr, Pacemaker, services managés). Quand les gens disent « le basculement Postgres est difficile », ils veulent généralement dire « nous n’avons pas
décidé, testé et répété comment le basculement fonctionne ».
La réplication Postgres vous confronte aussi à la rétention WAL, aux replication slots et à la croissance disque. Ignorez cela et vous apprendrez à quelle vitesse
un disque peut se remplir à 03:00.
MongoDB : le basculement est intégré ; les sémantiques sont votre affaire
Les replica sets élisent un primary. C’est bien. Mais votre application doit gérer les erreurs transitoires, les writes ré-essayables et la réalité que le « primary »
peut bouger. De plus, votre politique de read preference définit si les utilisateurs voient des données obsolètes pendant certains modes de panne.
La douleur opérationnelle de MongoDB arrive quand les gens traitent les élections comme des événements rares. Elles ne le sont pas. Les réseaux vacillent. Les nœuds rebootent.
Les mises à jour du noyau arrivent. Si le comportement du client n’est pas testé contre des stepdowns, vous n’avez pas de HA ; vous avez de l’optimisme.
Sauvegardes et restauration : votre seul vrai SLA
Les sauvegardes ne sont pas les fichiers que vous copiez. Les sauvegardes sont les restaurations que vous avez testées. Tout le reste est de l’artisanat.
PostgreSQL
La norme est les base backups plus l’archivage WAL (point-in-time recovery). Les dumps logiques conviennent pour les petits systèmes ou les migrations,
mais ce ne sont pas une machine à remonter le temps. La question opérationnelle est : pouvez-vous restaurer vers un nouveau cluster, vérifier la cohérence et basculer
sans improvisation ?
MongoDB
Vous pouvez faire des sauvegardes basées sur des snapshots, des snapshots au niveau système de fichiers (avec précautions et garanties de cohérence), ou des sauvegardes logiques de type mongodump.
La partie critique est de comprendre si votre sauvegarde capture une vue cohérente à travers les shards et replica sets (si applicable).
Les clusters sharded compliquent tout. Ils le font toujours.
Changements de schéma : migrations vs « on déploie et c’est tout »
Les migrations Postgres sont explicites et donc gérables
Dans Postgres, les changements de schéma sont un workflow de première classe. Cela signifie que vous pouvez les relire, les mettre en staging et les appliquer délibérément.
Vous pouvez toujours vous planter (le DDL verrouillant en heures de pointe est un classique), mais au moins le travail est visible.
Les changements de schéma MongoDB sont implicites et donc sournois
Dans MongoDB, les changements de schéma surviennent souvent comme effet secondaire d’un nouveau code déployé. Les anciens documents restent dans leurs anciennes formes jusqu’à
ce qu’ils soient touchés ou rétro-remplis. Cela peut être un avantage — migration graduelle sans verrou DDL massif. Cela peut aussi être une incohérence longue durée
qui fuit dans l’analytics, les index de recherche et le comportement côté client.
La question opérationnelle est simple : préférez-vous une grande migration contrôlée, ou de nombreuses petites migrations partielles avec une période plus longue de réalité mixte ?
Les deux peuvent marcher. La réalité mixte tend à devenir permanente à moins d’imposer une date limite de nettoyage.
Tâches opérationnelles pratiques avec commandes : quoi lancer, ce que ça signifie, que faire ensuite
Ce ne sont pas des « commandes tutoriel ». Ce sont les choses que vous lancez quand un graphe semble faux et que vous devez décider quoi faire dans les 15 prochaines minutes.
Chaque tâche inclut : commande, ce que la sortie signifie, et la décision que vous prenez.
Tâches PostgreSQL
1) Vérifier les requêtes actives et si vous êtes bloqué
cr0x@server:~$ psql -h pg01 -U postgres -d appdb -c "select pid, usename, state, wait_event_type, wait_event, now()-query_start as age, left(query,120) as q from pg_stat_activity where state <> 'idle' order by age desc;"
pid | usename | state | wait_event_type | wait_event | age | q
------+--------+--------+-----------------+---------------+----------+------------------------------------------------------------
8421 | app | active | | | 00:02:14 | SELECT ... FROM orders JOIN customers ...
9110 | app | active | Lock | relation | 00:01:09 | ALTER TABLE orders ADD COLUMN ...
8788 | app | active | Lock | transactionid | 00:00:57 | UPDATE orders SET ...
Signification : DDL de longue durée en attente de verrous, plus des requêtes attendant des verrous transactionnels.
Décision : Si le DDL bloque le trafic business, annulez le DDL (ou le bloqueur), replanifiez avec une stratégie de migration plus sûre.
2) Trouver qui bloque qui
cr0x@server:~$ psql -h pg01 -U postgres -d appdb -c "select blocked.pid as blocked_pid, blocked.query as blocked_query, blocking.pid as blocking_pid, blocking.query as blocking_query from pg_locks blocked_locks join pg_stat_activity blocked on blocked.pid=blocked_locks.pid join pg_locks blocking_locks on blocking_locks.locktype=blocked_locks.locktype and blocking_locks.database is not distinct from blocked_locks.database and blocking_locks.relation is not distinct from blocked_locks.relation and blocking_locks.page is not distinct from blocked_locks.page and blocking_locks.tuple is not distinct from blocked_locks.tuple and blocking_locks.virtualxid is not distinct from blocked_locks.virtualxid and blocking_locks.transactionid is not distinct from blocked_locks.transactionid and blocking_locks.classid is not distinct from blocked_locks.classid and blocking_locks.objid is not distinct from blocked_locks.objid and blocking_locks.objsubid is not distinct from blocked_locks.objsubid and blocking_locks.pid != blocked_locks.pid join pg_stat_activity blocking on blocking.pid=blocking_locks.pid where not blocked_locks.granted;"
blocked_pid | blocked_query | blocking_pid | blocking_query
------------+---------------------+--------------+-------------------------
8788 | UPDATE orders SET.. | 6502 | BEGIN; SELECT ...;
Signification : Une transaction tenant des verrous bloque des mises à jour.
Décision : Tuez la session bloquante si c’est sûr, ou corrigez le pattern applicatif (par ex. transactions longues).
3) Vérifier le retard de réplication
cr0x@server:~$ psql -h pg01 -U postgres -d appdb -c "select application_name, state, sync_state, write_lag, flush_lag, replay_lag from pg_stat_replication;"
application_name | state | sync_state | write_lag | flush_lag | replay_lag
------------------+-----------+------------+-----------+-----------+------------
pg02 | streaming | async | 00:00:02 | 00:00:03 | 00:00:05
Signification : La réplique a quelques secondes de retard.
Décision : Si le retard augmente, réduisez les pics d’écriture, vérifiez l’I/O sur la réplique, ou déplacez les lectures lourdes hors du primary prudemment.
4) Identifier les requêtes lentes par temps total
cr0x@server:~$ psql -h pg01 -U postgres -d appdb -c "select queryid, calls, total_exec_time::int as total_ms, mean_exec_time::int as mean_ms, rows, left(query,120) as q from pg_stat_statements order by total_exec_time desc limit 5;"
queryid | calls | total_ms | mean_ms | rows | q
----------+-------+----------+---------+-------+------------------------------------------------------------
91233123 | 18000 | 941200 | 52 | 18000 | SELECT * FROM events WHERE user_id=$1 ORDER BY ts DESC LIMIT 50
Signification : Une requête fréquente domine le temps total.
Décision : Ajouter le bon index, réécrire la requête ou mettre en cache côté applicatif — selon l’analyse du plan.
5) Expliquer une requête avec buffers pour voir la douleur I/O
cr0x@server:~$ psql -h pg01 -U postgres -d appdb -c "explain (analyze, buffers) select * from events where user_id=42 order by ts desc limit 50;"
Limit (cost=0.43..12.77 rows=50 width=128) (actual time=0.212..24.981 rows=50 loops=1)
Buffers: shared hit=120 read=1800
-> Index Scan Backward using events_user_id_ts_idx on events (cost=0.43..4212.10 rows=17000 width=128) (actual time=0.211..24.964 rows=50 loops=1)
Index Cond: (user_id = 42)
Planning Time: 0.188 ms
Execution Time: 25.041 ms
Signification : De nombreuses lectures de buffers indiquent de l’I/O disque ; l’index existe mais lit encore beaucoup de pages.
Décision : Envisager un index couvrant, réduire la largeur des lignes, ou améliorer le cache (RAM) si le working set dépasse la mémoire.
6) Vérifier les signaux de bloat et la santé du vacuum
cr0x@server:~$ psql -h pg01 -U postgres -d appdb -c "select relname, n_live_tup, n_dead_tup, round(100.0*n_dead_tup/greatest(n_live_tup,1),2) as dead_pct, last_vacuum, last_autovacuum from pg_stat_user_tables order by n_dead_tup desc limit 5;"
relname | n_live_tup | n_dead_tup | dead_pct | last_vacuum | last_autovacuum
---------+------------+------------+----------+---------------------+---------------------
events | 94000000 | 21000000 | 22.34 | | 2025-12-30 01:02:11
Signification : Beaucoup de tuples morts ; autovacuum est passé, mais peut être en retard par rapport au churn.
Décision : Tuner l’autovacuum pour les tables chaudes, envisager le partitionnement et rechercher les transactions longues empêchant le nettoyage.
7) Vérifier le risque WAL et des replication slots
cr0x@server:~$ psql -h pg01 -U postgres -d appdb -c "select slot_name, active, pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), restart_lsn)) as retained from pg_replication_slots;"
slot_name | active | retained
-----------+--------+----------
wal_slot | f | 128 GB
Signification : Slot inactif retenant 128 GB de WAL. Risque de remplissage disque.
Décision : Si le consommateur a disparu, supprimez le slot ; sinon corrigez le consommateur et augmentez le disque ou la politique de rétention.
8) Vérifier la pression de checkpoint
cr0x@server:~$ psql -h pg01 -U postgres -d appdb -c "select checkpoints_timed, checkpoints_req, round(100.0*checkpoints_req/greatest(checkpoints_timed+checkpoints_req,1),2) as req_pct, buffers_checkpoint, buffers_backend from pg_stat_bgwriter;"
checkpoints_timed | checkpoints_req | req_pct | buffers_checkpoint | buffers_backend
------------------+-----------------+---------+--------------------+----------------
120 | 98 | 44.96 | 81234012 | 12999876
Signification : Beaucoup de checkpoints demandés ; des buffers backend flushés par les writers — pics de latence probables.
Décision : Ajuster les paramètres de checkpoint, évaluer le volume WAL et envisager un stockage plus rapide ou le regroupement des écritures.
Tâches MongoDB
9) Vérifier la santé du replica set et qui est primary
cr0x@server:~$ mongosh --host mongo01:27017 --quiet --eval 'rs.status().members.map(m => ({name:m.name,stateStr:m.stateStr,health:m.health,uptime:m.uptime,lag:m.optimeDate}))'
[
{ name: 'mongo01:27017', stateStr: 'PRIMARY', health: 1, uptime: 90233, lag: ISODate('2025-12-30T02:10:01.000Z') },
{ name: 'mongo02:27017', stateStr: 'SECONDARY', health: 1, uptime: 90110, lag: ISODate('2025-12-30T02:09:58.000Z') },
{ name: 'mongo03:27017', stateStr: 'SECONDARY', health: 1, uptime: 89987, lag: ISODate('2025-12-30T02:09:57.000Z') }
]
Signification : Cluster sain ; secondaries quelques secondes en retard.
Décision : Si la santé baisse ou le retard augmente, arrêtez les lectures lourdes sur les secondaires, vérifiez le disque et le réseau, et vérifiez les paramètres de write concern.
10) Vérifier les opérations courantes pour contention de verrou ou travail lent
cr0x@server:~$ mongosh --host mongo01:27017 --quiet --eval 'db.currentOp({active:true, secs_running: {$gte: 5}}).inprog.map(op => ({opid:op.opid,secs:op.secs_running,ns:op.ns,command:Object.keys(op.command||{}),waitingForLock:op.waitingForLock}))'
[
{ opid: 12345, secs: 22, ns: 'app.events', command: [ 'aggregate' ], waitingForLock: false },
{ opid: 12388, secs: 9, ns: 'app.orders', command: [ 'update' ], waitingForLock: true }
]
Signification : Une mise à jour attend un verrou ; peut être un document point chaud ou une contention au niveau collection.
Décision : Identifier le pattern fautif (compteur single-doc, mises à jour d’array non bornées), puis redessiner pour réduire la contention.
11) Inspecter le profile des requêtes lentes (si activé) ou utiliser explain
cr0x@server:~$ mongosh --host mongo01:27017 --quiet --eval 'db.events.find({userId:42}).sort({ts:-1}).limit(50).explain("executionStats").executionStats'
{
nReturned: 50,
totalKeysExamined: 18050,
totalDocsExamined: 18050,
executionTimeMillis: 84
}
Signification : 18k docs examinés pour en retourner 50. L’index ne correspond pas à la forme de la requête.
Décision : Créer un index composé comme {userId:1, ts:-1} et revérifier les stats ; éviter d’ajouter plusieurs indexes quasi-duppliqués.
12) Vérifier le retard de réplication plus directement
cr0x@server:~$ mongosh --host mongo01:27017 --quiet --eval 'db.adminCommand({replSetGetStatus:1}).members.map(m => ({name:m.name,state:m.stateStr,lagSeconds: (new Date()-m.optimeDate)/1000}))'
[
{ name: 'mongo01:27017', state: 'PRIMARY', lagSeconds: 0 },
{ name: 'mongo02:27017', state: 'SECONDARY', lagSeconds: 3.2 },
{ name: 'mongo03:27017', state: 'SECONDARY', lagSeconds: 4.1 }
]
Signification : Les secondaires ont quelques secondes de retard.
Décision : Si le retard dépasse votre tolérance, réduisez la charge d’écriture, augmentez les IOPS, ajustez journaling/checkpoint (prudemment), ou scale-out/shard avec intention.
13) Vérifier la pression du cache WiredTiger (limiteur de performance courant)
cr0x@server:~$ mongosh --host mongo01:27017 --quiet --eval 'var s=db.serverStatus(); ({cacheBytesUsed:s.wiredTiger.cache["bytes currently in the cache"], cacheMax:s.wiredTiger.cache["maximum bytes configured"], evicted:s.wiredTiger.cache["pages evicted by application threads"]})'
{
cacheBytesUsed: 32212254720,
cacheMax: 34359738368,
evicted: 1902231
}
Signification : Cache proche du maximum, avec beaucoup d’évictions. Vous êtes limité par l’I/O ou à court de mémoire.
Décision : Ajouter de la RAM, réduire le working set (indexes, projections), ou redessiner les requêtes pour qu’elles soient plus sélectives.
14) Vérifier la taille des index par collection (vérification budgétaire)
cr0x@server:~$ mongosh --host mongo01:27017 --quiet --eval 'db.events.stats().indexSizes'
{
_id_: 2147483648,
userId_1_ts_-1: 4294967296,
type_1_ts_-1: 3221225472
}
Signification : Les index font plusieurs Go. Le working set peut ne pas tenir en cache.
Décision : Supprimer les index inutiles, consolider ou déplacer les requêtes froides vers un stockage d’analytics. Les index ne sont pas gratuits.
15) Vérifier la distribution des shards (si sharded) pour attraper les hotspots
cr0x@server:~$ mongosh --host mongos01:27017 --quiet --eval 'db.getSiblingDB("config").chunks.aggregate([{$match:{ns:"app.events"}},{$group:{_id:"$shard",chunks:{$sum:1}}}]).toArray()'
[
{ _id: 'shard01', chunks: 412 },
{ _id: 'shard02', chunks: 398 },
{ _id: 'shard03', chunks: 401 }
]
Signification : Le nombre de chunks semble équilibré, mais l’équilibre n’est pas identique à un équilibre de charge.
Décision : Si un shard est chaud, réévaluez la shard key et le routage des requêtes ; le seul équilibre des chunks peut être un leurre rassurant.
16) Valider rapidement l’utilisation disque Postgres (parce que le stockage est toujours coupable jusqu’à preuve du contraire)
cr0x@server:~$ df -h /var/lib/postgresql
Filesystem Size Used Avail Use% Mounted on
/dev/nvme0n1p2 1.8T 1.6T 150G 92% /var/lib/postgresql
Signification : Vous êtes proche du plein. Postgres déteste les disques pleins ; tout devient passionnant de la pire des façons.
Décision : Identifier la croissance (WAL, tables, fichiers temporaires), atténuer immédiatement et ajouter des alertes avant que cela ne se reproduise.
Blague #2 : La seule chose qui s’échelle infiniment est le nombre d’indexes que quelqu’un proposera pendant une panne.
Playbook de diagnostic rapide (triage des goulots)
Quand la latence explose, ne commencez pas par débattre d’architecture. Commencez par localiser le goulot. L’objectif est d’identifier
si vous êtes CPU-bound, I/O-bound, lock-bound ou network-bound, et si le problème est une mauvaise requête ou une pression systémique.
Première étape : confirmer le périmètre affecté
- Est-ce un seul endpoint ou tout le système ?
- Est-ce uniquement les écritures, uniquement les lectures, ou les deux ?
- La base est-elle lente, ou l’appli est-elle lente alors que la BD est correcte (épuisement du pool de connexions, retries, timeouts) ?
Deuxième étape : vérifier les signaux de saturation
- CPU : Si le CPU DB est saturé et que la charge suit, cherchez quelques requêtes coûteuses, des index manquants ou des agrégations incontrôlées.
- I/O : Latence de lecture élevée, forte utilisation disque, pics d’éviction de cache (WiredTiger) ou lectures de buffers (Postgres) suggèrent que le working set ne tient pas en mémoire.
- Verrous : Beaucoup de sessions en attente de verrous, ou transactions longues, pointent vers de la contention ou un mauvais timing de migration.
- Réseau : Timeouts sporadiques, élections de replica ou trafic inter-AZ peuvent masquer des problèmes de base de données.
Troisième étape : identifier le principal fautif
- Postgres : Vérifier
pg_stat_activitypour les waits etpg_stat_statementspour les requêtes lourdes ; confirmer avecEXPLAIN (ANALYZE, BUFFERS). - MongoDB : Vérifier
currentOp, les requêtes lentes (profiler/métriques) etexplain("executionStats"); vérifier l’éviction du cache et le retard de réplication.
Quatrième étape : choisir l’atténuation la plus sûre
- Annuler/tuer la pire requête ou le job s’il est clairement pathologique.
- Scale up temporairement (CPU/RAM/IOPS) si vous avez besoin d’oxygène.
- Réduire la charge : limiter le débit, désactiver les endpoints lourds, suspendre les jobs batch.
- Faire le plus petit changement d’index qui corrige le pattern de requête dominant (et planifier un suivi approprié).
Cinquième étape : noter le « pourquoi » tant que c’est frais
Ne faites pas confiance au vous futur. Capturez la requête, le plan, le type d’attente et l’atténuation. La différence entre une équipe qui s’améliore et une équipe
qui revit la même panne est la capacité à recréer volontairement le mode de défaillance.
Trois mini-histoires d’entreprise vues du front
1) Incident causé par une mauvaise hypothèse : « Les secondaires sont sûres pour les lectures »
Une entreprise SaaS de taille moyenne utilisait MongoDB avec un replica set standard. L’équipe applicative voulait réduire la charge sur le primary, alors ils ont
basculé les lectures de certains endpoints « non-critique » vers les secondaires en utilisant une read preference. Les endpoints étaient « juste des tableaux de bord »,
et les tableaux de bord étaient « pas en production ». Tout le monde a dit cette phrase. Tout le monde s’est trompé.
L’incident a commencé par une plainte client : « Mes chiffres vont à l’envers. » Ce n’était pas qu’un problème d’UX ; cela a déclenché des alertes automatiques et fait exploser
les tickets support. Les tableaux de bord alimentaient d’autres systèmes : workflows de renouvellement, limites d’utilisation et prévisions internes. Une lecture obsolète est
devenue une entrée logique métier, qui s’est ensuite traduite par des actions critiques.
La cause racine n’était pas que MongoDB « a perdu des données ». Il a fait ce pour quoi il était configuré : servir des lectures depuis des secondaires qui peuvent avoir du retard.
Pendant une période d’écriture intense, le retard a grandi. Les tableaux de bord lisaient de vieilles données, puis un rafraîchissement lisait des données plus récentes, et les utilisateurs
ont vu du voyage dans le temps. Personne n’avait écrit la fenêtre d’obsolescence acceptable, et personne n’avait testé le comportement pendant le retard.
La solution n’a pas été héroïque. Ils ont remis la préférence de lecture sur le primary pour ces endpoints et introduit un cache explicite avec un TTL clair et une sémantique
« les données peuvent avoir jusqu’à N minutes ». Ils ont aussi ajouté du monitoring pour le retard de réplication avec des seuils d’alerte liés à la tolérance métier.
Résultat : moins de surprises, et les tableaux de bord ont cessé de provoquer des conflits.
2) Optimisation qui s’est retournée contre eux : « Dénormaliser tout dans un document »
Une autre entreprise utilisait MongoDB pour les profils utilisateur. Une revue de performance montrait trop d’aller-retours pour assembler la « vue utilisateur » :
infos de profil, préférences, état d’abonnement et liste d’événements récents. Quelqu’un a proposé l’optimisation évidente : tout imbriquer dans le document user et « mettre à jour à l’écriture ».
Une lecture, c’était fini.
Ça a fonctionné en staging. Même en production pendant un temps. Puis l’appli a ajouté plus d’« événements récents », puis plus d’historique, puis des paramètres par fonctionnalité.
Le document utilisateur a grossi progressivement. Les mises à jour ont commencé à toucher des documents de plus en plus gros. La latence a augmenté, puis a explosé. Le retard de réplication
a augmenté en période de pointe, et les élections sont devenues plus fréquentes parce que le primary subissait une pression soutenue.
Le véritable retour de bâton n’était pas seulement la taille. C’était l’amplification d’écriture et la contention. Le document chaud d’un seul utilisateur était mis à jour par plusieurs
processus concurrents (événements, facturation, feature flags), créant un goulot sérialisé. Certaines mises à jour ont réessayé après des erreurs transitoires, augmentant encore la charge.
Le système est devenu « lectures rapides, tout le reste lent », ce qui est une façon polie de dire « pager ».
Ils ont défait cela vers une solution hybride : les champs cœur du profil sont restés imbriqués, mais les tableaux changeant rapidement et non bornés sont passés dans des collections séparées avec
une indexation claire. Ils ont aussi ajouté une approche append-only pour le flux d’événements récents plutôt que de réécrire un array en croissance. Les lectures sont redevenues un peu plus complexes,
mais le système a arrêté de se dévorer lui-même.
3) Pratique ennuyeuse mais correcte qui a sauvé la mise : « Répéter les restaurations comme des exercices incendie »
Une organisation régulée utilisait Postgres pour les données transactionnelles. Ce n’était pas glamour. Ils avaient des fenêtres de changement, des runbooks et une « répétition de restauration hebdomadaire »
vers un staging suffisamment fidèle pour être agaçant. Les ingénieurs se plaignaient du temps perdu. Les managers se plaignaient du coût. La sécurité se plaignait de tout. Normal.
Un jour, un problème de stockage a corrompu un volume sur une réplique. Le basculement a été propre, mais l’équipe a découvert que leurs « sauvegardes connues bonnes » manquaient d’un petit mais critique
élément : un changement récent dans la logique de rétention d’archivage WAL. Ils ne l’ont pas découvert pendant l’incident. Ils l’ont découvert parce que la répétition de restauration de la semaine précédente
avait déjà échoué et avait été corrigée. Ils avaient un chemin testé, un RPO vérifié et une procédure de cutover documentée.
L’incident a quand même fait mal, car les incidents font toujours mal. Mais il est resté dans la tolérance business. Le postmortem n’a pas été « nous n’avions pas de sauvegardes ».
Il a été « nous avions répété les restaurations, donc les sauvegardes étaient réelles ». Cette différence est la ligne entre une panne et un événement de carrière.
Erreurs courantes : symptômes → cause racine → fix
1) Symptomatique : le CPU Postgres est correct, mais tout est lent et les disques sont chauds
Cause racine : Tempête de cache misses + indexation pauvre + scans de tables larges, souvent aggravés par des stats obsolètes ou un autovacuum en retard.
Correction : Identifier les requêtes principales avec pg_stat_statements, exécuter EXPLAIN (ANALYZE, BUFFERS), ajouter des index ciblés et tuner l’autovacuum pour les tables chaudes.
2) Symptomatique : bloat « aléatoire » Postgres et stockage croissant, puis effondrement soudain de performance
Cause racine : Transactions longues empêchent le nettoyage du vacuum, créant accumulation de tuples morts et bloat d’index.
Correction : Trouver les vieilles transactions dans pg_stat_activity, appliquer des timeouts de transaction, repenser les jobs batch et envisager le partitionnement.
3) Symptomatique : primary MongoDB colle le CPU en pointe, le retard de réplication croît, puis élections
Cause racine : Le working set ne tient pas en cache + requêtes inefficaces qui scannent trop + patterns d’écriture touchant des documents chauds.
Correction : Utiliser explain("executionStats"), corriger les indexes composés, réduire la croissance des documents et ajouter RAM/IOPS quand justifié.
4) Symptomatique : lectures MongoDB « incohérentes » entre requêtes
Cause racine : La read preference pointe vers des secondaires et il y a du retard, ou le write concern n’est pas majority et un rollback survient après un failover.
Correction : Aligner read/write concerns avec les besoins de correction ; éviter les lectures secondaires pour toute chose alimentant la logique métier sauf si l’obsolescence est explicitement acceptable.
5) Symptomatique : le basculement Postgres a fonctionné, mais les erreurs applicatives augmentent pendant des minutes
Cause racine : Gestion des connexions client et comportement DNS/endpoint non ajustés ; les pools de connexion ne récupèrent pas proprement.
Correction : Utiliser une stratégie de proxy/endpoint stable, appliquer une logique de retry sûre et tester le basculement avec des pools proches de la prod.
6) Symptomatique : un shard MongoDB semble « équilibré » mais un shard fond
Cause racine : La shard key route des requêtes chaudes vers un shard ; le nombre de chunks équilibrés ne reflète pas la distribution du trafic.
Correction : Revoir la shard key selon les patterns de requête ; valider avec des métriques par shard et un échantillonnage ciblé du routage des requêtes.
7) Symptomatique : disque Postgres se remplit vite même si les tables n’ont pas beaucoup grossi
Cause racine : Rétention WAL due à des replication slots ou logique d’archivage mal configurée, ou fichiers temporaires runaway provenant de sorts/jointures hash.
Correction : Vérifier la taille retenue par les replication slots, la pipeline d’archivage et l’usage de fichiers temporaires ; ajouter des alertes disque avec de vrais seuils de marge.
8) Symptomatique : la « flexibilité du schéma » devient un chaos analytique
Cause racine : Multiples formes de documents au fil du temps sans nettoyage ; les systèmes aval ne peuvent pas compter sur l’existence des champs ou la correspondance des types.
Correction : Établir des contrats de schéma à la frontière applicative, rétro-remplir les anciennes données selon un calendrier et appliquer des règles de validation quand c’est possible.
Listes de contrôle / plan étape par étape
Choisir Postgres sans le regretter
- Concevoir d’abord les invariants : Qu’est-ce qui ne doit jamais arriver ? Encodez-le avec des contraintes (unique, foreign keys, check constraints).
- Planifier le vacuum : Identifier les tables les plus chaudes, tuner les seuils autovacuum et surveiller les tuples morts.
- Utiliser JSONB intentionnellement : Garder le cœur relationnel ; stocker les attributs longue traîne en JSONB avec des GIN ciblés si nécessaire.
- Rendre les migrations ennuyeuses : Éviter les locks longs ; préférer backfill + swap ; tester sur des données de taille production-like.
- Définir sauvegarde/restauration : Base backup + archivage WAL ; répéter les restaurations ; documenter RPO/RTO.
- Décider de la HA : Choisir une approche de basculement, la tester trimestriellement et rendre le comportement de connexion applicatif compatible.
Choisir MongoDB sans accumuler de « dette schéma flexible »
- Écrire quand même un contrat de schéma : Définir les champs requis, types et stratégie de versioning des documents.
- Imbriquer avec une limite : Éviter les arrays non bornés et les documents en croissance constante ; préférer des collections append-only pour l’historique d’événements.
- Indexer depuis les patterns de requête : Pour chaque endpoint critique : champs de filtre, ordre de tri et projection ; construire des index composés en conséquence.
- Définir read/write concerns délibérément : Décider l’obsolescence et la durabilité acceptables ; ne pas laisser cela aux valeurs par défaut et aux impressions.
- Répéter les élections : Tester le comportement client pendant les stepdowns ; vérifier que les timeouts sont réalistes et idempotents.
- Budgéter le cache : Surveiller l’éviction du WiredTiger ; dimensionner la RAM pour le working set, pas pour l’espoir.
Approche hybride qui gagne souvent : Postgres + JSONB (avec discipline)
- Mettre la vérité transactionnelle dans des tables relationnelles avec contraintes.
- Utiliser JSONB pour les attributs évolutifs et les champs optionnels clairsemés.
- Indexer uniquement ce qui est interrogé ; accepter que tous les champs JSON ne méritent pas un index.
- Séparer l’analytics si les patterns de requête deviennent incompatibles avec l’OLTP.
FAQ
1) Les startups devraient-elles par défaut choisir MongoDB pour la vitesse ?
Choisissez par défaut ce que votre équipe peut exploiter. Si vous n’avez pas une forte discipline autour de la forme des documents et de l’indexation,
Postgres sera plus rapide de la seule manière qui compte : moins de surprises à 2 h du matin.
2) Postgres est-il « lent à l’échelle » parce que les jointures sont coûteuses ?
Non. Les mauvais plans sont coûteux. Avec des index et des stats corrects, Postgres peut gérer de larges charges de jointures. Quand il échoue, c’est généralement parce que
la forme de la requête a changé et que personne n’a revu l’indexation et le vacuum.
3) Les transactions MongoDB sont-elles « aussi bonnes que Postgres » maintenant ?
Elles peuvent fournir de fortes garanties, mais vous payez en complexité et parfois en débit, surtout si vous vous appuyez lourdement sur des transactions multi-documents.
Si vous avez besoin de transactions partout, Postgres est le pari le plus simple.
4) Postgres peut-il gérer des schémas flexibles comme MongoDB ?
Postgres peut stocker et requêter JSONB efficacement, et c’est souvent suffisant. Mais ce n’est pas un laissez-passer gratuit : vous avez toujours besoin de structure, et des requêtes JSON lourdes
peuvent devenir une histoire d’indexation et de bloat. Utilisez JSONB pour la longue traîne, pas comme philosophie de base de données entière.
5) Quand MongoDB gagne-t-il clairement ?
Quand vos objets de domaine sont naturellement en forme de document, que vous y accédez principalement comme agrégats complets et que vous pouvez garder la structure des documents cohérente.
Aussi quand le scale horizontal via sharding est une exigence connue et que vous êtes prêt à concevoir pour cela dès le premier jour.
6) Quand Postgres gagne-t-il clairement ?
Quand vous avez besoin d’une stricte correction, de requêtes relationnelles complexes, de contraintes comme garde-fous et d’une boîte à outils opérationnelle mature.
Si vous construisez la facturation, l’inventaire, les permissions ou tout ce qui implique des avocats, Postgres est le choix le plus calme.
7) Quelle est la raison la plus courante du « on a choisi MongoDB et on le regrette » ?
Traiter « sans schéma » comme « sans structure ». La dette apparaît sous forme de documents inconsistants, de performances de requête imprévisibles et de pipelines d’analytics qui ne peuvent pas faire confiance aux données.
MongoDB ne cause pas ça ; ce sont les équipes qui ne font pas respecter les contrats.
8) Quelle est la raison la plus courante du « on a choisi Postgres et on le regrette » ?
Sous-estimer le travail opérationnel autour du vacuum, du bloat et du verrouillage des migrations — ou le traiter comme un jouet jusqu’à ce que ce soit critique.
Postgres est stable, mais il attend de vous des opérations routinières et une planification de capacité.
9) Devrions-nous exécuter les deux ?
Seulement si vous avez une séparation claire des responsabilités et la maturité opérationnelle pour deux systèmes. Exécuter deux bases « parce que chacune est la meilleure pour quelque chose »
est valide. En exécuter deux parce que vous n’avez pas pu décider double votre charge d’astreinte.
10) Service managé ou auto-hébergé ?
Si la disponibilité compte et que votre équipe est petite, le managé gagne généralement. L’auto-hébergement peut être excellent quand vous avez besoin d’un contrôle profond et du personnel
qui aime les mises à jour du noyau et les conversations sur le page cache.
Prochaines étapes concrètes
Si vous hésitez entre PostgreSQL et MongoDB pour un nouveau système, ne partez pas d’une idéologie. Commencez par les modes de panne et les habitudes opérationnelles.
Puis décidez ce que vous pouvez faire respecter par la technologie versus ce que vous attendez que les humains se souviennent de faire.
- Écrivez vos invariants : ce qui ne doit jamais arriver (doubles facturations, enregistrements orphelins, lectures obsolètes alimentant des décisions).
- Listez vos 10 principales requêtes : forme, filtres, tris, latence attendue et attentes de croissance.
- Choisissez votre position de cohérence : définir l’obsolescence et la durabilité acceptables ; dans MongoDB, encodez-le dans read/write concerns ; dans Postgres, encodez-le dans transactions et contraintes.
- Décidez votre histoire de sauvegarde : comment restaurer, combien de temps ça prend, qui l’exécute et à quelle fréquence vous répétez.
- Réalisez un test de charge avec des données de taille production-like : pas pour un nombre de benchmark, mais pour faire ressortir les patterns de requête qui deviennent des mines opérationnelles.
- Choisissez l’option « qui fera moins mal plus tard » : celle qui correspond à la façon dont votre équipe se comporte réellement un mardi après-midi et pendant un incident un samedi soir.
Mon choix par défaut et argumenté : si vous n’avez pas une raison forte pour le modèle document de MongoDB (et un plan pour maintenir la cohérence des documents), utilisez PostgreSQL.
Ce n’est pas parfait, mais c’est imparfait de façon prévisible — ce que vous voulez quand votre pager sonne.