Skip to content
Learni
Voir tous les tutoriels
Bases de données

Comment optimiser PostgreSQL pour la production en 2026

Read in English

Introduction

PostgreSQL, leader des SGBD open-source, excelle en production grâce à sa robustesse et ses fonctionnalités avancées. En 2026, avec la croissance des données (IoT, AI, logs), l'optimisation devient critique pour éviter les goulots d'étranglement. Ce tutoriel expert vous guide pas à pas pour implémenter des partitions temporelles, des index GIN/BRIN pour JSONB, du full-text search, des vues matérialisées et l'analyse EXPLAIN ANALYZE. Imaginez réduire vos temps de query de 90% sur des tables de millions de lignes, comme un mécanicien affinant un moteur V12. Chaque étape inclut du code SQL complet, testable immédiatement sur PostgreSQL 17+. À la fin, votre base sera prête pour l'échelle horizontale et les workloads intensifs. (142 mots)

Prérequis

  • PostgreSQL 17+ installé (Docker : docker run -p 5432:5432 -e POSTGRES_PASSWORD=pass postgres:17)
  • Client SQL : psql ou pgAdmin
  • Connaissances avancées en SQL (JOINs complexes, CTE)
  • Dataset de test : 1M+ lignes simulées
  • Extensions activées : CREATE EXTENSION IF NOT EXISTS pg_trgm; (optionnel)

Initialiser la base et créer une table partitionnée

01_init_partitions.sql
CREATE DATABASE perf_test;
\c perf_test;

-- Table parent partitionnée par range (date)
CREATE TABLE sales (
  id SERIAL,
  sale_date DATE NOT NULL,
  amount DECIMAL(10,2),
  customer_id INT,
  metadata JSONB,
  PRIMARY KEY (id, sale_date)
) PARTITION BY RANGE (sale_date);

-- Partitions pour 2025-2026
CREATE TABLE sales_2025 PARTITION OF sales
  FOR VALUES FROM ('2025-01-01') TO ('2026-01-01');
CREATE TABLE sales_2026 PARTITION OF sales
  FOR VALUES FROM ('2026-01-01') TO ('2027-01-01');

-- Insérer 100k lignes de test (simulé)
INSERT INTO sales (sale_date, amount, customer_id, metadata)
SELECT
  ('2025-01-01'::DATE + (random() * 365)::INT) AS sale_date,
  random() * 1000 AS amount,
  (random() * 1000)::INT AS customer_id,
  jsonb_build_object('category', 'tech', 'tags', array['laptop', 'sale']) AS metadata
FROM generate_series(1, 100000);

Ce script crée une DB perf_test et une table sales partitionnée par sale_date, limitant les scans à une partition par query. Les 100k lignes de test simulent un workload réel avec JSONB. Piège : Oublier les contraintes PK sur les colonnes de partition cause des doublons.

Pourquoi et comment les partitions boostent les performances

Les partitions divisent physiquement les données, comme des tiroirs organisés dans un bureau géant. Pour des queries filtrées par date (90% des cas en prod), pg_prune élimine les partitions inutiles, réduisant I/O de 80%. Avantage expert : maintenance (DROP partition) sans lock global. Testez avec EXPLAIN SELECT * FROM sales WHERE sale_date >= '2025-06-01'; – une seule partition scannée.

Index avancés : GIN pour JSONB et BRIN pour dates

02_indexes.sql
CREATE INDEX idx_sales_date_brin ON sales USING BRIN (sale_date);
CREATE INDEX idx_sales_amount_btree ON sales (amount) WHERE amount > 500;
CREATE INDEX idx_sales_metadata_gin ON sales USING GIN (metadata);
CREATE INDEX idx_sales_customer_trgm ON sales USING gin (customer_id gin_trgm_ops); -- Nécessite pg_trgm

-- ANALYZE pour stats précises
ANALYZE sales;

-- Vacuum pour réclamation espace
VACUUM ANALYZE sales_2025;

BRIN est idéal pour données triées/temporelles (compression 10x), GIN excelle sur JSONB pour ?| ou @>. Partial index filtre amount > 500. Activez pg_trgm pour trigram similarity. Piège : Sur-indexer gonfle le stockage ; monitorez avec pg_stat_user_indexes.

Choix stratégique des index pour workloads variés

BRIN : Colonnes corrélées/croissantes (dates, IDs séquentiels), faible overhead. GIN : Arrays/JSONB/full-text, plus lourd mais rapide sur containment. BTREE : Égalités/ordres standards. Mesurez impact avec EXPLAIN (ANALYZE, BUFFERS) avant/après. En prod, partial indexes réduisent 50% la taille pour queries filtrées.

Full-Text Search avec ts_vector et ranking

03_fts.sql
-- Ajouter colonnes FTS
ALTER TABLE sales ADD COLUMN tsv tsvector;
CREATE INDEX idx_sales_tsv_gin ON sales USING GIN(tsv);

