Cours PostgreSQL Complet 🐘
Les 12 chapitres essentiels — SQL avancé, JSONB, full-text, index, extensions et administration
7. JSONB et types avancés
2. Bases de données et tables
8. Full-text search
3. Types de données
9. Index et performance
4. CRUD (INSERT, SELECT, UPDATE, DELETE)
10. Transactions et concurrence
5. Jointures et requêtes avancées
11. Administration et sécurité
6. Fonctions et agrégation
12. Bonnes pratiques
🏠 Hub Programmation
🐬 MySQL
🍃 MongoDB
🔌 APIs REST
🎸 Django
🟢 Node.js
Introduction et installation
— 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.
| 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 |
Bases de données et tables
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.
Types de données
| 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 |
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
);
CRUD (INSERT, SELECT, UPDATE, DELETE)
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.
Jointures et requêtes avancées
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.
Fonctions et agrégation
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
JSONB et types avancés
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).
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);
Full-text search
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é.
Index et performance
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) |
Transactions et concurrence
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.
Administration et sécurité
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;
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;
Bonnes pratiques
| 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) |
✅ À 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
🏠 Hub Programmation
🐬 Cours MySQL
🍃 Cours MongoDB
🔌 Cours APIs REST
🎸 Cours Django
🟢 Cours Node.js
🐍 Cours Python
Cours PostgreSQL Complet — SQL avancé, JSONB, full-text search, index et administration
Référence : PostgreSQL Docs | Supabase | Neon

