Retour au Blog
Databases & Backend2 octobre 202511 min de lecture

L'échelle d'adoption de Postgres vue par un senior : ce qu'on sort vraiment avant d'invoquer Redis

DR

Doug Ribeiro

11 min de lecture

On voit passer un genre d'articles qui prétend qu'on peut remplacer toute sa stack par Postgres. Jeter Redis, jeter Kafka, jeter Elasticsearch, jeter sa base vectorielle, tout balancer. "just use Postgres".

Sur la liste des fonctionnalités, ces articles ont raison. Sur la réalité opérationnelle, beaucoup moins. Techniquement, chaque affirmation tient debout : JSONB avec des index GIN vous offre vraiment une souplesse façon MongoDB, FOR UPDATE SKIP LOCKED vous construit vraiment une queue, pg_trgm fait réellement du fuzzy search, pgvector fait réellement de la recherche ANN. Ce qu'ils ne disent pas, en revanche, c'est où chaque feature casse, ce qu'elle vous coûte au quotidien et à partir de quel moment l'outil spécialisé que vous comptiez dégager redevient raisonnable.

Voici le post que j'aurais voulu qu'on écrive pour moi : une échelle d'adoption concrète, avec des chiffres, pas une visite guidée du catalogue. Quatre paliers, d'"adopter sans se poser de questions" à "à éviter". Chaque item vient avec un seuil qui vous indique quand arrêter.

Palier 1 : Adopter sans hésiterJSONB+GIN, pg_trgm, vues matérialisées, pgvector <5M, BRIN + partitionn… 1 Palier 2 : Adopter avec discipline opérationnelleQueues SKIP LOCKED (VACUUM à tuner), full-text search (<1M docs), PgBou… 2 Palier 3 : Réfléchir à deux foisLISTEN/NOTIFY à haut débit, RLS comme seule couche d'autorisation, Post… 3 Palier 4 : À éviter100M+ vecteurs, millions d'événements WS/s, milliers d'écouteurs, écrit… 4

Palier 1 : Adopter sans hésiter

Les briques porteuses de Postgres qu'un ingénieur senior sort dès le jour un. Stables, bon marché, sans dette opérationnelle.

JSONB avec index GIN

C'est ici que vivent les données "structurées mais flexibles". Un profil utilisateur avec un schéma prévisible et un blob de préférences ? Une table, deux colonnes, un index GIN sur le blob. Vous gardez les jointures, l'ACID, et le schéma-on-read au même endroit. La critique classique porte sur le surcoût de stockage (JSONB répète le nom des champs sur chaque ligne), mais on parle d'un problème qui apparaît à des centaines de millions de documents, pas à votre échelle pré-Série B.

pg_trgm pour la recherche tolérante aux fautes

Les index trigrammes règlent le classique "la barre de recherche qui pardonne les fautes de frappe" en cinq lignes de SQL. C'est le cas 90%. Sortir Elasticsearch pour animer un typeahead sur 200k produits, c'est de la sur-ingénierie.

Vues matérialisées avec REFRESH CONCURRENTLY

Votre requête de dashboard agrège un million de lignes et met six secondes ? La réponse n'est pas un contrat Snowflake. C'est une vue matérialisée rafraîchie toutes les cinq minutes. Avec un index unique, REFRESH CONCURRENTLY calcule en arrière-plan et bascule les lignes sans bloquer les lecteurs.

pgvector sous les 5M de vecteurs

Le propre benchmark 2024 de Timescale (mené par l'éditeur, 50M d'embeddings à 99 % de recall) place pgvectorscale (une extension distincte de Tiger Data construite par-dessus pgvector, pas pgvector vanilla) devant l'index s1 pod storage-optimized de Pinecone : 28 ms p95 contre 784 ms p95. En dessous de 5M de vecteurs avec des index HNSW, pgvector n'est pas un compromis, c'est la bonne réponse. Encore plus vrai dès qu'il faut des filtres hybrides ("similarité sémantique, mais uniquement les documents de ce tenant sur la dernière semaine"). C'est justement sur les filtres hybrides que les DB vectorielles spécialisées souffrent encore.

Index BRIN et partitionnement déclaratif pour le time-séries

