Le post sur l'échelle d'adoption traitait des décisions macro : sur quelles features de Postgres bâtir sa stack, et où s'arrêter. Ici, on descend d'un cran, du côté des décisions micro. Des outils planqués dans Postgres qui remplacent chacun un petit bout de votre infra, et que personne ne met dans le tour du propriétaire habituel parce qu'ils ne sont pas assez tape-à-l'œil pour tenir une keynote.
La plupart des ingénieurs seniors que je connais en ont adopté deux ou trois, et ignorent complètement l'existence des autres. Ils paient un service de scheduling parce qu'ils n'ont jamais remarqué pg_cron. Ils montent un Redis parce que personne ne leur a parlé des advisory locks. Ce ne sont pas des features "Postgres peut tout faire". Ce sont des features "Postgres l'a déjà fait et vous ne le saviez pas".
LISTEN/NOTIFY, bien utilisé
Le post sur l'échelle rangeait LISTEN/NOTIFY dans le Palier 3, parce que les équipes essaient d'en faire le système nerveux d'un produit à fort trafic et se brûlent. Ce verdict tient toujours. Mais la feature a un cas d'usage légitime qui mérite le respect : la coordination basse fréquence entre l'application et la base.
Des exemples qui tournent bien : un dashboard admin en Angular qui doit se rafraîchir dès qu'un agent de support met à jour un ticket. Une couche de cache qui doit invalider une clé précise quand une ligne change. Un worker qui se réveille à l'apparition de nouveaux jobs (couplé à SKIP LOCKED, ça vous donne des queues event-driven sans polling).
Règles de pouce : sous une centaine d'écouteurs, payloads inférieurs à 1 Ko, quelques centaines d'écritures par seconde. C'est la zone confortable, pas le plafond. L'article sur l'échelle situe le plafond pratique autour de 1000 écouteurs, et l'écart entre les deux chiffres, c'est la marge que vous voulez garder pour le jour où la charge d'écriture grimpe. Dans la zone confortable, c'est élégant, fiable, et totalement gratuit. En dehors, relisez l'échelle.
Foreign Data Wrappers
postgres_fdw, mysql_fdw, mongo_fdw, file_fdw, sqlite_fdw. Vous interrogez des tables dans d'autres bases, des CSV sur disque, des fichiers Parquet (via parquet_fdw, ou parquet_s3_fdw, un dérivé qui ajoute le support S3), comme s'il s'agissait de tables Postgres ordinaires. Les jointures entre elles marchent. Les vues par-dessus aussi.
C'est la feature qui compte le plus en migration. Déplacer une table MySQL legacy vers Postgres, autrefois, ça voulait dire un pipeline ETL, une fenêtre de cutover, et un samedi all-hands désagréable. Avec mysql_fdw, vous créez la table étrangère, vous montez des vues qui mélangent le legacy et votre nouveau schéma, vous migrez par incréments, et vous supprimez le FDW une fois la dernière ligne rapatriée. Zéro code ETL, pas de panique le jour du cutover.
pg_cron
Des jobs planifiés, à l'intérieur de Postgres, définis en SQL :
SELECT cron.schedule(
'cleanup-old-events',
'0 2 * * *',
$$DELETE FROM events WHERE created_at < NOW() - INTERVAL '90 days'$$
);Et voilà tout votre job de rétention. Pas de DAG Airflow, pas de CronJob Kubernetes, pas de Lambda plus EventBridge. Dès lors que la chose à planifier est une instruction SQL ou une procédure stockée, pg_cron suffit.
C'est un vrai élimineur d'infra pour rafraîchir des vues matérialisées à heure fixe, expirer des tokens, archiver d'anciennes partitions, faire des contrôles qualité périodiques. Le jour où le travail planifié a besoin d'appels d'API externes, on sort du cadre. D'ici là, ça vous économise un service entier.
Advisory locks
Postgres a deux systèmes de verrous bien distincts. D'un côté, le verrouillage implicite de lignes et de tables hérité des transactions. De l'autre, des verrous définis par l'application, identifiés par des entiers arbitraires. Vous appelez pg_advisory_lock(id), et vous tenez le verrou jusqu'à ce que vous le relâchiez ou que la session se termine.
Ça remplace Redis pour le verrouillage coopératif dans l'immense majorité des cas. "Un seul worker doit exécuter ce cron sur toutes les instances", c'est un advisory lock sur un ID connu. "Un seul service doit rafraîchir cette entrée de cache à la fois", c'est un advisory lock calculé sur un hash de la clé. "Empêcher que le même utilisateur soit traité par deux workers en parallèle", c'est un advisory lock sur l'ID utilisateur.
Le piège à connaître : les advisory locks à portée de session ne fonctionnent pas avec le transaction pooling (PgBouncer en mode transaction). Si vous êtes derrière un pooler, prenez la variante à portée de transaction (pg_advisory_xact_lock).
Types range et contraintes d'exclusion
Le problème dur classique : deux personnes ne doivent pas pouvoir réserver la même chambre d'hôtel sur des dates qui se chevauchent. La solution naïve, c'est un lock, un select, un check, un insert, et une boucle de retry quand la race condition frappe. La solution Postgres, c'est une contrainte :
CREATE EXTENSION IF NOT EXISTS btree_gist;
CREATE TABLE bookings (
room_id int,
period tstzrange,
EXCLUDE USING gist (room_id WITH =, period WITH &&)
);L'extension btree_gist est ce qui permet à GiST d'indexer l'opérateur d'égalité sur des types scalaires comme int ; sans elle, room_id WITH = ne fonctionne pas. Cette contrainte rend les chevauchements impossibles au niveau base. Pas de race condition, pas de logique de retry, pas de verrou distribué. Réservations, planification, allocation de ressources, n'importe quel problème de plage temporelle se traduit proprement dans ce pattern. L'opérateur && teste le chevauchement de ranges, la contrainte d'exclusion l'interdit à l'intérieur d'un même room_id.
J'ai vu des équipes bâtir des microservices entiers pour régler ça à coups de locks Redis, avec les bugs de correction qui vont avec, alors que sept lignes de SQL auraient fait le travail.
pg_stat_statements
L'observabilité des performances de requêtes, intégrée à Postgres. Chaque instruction exécutée, agrégée par forme (paramètres dépouillés), avec temps total, temps moyen, nombre d'appels et lectures de buffers. Vous ajoutez pg_stat_statements à shared_preload_libraries dans postgresql.conf, vous redémarrez Postgres, puis vous lancez CREATE EXTENSION pg_stat_statements. C'est le redémarrage qui piège les gens : un simple CREATE EXTENSION sans le preload ne fonctionne pas.
C'est la première chose que j'active sur toute nouvelle instance Postgres. Ça répond à la question la plus fréquente, "pourquoi la base de données est-elle lente ?", sans APM. Le tracing distribué et le contexte applicatif en ont toujours besoin. Ça couvre le côté base de données et rien au-dessus.
Couplez-le à pg_stat_activity (requêtes en cours) et pg_stat_user_indexes (utilisation des index), et vous obtenez une stack d'observabilité de base parfaitement fonctionnelle, sans la moindre dépendance externe.
Tables unlogged
Les tables Postgres classiques écrivent dans le WAL (write-ahead log) pour garantir la durabilité. Les tables unlogged, non. Elles ne sont pas crash-safe (tronquées en cas d'arrêt brutal), mais les écritures sont 2 à 3 fois plus rapides et ne pèsent pas sur la réplication.
Cas d'usage adaptés : stockage de sessions, compteurs de rate-limiting, métriques agrégées à rétention courte, seaux analytiques éphémères. Bref, toute charge "en forme de Redis" où perdre des données sur un crash rare reste acceptable.
Ce n'est pas un remplaçant général de Redis (pas de sémantique in-memory, pas de pub/sub, pas de TTL). En revanche, pour les cas précis où vous voulez des performances d'écriture proches de Redis sans faire tourner un service supplémentaire, c'est une option légitime dont plus personne ne se souvient.
L'effet cumulé
Prise isolément, chacune de ces features est un petit confort. L'intérêt, c'est l'accumulation : chaque adoption, c'est un service que vous ne faites pas tourner. Pas d'instance de scheduler, pas de cluster Redis pour les locks, pas de pipeline ETL, pas de broker pub/sub séparé, pas d'APM juste pour voir les requêtes lentes.
Voilà le vrai argument pour "just use Postgres", débarrassé du hype : moins de choses à monitorer, moins de choses à upgrader, moins de choses pour vous biper à 3 h du matin. L'échelle vous dit où tracer la ligne. La boîte à outils vous dit combien de terrain vous couvrez avant d'y arriver.
Références
- LISTEN et NOTIFY (docs PostgreSQL) : https://www.postgresql.org/docs/current/sql-notify.html
- Foreign Data Wrappers / postgres_fdw (docs PostgreSQL) : https://www.postgresql.org/docs/current/postgres-fdw.html
- Advisory Locks (docs PostgreSQL) : https://www.postgresql.org/docs/current/explicit-locking.html
- Types range (docs PostgreSQL) : https://www.postgresql.org/docs/current/rangetypes.html
- Contraintes d'exclusion (docs PostgreSQL) : https://www.postgresql.org/docs/current/ddl-constraints.html
- pg_stat_statements (docs PostgreSQL) : https://www.postgresql.org/docs/current/pgstatstatements.html
- CREATE UNLOGGED TABLE (docs PostgreSQL) : https://www.postgresql.org/docs/current/sql-createtable.html
- pg_cron (Citus Data, désormais Microsoft) : https://github.com/citusdata/pg_cron
- mysql_fdw (EnterpriseDB) : https://github.com/EnterpriseDB/mysql_fdw
- mongo_fdw (EnterpriseDB) : https://github.com/EnterpriseDB/mongo_fdw
- parquet_fdw (interroger des fichiers Parquet locaux comme des tables) : https://github.com/adjust/parquet_fdw
- parquet_s3_fdw (PGSpider, dérivé de parquet_fdw, ajoute le support Amazon S3) : https://github.com/pgspider/parquet_s3_fdw
- file_fdw (docs PostgreSQL) : https://www.postgresql.org/docs/current/file-fdw.html
- PostgreSQL Unlogged Tables (Crunchy Data) : https://www.crunchydata.com/blog/postgresl-unlogged-tables
- Unlogged tables in Postgres: Is their data truly gone after a crash? (pganalyze) : https://pganalyze.com/blog/5mins-postgres-unlogged-tables
- Logged vs Unlogged table performance comparison : https://dev.to/yet_anotherdev/logged-vs-unlogged-table-performance-in-postgres-11pk