Cours PostgreSQL Complet 🐘

Les 12 chapitres essentiels — SQL avancé, JSONB, full-text, index, extensions et administration

12
Chapitres
130+
Requêtes SQL
PG 16+
Version
A1→C2
Niveaux

CHAPITRE 01

Introduction et installation

🐘 Premiers pas
— Installer PostgreSQL
— macOS : brew install postgresql@16 && brew services start postgresql@16
— Ubuntu : sudo apt install postgresql postgresql-contrib
— Windows : https://www.postgresql.org/download/windows/
— Docker : docker run -d -p 5432:5432 -e POSTGRES_PASSWORD=secret postgres:16-alpine

— Se connecter (CLI)
psql -U postgres
psql -U postgres -d mon_app — Directement dans une BDD

— Commandes psql
\l — Lister les bases
\c mon_app — Se connecter à une base
\dt — Lister les tables
\d users — Structure d'une table
\di — Lister les index
\q — Quitter

— Clients graphiques
— • DBeaver (gratuit, multi-BDD)
— • pgAdmin (officiel PostgreSQL)
— • TablePlus / DataGrip

PostgreSQL est le SGBD relationnel open-source le plus avancé. Né en 1986 à l'Université de Berkeley, c'est la référence pour les projets exigeants. Il supporte le JSONB, les arrays, le full-text search, les types géographiques (PostGIS), les CTEs récursifs, les window functions et des dizaines d'extensions. Utilisé par Instagram, Spotify, Reddit, Twitch, Apple, Bloomberg et la NASA.

⚖️ PostgreSQL vs MySQL
Aspect PostgreSQL MySQL
Conformité SQL Très stricte (standard ANSI) Moins stricte (modes permissifs)
Types avancés JSONB, arrays, hstore, UUID, INET JSON (moins performant), ENUM
Full-text search Intégré et puissant Basique
Concurrence MVCC natif (pas de verrous en lecture) Dépend du moteur (InnoDB)
Extensions PostGIS, pgvector, pg_trgm… Limitées
Performances Excellent en lecture et écriture complexe Excellent en lecture simple
Écosystème Django, Rails, Node.js, Go WordPress, Laravel, PHP

CHAPITRE 02

Bases de données et tables

🏗️ Créer des tables
— Créer une base
CREATE DATABASE mon_app;

— Créer une table avec des types PostgreSQL
CREATE TABLE users (
id SERIAL PRIMARY KEY, — Auto-incrémenté (ou BIGSERIAL)
name VARCHAR(100) NOT NULL,
email VARCHAR(255) NOT NULL UNIQUE,
role VARCHAR(20) NOT NULL DEFAULT 'user'
CHECK (role IN ('user', 'admin', 'editor')),
bio TEXT,
is_active BOOLEAN NOT NULL DEFAULT TRUE,
metadata JSONB DEFAULT ''::jsonb,
tags TEXT[] DEFAULT '', — Array de textes !
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),
updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

— Table avec clé étrangère
CREATE TABLE posts (
id SERIAL PRIMARY KEY,
title VARCHAR(255) NOT NULL,
slug VARCHAR(255) NOT NULL UNIQUE,
content TEXT,
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
published BOOLEAN NOT NULL DEFAULT FALSE,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);

— Trigger pour updated_at automatique
CREATE OR REPLACE FUNCTION update_updated_at()
RETURNS TRIGGER AS $$
BEGIN
NEW.updated_at = NOW();
RETURN NEW;
END;
$$ LANGUAGE plpgsql;

CREATE TRIGGER users_updated_at
BEFORE UPDATE ON users
FOR EACH ROW EXECUTE FUNCTION update_updated_at();

PostgreSQL utilise CHECK au lieu d'ENUM pour contraindre les valeurs. CHECK est plus flexible : pas besoin d'ALTER TABLE pour ajouter une valeur. PostgreSQL supporte aussi les vrais ENUM (CREATE TYPE) mais CHECK est généralement préféré. Notez TIMESTAMPTZ (avec timezone) au lieu de TIMESTAMP — toujours utiliser TIMESTAMPTZ.

CHAPITRE 03

Types de données