Si vos données sont globalement monotones (logs, événements, métriques) et que vous partitionnez par jour ou par semaine, BRIN vous donne un index un à trois ordres de grandeur plus petit qu'un B-tree sur la même donnée. Où vous tombez dans cette fourchette dépend entièrement du degré auquel l'ordre physique des lignes suit la colonne indexée : une corrélation quasi parfaite vous place en haut de la fourchette, une table mélangée vous place en bas et fait de BRIN le mauvais index. Les range scans restent rapides, le disque reste bon marché.

PostGIS pour tout ce qui touche au géospatial

PostGIS n'est pas une alternative aux SIG commerciaux, c'est le standard face auquel ces SIG perdent souvent. Dès que votre produit touche à des coordonnées, commencez ici et arrêtez de vous poser la question.

Palier 2 : Adopter avec discipline opérationnelle dès le jour un

Ça marche, à condition d'y faire attention. Passer en production sans la discipline qui va avec, et vous l'apprendrez à vos dépens.

Queues avec FOR UPDATE SKIP LOCKED

La feature SQL la plus élégante de Postgres. Elle transforme une table banale en queue de travail concurrente. Le piège est ailleurs : le churn. Une queue à haut débit, ce sont des INSERT, UPDATE, DELETE en continu sur la même table, donc des tuples morts, donc de la pression sur VACUUM. Si autovacuum ne suit pas, le planner se met à choisir de mauvais plans, votre "prise de job instantanée" devient un seq scan à 400 ms, et vos workers paraissent cassés.