-- Trigger pour mise à jour auto (config_fr pour français)
CREATE OR REPLACE FUNCTION sales_fts_trigger() RETURNS trigger AS $$
BEGIN
  NEW.tsv := to_tsvector('french', coalesce(metadata::text, ''));
  RETURN NEW;
END$$ LANGUAGE plpgsql;

CREATE TRIGGER sales_fts_update BEFORE INSERT OR UPDATE ON sales
  FOR EACH ROW EXECUTE FUNCTION sales_fts_trigger();

-- Refresh sur données existantes
UPDATE sales SET metadata = metadata WHERE id <= 100000;

-- Query FTS avec ranking
SELECT id, sale_date, ts_rank(tsv, query) AS rank
FROM sales, plainto_tsquery('french', 'laptop vente') query
WHERE tsv @@ query
ORDER BY rank DESC
LIMIT 10;

ts_vector indexe le texte (français via 'french'), trigger auto-maj. Ranking priorise pertinence. GIN accélère @@. Piège : Sans trigger, FTS obsolète ; testez stemming (french gère accents).

Full-Text Search : Au-delà de LIKE pour la pertinence

Remplacez LIKE '%laptop%' (lent, non-stemming) par FTS : supporte ranking, stemming, poids. Analogie : Google vs recherche basique. Scale à milliards de docs avec pg_bigm ou pg_trgm pour fuzzy search.

Vues matérialisées pour analytics rapides

04_materialized.sql
-- Vue matérialisée : CA mensuel
CREATE MATERIALIZED VIEW sales_monthly_mv AS
SELECT
  DATE_TRUNC('month', sale_date) AS month,
  SUM(amount) AS total,
  COUNT(*) AS count,
  AVG(amount) AS avg
FROM sales
GROUP BY month;

CREATE UNIQUE INDEX idx_mv_month ON sales_monthly_mv (month);

-- Query rapide
SELECT * FROM sales_monthly_mv WHERE month >= '2025-06-01';

-- Refresh périodique (cron/job)
REFRESH MATERIALIZED VIEW CONCURRENTLY sales_monthly_mv;

Matview pré-calcule agrégats, 100x plus rapide que query live. CONCURRENTLY évite locks. Index unique sur clé. Piège : Refresh coûteux ; programmez via pg_cron (SELECT cron.schedule('0 2 *', 'REFRESH...');).

Window functions et CTE pour queries analytiques expertes

05_window_cte.sql
WITH ranked_sales AS (
  SELECT
    sale_date,
    amount,
    customer_id,
    ROW_NUMBER() OVER (PARTITION BY customer_id ORDER BY sale_date DESC) as rn,
    LAG(amount) OVER (PARTITION BY customer_id ORDER BY sale_date) as prev_amount
  FROM sales
  WHERE sale_date >= '2025-01-01'
)
SELECT
  customer_id,
  SUM(amount) FILTER (WHERE rn = 1) as latest_monthly,
  AVG(amount - COALESCE(prev_amount, 0)) as growth
FROM ranked_sales
GROUP BY customer_id
HAVING COUNT(*) > 5
ORDER BY growth DESC
LIMIT 20;

-- EXPLAIN ANALYZE pour valider
EXPLAIN (ANALYZE, BUFFERS) WITH ranked_sales AS ... [même query];

Window ROW_NUMBER/LAG ranke par client sans sous-queries corrélées. FILTER conditionne agrégats. CTE réutilisable. EXPLAIN révèle seq_scan vs index_scan. Piège : Sans PARTITION, perf chute ; buffers montrent cache hits.

Analyse EXPLAIN : Le détecteur de goulots

EXPLAIN (ANALYZE, BUFFERS, VERBOSE) décortique plans : cherchez Seq Scan (mauvais), Index Only Scan (idéal). cost estime temps CPU/I/O, actual time mesure réel. Astuce : SET work_mem = '256MB'; pour sorts/hashs en mémoire.

Bonnes pratiques essentielles

  • Autovacuum tuning : autovacuum_vacuum_scale_factor = 0.05 pour tables actives.
  • Connection pooling : PgBouncer ou Pgpool-II (max 100 connex).
  • Monitoring : pg_stat_statements + Prometheus exporter.
  • Backups : pg_basebackup + WAL (point-in-time recovery).
  • Config prod : shared_buffers=25% RAM, effective_cache_size=75% RAM, work_mem=4GB/max_parallel_workers.

Erreurs courantes à éviter

  • Ignorer ANALYZE : Stats obsolètes → mauvais plans d'exécution.
  • Index sur cardinalité basse (<10% distinct) : overhead inutile.
  • Oublier CONCURRENTLY sur matviews/index : locks bloquent prod.
  • JSONB non-indexé : Scans séquentiels sur TB de données.

Pour aller plus loin

Plongez dans les formations Learni sur PostgreSQL avancé. Ressources : Docs PG 17 partitioning, EXPLAIN, Citus pour sharding. Projet open : pgBadger pour logs analysis.