📋 Types PostgreSQL
Type Description Usage
Classiques
SERIAL / BIGSERIAL Auto-incrémenté 4 / 8 octets Clés primaires
INTEGER / BIGINT Entier 4 / 8 octets Compteurs, FKs
NUMERIC(p,s) Précision exacte Argent (jamais FLOAT)
VARCHAR(n) / TEXT Texte variable / illimité Noms, contenus
BOOLEAN TRUE / FALSE Flags
TIMESTAMPTZ Date + heure + timezone Toujours TIMESTAMPTZ
Spécifiques PostgreSQL
UUID Identifiant universel unique IDs distribués, APIs publiques
JSONB JSON binaire (indexable, requêtable) Métadonnées, données flexibles
TEXT[] Array de textes Tags, catégories
INET / CIDR Adresse IP / réseau Logs, géo-restriction
POINT Coordonnées (x, y) Géolocalisation basique
TSVECTOR Vecteur full-text search Recherche textuelle
— UUID comme clé primaire (recommandé pour les APIs)
CREATE EXTENSION IF NOT EXISTS « pgcrypto »;

CREATE TABLE products (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
name VARCHAR(255) NOT NULL,
price NUMERIC(10,2) NOT NULL CHECK (price > 0),
tags TEXT[] DEFAULT '',
attributes JSONB DEFAULT ''::jsonb
);

CHAPITRE 04

CRUD (INSERT, SELECT, UPDATE, DELETE)

✏️ Opérations CRUD
— INSERT avec RETURNING (retourner les données insérées)
INSERT INTO users (name, email, role) VALUES
('Alice', 'alice@mail.com', 'admin')
RETURNING id, name, created_at; — ← Spécifique PostgreSQL !

— Insertion multiple
INSERT INTO users (name, email) VALUES
('Bob', 'bob@mail.com'),
('Claire', 'claire@mail.com')
RETURNING *;

— UPSERT (INSERT ou UPDATE si conflit)
INSERT INTO users (email, name) VALUES ('alice@mail.com', 'Alice M.')
ON CONFLICT (email) DO UPDATE SET
name = EXCLUDED.name,
updated_at = NOW();

— ON CONFLICT DO NOTHING (ignorer les doublons)
INSERT INTO users (email, name) VALUES ('alice@mail.com', 'Alice')
ON CONFLICT (email) DO NOTHING;

— UPDATE avec RETURNING
UPDATE users SET role = 'editor' WHERE id = 2
RETURNING *;

— DELETE avec RETURNING
DELETE FROM users WHERE id = 3
RETURNING id, name;

RETURNING est une des killer features de PostgreSQL. En MySQL, après un INSERT, vous devez faire un SELECT séparé pour récupérer les données. En PostgreSQL, RETURNING * retourne la ligne insérée/modifiée/supprimée en une seule requête. L'upsert (ON CONFLICT) remplace élégamment l'INSERT … ON DUPLICATE KEY de MySQL.

CHAPITRE 05

Jointures et requêtes avancées

🔗 Jointures + Window Functions
— Jointures (identiques à MySQL)
SELECT u.name, p.title
FROM posts p
INNER JOIN users u ON p.user_id = u.id
WHERE p.published = TRUE
ORDER BY p.created_at DESC;

— CTE (Common Table Expression)
WITH top_authors AS (
SELECT user_id, COUNT(*) AS nb
FROM posts WHERE published = TRUE
GROUP BY user_id
ORDER BY nb DESC LIMIT 10
)
SELECT u.name, t.nb
FROM top_authors t
JOIN users u ON u.id = t.user_id;

— CTE récursif (hiérarchies : catégories, commentaires imbriqués)
WITH RECURSIVE tree AS (
SELECT id, name, parent_id, 0 AS depth
FROM categories WHERE parent_id IS NULL
UNION ALL
SELECT c.id, c.name, c.parent_id, t.depth + 1
FROM categories c
JOIN tree t ON c.parent_id = t.id
)
SELECT * FROM tree ORDER BY depth, name;

— WINDOW FUNCTIONS — calculs sur un « fenêtre » de lignes
SELECT
name, role, salary,
RANK() OVER (ORDER BY salary DESC) AS rang,
AVG(salary) OVER (PARTITION BY role) AS avg_role,
salary AVG(salary) OVER (PARTITION BY role) AS ecart_moyenne,
LAG(salary) OVER (ORDER BY salary) AS salaire_precedent
FROM employees;