Il faut tuner autovacuum table par table (descendre autovacuum_vacuum_scale_factor de 0,2 à 0,02 sur les tables de queue est devenu le standard de l'industrie, ce qui déclenche le vacuum à 2 % de tuples morts au lieu de 20 %), garder des transactions courtes, surveiller le ratio de tuples morts. Ce n'est pas optionnel, ça fait partie du pattern.

Full-text search avec tsvector

Excellent sous le million de documents dans une seule langue. Au-dessus, on commence à sentir ce qu'Elasticsearch vous apporte vraiment : un scoring BM25 vraiment ajustable, de vrais analyseurs multilingues, la recherche à facettes, du fuzzy matching qui va au-delà des trigrammes. Le seuil de graduation tourne autour de 1 à 5 millions d'enregistrements, ou la première fois qu'un PM vous demande "une pertinence comme Algolia".

PgBouncer devant tout

Postgres fonctionne en process-par-connexion. Chaque connexion, c'est un processus OS d'environ 5 Mo. Le max_connections par défaut est à 100, et les performances se dégradent dès 200 à 500. En production, on dépasse ces seuils bien plus vite qu'on ne l'imagine, surtout avec les frameworks modernes qui ouvrent des connexions paresseuses un peu partout. PgBouncer n'est pas un outil "pour plus tard", il fait partie de la stack de base.

PgBouncer propose trois modes de pooling : session (une connexion serveur par client, le plus sûr mais le pool se remplit vite), transaction (la connexion serveur retourne au pool dès que chaque transaction est commitée, le choix habituel pour la plupart des charges) et statement (relâchée après chaque instruction, casse les transactions, rarement le bon choix). Mode transaction pooling, pool de 20 à 30 connexions par instance applicative, et on surveille la file d'attente. À savoir : certaines features de Postgres cessent de fonctionner correctement en transaction pooling, notamment LISTEN/NOTIFY, les advisory locks à portée de session et PREPARE au niveau SQL. (Les prepared statements au niveau protocole sont supportés depuis PgBouncer 1.21 et activés par défaut depuis la 1.24.) Sachez ce que vous perdez avant d'activer le mode.

Palier 3 : Réfléchir à deux fois

Des features qui brillent dans les articles de blog et qui deviennent load-bearing de façons que vous allez regretter.

LISTEN/NOTIFY comme pub/sub à haut débit

Ça paraît gratuit parce que c'est intégré. Ce que la visite guidée oublie de mentionner : NOTIFY prend un verrou exclusif lourd juste avant le commit et le garde jusqu'à la fin de la transaction, si bien que les transactions qui émettent un NOTIFY commitent une par une. Un simple UPDATE qui ne notifie personne ne touche jamais ce verrou. Ça semble anodin, jusqu'au moment où le notify se retrouve sur votre chemin d'écriture le plus chaud et où votre débit d'écriture est plafonné par une seule file sérialisée. Les messages ne sont pas persistés, le payload est plafonné à 8 Ko, et l'ensemble ne fonctionne pas avec le transaction pooling. À utiliser pour les dashboards internes, les back-offices, les signaux d'invalidation peu fréquents. À ne pas utiliser comme système nerveux d'un produit à fort trafic.

Row-Level Security comme seule couche d'autorisation

RLS est puissant, et pour une application multi-tenant c'est un excellent outil. Le piège, c'est la performance : chaque requête que voit le planner intègre votre policy RLS, et si cette policy touche une colonne non indexée ou une sous-requête, un simple SELECT peut se transformer en quelque chose qui ressemble à une jointure sur quatorze tables. Traitez RLS comme le filet de sécurité et gardez malgré tout des vérifications applicatives sur les chemins critiques. Adoptez RLS avec le même respect qu'un trigger de base : utile, invisible, et capable de vous surprendre.

PostgREST ou pg_graphql comme backend entier

Les APIs générées automatiquement à partir du schéma sont un merveilleux outil de prototypage. Elles couvrent le CRUD proprement. À la seconde où la vraie logique métier débarque (workflows multi-étapes, appels d'API externes, règles qui méritent d'être relues en PR), vous êtes en train d'écrire des fonctions PL/pgSQL. Or PL/pgSQL n'a pas de stack traces modernes, un outillage famélique, une discipline git par défaut médiocre, et aucun language server digne de ce nom. Le discours "éliminer le backend" devient alors "toute votre logique métier vit dans des fichiers .sql que plus personne ne se souvient d'avoir écrits".

Palier 4 : À éviter

Certaines charges ne sont pas faites pour une instance Postgres unique, peu importe le nombre d'extensions que vous chargez.

100M+ vecteurs

Au-dessus de ce seuil, les DB vectorielles dédiées (Qdrant, Milvus, Weaviate) gagnent significativement sur le couple recall/latence et sur l'empreinte mémoire. Si votre système RAG dépasse ce cap, la spécialisation est méritée.

Des millions d'événements WebSocket par seconde

Redis Streams, Kafka et NATS existent pour ça. Postgres n'est pas conçu pour du fan-out à très haut débit, que le backing store soit en mémoire (Redis) ou un log durable (Kafka).

Pub/sub avec des milliers d'écouteurs simultanés

LISTEN/NOTIFY plafonne en pratique autour de 1000 écouteurs, et se dégrade bien avant ça sous charge d'écriture. C'est autour de 100 écouteurs qu'il reste tranquille, et c'est ce chiffre qu'il faut viser à la conception. Le millier, c'est le mur, pas la cible.

Écritures multi-région actif-actif

Postgres a un seul primaire. La réplication logique et les solutions actif-actif existent, mais elles renvoient les problèmes durs (résolution de conflits, amplification d'écriture) dans votre application. Si vous devez vraiment écrire depuis trois régions, la base n'est pas la couche à laisser simple.

Le modèle mental

L'échelle ne dit pas "Postgres ne suffit pas". Elle dit que Postgres suffit presque toujours, et bien plus longtemps qu'on ne le pense, à condition d'accepter la discipline opérationnelle qu'il exige. La plupart des boîtes qui embarquent un Redis, un Kafka, un Elasticsearch et une base vectorielle dès leur première année paient pour une complexité qu'elles n'ont pas méritée.

La règle que j'applique : on démarre au Palier 1 sans discuter, on monte au Palier 2 uniquement quand la maturité ops suit, on traite le Palier 3 comme une décision délibérée avec un plan de sortie, et on reconnaît le Palier 4 comme "autre outil" plutôt que "Postgres en plus gros".

La réponse peu glamour à "qu'est-ce que j'utilise ?" est presque toujours "Postgres, avec soin". Les articles qui disent "just use Postgres" ont raison sur la direction et tort sur la facilité.

Références

PostgreSQLDatabasesBackendArchitecture