« Base de données gérée » est une expression rassurante jusqu’à ce que votre latence p95 double pendant une promotion, que le graphique CPU ressemble à un code-barres et que votre équipe produit demande si vous « pouvez juste ajouter plus de RAM ». Vous le pouvez. Ça peut aider. Cela peut aussi ne rien changer du tout pendant que le vrai problème — verrous, bloat, pression WAL ou une requête tragique — continue de trancher vos SLO.
RDS PostgreSQL supprime des catégories entières de travail : échanges de matériel, correctifs de versions mineures, sauvegardes automatisées et beaucoup de plomberie ennuyeuse mais critique. Il ne supprime pas l’optimisation. Il ne supprime pas la physique. Et il ne supprime certainement pas la responsabilité.
Le géré ne veut pas dire optimisé
Exécuter PostgreSQL vous-même sur EC2 ou du bare metal vous donne des réglages. Exécuter PostgreSQL sur RDS vous donne… moins de réglages, mais encore beaucoup de façons de vous faire mal.
La différence porte surtout sur qui possède la plateforme :
- PostgreSQL autogéré : vous possédez tout : noyau, système de fichiers, disques, sysctls, options de compilation de Postgres, extensions, sauvegardes, basculement, monitoring et les réveils à 3 h du matin.
- RDS PostgreSQL : AWS possède l’hôte. Vous possédez le comportement de la base de données : schéma, requêtes, index, stratégie d’autovacuum, réglage du parameter group, planification de capacité et la façon dont vous observez et réagissez.
En pratique, l’optimisation reste votre travail parce que la performance est une propriété émergente de la distribution des données, de la charge de travail et du comportement applicatif. RDS ne connaît pas votre charge. RDS ne peut pas réécrire votre ORM. RDS ne peut pas dire « non » à votre équipe produit quand elle déploie une jointure cartésienne déguisée en « endpoint de reporting rapide ».
Une vérité sèche : RDS rend plus difficile certains optimisations bas-niveau, mais il facilite aussi la survie face aux pannes. Si vous voulez être à la fois rapide et fiable, vous devez toujours traiter RDS comme un système de production que vous exploitez — juste avec moins de bords tranchants exposés.
Blague #1 : Le cloud, c’est juste l’ordinateur de quelqu’un d’autre. RDS, c’est l’ordinateur de quelqu’un d’autre avec une belle interface et les mêmes lois de la thermodynamique.
Faits intéressants et contexte historique
- Les racines de PostgreSQL remontent aux années 1980 (POSTGRES à l’UC Berkeley). Certaines idées de durabilité d’aujourd’hui étaient débattues avant l’existence de votre pipeline CI.
- MVCC explique pourquoi les lectures de PostgreSQL ne bloquent pas les écritures (généralement). C’est aussi la raison de l’existence du vacuum et pourquoi « supprimer des lignes » n’est pas un plan de performance.
- Autovacuum n’a pas toujours existé ; les premières versions de Postgres exigeaient un vacuum manuel. Les équipes ont « appris » le vacuum à la dure : pannes en production.
- PostgreSQL privilégie la correction plutôt que des comportements « rapides mais faux ». Cela influence les choix du planificateur et la sémantique de concurrence.
- RDS pour PostgreSQL a été lancé au début des années 2010 et a normalisé l’idée que beaucoup d’équipes n’ont pas besoin d’exécuter leurs propres hôtes de base de données.
- pg_stat_statements est devenu grand public à mesure que Postgres est devenu une base de données axée sur l’observabilité. On ne règle pas ce qu’on ne peut pas mesurer.
- Les améliorations de Postgres entre 9.6 et 13+ ont modifié le comportement du vacuum, le parallélisme et les performances d’indexation d’une façon qui rend les « vieilles règles » peu fiables.
- Les schémas d’E/S comptent plus que le CPU brut dans beaucoup de systèmes OLTP ; la base peut être « idle CPU » en attendant le stockage. Les graphiques aiment mentir par omission.
- Le réglage des checkpoints est un thème récurrent depuis deux décennies parce que le WAL et les checkpoints sont le rythme de la durabilité — et la source de douleurs périodiques.
Où RDS aide vraiment — et où il n’aide pas
Ce que RDS vous offre gratuitement (ou presque)
RDS est réellement performant sur les parties « infrastructure ennuyeuse » :
- Sauvegardes automatisées et workflows de restauration point-in-time.
- Gestion des patchs (dans les limites des fenêtres de maintenance et de votre appétence au risque).
- Multi-AZ orchestration de basculement, ce qui n’est pas la même chose que « sans downtime », mais reste précieux.
- Métriques opérationnelles dans CloudWatch et Performance Insights, ce qui vous évite de tout reconstruire de zéro.
- Options de montée en capacité du stockage (selon le type de stockage), réduisant le scénario « on a manqué d’espace disque à 2 h du matin ».
Ce que RDS ne résout pas (et ce qu’il masque parfois)
RDS ne :
- Ne corrige pas les requêtes lentes causées par des index manquants, un mauvais ordre de jointure, des statistiques obsolètes ou le comportement applicatif.
- Ne prévient pas la contention de verrous quand votre migration prend un verrou ACCESS EXCLUSIVE à midi.
- Ne supprime pas le bloat des tables à fort turnover. MVCC a toujours besoin de vacuum, et vacuum a toujours besoin de marge.
- Ne rend pas l’I/O infinie. EBS a des limites ; le débit de stockage est une ressource facturable et saturable.
- Ne choisit pas la bonne classe d’instance pour votre charge. Vous pouvez payer pour du CPU inutilisé ou manquer de mémoire dont vous avez besoin.
- Ne vous protège pas des « trop nombreuses connexions » quand votre app scale horizontalement et que chaque pod ouvre 100 sessions comme si c’était 2009.
La différence la plus importante : sur un Postgres autogéré, vous pouvez souvent « instrumenter l’hôte » pour apprendre ce que la base attend. Sur RDS, vous êtes surtout limité à ce qu’AWS expose : métriques moteur, proxys de type OS, logs et Performance Insights. C’est suffisant pour gérer un excellent système, mais seulement si vous prenez l’observabilité au sérieux.
Les réglages que vous devez encore faire (même sur RDS)
1) Optimisation des requêtes : le planificateur n’est pas votre ennemi, mais il n’est pas psy
Si vous ne réglez rien d’autre, optimisez vos requêtes. La performance PostgreSQL est dominée par les chemins d’accès : scans d’index vs scans séquentiels, stratégies de jointure et la quantité de données que vous forcez le moteur à toucher.
Sur RDS, vous ressentirez les erreurs de requêtes plus vite parce que vous ne pouvez pas « simplement ajuster le noyau » ou coller des processus sur des CPU. Ce n’est pas un problème : l’ajustement du noyau est rarement votre premier goulot.
Que faire :
- Activez pg_stat_statements et traitez-le comme votre tableau de la honte.
- Utilisez EXPLAIN (ANALYZE, BUFFERS) et lisez-le comme un rapport d’enquête : qu’est-ce qui a touché le disque, qu’est-ce qui ne l’a pas fait, et pourquoi.
- Gardez les statistiques fraîches (ANALYZE) et ne supposez pas qu’autovacuum vous couvre sur des tables à forte skew.
2) Autovacuum : la différence entre « ça va » et « pourquoi le disque est plein ? »
MVCC signifie que les anciennes versions de lignes subsistent jusqu’à ce que le vacuum les récupère. Sur RDS, le vacuum reste votre responsabilité pour dimensionner et planifier. Si autovacuum prend du retard, vous n’obtenez pas seulement du bloat. Vous obtenez :
- Une moins bonne efficacité du cache (plus de pages, moins de pages utiles).
- Plus d’E/S (lire des tuples morts, c’est toujours lire).
- Une plus grande variance de latence (vacuum qui s’active aux mauvais moments).
- Un risque potentiel de wraparound d’ID de transaction si vous le négligez assez longtemps.
Le réglage d’autovacuum est spécifique à la charge. Les tables OLTP à fort turnover ont souvent besoin de paramètres par table. Les seuils par défaut sont polis. Votre production ne l’est pas.
3) Mémoire : shared_buffers n’est pas un bouton magique
Sur un Postgres autogéré, vous pouvez vous lâcher sur le tuning mémoire. Sur RDS, vous réglez toujours les paramètres de la base, mais vous vivez aussi avec les choix d’OS d’AWS. Vous ne contrôlez pas directement le cache de pages ; vous l’influencez en évitant de faire des E/S stupides.
Priorités typiques :
- work_mem : trop bas et vos tris/jointures par hachage débordent ; trop haut et la concurrence transforme la RAM en cratère fumant.
- shared_buffers : sur RDS il est souvent réglé de façon sensée par défaut, mais « sensé » n’est pas « optimal ».
- effective_cache_size : un indice pour le planificateur ; une valeur incorrecte mène à de mauvais choix de plan.
4) E/S : RDS les rend achetables, pas optionnelles
La plupart des « incidents de performance » sur RDS PostgreSQL sont en réalité des incidents E/S avec un accent SQL.
Vos choix principaux :
- Type et dimensionnement du stockage (general purpose vs provisioned IOPS, plafonds de débit, comportement en burst).
- Comportement des checkpoints (pics d’écriture vs état stable).
- Vacuum et maintenance d’index (I/O en arrière-plan qui devient douleur au premier plan).
5) WAL et checkpoints : les pics de latence arrivent en vagues
Les systèmes à forte écriture finissent par rencontrer le sous-système WAL dans une ruelle sombre. Les symptômes ressemblent à des ralentissements périodiques : les commits ralentissent, l’I/O bondit et le débit s’effondre à intervalles réguliers.
Ce que vous réglez :
- max_wal_size et checkpoint_timeout pour réduire la fréquence des checkpoints.
- checkpoint_completion_target pour étaler les écritures.
- wal_compression et wal_buffers selon la version/la charge.
Sur RDS, vous modifiez ces paramètres via les parameter groups. Surveillez aussi le lag des réplicas : la pression WAL se manifeste en aval.
6) Connexions : la lente mort par mille sessions
PostgreSQL utilise un modèle processus-par-connexion. C’est robuste et simple. C’est aussi coûteux quand vous traitez la base comme un serveur HTTP sans état.
Sur RDS, max_connections peut être élevé, mais cela ne veut pas dire que c’est souhaitable. Une tempête de connexions signifie des changements de contexte, un surcoût mémoire et plus de contention sur les structures partagées.
Utilisez un pooler (PgBouncer) à moins d’avoir une bonne raison de ne pas le faire. Oui, même avec RDS. Surtout avec Kubernetes. Vos pods ne sont pas polis.
7) Verrous et migrations : vos changements de schéma sont aussi du trafic de production
RDS ne vous empêchera pas d’exécuter ALTER TABLE qui bloque les écritures pendant des minutes. Il préserve toutefois fidèlement la panne dans les graphiques CloudWatch pour que votre futur vous l’admire.
Opérationnellement :
- Préférez les modèles « en ligne » : create index concurrently, backfill par lots, swap de colonnes avec précaution.
- Définissez des lock timeouts pour les sessions de migration afin qu’elles échouent vite au lieu de prendre le système en otage.
- Surveillez les graphes de verrous pendant les déploiements comme vous surveillez les taux d’erreur.
8) Réplication et montée en lecture : les réplicas ne sont pas un cheat code
Les réplicas de lecture peuvent décharger le trafic en lecture. Ils peuvent aussi introduire de nouveaux modes de panne : lectures obsolètes, lag des réplicas lors d’explosions d’écriture, et plans de requête qui diffèrent à cause de la chaleur du cache.
Multi-AZ améliore la disponibilité, mais peut affecter légèrement la latence, et les événements de basculement feront toujours des dégâts sauf si votre application réessaie correctement.
9) Observabilité : si vous ne pouvez pas expliquer le p95, vous ne le possédez pas
Sur un Postgres autogéré, vous pouvez utiliser des outils système (iostat, vmstat, perf). Sur RDS, vos meilleurs alliés sont :
- Performance Insights (événements d’attente, top SQL).
- Enhanced Monitoring (CPU, mémoire, charge, disque de type OS).
- Logs de la base (logging des requêtes lentes, autovacuum, checkpoints).
- vues pg_stat_* (vérité terrain depuis l’intérieur du moteur).
Citation (idée paraphrasée) : Werner Vogels a défendu l’idée que « tout tombe en panne, tout le temps », donc on conçoit et on exploite en partant du principe de la panne, pas en espérant qu’elle n’arrivera pas.
Mode opératoire de diagnostic rapide : trouver le goulot d’étranglement vite
Ceci est le flux de triage qui fonctionne quand le pager hurle et que votre agenda est plein de réunions que vous allez maintenant manquer.
Premièrement : est-ce le CPU, l’E/S, les verrous ou les connexions ?
- Vérifiez la charge de la base et les événements d’attente (Performance Insights si sur RDS ; pg_stat_activity partout). Si vous voyez beaucoup d’attentes de type
Lock, arrêtez de deviner et cherchez les bloqueurs. - Vérifiez la saturation du stockage : IOPS de lecture/écriture, débit et latence. Si la latence est élevée et que les IOPS sont plafonnés, vous êtes I/O-bound peu importe le nombre de vCPU que vous achetez.
- Vérifiez le nombre de connexions et les sessions actives. Si les sessions actives dépassent largement les cœurs CPU, vous êtes probablement en file d’attente et en changement de contexte.
- Vérifiez le top SQL par temps total. Une seule requête peut dominer. Ce n’est souvent pas celle que vous suspectez.
Deuxièmement : est-ce un problème de plan/statistiques ou un problème de données ?
- Cherchez des régressions de plan : même requête, nouveau plan, performance pire.
- Vérifiez le bloat des tables et les tuples morts. Si les tuples morts sont élevés et que le vacuum prend du retard, vous payez une taxe E/S.
- Vérifiez l’utilisation des index vs scans séquentiels. Les scans séquentiels ne sont pas diaboliques ; ceux inattendus sont suspects.
Troisièmement : confirmez avec une seule requête « coupable » et corrigez en sécurité
- Sélectionnez le principal coupable depuis pg_stat_statements.
- Exécutez EXPLAIN (ANALYZE, BUFFERS) dans un environnement sûr si possible ; en production, exécutez d’abord un EXPLAIN simple.
- Décidez : ajouter un index, réécrire la requête, ajuster work_mem, mettre à jour les stats ou changer la politique d’autovacuum.
- Validez l’amélioration en utilisant la même mesure que celle qui a déclaré l’incident.
Tâches pratiques : commandes, sorties et décisions
Ceci est délibérément opérationnel. Chaque tâche inclut une commande exécutable, une sortie d’exemple, ce que cela signifie et la décision à prendre. Utilisez-les sur un Postgres autogéré et sur RDS (depuis un bastion ou votre laptop avec accès réseau), sauf indication contraire.
Task 1: Verify basic instance identity and version
cr0x@server:~$ psql "$DATABASE_URL" -X -c "select version();"
version
-----------------------------------------------------------------------------------------------------------------------------
PostgreSQL 15.4 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 11.3.0, 64-bit
(1 row)
Ce que cela signifie : Confirme la version majeure et les infos de build. La version gouverne le comportement du planificateur, les améliorations de vacuum et les fonctionnalités disponibles.
Décision : Si vous êtes sur une version majeure ancienne, priorisez un plan de mise à jour avant un tuning héroïque. Beaucoup de problèmes de « tuning » disparaissent avec Postgres moderne.
Task 2: Confirm critical extensions (pg_stat_statements)
cr0x@server:~$ psql "$DATABASE_URL" -X -c "select extname, extversion from pg_extension order by 1;"
extname | extversion
--------------------+------------
pg_stat_statements | 1.10
plpgsql | 1.0
(2 rows)
Ce que cela signifie : Si pg_stat_statements n’est pas installé, vous déboguez aveuglément.
Décision : Sur RDS, activez-le dans le parameter group (shared_preload_libraries) et redémarrez si nécessaire ; puis faites create extension dans chaque base qui en a besoin.
Task 3: Find the current pain: active sessions and wait events
cr0x@server:~$ psql "$DATABASE_URL" -X -c "\x on" -c "select pid, usename, application_name, state, wait_event_type, wait_event, now()-query_start as age, left(query,120) as query from pg_stat_activity where state <> 'idle' order by age desc limit 10;"
-[ RECORD 1 ]-------+--------------------------------------------
pid | 21435
usename | app
application_name | api
state | active
wait_event_type | Lock
wait_event | relation
age | 00:02:11.4123
query | update orders set status='paid' where id=$1
-[ RECORD 2 ]-------+--------------------------------------------
pid | 21398
usename | app
application_name | migrate
state | active
wait_event_type | Lock
wait_event | relation
age | 00:05:43.9921
query | alter table orders add column reconciliation_id uuid;
Ce que cela signifie : Vous êtes bloqué sur un verrou de relation ; une migration bloque probablement des mises à jour OLTP.
Décision : Identifiez le bloqueur, arrêtez/rollback la migration, et changez votre approche de changement de schéma (patterns en ligne, lock_timeout, séquencement des déploiements).
Task 4: Identify blocking chains (who is blocking whom)
cr0x@server:~$ psql "$DATABASE_URL" -X -c "select blocked.pid as blocked_pid, blocked.query as blocked_query, blocker.pid as blocker_pid, blocker.query as blocker_query from pg_locks bl join pg_stat_activity blocked on blocked.pid=bl.pid join pg_locks kl on kl.locktype=bl.locktype and kl.database is not distinct from bl.database and kl.relation is not distinct from bl.relation and kl.page is not distinct from bl.page and kl.tuple is not distinct from bl.tuple and kl.virtualxid is not distinct from bl.virtualxid and kl.transactionid is not distinct from bl.transactionid and kl.classid is not distinct from bl.classid and kl.objid is not distinct from bl.objid and kl.objsubid is not distinct from bl.objsubid and kl.pid <> bl.pid join pg_stat_activity blocker on blocker.pid=kl.pid where not bl.granted;"
blocked_pid | blocked_query | blocker_pid | blocker_query
------------+----------------------------------------+------------+-----------------------------------------------
21435 | update orders set status='paid' ... | 21398 | alter table orders add column ...
(1 row)
Ce que cela signifie : Identification concrète du bloqueur. Ce n’est plus « Postgres est lent », c’est « ce PID bloque 40 sessions ».
Décision : Terminez le bloqueur si approprié, puis corrigez le processus de déploiement pour éviter les verrous ACCESS EXCLUSIVE aux heures de pointe.
Task 5: Check connection pressure
cr0x@server:~$ psql "$DATABASE_URL" -X -c "select count(*) as total, sum(case when state='active' then 1 else 0 end) as active from pg_stat_activity;"
total | active
-------+--------
612 | 184
(1 row)
Ce que cela signifie : 612 connexions totales n’est pas automatiquement mauvais ; 184 sessions actives peut être désastreux sur une machine 16 vCPU.
Décision : Si les sessions actives dépassent les cœurs de façon soutenue, mettez en place du pooling, baissez max_connections et corrigez les patterns bavards de l’application (N+1, transactions par requête, etc.).
Task 6: Find the top SQL by total time (pg_stat_statements)
cr0x@server:~$ psql "$DATABASE_URL" -X -c "select queryid, calls, round(total_exec_time::numeric,1) as total_ms, round(mean_exec_time::numeric,3) as mean_ms, rows, left(query,120) as query from pg_stat_statements order by total_exec_time desc limit 5;"
queryid | calls | total_ms | mean_ms | rows | query
----------+---------+----------+---------+---------+-----------------------------------------------------------
91827364 | 184920 | 812349.7 | 4.392 | 184920 | select * from users where email=$1
11223344 | 12001 | 643221.9 | 53.599 | 12001 | select o.* from orders o join order_items i on ...
(2 rows)
Ce que cela signifie : Le temps total montre où la base a passé son temps. Un mean élevé indique une « requête lente ». Beaucoup d’appels avec un mean modéré peuvent aussi dominer.
Décision : Commencez par la requête ayant le plus gros temps total sauf si une seule requête cause une latence tail visible. Inspectez ensuite les plans et les index.
Task 7: Check index usage vs sequential scans
cr0x@server:~$ psql "$DATABASE_URL" -X -c "select relname, seq_scan, idx_scan, n_live_tup from pg_stat_user_tables order by seq_scan desc limit 10;"
relname | seq_scan | idx_scan | n_live_tup
---------------+----------+----------+------------
events | 982134 | 21011 | 4839201
order_items | 431201 | 892301 | 983211
(2 rows)
Ce que cela signifie : Un seq_scan élevé sur une grosse table peut être normal en analytique ; il est suspect pour des endpoints OLTP.
Décision : Si les seq scans sont corrélés à des requêtes lentes, ajoutez des index ciblés ou réécrivez les requêtes. Si la charge est analytique, envisagez le partitionnement, des index BRIN ou déplacer le reporting ailleurs.
Task 8: Spot bloat risk via dead tuples and vacuum history
cr0x@server:~$ psql "$DATABASE_URL" -X -c "select relname, n_live_tup, n_dead_tup, last_vacuum, last_autovacuum, last_analyze, last_autoanalyze from pg_stat_user_tables order by n_dead_tup desc limit 10;"
relname | n_live_tup | n_dead_tup | last_vacuum | last_autovacuum | last_analyze | last_autoanalyze
------------+------------+------------+-------------+--------------------------+--------------+--------------------------
events | 4839201 | 1720033 | | 2025-12-30 08:12:01+00 | | 2025-12-30 08:13:44+00
orders | 312001 | 91011 | | 2025-12-29 23:02:11+00 | | 2025-12-29 23:03:09+00
(2 rows)
Ce que cela signifie : Un grand nombre de tuples morts indique que le vacuum est soit en retard, soit que les seuils sont trop hauts pour le churn. C’est aussi un indice que le bloat d’index peut augmenter.
Décision : Ajustez les seuils d’autovacuum par table et envisagez un vacuum plus agressif hors pics. Si le bloat est déjà sévère, planifiez une reconstruction contrôlée (VACUUM FULL est rarement la bonne réponse en production).
Task 9: Check autovacuum settings (global and per table)
cr0x@server:~$ psql "$DATABASE_URL" -X -c "show autovacuum; show autovacuum_vacuum_scale_factor; show autovacuum_vacuum_threshold; show autovacuum_max_workers;"
autovacuum
------------
on
(1 row)
autovacuum_vacuum_scale_factor
-------------------------------
0.2
(1 row)
autovacuum_vacuum_threshold
----------------------------
50
(1 row)
autovacuum_max_workers
-----------------------
3
(1 row)
Ce que cela signifie : Un scale factor de 0.2 signifie que le vacuum se déclenche après ~20% de changements + threshold. Sur de grosses tables à fort churn, c’est souvent trop tard.
Décision : Pour les tables chaudes, définissez un scale factor plus bas via ALTER TABLE ... SET (autovacuum_vacuum_scale_factor=0.02, autovacuum_vacuum_threshold=1000) et augmentez les workers si l’I/O le permet. Validez via les logs de vacuum.
Task 10: Detect checkpoint pressure
cr0x@server:~$ psql "$DATABASE_URL" -X -c "select checkpoints_timed, checkpoints_req, round(checkpoint_write_time/1000.0,1) as write_s, round(checkpoint_sync_time/1000.0,1) as sync_s from pg_stat_bgwriter;"
checkpoints_timed | checkpoints_req | write_s | sync_s
------------------+-----------------+---------+--------
412 | 289 | 9821.4 | 612.7
(1 row)
Ce que cela signifie : Un checkpoints_req élevé par rapport aux timed suggère que vous atteignez les limites de taille WAL et forcez des checkpoints. C’est une cause commune de pics d’écriture périodiques.
Décision : Augmentez max_wal_size, ajustez checkpoint_timeout et checkpoint_completion_target, puis surveillez la latence d’écriture et le lag des réplicas.
Task 11: Check WAL generation rate proxy
cr0x@server:~$ psql "$DATABASE_URL" -X -c "select pg_size_pretty(pg_wal_lsn_diff(pg_current_wal_lsn(), '0/0')) as wal_since_boot;"
wal_since_boot
----------------
512 GB
(1 row)
Ce que cela signifie : C’est une approximation. Si le churn WAL est massif, la réplication et le stockage le ressentiront. (Sur certains systèmes vous utiliserez des stats WAL plus précises selon la version.)
Décision : Si le volume WAL est inattendu, enquêtez sur les mises à jour en masse, la maintenance d’index et le comportement d’autovacuum ; envisagez de regrouper les écritures et d’éviter les mises à jour inutiles.
Task 12: Validate table and index sizes (find the elephants)
cr0x@server:~$ psql "$DATABASE_URL" -X -c "select relname, pg_size_pretty(pg_total_relation_size(relid)) as total_size from pg_catalog.pg_statio_user_tables order by pg_total_relation_size(relid) desc limit 10;"
relname | total_size
-----------+------------
events | 219 GB
orders | 38 GB
(2 rows)
Ce que cela signifie : Les grandes relations pilotent le comportement du cache et le coût du vacuum. Sur RDS, elles pilotent aussi le coût de stockage et la durée des snapshots.
Décision : Pour les tables append-heavy vraiment volumineuses, envisagez le partitionnement, des index BRIN ou une politique de rétention. Si la taille ne correspond pas à votre modèle mental, vous avez probablement du bloat ou une rétention incontrôlée.
Task 13: Check if sorts/hash joins are spilling to disk
cr0x@server:~$ psql "$DATABASE_URL" -X -c "select datname, temp_files, pg_size_pretty(temp_bytes) as temp_written from pg_stat_database order by temp_bytes desc;"
datname | temp_files | temp_written
-----------+------------+--------------
appdb | 18231 | 97 GB
(1 row)
Ce que cela signifie : Une grosse utilisation temporaire signifie que des requêtes débordent (souvent parce que work_mem est trop bas pour la forme de la requête ou parce que la requête est mal formée).
Décision : Identifiez les requêtes fautives via pg_stat_statements ; envisagez d’augmenter work_mem avec précaution (ou par rôle), ou de réécrire les requêtes pour réduire les besoins en tri/hachage.
Task 14: Confirm statistics freshness on key tables
cr0x@server:~$ psql "$DATABASE_URL" -X -c "select relname, last_analyze, last_autoanalyze from pg_stat_user_tables where relname in ('orders','order_items','events');"
relname | last_analyze | last_autoanalyze
--------------+-----------------------+--------------------------
orders | | 2025-12-29 23:03:09+00
order_items | 2025-12-28 01:12:43+00| 2025-12-30 08:13:44+00
events | | 2025-12-30 08:13:44+00
(3 rows)
Ce que cela signifie : Si analyze est obsolète, le planificateur estime mal le nombre de lignes, menant à de mauvais choix de jointure et de scan.
Décision : Exécutez ANALYZE manuellement pour les tables critiques après des chargements massifs ou des événements de churn importants ; ajustez autovacuum_analyze_scale_factor si nécessaire.
Task 15: Measure cache hit ratio (with skepticism)
cr0x@server:~$ psql "$DATABASE_URL" -X -c "select datname, round(100.0*blks_hit/nullif(blks_hit+blks_read,0),2) as cache_hit_pct from pg_stat_database where datname=current_database();"
datname | cache_hit_pct
---------+---------------
appdb | 98.71
(1 row)
Ce que cela signifie : Un taux de hit élevé est bon, mais il peut masquer des problèmes : vous pouvez être à 99% en cache et lent à cause des verrous ou du CPU.
Décision : Utilisez le taux de hit comme contexte, pas comme preuve. Si la latence I/O est élevée malgré un hit élevé, vous payez peut-être pour les WAL/checkpoints ou des débordements temporaires.
Task 16 (RDS-specific): pull recent engine logs for autovacuum and checkpoints
cr0x@server:~$ aws rds describe-db-log-files --db-instance-identifier prod-pg --file-last-written 1735530000000
{
"DescribeDBLogFiles": [
{
"LogFileName": "postgresql.log.2025-12-30-08",
"LastWritten": 1735543561000,
"Size": 12839210
}
]
}
Ce que cela signifie : Vous ne pouvez pas tailer /var/log comme sur un système autogéré. Vous utilisez les APIs RDS pour trouver les segments de logs qui vous intéressent.
Décision : Si vous n’avez pas activé le logging autovacuum et checkpoints, activez-le maintenant. Tuner sans logs, c’est de la superstition avec des étapes en plus.
Trois mini-récits d’entreprise depuis le terrain
Mini-récit 1 : L’incident causé par une mauvaise hypothèse
L’entreprise avait déplacé une base OLTP critique de PostgreSQL autogéré sur EC2 vers RDS PostgreSQL. La migration fut une réussite : moins d’alertes on-call, les sauvegardes fonctionnaient, les patchs n’étaient plus une fête de la peur trimestrielle. Un mois plus tard, le premier vrai pic de croissance arriva.
L’équipe supposait que le stockage RDS « montait automatiquement ». Il a monté en capacité. Les performances n’ont pas monté comme attendu. Lors d’un pic de trafic, la latence p95 API a augmenté alors que le CPU restait modéré. La réaction initiale fut classique : « ce n’est pas le CPU, donc ça doit être l’app ». Ils ont rollbacké un déploiement deux fois. Rien n’a changé.
Performance Insights montra des sessions en attente d’I/O. La latence EBS avait augmenté et les IOPS étaient au plafond de la volume. La charge avait changé : plus d’écritures par requête, des lignes plus larges, plus de churn. L’autovacuum avait aussi pris du retard, car le bloat alourdissait chaque lecture et rendait chaque vacuum plus coûteux.
La mauvaise hypothèse n’était pas qu’AWS tomberait en panne. C’était de croire que « géré » impliquait « performance élastique ». La correction fut terne et efficace : passer à une configuration de stockage avec IOPS/débit appropriés, régler les paramètres de checkpoint pour réduire les pics d’écriture et durcir l’autovacuum sur les tables les plus chaudes. Ils ajoutèrent aussi une alerte sur la latence de stockage et la saturation d’IOPS pour ne plus jamais découvrir la physique du stockage via des plaintes utilisateurs.
Mini-récit 2 : L’optimisation qui s’est retournée contre eux
Une autre équipe avait une habitude : chaque fois qu’une requête était lente, quelqu’un ajoutait un index. Au début, ça fonctionnait. Les tableaux de bord allaient mieux. Les gens se sentaient productifs. Puis la charge d’écriture augmenta.
Sur RDS PostgreSQL, ils ajoutèrent plusieurs index qui se recoupaient sur une table à fort churn — variantes des mêmes clés composites, plus quelques index « au cas où » pour le reporting. La latence d’écriture monta lentement. Autovacuum commença à tourner plus longtemps. Les checkpoints devinrent plus lourds. Le lag des réplicas devint visiteur quotidien.
Le retour de bâton était prévisible : chaque nouvel index augmentait l’amplification des écritures. Chaque INSERT/UPDATE demandait plus de maintenance d’index. Le vacuum devait nettoyer plus de pages d’index. La charge devint plus I/O-bound, et la facture de stockage augmenta poliment alors qu’ils « corrigeaient la performance » en balançant des IOPS supplémentaires.
Quand ils auditarent enfin l’usage des index, plusieurs avaient presque zéro scan. L’équipe supprima les index redondants, remplaça quelques-uns par de meilleurs index composites alignés sur les véritables prédicats et déplaça les requêtes de reporting vers un replica avec une gouvernance stricte. Les écritures se firent plus rapides, le vacuum se calma et le lag des réplicas cessa d’être un trait de personnalité.
Mini-récit 3 : La pratique ennuyeuse mais correcte qui a sauvé la mise
Une société fintech-like exécutait RDS PostgreSQL avec Multi-AZ et un replica de lecture pour le reporting. Rien de glamour. Leur culture de tuning était encore moins glamour : revue hebdomadaire des top SQL, vérifications mensuelles de vacuum/analyze, et une politique selon laquelle chaque migration devait déclarer son comportement de verrou.
Puis une intégration tierce commença à marteler leur API avec un trafic en rafales, amplifiant un endpoint effectuant une jointure coûteuse. La requête n’était pas catastrophique isolément, mais le burst créa une foule tonitruante de requêtes identiques. Le nombre de connexions augmenta, les sessions actives s’accumulèrent et le système frôla les timeouts.
Parce qu’ils avaient les pratiques ennuyeuses, le diagnostic prit quelques minutes : pg_stat_statements montra l’explosion de la requête, Performance Insights montra du CPU plus un peu d’I/O, et leurs tableaux de bord distinguaient déjà la charge DB par type d’attente. Ils appliquèrent une correction en deux volets : un index ciblé correspondant au prédicat de jointure et un changement dans l’app pour imposer du coalescing et du caching pour cet endpoint.
Le meilleur : ils n’eurent pas besoin d’une salle de crise. Pas d’exploits. Pas de « on devrait réécrire en Rust ». Ils ont juste suivi le playbook qu’ils appliquaient toute l’année, ce qui est la chose la plus proche de la magie que permettent les systèmes en production.
Blague #2 : La seule base de données « set and forget » est celle que personne n’utilise. Dès qu’elle devient populaire, c’est redevenu du travail.
Erreurs courantes : symptôme → cause racine → correction
-
Symptôme : Pics de latence périodiques toutes les quelques minutes
Cause racine : Tempêtes de checkpoints dues à un max_wal_size trop bas ou un checkpoint_timeout agressif
Correction : Augmenter max_wal_size, augmenter checkpoint_timeout, régler checkpoint_completion_target près de 0.9 et surveiller la latence d’écriture et checkpoints_req. -
Symptôme : CPU bas, requêtes lentes, affirmations « la base est ok » dans le chat
Cause racine : Charge I/O : latence EBS élevée, plafond IOPS/débit, débordements temporaires, ou pression WAL
Correction : Validez avec les événements d’attente et les métriques de stockage ; réduisez l’I/O via indexes/fixes de requêtes, ajustez work_mem, augmentez IOPS/débit si nécessaire. -
Symptôme : Utilisation du disque qui augmente régulièrement, requêtes qui ralentissent sur des semaines
Cause racine : Bloat table/index provenant de MVCC + autovacuum en retard ou seuils mal configurés
Correction : Ajustez autovacuum par table ; planifiez une maintenance ; reconstruisez les pires cas via des méthodes sûres en ligne quand possible. -
Symptôme : Panne soudaine pendant un déploiement ; beaucoup de sessions en attente de verrous
Cause racine : Migration ayant pris un verrou ACCESS EXCLUSIVE (ALTER TABLE, index build sans CONCURRENTLY, transaction longue)
Correction : Utilisez CREATE INDEX CONCURRENTLY, backfill par lots, lock_timeout et des outils/processus de migration sensibles aux verrous. -
Symptôme : Pics de lag du replica pendant des mises à jour massives
Cause racine : Génération WAL dépassant la capacité d’application du replica ; vacuum/maintenance d’index lourde ajoute plus de WAL
Correction : Regrouper les écritures, réduire les index redondants, régler checkpoint/WAL et ne pas diriger les lectures sensibles à la fraîcheur vers des réplicas sans garde-fous. -
Symptôme : Timeouts croissants à mesure que l’app scale horizontalement
Cause racine : Tempête de connexions ; trop de sessions et surcharge mémoire ; contention de verrous amplifiée par la concurrence
Correction : Ajouter PgBouncer, limiter la concurrence applicative, baisser max_connections, utiliser des tailles de pool sensées et éliminer la création de connexions par requête. -
Symptôme : Plan de requête soudainement pire après un événement de croissance des données
Cause racine : Statistiques obsolètes ou distributions biaisées ; le planificateur estime mal les cardinalités
Correction : ANALYZE des tables critiques, augmenter statistics target pour les colonnes biaisées, revoir les prédicats et les index. -
Symptôme : « On a ajouté un index et les écritures sont plus lentes »
Cause racine : Surutilisation d’index causant amplification d’écriture et overhead de vacuum
Correction : Auditer l’usage des index ; supprimer les index inutiles/redondants ; concevoir des index composites alignés sur les prédicats réels ; envisager des index partiels.
Listes de contrôle / plan étape par étape
Semaine 1 : Rendre la performance observable
- Activer pg_stat_statements (et s’assurer qu’il survive aux redémarrages via shared_preload_libraries).
- Activer le logging des requêtes lentes avec un seuil sensé pour vos SLO ; logger la durée et les waits de verrous.
- Activer le logging d’autovacuum (au moins pour les vacuums longs) et le logging des checkpoints.
- Mettre en place des dashboards : charge DB, événements d’attente, sessions actives, IOPS/débit, latence stockage, lag des réplicas.
- Décider qui est responsable de la « revue top queries » hebdomadaire. Si c’est « personne », ce sera « commandant d’incident » plus tard.
Semaine 2 : Arrêter l’hémorragie (corrections ROI élevées)
- Depuis pg_stat_statements, choisir le top 5 par temps total et le top 5 par temps moyen ; corriger au moins deux d’entre eux.
- Ajouter ou corriger des index seulement après avoir lu EXPLAIN et confirmé l’alignement avec les prédicats.
- Réduire le nombre de connexions : implémenter du pooling et définir des tailles de pool sensées par service.
- Identifier les tables à fort churn ; définir des seuils autovacuum par table et ajuster les thresholds d’analyse.
Semaine 3–4 : Rendre ça ennuyeux
- Créer une politique de migration : lock_timeout, statement_timeout, index en concurrently, backfills par lots.
- Planifier la capacité stockage IOPS/débit ; alerter avant saturation, pas après.
- Revoir les réglages WAL/checkpoint ; éliminer les patterns de checkpoint forcés.
- Établir une culture de « budget requête » : les endpoints lourds en base ont des tests de performance et des garde-fous.
FAQ
1) RDS PostgreSQL est-il plus lent que PostgreSQL autogéré ?
Pas intrinsèquement. Beaucoup de charges tournent de la même façon ou mieux parce que la plateforme est stable et bien maintenue. La « lenteur » vient généralement d’un mauvais dimensionnement instance/stockage, d’un manque d’observabilité ou de croire que les valeurs par défaut sont optimales.
2) Quels réglages est-ce que je perds sur RDS qui importent ?
Vous perdez le contrôle au niveau hôte : choix du système de fichiers, paramètres noyau, outils disque directs et une certaine flexibilité d’extensions. La plupart des gains de performance viennent toujours de la conception des requêtes/index, de la stratégie autovacuum et du tuning I/O/WAL — des réglages que vous gardez.
3) Ai-je vraiment besoin de pg_stat_statements sur RDS ?
Oui. Sans lui vous pouvez toujours deviner à partir des logs, mais vous perdrez du temps et livrerez des demi-corrections. pg_stat_statements vous donne le classement par temps total et moyen, ce qui permet de prioriser.
4) Dois-je augmenter work_mem pour éviter les débordements temporaires ?
Seulement avec des calculs. work_mem est par opération, par requête, par session. Si vous le montez et que la concurrence est élevée, vous pouvez OOM l’instance ou pousser l’OS dans un swap détestable. Préférez des corrections ciblées : réécritures de requêtes, index, ou réglages par rôle pour les sessions lourdes connues.
5) Pourquoi mon CPU est faible mais la latence élevée ?
Parce que la base attend, elle ne travaille pas. Les attentes de verrous, d’I/O et réseau n’apparaissent pas en CPU. Utilisez les événements d’attente et les métriques de stockage pour identifier la vraie contrainte.
6) Les réplicas de lecture sont-ils une voie sûre pour scaler les lectures ?
Ils sont utiles, mais pas gratuits. Le lag des réplicas est réel, et votre application doit tolérer la staleness ou diriger les lectures sensibles à la fraîcheur vers le primaire. De plus, des requêtes de reporting lourdes peuvent encore blesser un replica et créer du bruit opérationnel.
7) Quelle est la plus grosse erreur d’autovacuum sur RDS ?
Compter sur les valeurs par défaut pour des tables à fort churn. Les valeurs par défaut sont conservatrices pour éviter des charges surprises. Les charges de production sont rarement conservatrices. Définissez des seuils par table pour les tables qui comptent, puis validez via les logs et les tendances de tuples morts.
8) Puis-je « juste scale up » l’instance RDS pour régler la performance ?
Parfois. Monter en taille aide les cas CPU-bound et mémoire-bound. Cela ne règle pas la contention de verrous, les mauvaises requêtes, les checkpoints forcés ou les plafonds de stockage. Scalez dans le cadre d’un diagnostic, pas en remplacement d’un diagnostic.
9) Comment empêcher les migrations de mettre la base hors service ?
Utilisez des patterns de migration sensibles aux verrous, définissez lock_timeout et statement_timeout pour les rôles de migration, exécutez les opérations lourdes hors-pointe et surveillez les verrous pendant les déploiements. Testez le comportement de verrou des migrations sur des volumes de données proches de la production.
10) Quelle est la première métrique sur laquelle alerter pour « base lente » ?
La charge de la base par type d’attente (depuis Performance Insights ou équivalent) plus la latence du stockage. « CPU > 80% » seul est une excellente façon de se tromper vite.
Conclusion : étapes à réaliser cette semaine
PostgreSQL autogéré vous donne plus de leviers ; RDS vous donne moins de leviers et plus de garde-fous. Dans les deux cas, la base reste un système vivant. Elle accumule de l’historique (MVCC), négocie la concurrence (verrous) et dépend de la physique du stockage (E/S et WAL). Le tuning reste votre responsabilité parce que la charge de travail vous appartient.
Faites ceci ensuite :
- Activez pg_stat_statements et le logging des requêtes lentes. Rendez impossible la discussion sur ce qui est lent.
- Exécutez le playbook de diagnostic rapide lors du prochain ralentissement et consignez le résultat : CPU, E/S, verrous ou connexions.
- Corrigez deux requêtes principales avec des changements guidés par EXPLAIN, pas par folklore.
- Choisissez vos trois tables à plus fort churn et réglez autovacuum par table ; validez avec les tuples morts et les logs de vacuum.
- Comparez checkpoints_req vs checkpoints_timed ; réglez WAL/checkpoint pour réduire les pics d’écriture.
- Limitez et pooler les connexions. Votre base n’est pas un salon de discussion.