Les window functions sont le superpouvoir de PostgreSQL. Elles permettent de calculer sur un « groupe » de lignes sans les agréger (contrairement à GROUP BY). RANK() = classement, ROW_NUMBER() = numérotation, LAG/LEAD() = ligne précédente/suivante, SUM() OVER = somme glissante. PARTITION BY = grouper, ORDER BY = ordonner dans la fenêtre.

CHAPITRE 06

Fonctions et agrégation

📊 Agrégation et fonctions
— Agrégations classiques (comme MySQL)
SELECT role, COUNT(*), AVG(salary)::NUMERIC(10,2)
FROM users
GROUP BY role
HAVING COUNT(*) > 5
ORDER BY COUNT(*) DESC;

— FILTER — agréger conditionnellement (très pratique !)
SELECT
COUNT(*) AS total,
COUNT(*) FILTER (WHERE published = TRUE) AS published,
COUNT(*) FILTER (WHERE published = FALSE) AS drafts
FROM posts;

— string_agg — concaténer en une string
SELECT user_id, string_agg(tag, ', ' ORDER BY tag) AS all_tags
FROM post_tags
GROUP BY user_id;

— array_agg — agréger en array
SELECT user_id, array_agg(title ORDER BY created_at DESC) AS post_titles
FROM posts
GROUP BY user_id;

— Casting (::)
SELECT '42'::INTEGER; — String → Integer
SELECT NOW()::DATE; — Timestamp → Date
SELECT price::NUMERIC(10,2); — Arrondir un float

CHAPITRE 07

JSONB et types avancés

📦 JSONB — le meilleur des deux mondes
— Insérer du JSONB
INSERT INTO products (name, price, attributes) VALUES
('MacBook Pro', 2499.99, ''::jsonb);

— Lire des champs JSONB
SELECT name,
attributes->>'brand' AS brand, — ->> retourne du texte
attributes->'ram' AS ram, — -> retourne du JSONB
attributes->'colors'->>0 AS first_color — Accès array
FROM products;

— Filtrer sur du JSONB
SELECT * FROM products WHERE attributes->>'brand' = 'Apple';
SELECT * FROM products WHERE (attributes->'ram')::int >= 16;

— Contient (@>)
SELECT * FROM products
WHERE attributes @> ''::jsonb;

— Modifier du JSONB
UPDATE products SET attributes = attributes || ''::jsonb; — Ajouter
UPDATE products SET attributes = attributes 'warranty'; — Supprimer une clé
UPDATE products SET attributes = jsonb_set(attributes, '', '32'); — Modifier

— Index GIN sur JSONB (accélère @>, ?, ?|, ?&)
CREATE INDEX idx_products_attrs ON products USING GIN (attributes);

JSONB = NoSQL dans PostgreSQL. Vous pouvez stocker des données flexibles (comme MongoDB) tout en gardant les jointures, les transactions ACID et les index SQL. Utilisez JSONB pour les métadonnées variables, les préférences utilisateur, les attributs produit — tout ce qui n'a pas de schéma fixe. Toujours JSONB, jamais JSON (JSONB est binaire, indexable et plus rapide).

📚 Arrays PostgreSQL
— Utiliser les arrays
INSERT INTO users (name, email, tags) VALUES
('Alice', 'alice@mail.com', ARRAY['python', 'django', 'postgresql']);

— Filtrer
SELECT * FROM users WHERE 'python' = ANY(tags); — Contient 'python'
SELECT * FROM users WHERE tags @> ARRAY['python', 'django']; — Contient les deux

— Ajouter/Retirer
UPDATE users SET tags = array_append(tags, 'react') WHERE id = 1;
UPDATE users SET tags = array_remove(tags, 'django') WHERE id = 1;

— Index GIN sur arrays
CREATE INDEX idx_users_tags ON users USING GIN (tags);

CHAPITRE 08

Full-text search

🔍 Recherche textuelle intégrée
— Recherche full-text basique
SELECT title, content
FROM posts
WHERE to_tsvector('french', title || ' ' || content)
@@ to_tsquery('french', 'postgresql & performance');

— Avec ranking (pertinence)
SELECT title,
ts_rank(to_tsvector('french', title || ' ' || content),
to_tsquery('french', 'postgresql')) AS rank
FROM posts
WHERE to_tsvector('french', title || ' ' || content)
@@ to_tsquery('french', 'postgresql')
ORDER BY rank DESC;

— Colonne tsvector pré-calculée (performance)
ALTER TABLE posts ADD COLUMN search_vector TSVECTOR;

UPDATE posts SET search_vector =
setweight(to_tsvector('french', COALESCE(title,  »)), 'A') ||
setweight(to_tsvector('french', COALESCE(content,  »)), 'B');

— Index GIN sur la colonne tsvector
CREATE INDEX idx_posts_search ON posts USING GIN (search_vector);

— Recherche rapide
SELECT title FROM posts
WHERE search_vector @@ to_tsquery('french', 'postgresql')
ORDER BY ts_rank(search_vector, to_tsquery('french', 'postgresql')) DESC;

Le full-text search PostgreSQL remplace Elasticsearch pour 90% des cas. Support du français (stemming, stop words), pondération des champs (titre plus important que contenu), recherche par proximité, et index GIN pour la performance. Pas besoin d'un service séparé.

CHAPITRE 09

Index et performance

⚡ Types d'index PostgreSQL
— B-tree (défaut) — égalité, comparaisons, tri
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_posts_user_pub ON posts(user_id, published);

— Index partiel — seulement certaines lignes
CREATE INDEX idx_posts_published ON posts(created_at)
WHERE published = TRUE;

— GIN — JSONB, arrays, full-text
CREATE INDEX idx_attrs ON products USING GIN (attributes);

— GiST — géographique, ranges
CREATE INDEX idx_location ON places USING GIST (coordinates);

— Index d'expression
CREATE INDEX idx_email_lower ON users(LOWER(email));

— EXPLAIN ANALYZE — analyser les performances
EXPLAIN ANALYZE SELECT * FROM users WHERE email = 'alice@mail.com';
— → Seq Scan (full scan) = lent, pas d'index
— → Index Scan = rapide, utilise l'index

Type d'index Usage Opérateurs
B-tree (défaut) Égalité, range, tri =, <, >, BETWEEN, ORDER BY
GIN JSONB, arrays, full-text @>, ?, @@, ANY
GiST Géographique, ranges &&, @>, <-> (distance)
BRIN Très grandes tables triées Tables chronologiques (logs)

CHAPITRE 10

Transactions et concurrence

🔄 MVCC et transactions
— Transaction basique
BEGIN;
UPDATE accounts SET balance = balance 100 WHERE user_id = 1;
UPDATE accounts SET balance = balance + 100 WHERE user_id = 2;
COMMIT; — ou ROLLBACK pour annuler

— Savepoints (rollback partiel)
BEGIN;
INSERT INTO orders (…) VALUES (…);
SAVEPOINT before_payment;
UPDATE accounts SET balance = balance 100 WHERE user_id = 1;
— Si erreur de paiement :
ROLLBACK TO before_payment; — Annule seulement le paiement
COMMIT; — La commande est quand même créée

— SELECT FOR UPDATE — verrouiller des lignes
BEGIN;
SELECT * FROM products WHERE id = 42 FOR UPDATE;
— Personne d'autre ne peut modifier ce produit jusqu'au COMMIT
UPDATE products SET stock = stock 1 WHERE id = 42;
COMMIT;

PostgreSQL utilise MVCC (Multi-Version Concurrency Control) : les lectures ne bloquent jamais les écritures et vice versa. Chaque transaction voit un « snapshot » cohérent de la base. C'est pour ça que PostgreSQL excelle en concurrence — pas de verrous en lecture, contrairement à MySQL/MyISAM.

CHAPITRE 11

Administration et sécurité

👤 Rôles et permissions
— Créer un rôle (utilisateur)
CREATE ROLE app_user WITH LOGIN PASSWORD 'motdepasse_fort';

— Permissions
GRANT SELECT, INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA public TO app_user;
GRANT USAGE, SELECT ON ALL SEQUENCES IN SCHEMA public TO app_user;

— Lecture seule
CREATE ROLE readonly WITH LOGIN PASSWORD 'readonly_pass';
GRANT SELECT ON ALL TABLES IN SCHEMA public TO readonly;

💾 Sauvegardes et extensions
— Sauvegarde (dump)
pg_dump -U postgres mon_app > backup.sql
pg_dump -U postgres -F c mon_app > backup.dump — Format custom (compressé)

— Restaurer
psql -U postgres mon_app < backup.sql
pg_restore -U postgres -d mon_app backup.dump

— Extensions populaires
CREATE EXTENSION IF NOT EXISTS « pgcrypto »; — UUID, cryptographie
CREATE EXTENSION IF NOT EXISTS « pg_trgm »; — Recherche floue (trigrammes)
CREATE EXTENSION IF NOT EXISTS « postgis »; — Données géographiques
CREATE EXTENSION IF NOT EXISTS « pgvector »; — Embeddings IA (RAG)
CREATE EXTENSION IF NOT EXISTS « pg_stat_statements »; — Requêtes lentes

— Recherche floue avec pg_trgm
SELECT name, similarity(name, 'postgre') AS sim
FROM products
WHERE name % 'postgre'
ORDER BY sim DESC;

CHAPITRE 12

Bonnes pratiques

🧩 Écosystème PostgreSQL
Outil Usage
Prisma ORM TypeScript/Node.js (migrations, typage, requêtes)
Drizzle ORM ORM TypeScript léger (SQL-like)
SQLAlchemy ORM Python (Django ORM aussi excellent)
Supabase Backend-as-a-Service basé sur PostgreSQL (auth, API, storage)
Neon PostgreSQL serverless (branching, auto-scaling)
pgBouncer Connection pooler (gestion des connexions)
PostGIS Données géographiques, SIG
pgvector Recherche vectorielle pour l'IA (embeddings)
✅ Bonnes pratiques

✅ À FAIRE
TIMESTAMPTZ (jamais TIMESTAMP sans TZ)
NUMERIC pour l'argent
• UUID pour les IDs exposés publiquement
RETURNING pour récupérer les résultats
• Index partiels (WHERE published = TRUE)
JSONB pour les données flexibles
EXPLAIN ANALYZE sur les requêtes lentes
CHECK constraints au lieu d'ENUM
• Extensions (pg_trgm, pgvector…)
• Connection pooler en production (pgBouncer)

❌ À ÉVITER
TIMESTAMP sans timezone
• FLOAT/DOUBLE pour les prix
• SELECT * en production
• Pas d'index sur les clés étrangères
• JSON au lieu de JSONB
• Ignorer VACUUM (maintenance auto)
• Trop de connexions directes (utiliser un pooler)
• Stocker des fichiers en BYTEA (utiliser S3)
• Pas de RETURNING (requête supplémentaire)
• Tables sans clé primaire

🧠 Quiz
JSONB vs une table relationnelle — quand choisir quoi ?
Table relationnelle = données avec un schéma fixe, requêtées par WHERE/JOIN, besoin de contraintes (FK, NOT NULL). Ex : users, orders, products. JSONB = données à schéma variable, métadonnées, attributs optionnels, configuration. Ex : préférences utilisateur, attributs produit (taille/couleur variable par catégorie), données d'API externe. Règle : si tous les objets ont les mêmes champs → colonne classique. Si les champs varient → JSONB.
Pourquoi TIMESTAMPTZ au lieu de TIMESTAMP ?
TIMESTAMP stocke une date/heure sans information de timezone — si votre serveur change de timezone, les dates seront interprétées différemment. TIMESTAMPTZ stocke en UTC et convertit automatiquement selon le timezone du client. C'est toujours correct, quel que soit le fuseau horaire. Utilisez toujours TIMESTAMPTZ.
Qu'est-ce que pg_trgm et pgvector ?
pg_trgm = recherche floue par trigrammes. Trouve « postgresql » même si l'utilisateur tape « postgre » ou « postgrSQL ». Idéal pour l'autocomplétion et la correction d'erreurs. pgvector = stockage et recherche de vecteurs (embeddings). Permet la recherche sémantique pour l'IA (RAG) — « trouve les documents similaires à cette question » — directement dans PostgreSQL, sans Pinecone ni Elasticsearch.

Cours PostgreSQL Complet — SQL avancé, JSONB, full-text search, index et administration

Référence : PostgreSQL Docs | Supabase | Neon