SELECT & bases des requêtes
SELECT, WHERE & ORDER BY
SELECT récupère des lignes d'une ou plusieurs tables. Spécifiez toujours les colonnes explicitement au lieu de * pour la performance et la clarté (les changements de schéma ne casseront pas votre application). WHERE filtre les lignes avant le regroupement. ORDER BY trie les résultats (ASC par défaut, DESC descendant). LIMIT/OFFSET implémente la pagination — pour les gros volumes de données, préférez la pagination par keyset (WHERE id > last_id).
-- basic query: select specific columns
SELECT id, name, email
FROM users
WHERE age >= 18 AND status = 'active'
ORDER BY name ASC, created_at DESC
LIMIT 10 OFFSET 0;
-- select all columns (avoid in production)
SELECT * FROM products;
-- column aliases with AS
SELECT name AS product_name, price * 1.1 AS price_with_tax
FROM products;DISTINCT & alias
DISTINCT supprime les lignes en double du résultat. Il opère sur la ligne entière, pas les colonnes individuelles — SELECT DISTINCT city, country renvoie des paires city+country uniques. Les alias de table (u, o) raccourcissent les requêtes et sont requis quand on joint une table à elle-même. Les alias de colonne renomment les colonnes de sortie pour la lisibilité.
-- unique values only
SELECT DISTINCT country FROM users;
SELECT DISTINCT city, country FROM users; -- unique combos
-- table aliases (essential for joins)
SELECT u.name, o.total
FROM users AS u
JOIN orders AS o ON u.id = o.user_id;
-- column alias (AS is optional)
SELECT name product_name, COUNT(*) count
FROM products
GROUP BY name;Filtrage : BETWEEN, IN, IS NULL
BETWEEN est inclusif aux deux extrémités. IN correspond à n'importe quelle valeur d'une liste ou sous-requête. NULL nécessite IS NULL / IS NOT NULL (ne peut pas utiliser = NULL). Attention avec NOT IN et les sous-requêtes — si la sous-requête renvoie un NULL, NOT IN ne renvoie aucune ligne. Utilisez NOT EXISTS à la place, qui gère les NULL correctement et est souvent plus rapide.
SELECT * FROM products
WHERE price BETWEEN 10 AND 100 -- inclusive range
AND category IN ('tech', 'home') -- match any value
AND stock IS NOT NULL -- exclude NULLs
AND discount IS NULL; -- only NULLs
-- NOT IN with NULL caveat: returns nothing if subquery has NULL!
SELECT * FROM users
WHERE id NOT IN (SELECT user_id FROM orders); -- risky if NULLs
-- safer: NOT EXISTS
SELECT * FROM users u
WHERE NOT EXISTS (
SELECT 1 FROM orders o WHERE o.user_id = u.id
);LIKE & correspondance de motifs
LIKE utilise % (zéro ou plusieurs caractères) et _ (exactement un caractère) comme wildcards. LIKE est sensible à la casse dans la plupart des bases sauf MySQL (insensible à la casse par défaut). Utilisez ILIKE dans PostgreSQL pour la correspondance insensible à la casse. Pour les motifs complexes, utilisez les regex (~ dans PostgreSQL, REGEXP dans MySQL). LIKE avec un % en tête ne peut pas utiliser d'index — envisagez la recherche en texte intégral pour la performance.
-- LIKE: basic pattern matching
SELECT * FROM users WHERE name LIKE 'A%'; -- starts with A
SELECT * FROM users WHERE name LIKE '%son'; -- ends with son
SELECT * FROM users WHERE name LIKE '%a%'; -- contains a
SELECT * FROM users WHERE name LIKE '_a%'; -- second char is a
-- ILIKE (PostgreSQL): case-insensitive
SELECT * FROM users WHERE name ILIKE 'a%';
-- SIMILAR TO (PostgreSQL): regex-like
SELECT * FROM users WHERE name SIMILAR TO '[AB]%';
-- full regex (PostgreSQL)
SELECT * FROM users WHERE name ~ '^A[a-z]+$';Expressions CASE
CASE est le if-then-else du SQL, évalué par ligne. Il peut apparaître dans SELECT, WHERE, ORDER BY et HAVING. Le motif 'pivot' (SUM de CASE) transforme les lignes en colonnes — utile pour le reporting. CASE renvoie NULL si aucun WHEN ne correspond et qu'il n'y a pas de ELSE. Incluez toujours ELSE pour des résultats prévisibles.
-- conditional logic in queries
SELECT
name,
price,
CASE
WHEN price < 10 THEN 'cheap'
WHEN price < 50 THEN 'moderate'
WHEN price < 100 THEN 'expensive'
ELSE 'luxury'
END AS price_category
FROM products;
-- CASE in aggregation (pivot table)
SELECT
category,
COUNT(*) AS total,
SUM(CASE WHEN status = 'active' THEN 1 ELSE 0 END) AS active_count,
SUM(CASE WHEN status = 'inactive' THEN 1 ELSE 0 END) AS inactive_count
FROM products
GROUP BY category;JOINs
INNER JOIN
INNER JOIN renvoie seulement les lignes qui ont des correspondances dans les deux tables. JOIN est un raccourci pour INNER JOIN. Pour les requêtes multi-tables, joignez les tables étape par étape. ON spécifie la condition de jointure ; USING(column) est un raccourci quand les deux tables ont le même nom de colonne. Les jointures internes excluent les lignes non correspondantes des deux côtés.
-- only matching rows from both tables
SELECT u.name, o.total, o.created_at
FROM users u
INNER JOIN orders o ON u.id = o.user_id
WHERE o.total > 100
ORDER BY o.total DESC;
-- multiple joins
SELECT u.name, o.total, p.product_name
FROM users u
JOIN orders o ON u.id = o.user_id
JOIN order_items oi ON o.id = oi.order_id
JOIN products p ON oi.product_id = p.id;
-- USING (when column names match)
SELECT * FROM users
JOIN profiles ON users.id = profiles.user_id;LEFT JOIN (LEFT OUTER JOIN)
LEFT JOIN renvoie TOUTES les lignes de la table de gauche, avec des NULL pour les lignes droites non correspondantes. C'est essentiel pour les requêtes « tout inclure ». Le motif anti-join (WHERE right.id IS NULL) trouve les lignes de la table de gauche sans correspondance à droite — utile pour « utilisateurs qui n'ont pas commandé ». COUNT(right.id) compte les valeurs non-NULL, donc renvoie 0 pour les utilisateurs sans commandes.
-- all users, with their orders (NULL if no orders)
SELECT u.name, o.total
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
ORDER BY u.name;
-- find users with NO orders (anti-join pattern)
SELECT u.name
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL;
-- count orders per user (including zero)
SELECT u.name, COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id, u.name;RIGHT & FULL OUTER JOIN
RIGHT JOIN renvoie toutes les lignes de la table de droite ; c'est équivalent à permuter les tables et utiliser LEFT JOIN (plus lisible). FULL OUTER JOIN renvoie toutes les lignes des deux tables, avec des NULL là où il n'y a pas de correspondance — utile pour la réconciliation de données. MySQL ne supporte pas FULL OUTER JOIN directement ; émulez-le avec LEFT JOIN UNION RIGHT JOIN.
-- RIGHT JOIN: all rows from right table
SELECT u.name, o.total
FROM users u
RIGHT JOIN orders o ON u.id = o.user_id;
-- returns all orders, even orphaned ones (user_id = NULL)
-- FULL OUTER JOIN: all rows from both tables
SELECT u.name, o.total
FROM users u
FULL OUTER JOIN orders o ON u.id = o.user_id;
-- returns all users AND all orders, matching where possible
-- Note: RIGHT JOIN is rarely used (just swap tables and use LEFT)
-- FULL OUTER JOIN is useful for finding mismatches between tables
SELECT u.name, o.total
FROM users u
FULL OUTER JOIN orders o ON u.id = o.user_id
WHERE u.id IS NULL OR o.user_id IS NULL;CROSS JOIN & self join
CROSS JOIN produit un produit cartésien — chaque ligne de A appariée avec chaque ligne de B. Utilisez-le pour générer des combinaisons (tailles × couleurs). Les self joins (joindre une table à elle-même) sont courants pour les données hiérarchiques (employé-manager), trouver les doublons, ou comparer des lignes dans la même table. Utilisez toujours des alias de table dans les self joins pour distinguer les deux « copies ».
-- CROSS JOIN: Cartesian product (every combination)
SELECT s.size, c.color
FROM sizes s
CROSS JOIN colors c;
-- produces all size+color combinations
-- Self join: join a table to itself
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;
-- self join for hierarchical data
SELECT c.name AS child, p.name AS parent
FROM categories c
JOIN categories p ON c.parent_id = p.id;
-- self join to find duplicates
SELECT a.id, a.email, b.id AS dup_id
FROM users a
JOIN users b ON a.email = b.email AND a.id < b.id;NATURAL JOIN & résumé des types de JOIN
NATURAL JOIN joint automatiquement sur les colonnes de même nom — pratique mais dangereux car les changements de schéma peuvent silencieusement changer le comportement de jointure. Évitez-le en production. Les jointures LATERAL permettent à une sous-requête de référencer des colonnes de la requête externe — puissant pour les requêtes « top N par groupe ». La syntaxe par virgule (FROM a, b) est équivalente à CROSS JOIN.
-- NATURAL JOIN: joins on all matching column names
-- (rarely recommended — implicit, fragile)
SELECT * FROM users NATURAL JOIN profiles;
-- joins on any column that exists in BOTH tables
-- Summary of join types:
-- INNER JOIN : matching rows only
-- LEFT JOIN : all left + matching right
-- RIGHT JOIN : all right + matching left
-- FULL JOIN : all from both sides
-- CROSS JOIN : Cartesian product
-- SELF JOIN : table joined to itself
-- LATERAL JOIN (PostgreSQL): subquery can reference outer query
SELECT u.name, recent.*
FROM users u,
LATERAL (
SELECT * FROM orders o
WHERE o.user_id = u.id
ORDER BY o.created_at DESC
LIMIT 3
) recent;GROUP BY & agrégation
GROUP BY & HAVING
GROUP BY regroupe les lignes en groupes, une ligne par groupe. Les fonctions d'agrégat (COUNT, SUM, AVG, MIN, MAX) opèrent sur chaque groupe. WHERE filtre les lignes individuelles AVANT le regroupement ; HAVING filtre les groupes APRÈS l'agrégation. Les colonnes non agrégées dans SELECT doivent apparaître dans GROUP BY (SQL standard). MySQL est permissif mais imprévisible — incluez toujours toutes les colonnes non agrégées dans GROUP BY.
-- aggregate per group
SELECT category, COUNT(*) AS cnt, AVG(price) AS avg_price
FROM products
GROUP BY category
HAVING COUNT(*) > 5
ORDER BY cnt DESC;
-- HAVING filters groups (after aggregation)
-- WHERE filters rows (before aggregation)
SELECT dept, AVG(salary) AS avg_sal
FROM employees
WHERE status = 'active' -- filter rows first
GROUP BY dept
HAVING AVG(salary) > 50000; -- then filter groupsFonctions d'agrégat
COUNT(*) compte toutes les lignes y compris les NULL ; COUNT(column) compte seulement les valeurs non-NULL. COUNT(DISTINCT col) compte les valeurs uniques. SUM/AVG ignorent les NULL. AVG = SUM/COUNT(non-NULL), donc les NULL affectent la moyenne. STRING_AGG (PostgreSQL) / GROUP_CONCAT (MySQL) concatènent des chaînes par groupe. BOOL_OR/BOOL_AND renvoient true si une/toutes les valeurs sont true.
SELECT
COUNT(*) AS total_rows, -- counts all rows
COUNT(email) AS emails_filled, -- counts non-NULL emails
COUNT(DISTINCT country) AS countries,
SUM(amount) AS total_revenue,
AVG(amount) AS avg_order,
MIN(amount) AS smallest_order,
MAX(amount) AS largest_order,
-- string aggregation (PostgreSQL)
STRING_AGG(name, ', ') AS all_names,
-- boolean aggregation
BOOL_OR(is_active) AS any_active,
BOOL_AND(is_active) AS all_active
FROM orders;GROUP BY sur plusieurs colonnes
Grouper par plusieurs colonnes crée une hiérarchie de groupes. WITH ROLLUP ajoute des lignes de sous-total et de total général (NULL dans la colonne groupée). GROUPING SETS vous permettent de spécifier exactement quelles combinaisons de regroupement vous voulez — plus flexible que ROLLUP. CUBE génère toutes les combinaisons de regroupement possibles. Essentiel pour le reporting et les requêtes OLAP.
-- multi-level grouping
SELECT
EXTRACT(YEAR FROM created_at) AS yr,
EXTRACT(MONTH FROM created_at) AS mon,
category,
COUNT(*) AS cnt,
SUM(total) AS revenue
FROM orders
GROUP BY yr, mon, category
ORDER BY yr DESC, mon DESC, cnt DESC;
-- GROUP BY with ROLLUP (subtotals + grand total)
SELECT category, COUNT(*) AS cnt
FROM products
GROUP BY category WITH ROLLUP;
-- last row has NULL category = grand total
-- GROUPING SETS (PostgreSQL): specify multiple groupings
SELECT category, status, COUNT(*)
FROM products
GROUP BY GROUPING SETS ((category, status), (category), ());HAVING vs WHERE
La distinction clé : WHERE filtre les lignes individuelles avant l'agrégation (ne peut pas utiliser SUM, COUNT, etc.), tandis que HAVING filtre les groupes après l'agrégation (peut utiliser des fonctions d'agrégat). Utilisez WHERE pour réduire les données tôt (meilleure performance), puis HAVING pour filtrer les résultats agrégés. Les deux peuvent apparaître dans la même requête — WHERE d'abord, puis GROUP BY, puis HAVING.
-- WHERE: filters rows BEFORE grouping
-- Cannot use aggregates
SELECT category, COUNT(*) AS cnt
FROM products
WHERE price > 10 -- OK: filter on raw column
GROUP BY category;
-- HAVING: filters groups AFTER grouping
-- Can use aggregates
SELECT category, COUNT(*) AS cnt
FROM products
GROUP BY category
HAVING COUNT(*) > 5 -- OK: filter on aggregate
AND AVG(price) > 20; -- OK: multiple aggregate filters
-- combining both
SELECT category, COUNT(*) AS cnt
FROM products
WHERE price > 10 -- filter rows
GROUP BY category -- group
HAVING COUNT(*) > 5; -- filter groupsAgrégation date/heure
La troncature de date est essentielle pour le reporting de séries temporelles. DATE(col) extrait juste la date ; EXTRACT/TIME_PART obtient des composants spécifiques (année, mois, heure). TO_CHAR formate les dates pour le regroupement et l'affichage. Pour l'analyse de séries temporelles, envisagez DATE_TRUNC('month', col) qui garde le type timestamp. Indexez les colonnes de date pour la performance sur les grandes tables.
-- group by date parts
SELECT
DATE(created_at) AS order_date,
COUNT(*) AS orders,
SUM(total) AS revenue
FROM orders
GROUP BY DATE(created_at)
ORDER BY order_date DESC;
-- group by hour
SELECT
EXTRACT(HOUR FROM created_at) AS hr,
COUNT(*) AS cnt
FROM orders
WHERE created_at >= CURRENT_DATE
GROUP BY hr
ORDER BY hr;
-- monthly revenue trend
SELECT
TO_CHAR(created_at, 'YYYY-MM') AS month,
SUM(total) AS revenue,
COUNT(*) AS orders,
AVG(total) AS avg_order
FROM orders
WHERE created_at >= '2024-01-01'
GROUP BY month
ORDER BY month;Sous-requêtes & CTEs
Sous-requêtes scalaires & de colonne
Les sous-requêtes scalaires renvoient une seule valeur et peuvent être utilisées partout où une valeur est attendue. Les sous-requêtes de colonne renvoient une colonne et sont utilisées avec IN, ANY, ALL. Les sous-requêtes dans SELECT (corrélées) s'exécutent une fois par ligne externe — peuvent être lentes sur de gros volumes. Envisagez de réécrire en JOIN avec GROUP BY pour une meilleure performance.
-- scalar subquery (returns single value)
SELECT name, age
FROM users
WHERE age > (SELECT AVG(age) FROM users);
-- column subquery (returns one column, multiple rows)
SELECT name
FROM users
WHERE id IN (SELECT user_id FROM orders WHERE total > 100);
-- subquery in SELECT
SELECT
u.name,
(SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS order_count
FROM users u;Sous-requêtes corrélées & EXISTS
Les sous-requêtes corrélées référencent la requête externe et s'exécutent une fois par ligne externe — potentiellement lentes. EXISTS/NOT EXISTS sont efficaces car ils court-circuitent (s'arrêtent à la première correspondance). NOT EXISTS est la manière préférée de trouver « des lignes sans lignes correspondantes » — il gère les NULL correctement et est souvent plus rapide que NOT IN. La base de données peut optimiser les sous-requêtes corrélées en jointures.
-- correlated: subquery references outer query
SELECT u.name
FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o
WHERE o.user_id = u.id
AND o.total > 1000
);
-- NOT EXISTS: users without any orders
SELECT u.name
FROM users u
WHERE NOT EXISTS (
SELECT 1 FROM orders o WHERE o.user_id = u.id
);
-- correlated subquery in SELECT (runs per row)
SELECT
u.name,
(SELECT MAX(o.total)
FROM orders o
WHERE o.user_id = u.id) AS max_order
FROM users u;Expressions de table communes (CTE)
Les CTE (clause WITH) créent des ensembles de résultats temporaires nommés qui rendent les requêtes complexes lisibles. Contrairement aux sous-requêtes, les CTE peuvent être référencés plusieurs fois et se lisent de haut en bas. Dans la plupart des bases, les CTE sont inlinés (l'optimisation se fait au niveau de la requête). PostgreSQL 12+ supporte les hints MATERIALIZED/NOT MATERIALIZED. Les CTE sont aussi requis pour les requêtes récursives.
-- CTE: named temporary result set
WITH active_users AS (
SELECT id, name FROM users WHERE status = 'active'
),
user_orders AS (
SELECT user_id, COUNT(*) AS cnt, SUM(total) AS revenue
FROM orders
GROUP BY user_id
)
SELECT au.name, uo.cnt, uo.revenue
FROM active_users au
LEFT JOIN user_orders uo ON au.id = uo.user_id
ORDER BY uo.revenue DESC NULLS LAST;
-- CTEs improve readability for complex queries
-- They are NOT materialized (just syntactic sugar) in most DBs
-- (PostgreSQL 12+ can materialize with MATERIALIZED keyword)CTE récursif
Les CTE récursifs se référencent eux-mêmes, permettant le parcours d'arbre/graphe et la génération de séquences. Structure : cas de base UNION ALL cas récursif. Le cas récursif référence le CTE et doit terminer (ajoutez un WHERE pour éviter les boucles infinies). Usages courants : organigrammes, arbres de catégories, graphes de dépendances, séquences de dates. Chaque base a une syntaxe légèrement différente — vérifiez la doc de votre SGBD.
-- hierarchical data: org chart
WITH RECURSIVE org_tree AS (
-- base case: top-level managers
SELECT id, name, manager_id, 1 AS level
FROM employees
WHERE manager_id IS NULL
UNION ALL
-- recursive case: direct reports
SELECT e.id, e.name, e.manager_id, ot.level + 1
FROM employees e
JOIN org_tree ot ON e.manager_id = ot.id
)
SELECT level, name FROM org_tree ORDER BY level, name;
-- generate a series of dates
WITH RECURSIVE dates AS (
SELECT DATE '2024-01-01' AS d
UNION ALL
SELECT d + 1 FROM dates WHERE d < '2024-01-31'
)
SELECT d FROM dates;
-- factorial
WITH RECURSIVE fact(n, result) AS (
SELECT 1, 1
UNION ALL
SELECT n + 1, result * (n + 1) FROM fact WHERE n < 10
)
SELECT * FROM fact;Opérateurs de sous-requête : ANY, ALL
ANY et ALL comparent une valeur à un ensemble de résultats de sous-requête. > ANY signifie « supérieur à au moins un ». > ALL signifie « supérieur à chacun ». = ANY est équivalent à IN. <> ALL est équivalent à NOT IN mais gère les NULL plus sûrement. Ces opérateurs sont moins couramment utilisés que IN/EXISTS mais peuvent exprimer certaines requêtes plus naturellement.
-- ANY: greater than ANY of the values (= at least one)
SELECT * FROM products
WHERE price > ANY (
SELECT price FROM products WHERE category = 'tech'
);
-- true if price exceeds at least one tech product's price
-- ALL: greater than ALL values (= every one)
SELECT * FROM products
WHERE price > ALL (
SELECT price FROM products WHERE category = 'tech'
);
-- true if price exceeds every tech product's price
-- = ANY is equivalent to IN
SELECT * FROM users
WHERE id = ANY (SELECT user_id FROM orders);
-- <> ALL is equivalent to NOT IN (but NULL-safe)
SELECT * FROM users
WHERE id <> ALL (SELECT user_id FROM orders WHERE total < 0);Fonctions de fenêtrage
ROW_NUMBER, RANK, DENSE_RANK
ROW_NUMBER attribue des numéros séquentiels uniques (1, 2, 3...). RANK donne le même rang aux égalités mais saute les numéros suivants (1, 1, 3). DENSE_RANK donne le même rang aux égalités sans sauter (1, 1, 2). PARTITION BY divise les lignes en groupes ; la fonction se réinitialise par partition. Le motif « top N par groupe » (ROW_NUMBER + WHERE rn <= N) est extrêmement courant en analytique.
SELECT
name,
salary,
dept,
ROW_NUMBER() OVER (ORDER BY salary DESC) AS row_num,
RANK() OVER (ORDER BY salary DESC) AS rank,
DENSE_RANK() OVER (ORDER BY salary DESC) AS dense_rank
FROM employees;
-- top 3 earners per department
SELECT * FROM (
SELECT
name,
dept,
salary,
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn
FROM employees
) ranked
WHERE rn <= 3;LAG & LEAD
LAG accède à la valeur d'une ligne précédente ; LEAD accède à la valeur d'une ligne suivante. Les deux acceptent un décalage optionnel (défaut 1) et une valeur par défaut (défaut NULL). Essentiel pour l'analyse de séries temporelles : variations jour-le-jour, comparaisons mobiles, détection de trous. NULLIF prévient la division par zéro dans les calculs de pourcentage. Spécifiez toujours ORDER BY dans la clause OVER pour des résultats déterministes.
-- compare each row to previous/next
SELECT
date,
revenue,
LAG(revenue) OVER (ORDER BY date) AS prev_day,
LEAD(revenue) OVER (ORDER BY date) AS next_day,
revenue - LAG(revenue) OVER (ORDER BY date) AS daily_change,
ROUND(
(revenue - LAG(revenue) OVER (ORDER BY date)) * 100.0
/ NULLIF(LAG(revenue) OVER (ORDER BY date), 0),
2
) AS pct_change
FROM daily_sales
ORDER BY date;
-- LAG with offset and default
SELECT
date,
revenue,
LAG(revenue, 7) OVER (ORDER BY date) AS revenue_7_days_ago
FROM daily_sales;Totaux cumulés & moyennes mobiles
Les cadres de fenêtre définissent sur quelles lignes la fonction opère. ROWS BETWEEN utilise des décalages physiques de lignes ; RANGE utilise des plages de valeurs logiques (mieux pour les trous de dates). UNBOUNDED PRECEDING signifie « depuis le début ». Les totaux cumulés (SUM cumulatif) et les moyennes mobiles sont les motifs analytiques les plus courants. Sans cadre, les fonctions de fenêtre d'agrégat utilisent le défaut : RANGE BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW.
-- cumulative sum (running total)
SELECT
date,
revenue,
SUM(revenue) OVER (ORDER BY date) AS running_total,
SUM(revenue) OVER (ORDER BY date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS last_7_days_sum,
AVG(revenue) OVER (ORDER BY date
ROWS BETWEEN 6 PRECEDING AND CURRENT ROW) AS moving_avg_7d
FROM daily_sales
ORDER BY date;
-- window frame options:
-- ROWS BETWEEN n PRECEDING AND n FOLLOWING -- physical rows
-- RANGE BETWEEN INTERVAL '7' DAY PRECEDING AND CURRENT ROW -- logical range
-- ROWS UNBOUNDED PRECEDING = from start to current
-- ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING = all rowsNTILE & PERCENT_RANK
NTILE(n) divise les lignes ordonnées en n groupes à peu près égaux (quartiles, déciles, percentiles). PERCENT_RANK donne le rang relatif (0 à 1). CUME_DIST donne la distribution cumulée. FIRST_VALUE/LAST_VALUE renvoient des valeurs de la première/dernière ligne du cadre — notez que LAST_VALUE a besoin d'un cadre explicite (UNBOUNDED FOLLOWING) car le cadre par défaut se termine à la ligne courante.
-- divide into quartiles
SELECT
name,
salary,
NTILE(4) OVER (ORDER BY salary DESC) AS quartile,
PERCENT_RANK() OVER (ORDER BY salary) AS pct_rank,
CUME_DIST() OVER (ORDER BY salary) AS cumulative_dist
FROM employees;
-- first/last value in a partition
SELECT
dept,
name,
salary,
FIRST_VALUE(name) OVER (PARTITION BY dept ORDER BY salary DESC) AS top_earner,
LAST_VALUE(name) OVER (
PARTITION BY dept ORDER BY salary DESC
ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING
) AS lowest_earner
FROM employees;Fonctions d'agrégat de fenêtre
Les agrégats de fenêtre (SUM, AVG, COUNT, etc. avec OVER) calculent des valeurs d'agrégat SANS regrouper les lignes — chaque ligne obtient l'agrégat attaché. C'est la différence clé avec GROUP BY : vous gardez toutes les lignes de détail tout en voyant aussi le résumé. Parfait pour comparer des valeurs individuelles aux moyennes de groupe, calculer des pourcentages et ajouter des colonnes de contexte aux rapports de détail.
-- aggregates over windows (no row collapse!)
SELECT
name,
dept,
salary,
-- compare to department average
AVG(salary) OVER (PARTITION BY dept) AS dept_avg,
salary - AVG(salary) OVER (PARTITION BY dept) AS diff_from_avg,
-- percentage of department total
salary * 100.0 / SUM(salary) OVER (PARTITION BY dept) AS pct_of_dept,
-- count per department
COUNT(*) OVER (PARTITION BY dept) AS dept_size
FROM employees
ORDER BY dept, salary DESC;
-- key advantage: aggregates without GROUP BY
-- every row is preserved, with the aggregate value attachedDDL : tables & schéma
CREATE TABLE & types de données
CREATE TABLE définit le schéma. SERIAL (PostgreSQL) / AUTO_INCREMENT (MySQL) génère automatiquement les IDs. VARCHAR(n) a une limite ; TEXT est illimité. DECIMAL(p,s) est exact (utilisez pour l'argent !), FLOAT est approximatif. Les contraintes CHECK imposent des règles métier. DEFAULT fournit des valeurs quand non spécifié. JSONB (PostgreSQL) permet des requêtes JSON indexées. Utilisez toujours TIMESTAMP WITH TIME ZONE pour les timestamps qui couvrent plusieurs fuseaux horaires.
CREATE TABLE users (
id SERIAL PRIMARY KEY, -- auto-increment (PostgreSQL)
-- MySQL: id INT AUTO_INCREMENT PRIMARY KEY
username VARCHAR(50) UNIQUE NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
age INT CHECK (age >= 0 AND age <= 150),
salary DECIMAL(10, 2) DEFAULT 0.00, -- precision, scale
bio TEXT, -- unlimited length
avatar BYTEA, -- binary (PostgreSQL)
metadata JSONB, -- JSON (PostgreSQL)
status VARCHAR(20) DEFAULT 'active',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- common types:
-- INT, BIGINT, SMALLINT, DECIMAL(p,s), NUMERIC
-- VARCHAR(n), CHAR(n), TEXT
-- DATE, TIME, TIMESTAMP, INTERVAL
-- BOOLEAN, UUID, JSON/JSONB, BYTEA/BLOBContraintes : PRIMARY, FOREIGN, UNIQUE, CHECK
Les contraintes imposent l'intégrité des données au niveau de la base. PRIMARY KEY identifie de manière unique les lignes (implique NOT NULL + UNIQUE). FOREIGN KEY maintient l'intégrité référentielle — ON DELETE CASCADE supprime les enfants quand le parent est supprimé. UNIQUE prévient les doublons. CHECK impose des règles personnalisées. Définir des contraintes dans la base (pas juste le code applicatif) garantit l'intégrité quelle que soit la manière dont les données sont accédées.
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
total DECIMAL(10,2) NOT NULL CHECK (total >= 0),
status VARCHAR(20) NOT NULL DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- table-level constraints
CONSTRAINT valid_status CHECK (status IN ('pending','paid','shipped','cancelled')),
CONSTRAINT unique_user_order UNIQUE (user_id, created_at)
);
-- foreign key actions:
-- ON DELETE CASCADE : delete child rows when parent deleted
-- ON DELETE SET NULL : set FK to NULL (column must be nullable)
-- ON DELETE RESTRICT : prevent parent deletion (default)
-- ON UPDATE CASCADE : update FK when parent PK changes
-- add constraint to existing table
ALTER TABLE orders ADD CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id);ALTER TABLE
ALTER TABLE modifie la structure de table existante. Ajouter des colonnes avec des valeurs par défaut est généralement rapide (PostgreSQL 11+ ne réécrit pas la table). Supprimer des colonnes peut verrouiller la table. Changer les types de colonne peut nécessiter une réécriture complète de la table et peut échouer si les données ne se convertissent pas. Testez toujours les migrations de schéma sur une copie d'abord. Utilisez des outils de migration (Flyway, Alembic, Rails migrations) pour des changements de schéma versionnés.
-- add column
ALTER TABLE users ADD COLUMN phone VARCHAR(20);
ALTER TABLE users ADD COLUMN verified BOOLEAN DEFAULT false;
-- drop column
ALTER TABLE users DROP COLUMN avatar;
-- rename column/table
ALTER TABLE users RENAME COLUMN username TO login;
ALTER TABLE users RENAME TO accounts;
-- change column type
ALTER TABLE users ALTER COLUMN age TYPE BIGINT;
-- MySQL: ALTER TABLE users MODIFY COLUMN age BIGINT;
-- add/drop constraints
ALTER TABLE users ADD CONSTRAINT email_unique UNIQUE (email);
ALTER TABLE users DROP CONSTRAINT email_unique;
-- set default
ALTER TABLE users ALTER COLUMN status SET DEFAULT 'active';DROP, TRUNCATE & index
DROP TABLE supprime entièrement la table ; TRUNCATE la vide mais garde la structure (bien plus rapide que DELETE, réinitialise l'identité). Les index accélèrent les requêtes mais ralentissent les écritures — indexez stratégiquement. Les index composites fonctionnent de gauche à droite : idx(a,b,c) aide WHERE a=?, WHERE a=? AND b=?, mais PAS WHERE b=?. Les index GIN permettent la recherche en texte intégral. Les index partiels économisent de l'espace en n'indexant que les lignes correspondantes.
-- DROP: permanently remove table (structure + data)
DROP TABLE IF EXISTS old_logs CASCADE;
-- CASCADE drops dependent objects (views, FKs)
-- TRUNCATE: remove all data, keep structure (faster than DELETE)
TRUNCATE TABLE logs;
TRUNCATE TABLE logs RESTART IDENTITY; -- reset SERIAL counter
TRUNCATE TABLE orders, order_items CASCADE; -- multiple tables
-- CREATE INDEX for query performance
CREATE INDEX idx_users_email ON users(email);
CREATE UNIQUE INDEX idx_users_username ON users(username);
CREATE INDEX idx_orders_user_date ON orders(user_id, created_at);
CREATE INDEX idx_products_name ON products USING gin(to_tsvector('english', name));
-- partial index (PostgreSQL)
CREATE INDEX idx_active_users ON users(last_login)
WHERE status = 'active';
-- DROP INDEX
DROP INDEX IF EXISTS idx_users_email;Vues & vues matérialisées
Les vues sont des requêtes sauvegardées qui agissent comme des tables virtuelles — elles exécutent la requête sous-jacente à chaque fois. Utilisez les vues pour simplifier les requêtes complexes, imposer la sécurité (accès au niveau colonne) et fournir des API stables. Les vues matérialisées stockent les résultats réels — plus rapides à interroger mais doivent être rafraîchies. Utilisez les vues matérialisées pour les agrégations coûteuses qui n'ont pas besoin de données en temps réel. CONCURRENTLY rafraîchit sans verrouiller (PostgreSQL).
-- VIEW: stored query (virtual table, runs on access)
CREATE VIEW active_users AS
SELECT id, name, email FROM users WHERE status = 'active';
SELECT * FROM active_users WHERE name LIKE 'A%';
-- updatable view (simple views can be INSERTed/UPDATEd)
CREATE VIEW user_summary AS
SELECT id, name, email, age FROM users;
-- MATERIALIZED VIEW: stored result (must refresh)
CREATE MATERIALIZED VIEW monthly_stats AS
SELECT
DATE_TRUNC('month', created_at) AS month,
COUNT(*) AS orders,
SUM(total) AS revenue
FROM orders
GROUP BY month;
REFRESH MATERIALIZED VIEW monthly_stats;
REFRESH MATERIALIZED VIEW CONCURRENTLY monthly_stats; -- no lock
-- drop
DROP VIEW IF EXISTS active_users;
DROP MATERIALIZED VIEW IF EXISTS monthly_stats;DML : Insert, Update, Delete
INSERT
INSERT ajoute des lignes. De multiples VALUES dans une seule instruction sont plus efficaces que des inserts séparés. INSERT...SELECT copie des données entre tables. RETURNING (PostgreSQL/Oracle) récupère les valeurs auto-générées (comme les ids SERIAL) en un seul aller-retour — essentiel pour le code applicatif. Utilisez DEFAULT VALUES pour insérer une ligne avec tous les défauts. Spécifiez toujours les noms de colonnes pour rendre votre code résilient aux changements de schéma.
-- single row
INSERT INTO users (name, email, age)
VALUES ('Alice', '[email protected]', 30);
-- multiple rows
INSERT INTO users (name, email) VALUES
('Bob', '[email protected]'),
('Carol', '[email protected]'),
('Dave', '[email protected]');
-- INSERT ... SELECT (copy data between tables)
INSERT INTO archive_users (name, email, deleted_at)
SELECT name, email, NOW()
FROM users
WHERE status = 'deleted';
-- INSERT with RETURNING (PostgreSQL)
INSERT INTO users (name, email)
VALUES ('Eve', '[email protected]')
RETURNING id, created_at; -- returns the generated id
-- DEFAULT values
INSERT INTO users DEFAULT VALUES;UPDATE
UPDATE modifie les lignes existantes. INCLUEZ TOUJOURS une clause WHERE sauf si vous avez l'intention de mettre à jour chaque ligne. La clause FROM (PostgreSQL) permet des jointures dans les updates. RETURNING montre quelles lignes ont été modifiées. Utilisez des transactions pour les mises à jour multi-étapes afin de pouvoir ROLLBACK si quelque chose tourne mal. Une erreur courante est d'oublier WHERE — envisagez d'exécuter un SELECT avec le même WHERE d'abord pour vérifier les lignes affectées.
-- basic update
UPDATE users
SET age = 31, status = 'verified', updated_at = NOW()
WHERE id = 1;
-- update based on another table
UPDATE products p
SET price = p.price * 1.1
FROM categories c
WHERE p.category_id = c.id AND c.name = 'electronics';
-- update with subquery
UPDATE users
SET status = 'premium'
WHERE id IN (
SELECT user_id FROM orders
GROUP BY user_id HAVING SUM(total) > 1000
);
-- UPDATE with RETURNING (PostgreSQL)
UPDATE users SET status = 'inactive'
WHERE last_login < '2023-01-01'
RETURNING id, name;
-- WARNING: UPDATE without WHERE affects ALL rows!DELETE & TRUNCATE
DELETE supprime les lignes une à une (journalisé, peut être annulé, plus lent). TRUNCATE supprime toutes les lignes d'un coup (journalisation minimale, bien plus rapide, réinitialise l'auto-incrément, ne peut pas être annulé dans certains SGBD). Pour les pistes d'audit, utilisez des soft deletes (un timestamp deleted_at) au lieu de hard deletes. Utilisez toujours WHERE avec DELETE. Considérez les contraintes de clé étrangère — ON DELETE CASCADE gère les lignes enfants automatiquement.
-- delete specific rows
DELETE FROM users WHERE status = 'inactive';
-- delete with subquery
DELETE FROM orders
WHERE user_id IN (
SELECT id FROM users WHERE status = 'deleted'
);
-- delete with RETURNING (PostgreSQL)
DELETE FROM users
WHERE last_login < '2020-01-01'
RETURNING id, name;
-- delete all rows (slow, logged, can be rolled back)
DELETE FROM logs;
-- TRUNCATE (fast, minimal logging, resets identity)
TRUNCATE TABLE logs;
TRUNCATE TABLE logs RESTART IDENTITY CASCADE;
-- soft delete pattern (preferred for audit)
UPDATE users SET deleted_at = NOW() WHERE id = 1;
SELECT * FROM users WHERE deleted_at IS NULL; -- active usersUPSERT (INSERT ... ON CONFLICT)
UPSERT (update or insert) gère les conflits de clé en double atomiquement. PostgreSQL utilise ON CONFLICT (column) DO UPDATE/DO NOTHING. MySQL utilise ON DUPLICATE KEY UPDATE. EXCLUDED (PostgreSQL) / VALUES() (MySQL) fait référence aux valeurs d'insertion proposées. C'est essentiel pour les opérations idempotentes et l'évitement des conditions de course. Sans upsert, vous auriez besoin de SELECT-then-INSERT/UPDATE qui est sujet aux conditions de course.
-- PostgreSQL: ON CONFLICT (upsert)
INSERT INTO users (id, name, email)
VALUES (1, 'Alice', '[email protected]')
ON CONFLICT (id)
DO UPDATE SET email = EXCLUDED.email, updated_at = NOW()
RETURNING *;
-- DO NOTHING on conflict
INSERT INTO users (id, name, email)
VALUES (1, 'Alice', '[email protected]')
ON CONFLICT (id) DO NOTHING;
-- MySQL: ON DUPLICATE KEY UPDATE
INSERT INTO users (id, name, email)
VALUES (1, 'Alice', '[email protected]')
ON DUPLICATE KEY UPDATE email = VALUES(email);
-- SQLite: ON CONFLICT
INSERT INTO users (id, name)
VALUES (1, 'Alice')
ON CONFLICT(id) DO UPDATE SET name = excluded.name;Instruction MERGE
MERGE (aka UPSERT sur stéroïdes) combine INSERT, UPDATE et DELETE dans une seule instruction atomique selon que les lignes correspondent. C'est la manière la plus efficace de synchroniser des données entre sources. WHEN MATCHED déclenche UPDATE/DELETE pour les lignes existantes ; WHEN NOT MATCHED déclenche INSERT pour les nouvelles lignes. Disponible dans SQL Server, Oracle, PostgreSQL 15+ et DB2. MySQL ne supporte pas MERGE — utilisez INSERT...ON DUPLICATE KEY.
-- MERGE: conditional insert/update/delete in one statement
-- (SQL Server, Oracle, PostgreSQL 15+)
MERGE INTO products AS target
USING (VALUES
(1, 'Widget', 9.99),
(2, 'Gadget', 19.99),
(3, 'Gizmo', 29.99)
) AS source (id, name, price)
ON target.id = source.id
WHEN MATCHED THEN
UPDATE SET name = source.name, price = source.price
WHEN NOT MATCHED THEN
INSERT (id, name, price) VALUES (source.id, source.name, source.price)
WHEN MATCHED AND source.price < 0 THEN
DELETE;
-- useful for:
-- - syncing data from external sources
-- - bulk upsert with conditional logic
-- - ETL operationsTransactions & ACID
BEGIN, COMMIT, ROLLBACK
Les transactions regroupent des opérations en une unité atomique — toutes réussissent (COMMIT) ou toutes échouent (ROLLBACK). C'est le « A » d'ACID. BEGIN/START TRANSACTION démarre une transaction. SAVEPOINT crée un point de retour nommé dans une transaction — vous pouvez faire un rollback vers lui sans annuler toute la transaction. Committez ou rollbackez toujours — laisser une transaction ouverte maintient des verrous et peut causer des deadlocks.
-- basic transaction
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
-- both updates succeed or both fail (atomicity)
-- rollback on error
BEGIN;
INSERT INTO orders (user_id, total) VALUES (1, 50.00);
-- oops, something went wrong
ROLLBACK;
-- the insert is undone
-- transaction with savepoints
BEGIN;
INSERT INTO logs (msg) VALUES ('step 1');
SAVEPOINT my_savepoint;
INSERT INTO logs (msg) VALUES ('step 2');
ROLLBACK TO my_savepoint; -- undo step 2, keep step 1
INSERT INTO logs (msg) VALUES ('step 3');
COMMIT; -- commits step 1 and step 3Niveaux d'isolation
Les niveaux d'isolation équilibrent cohérence vs concurrence. READ COMMITTED (défaut dans PostgreSQL/Oracle) prévient les lectures sales mais permet les lectures non reproductibles. REPEATABLE READ prévient les lectures non reproductibles mais permet les lectures fantômes. SERIALIZABLE prévient toutes les anomalies mais réduit la concurrence. Isolation plus élevée = plus de verrous = moins de concurrence. Choisissez le niveau le plus bas qui répond à vos exigences de correction.
-- set isolation level for transaction
BEGIN TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- can see committed data from other transactions
SELECT balance FROM accounts WHERE id = 1;
COMMIT;
-- isolation levels (from weakest to strongest):
-- READ UNCOMMITTED: can read uncommitted (dirty) data
-- READ COMMITTED: only committed data (PostgreSQL default)
-- REPEATABLE READ: same query returns same results within txn
-- SERIALIZABLE: transactions appear to run sequentially
-- PostgreSQL: set per transaction
SET TRANSACTION ISOLATION LEVEL SERIALIZABLE;
-- MySQL: set per session
SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- check current level
SHOW TRANSACTION ISOLATION LEVEL;Verrouillage & SELECT FOR UPDATE
SELECT FOR UPDATE verrouille les lignes pour que les autres transactions ne puissent pas les modifier jusqu'à votre commit. Cela implémente le contrôle de concurrence pessimiste. SKIP LOCKED est essentiel pour les files de jobs — de multiples workers peuvent prendre des jobs sans se bloquer. NOWAIT échoue vite au lieu d'attendre. Utilisez le verrouillage avec parcimonie — il réduit la concurrence et peut causer des deadlocks. Préférez la concurrence optimiste (colonnes de version) pour la plupart des cas d'usage.
-- pessimistic locking: lock rows for update
BEGIN;
SELECT balance FROM accounts WHERE id = 1 FOR UPDATE;
-- row is locked; other transactions must wait
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
COMMIT; -- lock released
-- NOWAIT: don't wait if locked, error immediately
SELECT * FROM accounts WHERE id = 1 FOR UPDATE NOWAIT;
-- SKIP LOCKED: skip locked rows (useful for job queues)
SELECT * FROM jobs WHERE status = 'pending'
ORDER BY created_at
FOR UPDATE SKIP LOCKED
LIMIT 10;
-- SHARE LOCK: allow reads but prevent updates
SELECT * FROM products WHERE id = 1 FOR SHARE;Deadlocks & gestion d'erreurs
Les deadlocks se produisent quand deux transactions détiennent des verrous dont chacune a besoin. La base de données détecte les deadlocks et annule une transaction (la victime). Prévenez les deadlocks en acquérant les verrous dans un ordre cohérent à travers toutes les transactions. Soyez toujours prêt à réessayer les transactions qui échouent à cause de deadlocks ou d'échecs de sérialisation. Gardez les transactions courtes pour réduire la contention de verrous. Le code applicatif devrait capturer SQLSTATE 40P01 (deadlock) et réessayer.
-- deadlock example:
-- Transaction A:
BEGIN;
UPDATE accounts SET balance = balance - 50 WHERE id = 1; -- locks row 1
UPDATE accounts SET balance = balance + 50 WHERE id = 2; -- waits for row 2
-- Transaction B (concurrent):
BEGIN;
UPDATE accounts SET balance = balance - 30 WHERE id = 2; -- locks row 2
UPDATE accounts SET balance = balance + 30 WHERE id = 1; -- waits for row 1
-- DEADLOCK! Database detects and kills one transaction
-- prevention: always lock in consistent order
-- Transaction A and B both lock id=1 first, then id=2
-- PostgreSQL: error codes for handling
-- 40P01: deadlock_detected
-- 40001: serialization_failure
-- 40P02: transaction_integrity_constraint_violation
-- retry pattern (pseudocode):
-- for attempt in range(3):
-- try:
-- BEGIN; ... COMMIT; break
-- except deadlock:
-- ROLLBACK; continuePropriétés ACID
ACID est la fondation des transactions de base de données fiables. Atomicité : toutes les opérations d'une transaction réussissent ou échouent ensemble. Cohérence : les transactions font passer la base d'un état valide à un autre (les contraintes sont imposées). Isolation : les transactions concurrentes ne s'interfèrent pas (contrôlée par le niveau d'isolation). Durabilité : une fois commitée, les données survivent aux crashes (atteint via write-ahead logging). Les bases NoSQL sacrifient souvent certaines propriétés ACID pour la scalabilité.
-- ACID guarantees for transactions:
-- A: Atomicity (all or nothing)
BEGIN;
INSERT INTO orders (id, total) VALUES (1, 100);
INSERT INTO order_items (order_id, product_id) VALUES (1, 5);
-- if either fails, both are rolled back
COMMIT;
-- C: Consistency (valid state to valid state)
-- constraints are checked at commit
ALTER TABLE accounts ADD CONSTRAINT balance_non_negative
CHECK (balance >= 0);
-- a transaction that would make balance negative fails
-- I: Isolation (concurrent transactions don't interfere)
-- controlled by isolation level (see previous section)
-- D: Durability (committed data survives crashes)
-- achieved via WAL (Write-Ahead Logging) + fsync
-- synchronous_commit = on (default) ensures durabilityIndex, vues & procédures stockées
Types & stratégies d'index
Les index B-tree (par défaut) gèrent les requêtes d'égalité (=) et de plage (<, >, BETWEEN). Les index composites suivent la règle du préfixe le plus à gauche — un index (a,b,c) aide WHERE a=?, WHERE a=? AND b=?, mais pas WHERE b=?. Les index partiels économisent de l'espace en n'indexant qu'un sous-ensemble. Les index d'expression permettent des requêtes indexées sur des fonctions (LOWER, colonnes calculées). Utilisez EXPLAIN ANALYZE pour vérifier que les index sont utilisés — un index inutilis é gaspille de l'espace et ralentit les écritures.
-- B-tree index (default): equality and range queries
CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_orders_date ON orders(created_at);
-- composite index (order matters!)
CREATE INDEX idx_orders_user_status ON orders(user_id, status);
-- helps: WHERE user_id = 1
-- helps: WHERE user_id = 1 AND status = 'paid'
-- does NOT help: WHERE status = 'paid' (leftmost prefix rule)
-- partial index: smaller, faster for common filters
CREATE INDEX idx_active_users ON users(email)
WHERE status = 'active';
-- expression index
CREATE INDEX idx_lower_email ON users(LOWER(email));
SELECT * FROM users WHERE LOWER(email) = '[email protected]';
-- unique index
CREATE UNIQUE INDEX idx_unique_email ON users(email);
-- EXPLAIN: see if index is used
EXPLAIN SELECT * FROM users WHERE email = '[email protected]';
EXPLAIN ANALYZE SELECT * FROM users WHERE email = '[email protected]';Procédures stockées & fonctions
Les fonctions renvoient des valeurs et peuvent être utilisées dans SELECT ; les procédures effectuent des actions et sont appelées avec CALL. Les procédures stockées encapsulent la logique métier dans la base — réduisant les allers-retours réseau et centralisant la logique. Cependant, elles peuvent rendre le scaling plus difficile (logique répartie entre app et DB) et sont spécifiques à la base. Utilisez-les pour les opérations data-intensive qui bénéficient de la proximité avec les données. PostgreSQL utilise PL/pgSQL ; MySQL utilise son propre SQL procédural.
-- PostgreSQL function
CREATE OR REPLACE FUNCTION get_user_orders(p_user_id INT)
RETURNS TABLE(order_id INT, total DECIMAL) AS $$
BEGIN
RETURN QUERY
SELECT id, total FROM orders WHERE user_id = p_user_id;
END;
$$ LANGUAGE plpgsql;
-- call function
SELECT * FROM get_user_orders(1);
-- PostgreSQL procedure (can manage transactions, PostgreSQL 11+)
CREATE PROCEDURE transfer_money(
from_id INT, to_id INT, amount DECIMAL
) LANGUAGE plpgsql AS $$
BEGIN
UPDATE accounts SET balance = balance - amount WHERE id = from_id;
UPDATE accounts SET balance = balance + amount WHERE id = to_id;
COMMIT;
END;
$$;
CALL transfer_money(1, 2, 100.00);
-- MySQL stored procedure
DELIMITER //
CREATE PROCEDURE GetActiveUsers()
BEGIN
SELECT * FROM users WHERE status = 'active';
END //
DELIMITER ;
CALL GetActiveUsers();Triggers
Les triggers s'exécutent automatiquement sur les changements de données. Les triggers BEFORE peuvent modifier les données entrantes (par exemple, définir des timestamps, valider). Les triggers AFTER effectuent des effets de bord (par exemple, journalisation d'audit, dénormalisation). Utilisez les triggers avec parcimonie — ils sont cachés au code applicatif, rendant le débogage plus difficile. Cas d'usage courants : pistes d'audit, colonnes calculées, imposition de contraintes complexes et synchronisation de données dénormalisées. Documentez toujours les triggers clairement.
-- PostgreSQL trigger: audit log on update
CREATE OR REPLACE FUNCTION audit_user_change()
RETURNS TRIGGER AS $$
BEGIN
INSERT INTO user_audit (user_id, old_name, new_name, changed_at)
VALUES (OLD.id, OLD.name, NEW.name, NOW());
RETURN NEW;
END;
$$ LANGUAGE plpgsql;
CREATE TRIGGER trg_user_audit
AFTER UPDATE ON users
FOR EACH ROW
EXECUTE FUNCTION audit_user_change();
-- trigger timing: BEFORE / AFTER / INSTEAD OF
-- trigger events: INSERT / UPDATE / DELETE / TRUNCATE
-- granularity: FOR EACH ROW / FOR EACH STATEMENT
-- MySQL trigger
CREATE TRIGGER before_user_insert
BEFORE INSERT ON users
FOR EACH ROW
SET NEW.created_at = NOW();
-- drop trigger
DROP TRIGGER IF EXISTS trg_user_audit ON users;Opérations JSON (PostgreSQL)
JSONB (PostgreSQL) stocke le JSON dans un format binaire, permettant l'indexation et des requêtes efficaces. -> renvoie du JSON, ->> renvoie du texte. @> vérifie l'inclusion (le JSON contient-il ceci ?). Les index GIN rendent les requêtes JSON rapides. Utilisez les colonnes JSON pour des données flexibles/semi-structurées (logs d'événements, réponses d'API, configuration) tout en gardant les données relationnelles dans des colonnes normales. JSONB est préférable à JSON (plus rapide, indexable, pas de clés dupliquées).
-- JSONB columns (PostgreSQL)
CREATE TABLE events (
id SERIAL PRIMARY KEY,
data JSONB NOT NULL
);
INSERT INTO events (data) VALUES
('{"type": "click", "user": {"id": 1, "name": "Alice"}, "tags": ["web", "mobile"]}');
-- extract fields (-> for JSON, ->> for text)
SELECT data->'type' AS type, -- "click" (JSON)
data->'user'->>'name' AS name, -- Alice (text)
data->'tags'->0 AS first_tag -- "web"
FROM events;
-- filter by JSON field
SELECT * FROM events WHERE data->>'type' = 'click';
SELECT * FROM events WHERE data @> '{"type": "click"}'; -- containment
-- GIN index for JSON queries
CREATE INDEX idx_events_data ON events USING gin(data);
SELECT * FROM events WHERE data @> '{"user": {"id": 1}}';
-- modify JSON
UPDATE events SET data = jsonb_set(data, '{user,name}', '"Bob"');Recherche en texte intégral
La recherche en texte intégral permet des requêtes en langage naturel (stemming, ranking, stopwords). to_tsvector convertit le texte en tokens recherchables ; to_tsquery crée une requête de recherche ; @@ correspond. ts_rank score les résultats ; ts_headline surligne les correspondances. Les index GIN rendent cela rapide. Pour la recherche à grande échelle, envisagez des moteurs dédiés (Elasticsearch, Solr), mais le FTS PostgreSQL est excellent pour des volumes de données modérés et évite la complexité d'infrastructure.
-- PostgreSQL full-text search
CREATE TABLE articles (
id SERIAL PRIMARY KEY,
title VARCHAR(200),
body TEXT
);
-- create a full-text search index
CREATE INDEX idx_articles_search ON articles
USING gin(to_tsvector('english', title || ' ' || body));
-- search with ranking
SELECT
title,
ts_rank(to_tsvector('english', body), query) AS rank,
ts_headline('english', body, query) AS snippet
FROM articles, to_tsquery('english', 'database & performance') query
WHERE to_tsvector('english', title || ' ' || body) @@ query
ORDER BY rank DESC
LIMIT 10;
-- simplified with generated column
ALTER TABLE articles ADD COLUMN search_vector tsvector
GENERATED ALWAYS AS (to_tsvector('english', title || ' ' || body)) STORED;
CREATE INDEX idx_articles_search_vec ON articles USING gin(search_vector);
SELECT * FROM articles WHERE search_vector @@ to_tsquery('database');Performance & optimisation de requêtes
EXPLAIN & plans de requête
EXPLAIN montre le plan de requête — comment la base exécutera votre requête. EXPLAIN ANALYZE l'exécute réellement et montre les timings réels. Cherchez les Sequential Scans sur de grandes tables (ajoutez des index), les Sorts coûteux (ajoutez des index) et les inadéquations d'estimation de lignes (exécutez ANALYZE pour mettre à jour les statistiques). Les nombres de coût sont relatifs, pas absolus. Comprendre les plans de requête est la compétence n°1 pour le tuning de performance SQL.
-- EXPLAIN: show query plan without running
EXPLAIN SELECT * FROM users WHERE email = '[email protected]';
-- EXPLAIN ANALYZE: run the query and show actual timing
EXPLAIN ANALYZE SELECT * FROM users WHERE email = '[email protected]';
-- EXPLAIN ANALYZE with buffers (I/O stats)
EXPLAIN (ANALYZE, BUFFERS) SELECT * FROM users
JOIN orders ON users.id = orders.user_id;
-- key things to look for in the plan:
-- Seq Scan: full table scan (bad for large tables — add index)
-- Index Scan: using an index (good)
-- Bitmap Index Scan: index + heap lookup (good for many rows)
-- Hash Join: builds hash table (good for large joins)
-- Nested Loop: good for small result sets
-- Sort: explicit sort (consider index to avoid)
-- cost: estimated cost (first row, all rows)
-- rows: estimated vs actual rows (big mismatch = stale stats)Pièges de performance courants
La sargabilité (Search Argument Able) signifie que la base peut utiliser des index. Les fonctions sur les colonnes (DATE(col), UPPER(col)) empêchent l'utilisation d'index — réécrivez en requêtes de plage ou utilisez des index d'expression. SELECT * gaspille les I/O et empêche les index de couverture. La pagination OFFSET est O(n) — utilisez la pagination par keyset (WHERE id > last_id) pour O(1). Les opérations batch importantes devraient être découpées pour éviter les verrous longs et le lag de réplication.
-- 1. Sargability: avoid functions on indexed columns
-- BAD: function prevents index usage
SELECT * FROM orders WHERE DATE(created_at) = '2024-01-01';
-- GOOD: range query uses index
SELECT * FROM orders
WHERE created_at >= '2024-01-01' AND created_at < '2024-01-02';
-- 2. Avoid SELECT * (more I/O, prevents covering indexes)
-- BAD
SELECT * FROM users WHERE status = 'active';
-- GOOD
SELECT id, name, email FROM users WHERE status = 'active';
-- 3. Use LIMIT with ORDER BY for pagination
-- BAD: loads all rows
SELECT * FROM products ORDER BY id;
-- GOOD: keyset pagination
SELECT * FROM products WHERE id > 1000 ORDER BY id LIMIT 20;
-- 4. Batch large updates
-- BAD: one giant transaction
DELETE FROM logs WHERE date < '2023-01-01';
-- GOOD: batch in chunks
DELETE FROM logs WHERE date < '2023-01-01' AND id <= 10000;
DELETE FROM logs WHERE date < '2023-01-01' AND id <= 20000;UNION, INTERSECT & EXCEPT
Les opérations ensemblistes combinent des résultats. UNION supprime les doublons (tri coûteux) ; UNION ALL les garde (plus rapide — préférez quand vous savez qu'il n'y a pas de doublons ou les voulez). INTERSECT renvoie les lignes dans les deux. EXCEPT renvoie les lignes dans le premier mais pas le second. Tous nécessitent des types de colonnes compatibles. UNION ALL peut remplacer des conditions OR complexes et souvent performe mieux car il peut utiliser différents index pour chaque branche.
-- UNION: combine results, remove duplicates
SELECT name FROM customers
UNION
SELECT name FROM suppliers;
-- UNION ALL: faster, keeps duplicates
SELECT name FROM customers
UNION ALL
SELECT name FROM suppliers;
-- INTERSECT: rows in BOTH results
SELECT product_id FROM sales_2023
INTERSECT
SELECT product_id FROM sales_2024;
-- EXCEPT (MINUS in Oracle): rows in first but not second
SELECT product_id FROM all_products
EXCEPT
SELECT product_id FROM discontinued_products;
-- rules:
-- - same number of columns
-- - compatible types
-- - column names come from first query
-- - UNION is often faster than OR conditionsConseils spécifiques à la base
VACUUM (PostgreSQL) récupère l'espace des lignes supprimées (MVCC laisse des « dead tuples »). ANALYZE met à jour les statistiques de table pour le planificateur de requêtes — exécutez après des chargements en masse. OPTIMIZE TABLE (MySQL) défragmente les tables. Indexez les clés étrangères explicitement (PostgreSQL ne les auto-indexe pas). Surveillez l'utilisation des index avec pg_stat_user_indexes et supprimez ceux inutilisés. Le pooling de connexions (PgBouncer, ProxySQL) est essentiel pour les applications à fort trafic — ouvrir des connexions est coûteux.
-- PostgreSQL: VACUUM to reclaim space
VACUUM ANALYZE users; -- update stats, reclaim dead rows
VACUUM FULL users; -- rewrites table (locks, but reclaims all space)
-- PostgreSQL: ANALYZE to update statistics
ANALYZE users; -- helps query planner make better decisions
-- MySQL: OPTIMIZE TABLE
OPTIMIZE TABLE users;
-- Common indexing rules across databases:
-- 1. Index foreign keys (not automatic in all DBs)
-- 2. Index columns used in WHERE, JOIN, ORDER BY, GROUP BY
-- 3. Composite indexes: high selectivity column first
-- 4. Don't over-index (slows writes, uses disk)
-- 5. Drop unused indexes (check with pg_stat_user_indexes)
-- Connection pooling (application level):
-- - Use PgBouncer (PostgreSQL) or ProxySQL (MySQL)
-- - Reuse connections instead of reconnecting per request
-- - Set appropriate pool size (not too high!)Types de données & gestion NULL
NULL représente des données inconnues/manquantes, pas zéro ou vide. Les comparaisons NULL donnent toujours NULL (inconnu), ce qui est falsy dans WHERE. Utilisez IS NULL / IS NOT NULL pour tester. COALESCE fournit des valeurs de repli. NULLIF convertit des valeurs spécifiques en NULL (utile pour la division par zéro). Les agrégats ignorent les NULL — COUNT(col) compte les non-NULL, COUNT(*) compte toutes les lignes. Dans les LEFT JOINs, utilisez COUNT(right_table.col) pour obtenir 0 pour les lignes non correspondantes.
-- NULL is not zero or empty string — it's "unknown"
SELECT NULL = NULL; -- NULL (not true!)
SELECT NULL IS NULL; -- true
SELECT NULL <> 1; -- NULL (unknown)
-- COALESCE: first non-NULL value
SELECT COALESCE(nickname, first_name, 'Anonymous') FROM users;
-- NULLIF: return NULL if two values are equal
SELECT NULLIF(score, 0) FROM tests; -- NULL instead of 0
-- useful for avoiding division by zero:
SELECT total / NULLIF(count, 0) FROM stats;
-- aggregate functions ignore NULL
SELECT AVG(score) FROM tests; -- avg of non-NULL scores only
SELECT COUNT(score) FROM tests; -- count of non-NULL
SELECT COUNT(*) FROM tests; -- count of all rows
-- use LEFT JOIN + COUNT carefully
SELECT u.name, COUNT(o.id) AS orders -- COUNT(o.id) = 0 for no orders
FROM users u LEFT JOIN orders o ON u.id = o.user_id
GROUP BY u.id;CTEs récursifs
Structure de base d'un CTE récursif
Un CTE récursif se référence lui-même pour générer des données hiérarchiques ou séquentielles. Il a deux parties jointes par UNION ALL : une requête ancre (le cas de base/point de départ) et une requête récursive (qui référence le CTE et ajoute au résultat). La récursion continue jusqu'à ce que la requête récursive ne renvoie plus de lignes. Utilisez les CTE récursifs pour le parcours d'arbre (organigrammes, systèmes de fichiers), la génération de séquences et la recherche de chemin dans un graphe. Incluez toujours une condition de terminaison dans la clause WHERE pour éviter les boucles infinies.
-- Recursive CTE: a CTE that references itself
-- Three parts: anchor, UNION ALL, recursive member
WITH RECURSIVE countdown(n) AS (
-- Anchor: starting point
SELECT 1 AS n
UNION ALL
-- Recursive member: references the CTE itself
SELECT n + 1 FROM countdown WHERE n < 10
)
SELECT n FROM countdown;
-- Result: 1, 2, 3, 4, 5, 6, 7, 8, 9, 10
-- PostgreSQL uses WITH RECURSIVE
-- SQL Server/MySQL 8+: WITH (RECURSIVE optional in MySQL)
-- SQLite: WITH RECURSIVEDonnées hiérarchiques (organigramme)
Les CTE récursifs excellent à parcourir des données hiérarchiques comme les organigrammes, les arbres de catégories ou les systèmes de fichiers. L'ancre sélectionne le nœud racine ; le membre récursif joint la table au CTE sur la relation parent-enfant (manager_id = id). Ajouter une colonne depth suit le nombre de niveaux de profondeur de chaque ligne, et une colonne path (concaténation de chaînes) montre la chaîne d'ascendance complète. Cela remplace le besoin de multiples self-joins ou de récursion côté application. Le CAST sur path prévient les erreurs de type pendant la récursion.
-- Employee hierarchy with manager relationships
CREATE TABLE employees (
id INT PRIMARY KEY,
name VARCHAR(100),
manager_id INT REFERENCES employees(id)
);
-- Find all direct and indirect reports of employee 1 (CEO)
WITH RECURSIVE org_chain AS (
-- Anchor: the starting employee
SELECT id, name, manager_id, 0 AS depth, CAST(name AS VARCHAR(500)) AS path
FROM employees WHERE id = 1
UNION ALL
-- Recursive: find employees whose manager is in the chain
SELECT e.id, e.name, e.manager_id, oc.depth + 1,
CAST(oc.path || ' > ' || e.name AS VARCHAR(500))
FROM employees e
JOIN org_chain oc ON e.manager_id = oc.id
)
SELECT id, name, depth, path FROM org_chain
ORDER BY depth, name;
-- depth shows hierarchy level, path shows the management chainGénération de séquences & dates
Les CTE récursifs peuvent générer des séquences et des plages de dates — utiles pour remplir les trous dans les rapports de séries temporelles. En générant toutes les dates d'une plage et en LEFT JOINant à vos données, vous garantissez que chaque date apparaît dans la sortie même quand il n'y a pas d'enregistrements. C'est un motif courant pour les tableaux de bord et graphiques. PostgreSQL a aussi generate_series() comme alternative plus simple. Définissez toujours une condition de terminaison (WHERE n < 100) pour éviter la récursion infinie. Certaines bases limitent la profondeur de récursion (par exemple, 100 par défaut dans MySQL via cte_max_recursion_depth).
-- Generate a series of numbers
WITH RECURSIVE numbers(n) AS (
SELECT 1
UNION ALL
SELECT n + 1 FROM numbers WHERE n < 100
)
SELECT n FROM numbers;
-- Generate a date range (all days in January 2024)
WITH RECURSIVE dates(d) AS (
SELECT DATE '2024-01-01'
UNION ALL
SELECT d + INTERVAL '1 day' FROM dates
WHERE d < DATE '2024-01-31'
)
SELECT d FROM dates;
-- Generate a time series for gap-filling in reports
-- (ensures every date appears even if no data exists)
SELECT d.date, COALESCE(SUM(o.amount), 0) AS daily_total
FROM dates d
LEFT JOIN orders o ON o.order_date = d.date
GROUP BY d.date ORDER BY d.date;Recherche de chemin dans un graphe (BFS)
Les CTE récursifs peuvent effectuer une recherche en largeur (BFS) sur des structures de graphe. L'ancre trouve les arêtes du nœud de départ ; le membre récursif étend les chemins en joignant les arêtes à l'extrémité du chemin courant. La prévention des cycles est critique dans les graphes cycliques — vérifiez que le nœud de destination n'est pas déjà dans le chemin (en utilisant LIKE ou une recherche de chaîne). La limite de hops est un filet de sécurité contre la récursion infinie. Cette approche fonctionne pour la recherche d'itinéraire, la résolution de dépendances et l'analyse de réseau. Pour les plus courts chemins pondérés, envisagez l'algorithme de Dijkstra dans le code applicatif.
-- Find all paths in a directed graph
CREATE TABLE edges (src VARCHAR(10), dst VARCHAR(10));
-- Find all reachable nodes from 'A' with the path taken
WITH RECURSIVE paths AS (
-- Anchor: start from node A
SELECT src, dst, CAST(src || '->' || dst AS VARCHAR(1000)) AS path,
1 AS hops
FROM edges WHERE src = 'A'
UNION ALL
-- Recursive: extend the path
SELECT p.src, e.dst,
CAST(p.path || '->' || e.dst AS VARCHAR(1000)),
p.hops + 1
FROM paths p
JOIN edges e ON p.dst = e.src
WHERE p.hops < 10 -- prevent infinite loops in cyclic graphs
AND p.path NOT LIKE '%' || e.dst || '%'
)
SELECT DISTINCT path, hops FROM paths ORDER BY hops;
-- Cycle prevention: check the path doesn't already contain the nodeFactorielle & agrégation avec récursion
Les CTE récursifs peuvent effectuer des calculs mathématiques comme les factorielles en portant l'état (n, fact) à travers chaque itération. L'ancre définit le cas de base (0! = 1), et le membre récursif calcule la valeur suivante à partir de la précédente. Les totaux cumulés peuvent aussi être calculés ainsi, bien que les fonctions de fenêtre (SUM(amount) OVER (ORDER BY id)) soient plus efficaces et idiomatiques pour les agrégats cumulatifs. Les CTE récursifs pour le calcul sont principalement éducatifs — utilisez-les quand les fonctions de fenêtre ou le code procédural ne peuvent pas exprimer la logique. Chaque niveau de récursion ajoute une ligne, donc le résultat grandit avec la profondeur.
-- Compute factorial using recursive CTE
WITH RECURSIVE factorial(n, fact) AS (
-- Anchor: 0! = 1
SELECT 0, 1
UNION ALL
-- Recursive: n! = n * (n-1)!
SELECT n + 1, fact * (n + 1) FROM factorial WHERE n < 10
)
SELECT n, fact FROM factorial;
-- Running accumulation: cumulative sum
WITH RECURSIVE running_total AS (
SELECT id, amount, amount AS cumulative
FROM transactions WHERE id = 1
UNION ALL
SELECT t.id, t.amount, rt.cumulative + t.amount
FROM transactions t
JOIN running_total rt ON t.id = rt.id + 1
)
SELECT * FROM running_total ORDER BY id;
-- Note: window functions (SUM OVER) are usually better for thisPIVOT & UNPIVOT
PIVOT (lignes vers colonnes)
PIVOT transforme les lignes en colonnes — parfait pour les rapports croisés où vous voulez des catégories comme en-têtes de colonnes. La liste IN spécifie quelles valeurs deviennent des colonnes. SQL Server et Oracle ont une syntaxe PIVOT native. La requête interne fournit les données source, et PIVOT applique un agrégat (SUM, AVG, COUNT) pour chaque groupe de colonnes. C'est équivalent à l'agrégation conditionnelle mais plus lisible pour les pivots larges. Utilisez PIVOT quand vous avez un ensemble fixe et connu de valeurs sur lesquelles pivoter.
-- Convert rows to columns (cross-tabulation)
-- Source: sales data with rows per quarter
CREATE TABLE sales (quarter VARCHAR(10), region VARCHAR(50), amount DECIMAL(10,2));
-- SQL Server PIVOT syntax:
SELECT region, [Q1], [Q2], [Q3], [Q4]
FROM (
SELECT quarter, region, amount FROM sales
) AS src
PIVOT (
SUM(amount) FOR quarter IN ([Q1], [Q2], [Q3], [Q4])
) AS pvt;
-- Result:
-- region | Q1 | Q2 | Q3 | Q4
-- North | 1000 | 1500 | 1200 | 1800
-- South | 800 | 1100 | 900 | 1300Agrégation conditionnelle (PIVOT universel)
L'agrégation conditionnelle (SUM + CASE) est la technique de pivot universelle qui fonctionne dans toutes les bases SQL. Chaque expression CASE filtre pour une catégorie, et SUM agrège les valeurs correspondantes. C'est souvent plus rapide que PIVOT et plus flexible. Le ELSE 0 garantit que les lignes non correspondantes contribuent à zéro. La fonction crosstab() de PostgreSQL (de l'extension tablefunc) est plus concise mais nécessite des colonnes de sortie fixes. Utilisez l'agrégation conditionnelle quand vous avez besoin de compatibilité inter-bases ou quand la syntaxe PIVOT n'est pas disponible.
-- Works in ALL databases (MySQL, PostgreSQL, SQLite, etc.)
SELECT
region,
SUM(CASE WHEN quarter = 'Q1' THEN amount ELSE 0 END) AS Q1,
SUM(CASE WHEN quarter = 'Q2' THEN amount ELSE 0 END) AS Q2,
SUM(CASE WHEN quarter = 'Q3' THEN amount ELSE 0 END) AS Q3,
SUM(CASE WHEN quarter = 'Q4' THEN amount ELSE 0 END) AS Q4,
SUM(amount) AS total
FROM sales
GROUP BY region
ORDER BY region;
-- PostgreSQL-specific: crosstab() from tablefunc extension
-- SELECT * FROM crosstab('SELECT region, quarter, amount FROM sales ORDER BY 1,2')
-- AS ct(region VARCHAR, Q1 DECIMAL, Q2 DECIMAL, Q3 DECIMAL, Q4 DECIMAL);PIVOT dynamique (SQL dynamique)
Le SQL dynamique construit une chaîne de requête à l'exécution quand les colonnes de pivot ne sont pas connues à l'avance (par exemple, pivoter par mois quand les mois varient). Le processus : requête des valeurs distinctes, construction d'une liste de colonnes, construction de l'instruction PIVOT, et exécution avec sp_executesql (SQL Server) ou PREPARE/EXECUTE (MySQL). Assainissez toujours avec QUOTENAME() ou quote_ident() pour prévenir l'injection SQL. Le SQL dynamique est puissant mais ajoute de la complexité et des risques de sécurité — utilisez-le avec parcimonie et préférez les pivots fixes quand possible. Le pivotage côté application est souvent une alternative plus sûre.
-- When pivot columns are unknown at write time, use dynamic SQL
-- SQL Server example:
DECLARE @cols AS NVARCHAR(MAX),
@query AS NVARCHAR(MAX);
-- Build the column list dynamically
SELECT @cols = STUFF((
SELECT DISTINCT ',' + QUOTENAME(quarter)
FROM sales FOR XML PATH(''), TYPE
).value('.', 'NVARCHAR(MAX)'), 1, 1, '');
-- Build and execute the pivot query
SET @query = 'SELECT region, ' + @cols + '
FROM (SELECT quarter, region, amount FROM sales) x
PIVOT (SUM(amount) FOR quarter IN (' + @cols + ')) p';
EXEC sp_executesql @query;
-- WARNING: dynamic SQL is vulnerable to SQL injection
-- — always use QUOTENAME() to sanitize column namesUNPIVOT (colonnes vers lignes)
UNPIVOT inverse PIVOT — il transforme les colonnes en lignes. C'est utile pour normaliser des données dénormalisées, convertir des fichiers d'import larges en format long, ou préparer des données pour les graphiques. SQL Server a une syntaxe UNPIVOT native. L'approche UNION ALL fonctionne partout : chaque SELECT extrait une colonne et l'étiquette avec une valeur fixe. UNION ALL (pas UNION) préserve les doublons et est plus rapide. UNPIVOT est courant dans les pipelines ETL quand les données source arrivent en format tableur (large) mais doivent être stockées normalisées (long).
-- Convert columns back to rows
-- Source: wide table with Q1-Q4 columns
-- Target: narrow table with quarter/amount rows
-- SQL Server UNPIVOT:
SELECT region, quarter, amount
FROM quarterly_sales
UNPIVOT (
amount FOR quarter IN (Q1, Q2, Q3, Q4)
) AS unpvt;
-- Universal UNION ALL approach (all databases):
SELECT region, 'Q1' AS quarter, Q1 AS amount FROM quarterly_sales
UNION ALL
SELECT region, 'Q2' AS quarter, Q2 AS amount FROM quarterly_sales
UNION ALL
SELECT region, 'Q3' AS quarter, Q3 AS amount FROM quarterly_sales
UNION ALL
SELECT region, 'Q4' AS quarter, Q4 AS amount FROM quarterly_sales;
-- Result: one row per region/quarter combinationExemple pratique de rapport pivot
Ce rapport pivot du monde réel combine des ventilations mensuelles avec une comparaison année sur année en une seule requête. L'agrégation conditionnelle (SUM + CASE) crée à la fois des colonnes mensuelles et des totaux annuels. La colonne yoy_change calcule la différence en ligne. HAVING filtre les produits sans ventes. Ce motif est courant dans les tableaux de bord BI et les rapports financiers. La fonction EXTRACT fonctionne dans la plupart des bases (utilisez DATEPART dans SQL Server, strftime dans SQLite). Pour des colonnes véritablement dynamiques, combinez avec du SQL dynamique ou gérez le pivotage dans la couche applicative.
-- Monthly sales pivot with year-over-year comparison
SELECT
product_name,
SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 1 THEN amount ELSE 0 END) AS jan,
SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 2 THEN amount ELSE 0 END) AS feb,
SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 3 THEN amount ELSE 0 END) AS mar,
SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 4 THEN amount ELSE 0 END) AS apr,
SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 5 THEN amount ELSE 0 END) AS may,
SUM(CASE WHEN EXTRACT(MONTH FROM order_date) = 6 THEN amount ELSE 0 END) AS jun,
SUM(CASE WHEN EXTRACT(YEAR FROM order_date) = 2024 THEN amount ELSE 0 END) AS total_2024,
SUM(CASE WHEN EXTRACT(YEAR FROM order_date) = 2023 THEN amount ELSE 0 END) AS total_2023,
SUM(CASE WHEN EXTRACT(YEAR FROM order_date) = 2024 THEN amount ELSE 0 END) -
SUM(CASE WHEN EXTRACT(YEAR FROM order_date) = 2023 THEN amount ELSE 0 END) AS yoy_change
FROM orders o
JOIN products p ON o.product_id = p.id
WHERE order_date BETWEEN '2023-01-01' AND '2024-12-31'
GROUP BY product_name
HAVING SUM(amount) > 0
ORDER BY total_2024 DESC;Recherche en texte intégral
LIKE vs recherche en texte intégral
LIKE '%pattern%' effectue un scan complet de table — il ne peut pas utiliser d'index et est O(n) sur les grandes tables. La recherche en texte intégral utilise un index inversé (mot → document), rendant les recherches O(1) par terme. La recherche en texte intégral supporte aussi le ranking de pertinence, le stemming et les opérateurs booléens. Utilisez LIKE pour les correspondances de préfixe simples (LIKE 'prefix%' peut utiliser un index B-tree) ou les petites tables. Utilisez la recherche en texte intégral pour chercher dans des articles, descriptions de produits ou tout contenu textuel. Chaque base a sa propre implémentation (MySQL FULLTEXT, PostgreSQL tsvector, SQL Server CONTAINS).
-- LIKE: simple pattern matching (slow on large tables)
SELECT * FROM articles WHERE title LIKE '%database%';
SELECT * FROM articles WHERE title LIKE 'data%'; -- prefix match (can use index)
-- Full-Text Search: indexed, fast, supports ranking
-- MySQL:
CREATE FULLTEXT INDEX ft_title_body ON articles(title, body);
SELECT * FROM articles
WHERE MATCH(title, body) AGAINST('database performance' IN NATURAL LANGUAGE MODE);
-- With relevance score:
SELECT id, title,
MATCH(title, body) AGAINST('database performance') AS relevance
FROM articles
WHERE MATCH(title, body) AGAINST('database performance')
ORDER BY relevance DESC;
-- LIKE '%word%' scans every row (no index usage)
-- Full-text search uses an inverted index — much fasterRecherche en texte intégral PostgreSQL (tsvector)
PostgreSQL a la recherche en texte intégral intégrée la plus puissante. tsvector est une représentation de document pré-tokenisée et normalisée (minuscules, stemming, stop-words supprimés). tsquery est l'expression de recherche supportant & (AND), | (OR), ! (NOT). L'opérateur @@ correspond un tsvector à un tsquery. Les index GIN rendent les recherches extrêmement rapides. ts_rank score les résultats par fréquence de terme. Utilisez to_tsvector pour l'indexation, plainto_tsquery pour l'entrée utilisateur (gère les mots simples), et phraseto_tsquery pour la correspondance de phrase exacte. Cela rivalise avec les moteurs de recherche dédiés pour de nombreux cas d'usage.
-- PostgreSQL uses tsvector (document) and tsquery (search)
-- Create a search-optimized column
ALTER TABLE articles ADD COLUMN search_vector tsvector;
CREATE INDEX idx_search ON articles USING GIN(search_vector);
-- Populate the search vector (tokenize + normalize)
UPDATE articles SET search_vector =
to_tsvector('english', coalesce(title,'') || ' ' || coalesce(body,''));
-- Search with tsquery
SELECT id, title,
ts_rank(search_vector, query) AS rank
FROM articles, to_tsquery('english', 'database & performance') query
WHERE search_vector @@ query
ORDER BY rank DESC;
-- ts_rank_cd gives coverage density ranking
-- plainto_tsquery: plain text → tsquery (user-friendly)
-- phraseto_tsquery: exact phrase matchingRecherche en mode booléen
Le mode booléen donne un contrôle précis sur les critères de recherche. Dans MySQL, + requiert un terme, - l'exclut, * est un wildcard de préfixe, et les guillemets correspondent à des phrases exactes. Dans PostgreSQL, & | ! sont les opérateurs, et <-> correspond à des mots adjacents (recherche de phrase). La recherche booléenne est idéale pour les interfaces de recherche avancées où les utilisateurs spécifient des exigences exactes. Notez que le mode booléen de MySQL ne calcule pas de scores de pertinence par défaut — combinez avec NATURAL LANGUAGE MODE pour le ranking. Les recherches avec wildcard (data*) peuvent être plus lentes car elles correspondent à de nombreux termes.
-- MySQL boolean full-text search
SELECT * FROM articles
WHERE MATCH(title, body) AGAINST('+database +performance' IN BOOLEAN MODE);
-- + = must contain, - = must NOT contain, * = wildcard
-- "" = exact phrase, () = grouping, ~ = fuzzy (negation)
-- Examples:
-- '+MySQL -Oracle' → must have MySQL, must NOT have Oracle
-- '"full text"' → exact phrase "full text"
-- 'data*' → words starting with "data" (database, dataset)
-- '+database >performance' → database required, performance boosts rank
-- PostgreSQL boolean operators in tsquery:
SELECT * FROM articles
WHERE search_vector @@ to_tsquery('english', 'database & !oracle');
-- & = AND, | = OR, ! = NOT, <-> = follows (phrase)Recherche avec surlignage & extraits
Le surlignage des résultats de recherche montre aux utilisateurs pourquoi un document correspondait en affichant un extrait avec les termes de recherche mis en évidence. ts_headline de PostgreSQL génère un extrait contextuel avec longueur configurable (MaxWords, MinWords) et balises de surlignage. MySQL manque de surlignage intégré — utilisez SUBSTRING avec LOCATE pour extraire le contexte manuellement. CONTAINSTABLE de SQL Server renvoie une table de ranking que vous joignez à la source. Le surlignage améliore l'UX dans les interfaces de recherche en montrant le contexte de pertinence. Pour la recherche en production, envisagez des outils dédiés comme Elasticsearch ou Meilisearch pour les fonctionnalités avancées.
-- PostgreSQL: highlight matching terms
SELECT id, title,
ts_headline('english', body, query, 'MaxWords=35, MinWords=15') AS snippet
FROM articles, to_tsquery('english', 'database') query
WHERE search_vector @@ query;
-- ts_headline returns a text snippet with matches highlighted
-- (wrapped in <b> tags by default, customizable)
-- MySQL: no built-in highlighting, but you can use SUBSTRING:
SELECT id,
SUBSTRING(body, GREATEST(1, LOCATE('database', body) - 50), 100) AS snippet
FROM articles
WHERE MATCH(body) AGAINST('database' IN NATURAL LANGUAGE MODE);
-- SQL Server CONTAINSTABLE with ranking:
-- SELECT a.id, a.title, KEY_TBL.RANK
-- FROM articles a
-- INNER JOIN CONTAINSTABLE(articles, body, 'database') AS KEY_TBL
-- ON a.id = KEY_TBL.[KEY]
-- ORDER BY KEY_TBL.RANK DESC;Recherche par trigrammes (correspondance floue)
La recherche par trigrammes découpe le texte en séquences de 3 caractères et compare le chevauchement — permettant une correspondance floue et tolérante aux fautes de frappe que LIKE ne peut pas faire. L'extension pg_trgm (PostgreSQL) fournit similarity() (score 0-1) et l'opérateur % (correspond au-dessus d'un seuil). Cela alimente l'autocomplétion, les suggestions « vouliez-vous dire ? » et la correction de fautes. Les index GIN avec gin_trgm_ops rendent les recherches floues rapides. Les trigrammes gèrent les fautes d'orthographe (« databse » → « database ») que la recherche en texte intégral exacte manquerait. Le similarity_threshold contrôle le degré de lâcheté de la correspondance — des valeurs plus basses correspondent plus mais avec plus de faux positifs.
-- PostgreSQL pg_trgm extension: fuzzy/typo-tolerant search
CREATE EXTENSION pg_trgm;
-- similarity() returns 0-1 based on trigram overlap
SELECT name, similarity(name, 'database') AS sim
FROM products
WHERE name % 'database' -- % operator: trigram similarity threshold
ORDER BY sim DESC;
-- Set similarity threshold (0-1)
SET pg_trgm.similarity_threshold = 0.3;
-- word_similarity: partial word matching
SELECT name, word_similarity('database', name) AS sim
FROM products WHERE name <% 'database';
-- Create a GIN index for fast fuzzy search
CREATE INDEX idx_products_trgm ON products USING GIN(name gin_trgm_ops);
-- Great for: autocomplete, typo correction, "did you mean?" featuresTriggers
Bases des triggers (AFTER/BEFORE)
Les triggers sont du code au niveau base qui s'exécute automatiquement quand les données changent. Les triggers AFTER journalisent ou propagent les changements (ne peuvent pas modifier NEW). Les triggers BEFORE valident ou transforment les données avant écriture (peuvent modifier NEW). FOR EACH ROW se déclenche une fois par ligne affectée ; FOR EACH STATEMENT se déclenche une fois par instruction. Utilisez les triggers pour la journalisation d'audit, l'imposition de contraintes complexes et la mise à jour automatique des colonnes dérivées. Évitez les triggers pour la logique métier — ils sont cachés, difficiles à déboguer et peuvent causer des effets en cascade. Chaque base a une syntaxe de trigger différente ; PostgreSQL utilise des fonctions comme corps de trigger.
-- A trigger fires automatically on INSERT/UPDATE/DELETE
-- MySQL syntax:
DELIMITER //
CREATE TRIGGER audit_log
AFTER INSERT ON employees
FOR EACH ROW
BEGIN
INSERT INTO audit_table (table_name, action, row_id, changed_at)
VALUES ('employees', 'INSERT', NEW.id, NOW());
END //
DELIMITER ;
-- BEFORE triggers can modify the NEW values:
CREATE TRIGGER validate_email
BEFORE INSERT ON users
FOR EACH ROW
BEGIN
IF NEW.email NOT LIKE '%@%.%' THEN
SIGNAL SQLSTATE '45000' SET MESSAGE_TEXT = 'Invalid email';
END IF;
END //
-- PostgreSQL uses CREATE FUNCTION + CREATE TRIGGER:
-- CREATE TRIGGER audit AFTER INSERT ON employees
-- FOR EACH ROW EXECUTE FUNCTION audit_func();Trigger de journalisation d'audit
Les triggers d'audit capturent chaque changement de données pour la conformité et le débogage. La table d'audit stocke le type d'action, les anciennes et nouvelles valeurs, qui a fait le changement (CURRENT_USER) et quand (CURRENT_TIMESTAMP). Vous avez besoin de triggers séparés pour INSERT, UPDATE et DELETE. OLD référence les valeurs pré-changement (disponible dans UPDATE/DELETE), NEW référence les valeurs post-changement (disponible dans INSERT/UPDATE). Les tables d'audit grandissent indéfiniment — partitionnez par date ou archivez les anciennes données. Ce motif satisfait les exigences SOX, HIPAA et GDPR pour le suivi des changements de données.
-- Track all changes to a critical table
CREATE TABLE employee_audit (
audit_id INT AUTO_INCREMENT PRIMARY KEY,
action VARCHAR(10), -- INSERT, UPDATE, DELETE
employee_id INT,
old_name VARCHAR(100),
new_name VARCHAR(100),
old_salary DECIMAL(10,2),
new_salary DECIMAL(10,2),
changed_by VARCHAR(50) DEFAULT CURRENT_USER,
changed_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TRIGGER trg_audit_insert
AFTER INSERT ON employees
FOR EACH ROW
INSERT INTO employee_audit (action, employee_id, new_name, new_salary)
VALUES ('INSERT', NEW.id, NEW.name, NEW.salary);
CREATE TRIGGER trg_audit_update
AFTER UPDATE ON employees
FOR EACH ROW
INSERT INTO employee_audit (action, employee_id, old_name, new_name, old_salary, new_salary)
VALUES ('UPDATE', NEW.id, OLD.name, NEW.name, OLD.salary, NEW.salary);
CREATE TRIGGER trg_audit_delete
AFTER DELETE ON employees
FOR EACH ROW
INSERT INTO employee_audit (action, employee_id, old_name, old_salary)
VALUES ('DELETE', OLD.id, OLD.name, OLD.salary);Trigger de colonne calculée/dérivée
Les triggers peuvent auto-calculer les colonnes dérivées, garantissant la cohérence sans code applicatif. Les triggers BEFORE INSERT/UPDATE définissent NEW.final_price basé sur d'autres colonnes. Cependant, les bases modernes supportent les colonnes GENERATED (calculées) nativement — elles sont toujours correctes, ne peuvent pas être surchargées manuellement et peuvent être indexées. Préférez les colonnes GENERATED aux triggers pour les valeurs calculées. Utilisez les triggers seulement quand le calcul implique des données externes, de la logique conditionnelle ou des dépendances inter-tables que les colonnes GENERATED ne peuvent pas gérer. Souvenez-vous que les triggers ajoutent du surcoût à chaque opération d'écriture.
-- Auto-update a derived column when source data changes
CREATE TABLE products (
id INT PRIMARY KEY,
price DECIMAL(10,2),
discount_percent DECIMAL(5,2),
final_price DECIMAL(10,2) -- computed: price * (1 - discount/100)
);
-- BEFORE INSERT: compute final_price
CREATE TRIGGER calc_final_price_insert
BEFORE INSERT ON products
FOR EACH ROW
SET NEW.final_price = NEW.price * (1 - NEW.discount_percent / 100);
-- BEFORE UPDATE: recompute if price or discount changes
CREATE TRIGGER calc_final_price_update
BEFORE UPDATE ON products
FOR EACH ROW
SET NEW.final_price = NEW.price * (1 - NEW.discount_percent / 100);
-- Alternative: use GENERATED columns (MySQL 5.7+, PostgreSQL 12+)
-- final_price DECIMAL(10,2) GENERATED ALWAYS AS
-- (price * (1 - discount_percent / 100)) STOREDPrévention des suppressions avec des triggers
Les triggers peuvent imposer des règles de protection de données que les contraintes CHECK ne peuvent pas exprimer. Les triggers BEFORE DELETE peuvent bloquer entièrement les suppressions (en utilisant SIGNAL/RAISE) ou implémenter des soft deletes (marquer les enregistrements comme supprimés au lieu de les supprimer). SIGNAL SQLSTATE '45000' est la manière MySQL de lever une erreur définie par l'utilisateur. PostgreSQL utilise RAISE EXCEPTION. C'est utile pour protéger les données de référence, prévenir la suppression d'enregistrements parents avec enfants, ou implémenter des pistes d'audit immuables. Soyez prudent : les triggers qui empêchent les opérations peuvent surprendre les développeurs — documentez-les clairement et envisagez des vérifications au niveau applicatif à la place.
-- Prevent deletion of critical records
CREATE TRIGGER prevent_delete_admin
BEFORE DELETE ON users
FOR EACH ROW
BEGIN
IF OLD.role = 'admin' THEN
SIGNAL SQLSTATE '45000'
SET MESSAGE_TEXT = 'Cannot delete admin users';
END IF;
END //
-- Soft delete instead of hard delete
CREATE TRIGGER soft_delete
BEFORE DELETE ON articles
FOR EACH ROW
BEGIN
-- Prevent actual deletion, mark as deleted instead
INSERT INTO articles (id, title, body, deleted_at)
VALUES (OLD.id, OLD.title, OLD.body, NOW())
ON DUPLICATE KEY UPDATE deleted_at = NOW();
-- Still need to prevent the DELETE — use SIGNAL or a flag
END //
-- PostgreSQL: raise exception in trigger function
-- IF OLD.role = 'admin' THEN
-- RAISE EXCEPTION 'Cannot delete admin users';
-- END IF;Gestion & débogage des triggers
Gérer les triggers est essentiel pour la maintenance. SHOW TRIGGERS (MySQL) et les vues information_schema listent tous les triggers. Supprimez les triggers avec DROP TRIGGER IF EXISTS. Désactiver temporairement les triggers est utile pour les chargements de données en masse (qui déclencheraient une journalisation d'audit coûteuse pour chaque ligne). PostgreSQL utilise ALTER TABLE ... DISABLE/ENABLE TRIGGER ; SQL Server utilise DISABLE/ENABLE TRIGGER. Réactivez toujours les triggers après la maintenance. Déboguer les triggers est difficile — ils s'exécutent silencieusement. Ajoutez de la journalisation à une table de débogage, ou testez la logique de trigger isolément d'abord. Les triggers excessifs créent une complexité cachée et des problèmes de performance.
-- View existing triggers
-- MySQL:
SHOW TRIGGERS;
SELECT * FROM information_schema.TRIGGERS WHERE TRIGGER_SCHEMA = 'mydb';
-- PostgreSQL:
SELECT tgname, tgrelid::regclass, tgtype FROM pg_trigger;
-- SQL Server:
SELECT name, type_desc FROM sys.triggers WHERE parent_id = OBJECT_ID('employees');
-- Drop a trigger
DROP TRIGGER IF EXISTS audit_log; -- MySQL
DROP TRIGGER IF EXISTS audit_log ON employees; -- PostgreSQL
-- Disable/enable triggers (PostgreSQL)
ALTER TABLE employees DISABLE TRIGGER ALL;
ALTER TABLE employees ENABLE TRIGGER ALL;
-- Disable for bulk operations (SQL Server)
DISABLE TRIGGER trg_audit ON employees;
-- ... bulk operations ...
ENABLE TRIGGER trg_audit ON employees;Fonctions définies par l'utilisateur (UDFs)
Fonctions scalaires (renvoient une valeur unique)
Les UDFs scalaires renvoient une seule valeur et peuvent être utilisées dans SELECT, WHERE et les colonnes calculées. DETERMINISTIC signifie que la sortie dépend seulement des entrées (active la mise en cache). READS SQL DATA déclare que la fonction lit depuis des tables. Les UDFs encapsulent de la logique réutilisable (remises, formatage, calculs) pour qu'elle soit cohérente à travers les requêtes. Cependant, les UDFs scalaires dans SQL Server peuvent causer des problèmes de performance (exécution ligne par ligne) — utilisez des fonctions de table en ligne ou des colonnes calculées à la place quand possible. MySQL 8.0+ optimise mieux les fonctions déterministes. Documentez toujours le but et les paramètres de la fonction.
-- MySQL: a function that returns one value
CREATE FUNCTION calculate_discount(
price DECIMAL(10,2),
customer_tier VARCHAR(20)
) RETURNS DECIMAL(10,2)
DETERMINISTIC
READS SQL DATA
BEGIN
DECLARE discount_rate DECIMAL(5,2);
SET discount_rate = CASE customer_tier
WHEN 'gold' THEN 0.20
WHEN 'silver' THEN 0.10
WHEN 'bronze' THEN 0.05
ELSE 0.00
END;
RETURN price * (1 - discount_rate);
END //
-- Usage in queries:
SELECT name, price, calculate_discount(price, tier) AS final_price
FROM orders;
-- DETERMINISTIC: same inputs always give same output (cacheable)
-- READS SQL DATA: function reads but doesn't modify tablesFonctions de table (renvoient des lignes)
Les fonctions de table (TVFs) renvoient un ensemble de résultats (lignes) que vous pouvez interroger comme une table. Les TVFs en ligne (SQL Server) sont aussi rapides que des vues — l'optimiseur de requêtes les inline. Les TVFs multi-instructions matérialisent les résultats dans une table temporaire d'abord, ce qui peut être plus lent. Les fonctions PostgreSQL renvoyant TABLE ou SETOF sont équivalentes. Les TVFs sont des vues paramétrées — utilisez-les quand vous avez besoin d'une vue avec paramètres. Elles sont géniales pour encapsuler des JOINs et filtres complexes. Préférez les TVFs en ligne aux TVFs multi-instructions pour la performance. Dans PostgreSQL, envisagez aussi d'utiliser des vues paramétrées avec des clauses WHERE.
-- SQL Server: Inline table-valued function (fast, like a view)
CREATE FUNCTION fn_OrdersByCustomer(@cust_id INT)
RETURNS TABLE
AS
RETURN (
SELECT o.id, o.order_date, o.total
FROM orders o
WHERE o.customer_id = @cust_id
);
-- Usage: SELECT * FROM fn_OrdersByCustomer(42);
-- PostgreSQL: function returning a table
CREATE OR REPLACE FUNCTION get_orders_by_customer(cust_id INT)
RETURNS TABLE(order_id INT, order_date DATE, total DECIMAL) AS $$
SELECT id, order_date, total
FROM orders
WHERE customer_id = cust_id;
$$ LANGUAGE SQL;
-- Usage:
SELECT * FROM get_orders_by_customer(42);
-- Multi-statement TVF (SQL Server) — slower, materializes result
-- CREATE FUNCTION fn_ComplexReport(@date DATE)
-- RETURNS @result TABLE (...)
-- AS BEGIN ... INSERT INTO @result ... RETURN ENDFonctions de manipulation de chaînes
Les fonctions de chaîne personnalisées encapsulent de la logique de traitement de texte que les fonctions intégrées ne couvrent pas. La fonction get_first_name utilise LOCATE et SUBSTRING pour extraire le premier mot. La fonction make_slug chaîne LOWER, REPLACE et REGEXP_REPLACE pour créer des slugs adaptés aux URL. Marquez-les DETERMINISTIC puisque la même entrée produit toujours la même sortie. Les fonctions de chaîne en SQL sont spécifiques à la base — PostgreSQL a split_part(), MySQL a SUBSTRING_INDEX(). Créer des UDFs standardise le comportement à travers votre application. Soyez conscient que la manipulation de chaînes complexe en SQL est souvent plus propre dans le code applicatif.
-- Create a function to split full name into parts
CREATE FUNCTION get_first_name(full_name VARCHAR(200))
RETURNS VARCHAR(100)
DETERMINISTIC
BEGIN
DECLARE space_pos INT;
SET space_pos = LOCATE(' ', full_name);
IF space_pos > 0 THEN
RETURN SUBSTRING(full_name, 1, space_pos - 1);
ELSE
RETURN full_name;
END IF;
END //
-- Function to generate slug from a title
CREATE FUNCTION make_slug(title VARCHAR(500))
RETURNS VARCHAR(500)
DETERMINISTIC
BEGIN
DECLARE slug VARCHAR(500);
SET slug = LOWER(title);
SET slug = REPLACE(slug, ' ', '-');
SET slug = REGEXP_REPLACE(slug, '[^a-z0-9-]', '');
RETURN slug;
END //
-- Usage:
SELECT get_first_name('John Doe Smith') AS first_name; -- John
SELECT make_slug('Hello World! 2024') AS slug; -- hello-world-2024Fonctions d'agrégat (personnalisées)
Les fonctions d'agrégat personnalisées vous permettent de définir une nouvelle logique d'agrégation au-delà de SUM, AVG, COUNT. Le CREATE AGGREGATE de PostgreSQL nécessite une fonction de transition d'état (SFUNC, appelée par ligne) et une fonction finale (FINALFUNC, appelée une fois à la fin). Cet exemple calcule la moyenne géométrique (la racine n-ième du produit). Les agrégats personnalisés sont puissants pour les calculs statistiques, financiers ou spécifiques au domaine. L'état s'accumule à travers les lignes ; la fonction finale calcule le résultat. MySQL et SQL Server ne supportent pas les agrégats personnalisés directement — utilisez des procédures stockées ou du calcul côté application à la place.
-- PostgreSQL: custom aggregate function
-- Step 1: Define a state transition function
CREATE OR REPLACE FUNCTION geom_mean_state(state numeric[], val numeric)
RETURNS numeric[] AS $$
BEGIN
IF val IS NULL THEN RETURN state; END IF;
IF state IS NULL THEN
RETURN ARRAY[val];
ELSE
RETURN state || val;
END IF;
END;
$$ LANGUAGE plpgsql;
-- Step 2: Define the final function
CREATE OR REPLACE FUNCTION geom_mean_final(vals numeric[])
RETURNS numeric AS $$
BEGIN
IF vals IS NULL OR array_length(vals, 1) IS NULL THEN
RETURN NULL;
END IF;
RETURN exp(avg(ln(v)) FROM unnest(vals) AS v);
END;
$$ LANGUAGE plpgsql;
-- Step 3: Create the aggregate
CREATE AGGREGATE geometric_mean(numeric) (
SFUNC = geom_mean_state,
STYPE = numeric[],
FINALFUNC = geom_mean_final,
INITCOND = '{}'
);
-- Usage:
SELECT category, geometric_mean(price) FROM products GROUP BY category;Fonction vs procédure stockée
Les fonctions et procédures stockées servent des buts différents. Les fonctions renvoient une valeur et peuvent être intégrées dans SELECT/WHERE — elles doivent être déterministes-ish (pas d'effets de bord dans la plupart des bases). Les procédures stockées peuvent modifier des données, gérer des transactions et renvoyer de multiples ensembles de résultats — mais ne peuvent pas être utilisées dans des requêtes (appelez avec CALL/EXEC). Utilisez les fonctions pour les calculs et la récupération de données ; utilisez les procédures pour les opérations multi-étapes (transferts, traitement batch, ETL). Les fonctions sont composables ; les procédures sont impératives. Dans PostgreSQL, les fonctions peuvent faire presque tout ce que les procédures peuvent (y compris la modification de données), brouillant la distinction.
-- FUNCTIONS: return values, usable in queries, no side effects
CREATE FUNCTION get_full_name(fname VARCHAR, lname VARCHAR)
RETURNS VARCHAR(200)
DETERMINISTIC
RETURN CONCAT(fname, ' ', lname);
-- Can be used in SELECT:
SELECT get_full_name(first, last) FROM users; -- OK
SELECT * FROM users WHERE get_full_name(first, last) LIKE 'J%'; -- OK
-- STORED PROCEDURES: can modify data, use transactions, return result sets
CREATE PROCEDURE transfer_funds(
IN from_acct INT, IN to_acct INT, IN amount DECIMAL(10,2)
)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
START TRANSACTION;
UPDATE accounts SET balance = balance - amount WHERE id = from_acct;
UPDATE accounts SET balance = balance + amount WHERE id = to_acct;
INSERT INTO transfers (from_id, to_id, amount) VALUES (from_acct, to_acct, amount);
COMMIT;
END //
-- Call procedure (can't use in SELECT):
CALL transfer_funds(1, 2, 100.00);Conception de base de données & normalisation
Première forme normale (1NF)
La première forme normale exige des valeurs atomiques — chaque cellule contient une donnée, pas des listes ou tableaux. Des valeurs séparées par des virgules dans une colonne violent la 1NF car vous ne pouvez pas interroger, indexer ou mettre à jour les éléments individuels. La solution : créez une ligne par élément (avec une clé primaire composite) ou séparez dans une table de détail. La 1NF exige aussi une clé primaire pour identifier de manière unique chaque ligne. Violer la 1NF fait que des requêtes comme « trouver toutes les commandes contenant une souris » nécessitent du parsing de chaînes — lent et sujet aux erreurs. Commencez toujours par la conformité 1NF.
-- 1NF: each column contains atomic (indivisible) values
-- no repeating groups, each row is unique
-- VIOLATION: comma-separated values in one column
CREATE TABLE bad_orders (
id INT,
customer_name VARCHAR(100),
products VARCHAR(500) -- "laptop, mouse, keyboard" — BAD!
);
-- 1NF COMPLIANT: separate row per product
CREATE TABLE orders (
order_id INT,
customer_name VARCHAR(100),
product_name VARCHAR(100), -- one product per row
PRIMARY KEY (order_id, product_name) -- composite key for uniqueness
);
-- Better: normalize further with separate tables
CREATE TABLE orders (order_id INT PRIMARY KEY, customer_name VARCHAR(100));
CREATE TABLE order_items (
order_id INT REFERENCES orders(order_id),
product_name VARCHAR(100),
quantity INT,
PRIMARY KEY (order_id, product_name)
);Deuxième & troisième forme normale (2NF, 3NF)
La 2NF élimine les dépendances partielles — chaque colonne non-clé doit dépendre de la clé primaire ENTIÈRE, pas juste d'une partie. Cela compte seulement avec des clés composites. La 3NF élimine les dépendances transitives — les colonnes non-clé doivent dépendre seulement de la clé primaire, pas d'autres colonnes non-clé. Par exemple, customer_name dépend de customer_id, qui dépend de order_id (transitive). La normalisation réduit la redondance de données (stocker chaque fait une fois) et les anomalies (mettre à jour le nom du client à un endroit, pas dans chaque commande). La plupart des bases pratiques visent 3NF ou BCNF.
-- 2NF: 1NF + no partial dependencies (non-key attrs depend on FULL key)
-- Problem: order_items has composite key (order_id, product_id)
-- but product_name depends only on product_id (partial dependency)
-- VIOLATION of 2NF:
CREATE TABLE bad_order_items (
order_id INT,
product_id INT,
product_name VARCHAR(100), -- depends on product_id only!
quantity INT,
PRIMARY KEY (order_id, product_id)
);
-- 2NF COMPLIANT: move product_name to products table
CREATE TABLE products (
product_id INT PRIMARY KEY,
product_name VARCHAR(100)
);
CREATE TABLE order_items (
order_id INT,
product_id INT REFERENCES products(product_id),
quantity INT,
PRIMARY KEY (order_id, product_id)
);
-- 3NF: 2NF + no transitive dependencies
-- (non-key attrs don't depend on other non-key attrs)
-- Problem: orders table has customer_name that depends on customer_id
-- Solution: separate customers table (see above)Dénormalisation (quand casser les règles)
La dénormalisation viole intentionnellement les formes normales pour améliorer la performance de lecture au coût de la complexité d'écriture et du stockage. Dans les bases normalisées, récupérer une commande complète nécessite 4 JOINs — coûteux pour les tableaux de bord à fort trafic. Les tables dénormalisées pré-jointent et pré-calculent les données pour des lectures rapides. Le compromis : les écritures doivent mettre à jour de multiples endroits (risque d'incohérence) et le stockage augmente. Utilisez la dénormalisation pour les systèmes à forte lecture (analytique, reporting, entrepôts de données). Les vues matérialisées fournissent une dénormalisation gérée — la base gère le rafraîchissement. Les systèmes OLTP devraient rester normalisés ; les systèmes OLAP sont typiquement dénormalisés (schémas en étoile/flocon).
-- Denormalization: intentionally adding redundancy for performance
-- Normalized (3NF): requires JOINs to get full order info
SELECT o.order_id, c.name, p.product_name, oi.quantity, p.price
FROM orders o
JOIN customers c ON o.customer_id = c.id
JOIN order_items oi ON o.order_id = oi.order_id
JOIN products p ON oi.product_id = p.id;
-- Denormalized: store redundant data for read speed
CREATE TABLE order_summary (
order_id INT PRIMARY KEY,
customer_id INT,
customer_name VARCHAR(100), -- redundant (also in customers)
customer_email VARCHAR(200), -- redundant
total_amount DECIMAL(10,2), -- pre-calculated
item_count INT, -- pre-calculated
order_date TIMESTAMP
);
-- Trade-off: faster reads, slower writes, risk of inconsistency
-- Use for: reporting tables, read-heavy dashboards, data warehouses
-- Materialized views are a managed form of denormalization:
CREATE MATERIALIZED VIEW sales_summary AS
SELECT date, region, SUM(amount) AS total FROM sales GROUP BY date, region;
REFRESH MATERIALIZED VIEW sales_summary;Clés primaires, clés étrangères & contraintes
Les contraintes imposent l'intégrité des données au niveau de la base. PRIMARY KEY identifie de manière unique les lignes et crée un index clusterisé. UNIQUE prévient les doublons (autorise de multiples NULL dans la plupart des bases). CHECK impose des règles personnalisées (salary > 0). FOREIGN KEY maintient l'intégrité référentielle — ON DELETE SET NULL/CASCADE/RESTRICT contrôle ce qui arrive quand une ligne parent est supprimée. ON UPDATE CASCADE propage les changements de PK aux FK. Les contraintes sont la dernière ligne de défense contre les mauvaises données — même si le code applicatif a des bugs, la base rejette les données invalides. Définissez toujours des contraintes ; elles sont documentation et imposition combinées.
CREATE TABLE departments (
dept_id INT PRIMARY KEY AUTO_INCREMENT,
dept_name VARCHAR(100) NOT NULL UNIQUE,
budget DECIMAL(12,2) CHECK (budget >= 0),
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
CREATE TABLE employees (
emp_id INT PRIMARY KEY AUTO_INCREMENT,
emp_name VARCHAR(100) NOT NULL,
email VARCHAR(200) UNIQUE,
dept_id INT,
salary DECIMAL(10,2) CHECK (salary > 0 AND salary < 1000000),
hire_date DATE NOT NULL,
manager_id INT REFERENCES employees(emp_id), -- self-reference
FOREIGN KEY (dept_id) REFERENCES departments(dept_id)
ON DELETE SET NULL -- don't delete dept if employees exist
ON UPDATE CASCADE, -- update FK if dept_id changes
INDEX idx_dept (dept_id),
INDEX idx_email (email)
);
-- Constraint types:
-- PRIMARY KEY: unique + not null (clustered index)
-- UNIQUE: no duplicates (allows NULLs)
-- NOT NULL: required field
-- CHECK: custom condition
-- FOREIGN KEY: referential integrity
-- DEFAULT: value when not specifiedStratégie d'indexation
Les index accélèrent considérablement les lectures mais ralentissent les écritures (chaque index doit être mis à jour sur INSERT/UPDATE/DELETE). Les index B-tree supportent l'égalité, la plage et le tri. Les index composites suivent la règle du préfixe le plus à gauche — vous pouvez utiliser (a, b) pour les requêtes sur a ou a+b, mais pas b seul. Les index de couverture (clause INCLUDE) stockent des colonnes supplémentaires pour que la requête ne touche jamais la table — extrêmement rapide. Les index partiels n'indexent qu'un sous-ensemble de lignes, économisant de l'espace. Surveillez l'utilisation des index (pg_stat_user_indexes dans PostgreSQL) et supprimez ceux inutilisés. Une bonne règle : indexez les clés étrangères et les colonnes dans les clauses WHERE/JOIN. La sur-indexation nuit à la performance d'écriture et gaspille du stockage.
-- B-tree index (default): good for =, <, >, BETWEEN, ORDER BY
CREATE INDEX idx_last_name ON employees(last_name);
-- Composite index: order matters! (leftmost prefix rule)
CREATE INDEX idx_dept_salary ON employees(dept_id, salary);
-- Usable for: WHERE dept_id = 5
-- Usable for: WHERE dept_id = 5 AND salary > 50000
-- NOT usable for: WHERE salary > 50000 (skips dept_id)
-- Covering index: includes all columns a query needs
CREATE INDEX idx_covering ON orders(customer_id, order_date)
INCLUDE (total_amount, status);
-- Query can be satisfied from index alone (no table lookup)
-- Partial/partial index: index only matching rows
CREATE INDEX idx_active_users ON users(last_login)
WHERE active = true; -- PostgreSQL
-- Don't over-index: every index slows writes
-- Index columns used in: WHERE, JOIN, ORDER BY, GROUP BY
-- Drop unused indexes: SELECT * FROM pg_stat_user_indexes;EXPLAIN & optimisation de requêtes
Lire la sortie EXPLAIN
EXPLAIN révèle comment la base exécute une requête — quels index sont utilisés, comment les tables sont jointes et combien de lignes sont examinées. EXPLAIN ANALYZE (PostgreSQL) ou EXPLAIN avec exécution (MySQL 8.0+) exécute réellement la requête et montre les timings réels. Cherchez : Seq Scan / ALL (scan complet de table — mauvais pour les grandes tables), Index Scan (bon), estimation de lignes (élevée = coûteux). 'Using filesort' ou 'Using temporary' dans MySQL indique du travail supplémentaire. Si EXPLAIN montre un scan complet de table sur une grande table, vous avez besoin d'un index. EXPLAINz toujours avant d'optimiser — ne devinez pas.
-- EXPLAIN shows the query execution plan
EXPLAIN SELECT * FROM orders WHERE customer_id = 42;
-- EXPLAIN ANALYZE actually runs the query (PostgreSQL)
EXPLAIN ANALYZE SELECT * FROM orders WHERE customer_id = 42;
-- PostgreSQL output:
-- Index Scan using idx_customer on orders (cost=0.29..8.31 rows=1 width=74)
-- Index Cond: (customer_id = 42)
-- Execution Time: 0.042 ms
-- Key columns to check:
-- - type: scan method (const > eq_ref > ref > range > index > ALL)
-- - rows: estimated rows examined (lower is better)
-- - key: which index is used (NULL = no index, bad!)
-- - Extra: "Using filesort" or "Using temporary" = warning signs
-- MySQL: EXPLAIN FORMAT=JSON for detailed output
-- SQL Server: SET SHOWPLAN_TEXT ON; or Actual Execution Plan in SSMSProblèmes de performance courants
Plusieurs motifs courants empêchent l'utilisation d'index et causent des scans complets de table. Les fonctions sur les colonnes indexées (YEAR(date), UPPER(name)) empêchent l'utilisation d'index — réécrivez en conditions de plage. Les wildcards en tête dans LIKE ('%pattern') ne peuvent pas utiliser d'index B-tree — utilisez la recherche en texte intégral à la place. SELECT * gaspille de la bande passante et empêche l'optimisation par index de couverture. Les conversions de type implicites (comparer une colonne chaîne à un entier) peuvent désactiver les index. Les conditions OR sont parfois moins efficaces que IN. Vérifiez toujours avec EXPLAIN que vos index sont réellement utilisés — un index inutilisé est du stockage gaspillé et du surcoût d'écriture.
-- 1. Missing index → full table scan
-- Bad: SELECT * FROM orders WHERE customer_id = 42; (no index)
-- Fix: CREATE INDEX idx_customer ON orders(customer_id);
-- 2. Index not used due to function on column
-- Bad: WHERE YEAR(order_date) = 2024 (function prevents index use)
-- Good: WHERE order_date >= '2024-01-01' AND order_date < '2025-01-01'
-- 3. SELECT * instead of specific columns
-- Bad: SELECT * FROM large_table WHERE id = 1;
-- Good: SELECT id, name, email FROM large_table WHERE id = 1;
-- 4. OR conditions preventing index use
-- Bad: WHERE dept = 'A' OR dept = 'B' OR dept = 'C'
-- Good: WHERE dept IN ('A', 'B', 'C')
-- 5. LIKE with leading wildcard
-- Bad: WHERE name LIKE '%son' (can't use index)
-- OK: WHERE name LIKE 'John%' (can use index)
-- 6. Implicit type conversion
-- Bad: WHERE string_column = 123 (converts to string, skips index)
-- Good: WHERE string_column = '123'Optimisation des JOINs
L'optimisation des JOINs est critique pour les requêtes multi-tables. Assurez-vous que les colonnes de jointure (généralement les clés étrangères) sont indexées — les jointures non indexées causent des scans de boucles imbriquées (O(n*m)). L'optimiseur de requêtes choisit généralement le meilleur ordre de jointure, mais vous pouvez aider en filtrant tôt (WHERE avant JOIN conceptuellement). INNER JOIN est plus rapide que OUTER JOIN quand vous n'avez pas besoin des lignes non correspondantes. EXISTS est souvent plus efficace que IN pour les sous-requêtes corrélées car il court-circuite à la première correspondance. Évitez de joindre des tables dont vous n'avez pas besoin — chaque jointure multiplie le travail. Pour les rapports complexes, envisagez les vues matérialisées ou les tables de résumé pré-agrégées.
-- Join order matters: smallest table first (optimizer usually handles this)
-- Ensure join columns are indexed (usually foreign keys)
CREATE INDEX idx_orders_customer ON orders(customer_id);
CREATE INDEX idx_order_items_order ON order_items(order_id);
-- Use INNER JOIN when you don't need unmatched rows (faster than OUTER)
SELECT c.name, COUNT(o.order_id) AS order_count
FROM customers c
INNER JOIN orders o ON c.id = o.customer_id
GROUP BY c.name;
-- Avoid joining unnecessary tables — fetch details lazily if needed
-- Bad: join 5 tables when you only need 2 columns
-- Good: split into simpler queries or use a covering index
-- EXISTS vs IN for subqueries
-- EXISTS is often faster for large subquery results:
SELECT name FROM customers c
WHERE EXISTS (
SELECT 1 FROM orders o WHERE o.customer_id = c.id
);
-- IN is better for small lists:
SELECT * FROM orders WHERE customer_id IN (1, 2, 3);Optimisation de la pagination
La pagination basée sur OFFSET (LIMIT 10 OFFSET 10000) est O(n) — la base doit scanner et écarter toutes les lignes sautées, rendant les pages profondes extrêmement lentes. La pagination par keyset (curseur) utilise WHERE last_value < cursor pour chercher directement — O(1) quelle que soit la profondeur de page. Cela nécessite un index sur la colonne de tri. Pour les égalités (même timestamp), utilisez un curseur composite (created_at, id). Évitez COUNT(*) pour les comptes totaux sur les grandes tables — il scanne toute la table. Utilisez des comptes approximatifs (pg_class.reltuples dans PostgreSQL) ou n'affichez pas les comptes totaux (défilement infini). La pagination par keyset est le standard pour les API haute performance.
-- Bad: OFFSET pagination (slow for large offsets)
SELECT * FROM orders ORDER BY created_at DESC LIMIT 10 OFFSET 10000;
-- Must scan and discard 10000 rows — gets slower as you page deeper
-- Good: Keyset (cursor) pagination using WHERE
SELECT * FROM orders
WHERE created_at < '2024-01-15 10:30:00' -- last seen value
ORDER BY created_at DESC
LIMIT 10;
-- Uses index efficiently — constant time regardless of page depth
-- For composite keyset pagination (handles ties):
SELECT * FROM orders
WHERE (created_at, id) < ('2024-01-15 10:30:00', 12345)
ORDER BY created_at DESC, id DESC
LIMIT 10;
-- Count total (expensive on large tables — avoid if possible)
-- Instead of COUNT(*), use an approximate count:
SELECT reltuples::bigint FROM pg_class WHERE relname = 'orders';Réécriture de requêtes & checklist d'optimisation
L'optimisation de requêtes est un processus itératif : EXPLAIN, identifier les goulots d'étranglement, réécrire, répéter. Techniques clés : remplacez les sous-requêtes IN par des JOINs (souvent plus rapides), utilisez UNION ALL au lieu de UNION (saute le tri de déduplication), batchez les INSERTs (1 requête vs 1000), et utilisez des prepared statements (met en cache le plan de requête). Les CTE améliorent la lisibilité mais dans les anciennes versions de PostgreSQL ils sont matérialisés (ne peuvent pas être optimisés) — PostgreSQL 12+ les inline. Gardez les statistiques de table à jour (ANALYZE) pour que le planificateur prenne de bonnes décisions. La règle d'or : mesurez avec EXPLAIN ANALYZE, ne devinez pas. Ce qui est rapide sur une base/version peut être lent sur une autre.
-- 1. Replace subqueries with JOINs when possible
-- Slow: SELECT * FROM orders WHERE customer_id IN
-- (SELECT id FROM customers WHERE active = true);
-- Fast: SELECT o.* FROM orders o
-- JOIN customers c ON o.customer_id = c.id WHERE c.active = true;
-- 2. Use UNION ALL instead of UNION (avoids dedup sort)
SELECT 'A' UNION ALL SELECT 'B'; -- fast
SELECT 'A' UNION SELECT 'B'; -- sorts to remove duplicates
-- 3. Batch operations instead of row-by-row
-- Slow: 1000 individual INSERTs
-- Fast: INSERT INTO t VALUES (1,'a'), (2,'b'), (3,'c'), ...;
-- 4. Use CTEs for readability, but know they may not optimize well
-- (PostgreSQL 12+ inlines CTEs; older versions materialize them)
-- 5. Avoid SELECT DISTINCT when you can use GROUP BY or EXISTS
-- 6. Use prepared statements for repeated queries (plan caching)
PREPARE get_user AS SELECT * FROM users WHERE id = $1;
EXECUTE get_user(42);
-- 7. Analyze tables for up-to-date statistics
ANALYZE orders; -- PostgreSQL: updates planner statisticsComparaison NoSQL vs SQL
SQL vs NoSQL : quand utiliser quoi
Le choix SQL vs NoSQL dépend de votre modèle de données, exigences de cohérence et échelle. Les bases SQL imposent le schéma, supportent les transactions ACID et excellent dans les requêtes complexes avec JOINs — idéales pour les systèmes financiers et toute application où l'intégrité des données est primordiale. Les bases NoSQL échangent la cohérence contre la scalabilité et la flexibilité : magasins de documents (MongoDB) pour les schémas évolutifs, magasins clé-valeur (Redis) pour la mise en cache, colonnes (Cassandra) pour le débit d'écriture massif, et bases de graphes (Neo4j) pour les données riches en relations. Les bases SQL modernes supportent maintenant JSON, la recherche en texte intégral et le scaling, réduisant le besoin de NoSQL dans de nombreux cas.
-- SQL (Relational): structured, consistent, queryable
-- Best for: financial systems, e-commerce, CRM, any ACID-requiring app
-- Examples: PostgreSQL, MySQL, SQL Server, Oracle
-- NoSQL: flexible schema, horizontal scaling, specific data models
-- Document: MongoDB, CouchDB — JSON-like documents, flexible schema
-- Key-Value: Redis, DynamoDB — fast lookups, caching, sessions
-- Column-Family: Cassandra, HBase — wide-column, time-series, write-heavy
-- Graph: Neo4j, ArangoDB — relationships, social networks, recommendations
-- Decision factors:
-- 1. Data structure: fixed → SQL, evolving/varied → NoSQL
-- 2. Consistency: strict ACID → SQL, eventual consistency OK → NoSQL
-- 3. Scale: vertical (bigger server) → SQL, horizontal (more servers) → NoSQL
-- 4. Queries: complex JOINs → SQL, simple lookups → NoSQL
-- 5. Team expertise: SQL is universal, NoSQL varies
-- Many modern databases blur the line:
-- PostgreSQL: JSON columns, full-text search, pub/sub
-- MongoDB: transactions (multi-document ACID since 4.0)Motifs de magasin de documents (style MongoDB)
Les magasins de documents intègrent les données liées dans un seul document plutôt que de normaliser à travers des tables. Cela élimine les JOINs pour les motifs d'accès à forte lecture mais duplique les données (info client dans chaque commande). L'intégration fonctionne quand les données sont accédées ensemble et ont une taille bornée. Pour les relations non bornées (un client avec des milliers de commandes), utilisez le référencement (stockez customer_id, récupérez séparément). Les colonnes JSONB de PostgreSQL vous donnent la flexibilité d'un magasin de documents dans une base relationnelle — vous obtenez les transactions ACID, l'indexation (GIN) et les requêtes SQL sur JSON. Cette approche hybride est de plus en plus populaire, réduisant le besoin d'une base NoSQL séparée.
-- In SQL, you'd normalize this into 3 tables:
-- customers, orders, order_items
-- In a document store (MongoDB), you might embed everything:
-- (pseudo-code, not SQL)
// db.orders.insertOne({
// _id: 1,
// customer: { name: "Alice", email: "[email protected]" },
// items: [
// { product: "Laptop", price: 999, qty: 1 },
// { product: "Mouse", price: 25, qty: 2 }
// ],
// total: 1049,
// status: "shipped",
// created_at: ISODate("2024-01-15")
// })
-- SQL equivalent with JSON column (PostgreSQL):
CREATE TABLE orders (
id SERIAL PRIMARY KEY,
data JSONB NOT NULL -- stores the entire document
);
INSERT INTO orders (data) VALUES ('{
"customer": {"name": "Alice", "email": "[email protected]"},
"items": [{"product": "Laptop", "price": 999, "qty": 1}],
"total": 999
}');
-- Query JSON data:
SELECT data->'customer'->>'name' AS name FROM orders
WHERE data->'total' > 500;Motifs de magasin clé-valeur (style Redis)
Les magasins clé-valeur comme Redis excellent dans les lookups ultra-rapides (sub-milliseconde) car les données vivent en mémoire. Cas d'usage courants : mise en cache de résultats de requêtes coûteux, stockage de sessions (avec expiration TTL), compteurs en temps réel (INCR atomique), et classements (sorted sets). Les structures de données Redis (listes, sets, sorted sets, hashes) vont au-delà du simple clé-valeur. Le compromis : les données sont en mémoire (limitées par la RAM) et la persistance est optionnelle. Utilisez Redis comme couche de cache devant SQL — motifs write-through ou cache-aside. Pour les données de session, l'expiration automatique de Redis (TTL) est idéale. Les bases SQL peuvent émuler la mise en cache avec une table de cache, mais ne peuvent pas égaler la vitesse de Redis pour les données chaudes.
-- Redis is an in-memory key-value store — not SQL
-- Common patterns (Redis commands, not SQL):
-- Caching: store expensive query results
// SET user:42:profile '{"name":"Alice","age":30}' EX 3600
// GET user:42:profile -- returns cached data, expires in 1 hour
-- Session storage: fast, ephemeral
// SET session:abc123 '{"user_id":42}' EX 1800 -- 30 min TTL
-- Counters & leaderboards:
// INCR page:home:views -- atomic counter
// ZADD leaderboard 1500 "alice" -- sorted set for rankings
// ZREVRANGE leaderboard 0 9 -- top 10 players
-- SQL equivalent for caching (materialized/precomputed):
CREATE TABLE cache (
cache_key VARCHAR(200) PRIMARY KEY,
cache_value TEXT,
expires_at TIMESTAMP,
INDEX idx_expires (expires_at)
);
-- Periodically: DELETE FROM cache WHERE expires_at < NOW();
-- When to use Redis vs SQL cache:
-- Redis: sub-millisecond reads, data structures (sets, sorted sets)
-- SQL: when you need ACID or already have a database connectionPersistance polyglotte (mixer les bases)
La persistance polyglotte utilise différentes bases pour différents besoins de données au sein d'une application. PostgreSQL gère les transactions, Redis gère le cache, Elasticsearch gère la recherche, S3 gère les fichiers. Le défi est de garder les données cohérentes à travers les magasins — la solution est l'architecture pilotée par événements : écrivez dans la base primaire (source de vérité), puis propagez asynchronement les changements vers les autres magasins via Change Data Capture (CDC) ou des files de messages (Kafka, RabbitMQ). Cela donne la cohérence à terme — les lectures depuis les magasins secondaires peuvent légèrement retarder. Le bénéfice : chaque magasin est optimisé pour sa charge de travail. Le coût : complexité opérationnelle. Commencez avec une seule base SQL ; ajoutez des magasins spécialisés seulement quand vous atteignez des limites de performance claires.
-- Modern applications often use MULTIPLE database types:
-- Each data store handles what it does best
-- Typical architecture:
-- 1. PostgreSQL: core transactional data (users, orders, payments)
-- ACID guarantees, complex queries, foreign keys
CREATE TABLE users (id SERIAL PRIMARY KEY, email VARCHAR UNIQUE);
CREATE TABLE orders (id SERIAL PRIMARY KEY, user_id INT REFERENCES users(id));
-- 2. Redis: session storage, caching, rate limiting
-- Fast in-memory access, auto-expiring keys
-- 3. Elasticsearch: full-text search, log analytics
-- Inverted index, faceted search, aggregations
-- 4. S3/Object storage: files, images, backups
-- Cheap, unlimited, HTTP-accessible
-- 5. TimescaleDB/InfluxDB: time-series metrics
-- Optimized for timestamped data, downsampling
-- Challenge: data consistency across stores
-- Solution: event-driven architecture (CDC, message queues)
-- 1. Write to PostgreSQL (source of truth)
-- 2. Publish event to Kafka
-- 3. Consumers update Redis cache, Elasticsearch index, etc.
-- 4. eventual consistency — reads may be slightly staleModèles de cohérence ACID vs BASE
ACID (Atomicité, Cohérence, Isolation, Durabilité) garantit une cohérence stricte — les transactions sont tout-ou-rien, et les données satisfont toujours les contraintes. C'est essentiel pour les systèmes financiers où les mises à jour partielles causeraient des erreurs. BASE (Basically Available, Soft state, Eventually consistent) échange la cohérence immédiate contre la disponibilité et la tolérance aux partitions — les données peuvent être temporairement incohérentes mais convergent dans le temps. Le théorème CAP stipule que vous ne pouvez pas avoir les trois (Cohérence, Disponibilité, Tolérance aux partitions) simultanément pendant les partitions réseau. Les bases SQL privilégient C+A (mono-nœud) ou C+P (distribué). Beaucoup de bases NoSQL privilégient A+P (Cassandra, DynamoDB). Choisissez ACID quand la correction est critique ; BASE quand la disponibilité et l'échelle comptent plus.
-- ACID (SQL databases):
-- Atomicity: all operations in a transaction succeed or fail together
-- Consistency: data always satisfies constraints (FK, CHECK, etc.)
-- Isolation: concurrent transactions don't interfere
-- Durability: committed data survives crashes
-- SQL transaction example:
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
-- If either fails, ROLLBACK undoes both
COMMIT; -- both updates are permanent
-- BASE (NoSQL databases):
-- Basically Available: system responds (may be stale)
-- Soft state: state changes without input (eventual consistency)
-- Eventually consistent: data converges over time
-- NoSQL trade-off: higher availability & partition tolerance
-- at the cost of immediate consistency
-- CAP Theorem: in a network partition, choose:
-- - CP (consistency): refuse writes (some NoSQL: HBase, MongoDB)
-- - AP (availability): accept writes, reconcile later (Cassandra, DynamoDB)
-- SQL databases are typically CA (consistent + available, no partition tolerance
-- in single-node setups; distributed SQL like CockroachDB adds P)CTE & CTE récursif
CTE de base
Un CTE (Common Table Expression) est un ensemble de résultats temporaire nommé. Améliore la lisibilité en découpant les requêtes complexes. De multiples CTE peuvent être chaînés avec des virgules. Les CTE ne sont valables que pour l'instruction unique.
WITH high_earners AS (
SELECT * FROM employees WHERE salary > 80000
), by_dept AS (
SELECT department, COUNT(*) AS cnt FROM high_earners GROUP BY department
)
SELECT * FROM by_dept ORDER BY cnt DESC;CTE récursif
Les CTE récursifs se référencent eux-mêmes. L'ancre est le cas de base. UNION ALL connecte à la partie récursive. Utilisé pour les données hiérarchiques : organigrammes, systèmes de fichiers, parcours de graphe. Doit avoir une condition de terminaison.
WITH RECURSIVE org_chart AS (
-- Anchor: top-level managers
SELECT id, name, manager_id, 1 AS level
FROM employees WHERE manager_id IS NULL
UNION ALL
-- Recursive: subordinates
SELECT e.id, e.name, e.manager_id, oc.level + 1
FROM employees e
JOIN org_chart oc ON e.manager_id = oc.id
)
SELECT * FROM org_chart;Fibonacci avec CTE
Les CTE récursifs peuvent générer des séquences. L'ancre fournit la première valeur. Chaque itération calcule la suivante. La clause WHERE prévient la récursion infinie. Utile pour les séquences mathématiques.
WITH RECURSIVE fib(n, a, b) AS (
SELECT 1, 0, 1
UNION ALL
SELECT n + 1, b, a + b FROM fib WHERE n < 10
)
SELECT n, a FROM fib;
-- Result: 0, 1, 1, 2, 3, 5, 8, 13, 21, 34Parcours d'arbre
Construisez des chemins en concaténant les noms à chaque récursion. Le CAST garantit que la colonne path est assez large. Utile pour les fils d'Ariane, chemins de fichiers et hiérarchies de catégories. ORDER BY path trie hiérarchiquement.
WITH RECURSIVE category_tree AS (
SELECT id, name, parent_id, CAST(name AS VARCHAR(1000)) AS path
FROM categories WHERE parent_id IS NULL
UNION ALL
SELECT c.id, c.name, c.parent_id, ct.path || ' > ' || c.name
FROM categories c JOIN category_tree ct ON c.parent_id = ct.id
)
SELECT id, path FROM category_tree ORDER BY path;CTE vs sous-requête
Les CTE améliorent la lisibilité et peuvent être référencés plusieurs fois. Les sous-requêtes sont en ligne et ne peuvent pas être réutilisées. Les CTE ne sont pas toujours matérialisés ; l'optimiseur peut les inliner. Utilisez les CTE pour la clarté.
-- CTE: more readable, can be referenced multiple times
WITH active_users AS (SELECT * FROM users WHERE active = 1)
SELECT * FROM active_users WHERE age > 18
UNION ALL
SELECT * FROM active_users WHERE age <= 18;
-- Subquery: inline, cannot be reused
SELECT * FROM (SELECT * FROM users WHERE active = 1) au WHERE au.age > 18;Approfondissement des index
Index B-Tree
B-Tree est le type d'index par défaut. Les index composites suivent la règle du préfixe le plus à gauche : une requête peut utiliser l'index si elle filtre sur les colonnes de tête. Ordonnez les colonnes par sélectivité et motifs de requête.
CREATE INDEX idx_email ON users(email);
CREATE INDEX idx_name_age ON users(last_name, first_name, age);
-- Composite index: useful for
-- WHERE last_name = 'Smith' AND first_name = 'John'
-- WHERE last_name = 'Smith' (leftmost prefix)Index partiel
Les index partiels n'incluent que les lignes correspondant à la clause WHERE. Plus petits et plus rapides que les index complets. Idéal pour les requêtes qui filtrent toujours sur une condition. Réduit le surcoût d'écriture.
CREATE INDEX idx_active_users ON users(last_login) WHERE active = 1;
-- Only indexes active users, saving space
-- Useful when queries always filter on active = 1Index de couverture
Un index de couverture inclut toutes les colonnes nécessaires à une requête, permettant des scans index-only. PostgreSQL utilise INCLUDE pour les colonnes non-clé. Accélère considérablement les requêtes SELECT en évitant les lookups de table.
-- PostgreSQL: INCLUDE clause
CREATE INDEX idx_users_covering ON users(last_name) INCLUDE (first_name, email);
-- The query is "covered" if all columns are in the index:
SELECT first_name, email FROM users WHERE last_name = 'Smith';
-- No table lookup needed (index-only scan)Types d'index
Différents types d'index servent différents besoins. B-Tree pour un usage général. Hash pour l'égalité seulement. GIN pour le texte intégral et JSON. GiST pour les données géométriques. Choisissez selon les motifs de requête.
-- B-Tree: default, good for equality and range
CREATE INDEX idx_btree ON users(email);
-- Hash: equality only (PostgreSQL)
CREATE INDEX idx_hash ON users(email) USING HASH;
-- GIN: full-text search, arrays, JSON
CREATE INDEX idx_gin ON docs USING GIN (tsv);
-- GiST: geometric, range types
CREATE INDEX idx_gist ON places USING GIST (location);Maintenance des index
Surveillez l'utilisation des index pour supprimer les index inutilisés qui ralentissent les écritures. REINDEX reconstruit les index fragmentés. ANALYZE met à jour les statistiques pour le planificateur de requêtes. Une maintenance régulière garde la performance optimale.
-- Check index usage (PostgreSQL)
SELECT * FROM pg_stat_user_indexes;
-- Find unused indexes
SELECT relname, indexrelname FROM pg_stat_user_indexes WHERE idx_scan = 0;
-- Rebuild fragmented index
REINDEX INDEX idx_users_email;
-- Analyze for query planner
ANALYZE users;Transactions
Propriétés ACID
ACID : Atomicité (tout ou rien), Cohérence (état valide), Isolation (les transactions concurrentes ne s'interfèrent pas), Durabilité (les données commitées persistent). BEGIN démarre, COMMIT sauvegarde, ROLLBACK annule.
BEGIN TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
-- Or: ROLLBACK to undoSavepoints
Les savepoints créent des points de rollback partiels dans une transaction. ROLLBACK TO annule jusqu'au savepoint sans terminer la transaction. Utile pour gérer les erreurs dans des opérations multi-étapes sans redémarrer.
BEGIN;
INSERT INTO orders VALUES (1);
SAVEPOINT sp1;
INSERT INTO orders VALUES (2);
-- Oops, rollback to savepoint
ROLLBACK TO sp1;
-- Only order 1 is inserted
INSERT INTO orders VALUES (3);
COMMIT;Niveaux d'isolation
Les niveaux d'isolation équilibrent cohérence vs performance. READ COMMITTED (par défaut) prévient les lectures sales. REPEATABLE READ prévient les lectures non reproductibles. SERIALIZABLE prévient les lectures fantômes mais est le plus lent.
SET TRANSACTION ISOLATION LEVEL READ COMMITTED;
-- Levels (increasing isolation):
-- READ UNCOMMITTED: dirty reads
-- READ COMMITTED: no dirty reads (default)
-- REPEATABLE READ: no non-repeatable reads
-- SERIALIZABLE: full isolationDeadlocks
Les deadlocks se produisent quand les transactions détiennent des verrous dont chacune a besoin. Les bases détectent les deadlocks et annulent une transaction. Prévenez en accédant aux tables dans un ordre cohérent. Gardez les transactions courtes.
-- Transaction 1
BEGIN;
UPDATE accounts SET balance = 0 WHERE id = 1;
UPDATE accounts SET balance = 0 WHERE id = 2; -- Waits
-- Transaction 2
BEGIN;
UPDATE accounts SET balance = 0 WHERE id = 2;
UPDATE accounts SET balance = 0 WHERE id = 1; -- Waits
-- Deadlock! Database aborts one transactionVerrouillage optimiste
Le verrouillage optimiste suppose que les conflits sont rares. La colonne version suit les changements. Si l'UPDATE affecte 0 ligne, les données ont été modifiées par une autre transaction. Réessayez ou notifiez l'utilisateur. Évite les longues détentions de verrou.
-- Add version column
ALTER TABLE products ADD COLUMN version INT DEFAULT 0;
-- Update with version check
UPDATE products SET price = 100, version = version + 1
WHERE id = 1 AND version = 5;
-- If 0 rows affected, someone else updated firstJSON en SQL
PostgreSQL JSONB
JSONB stocke le JSON dans un format binaire, permettant l'indexation et des requêtes rapides. ->> extrait comme texte, -> extrait comme JSON. JSONB est préférable à JSON pour le requêtage. Utilisez des index GIN pour les colonnes JSONB.
CREATE TABLE events (id SERIAL, data JSONB);
INSERT INTO events (data) VALUES ('{"user": "alice", "action": "login"}');
SELECT data->>'user' AS user_name FROM events;
SELECT * FROM events WHERE data->>'action' = 'login';Requêtes JSON
-> navigue dans le JSON, ->> renvoie du texte. @> vérifie l'inclusion. jsonb_set met à jour les valeurs imbriquées. jsonb_object_keys renvoie les clés de premier niveau. Ces opérateurs permettent un puissant requêtage JSON.
SELECT data->'address'->'city' AS city FROM users;
SELECT * FROM users WHERE data @> '{"role": "admin"}';
SELECT jsonb_object_keys(data) FROM users;
-- Update JSON
UPDATE users SET data = jsonb_set(data, '{last_login}', '"2024-01-01"');Agrégation JSON
json_agg agrège les lignes en un tableau JSON. json_build_object construit des objets JSON à partir de colonnes. Utile pour générer des réponses d'API directement depuis SQL. Combine données relationnelles et document.
SELECT department,
json_agg(json_build_object('name', name, 'salary', salary)) AS employees
FROM employees
GROUP BY department;
-- Result: {"department": "Eng", "employees": [{"name": "Alice", "salary": 90000}, ...]}MySQL JSON
MySQL utilise la syntaxe $.path pour le JSON. JSON_EXTRACT obtient les valeurs, JSON_SET met à jour. ->> est un raccourci pour JSON_EXTRACT avec résultat texte. Le JSON MySQL est validé à l'insertion.
CREATE TABLE config (id INT, settings JSON);
INSERT INTO config VALUES (1, '{"theme": "dark", "lang": "en"}');
SELECT settings->>'$.theme' FROM config;
SELECT * FROM config WHERE JSON_EXTRACT(settings, '$.lang') = 'en';
-- Update
UPDATE config SET settings = JSON_SET(settings, '$.theme', 'light');Index JSON
Les index GIN sur JSONB permettent un requêtage rapide de n'importe quelle clé. Les index d'expression sur des chemins spécifiques sont plus petits et plus rapides pour les requêtes ciblées. Indexez les chemins JSON fréquemment interrogés pour la performance.
-- PostgreSQL GIN index on JSONB
CREATE INDEX idx_events_data ON events USING GIN (data);
-- Index specific path
CREATE INDEX idx_events_user ON events ((data->>'user'));
-- MySQL functional index
CREATE INDEX idx_theme ON config ((CAST(settings->>'$.theme' AS CHAR(50))));Réglage de la performance
EXPLAIN ANALYZE
EXPLAIN montre le plan de requête ; ANALYZE l'exécute avec chronométrage. Seq Scan indique un index manquant. Index Scan est idéal. Cherchez les nombres de coût élevés et les opérations lentes. EXPLAINz toujours avant d'optimiser.
EXPLAIN ANALYZE
SELECT * FROM users WHERE email = '[email protected]';
-- Shows: scan type, cost, rows, actual time
-- Seq Scan: full table scan (slow)
-- Index Scan: uses index (fast)
-- Bitmap Heap Scan: index + table lookupOptimisation de requêtes
Sélectionnez seulement les colonnes nécessaires pour réduire les I/O. Évitez les fonctions sur les colonnes indexées (non sargable). Les requêtes sargables (Search Argument Able) peuvent utiliser des index. Utilisez des conditions de plage au lieu de fonctions.
-- BAD: SELECT * fetches all columns
SELECT * FROM users;
-- GOOD: select only needed columns
SELECT id, name FROM users;
-- BAD: function on indexed column
WHERE YEAR(created_at) = 2024
-- GOOD: sargable
WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01'Optimisation des JOINs
Indexez toutes les colonnes de jointure. L'optimiseur choisit l'ordre de jointure selon les stats. INNER JOIN est généralement le plus rapide. Évitez de joindre sur des expressions. Pour les gros volumes de données, envisagez la dénormalisation ou les vues matérialisées.
-- Use INNER JOIN for required relationships
SELECT u.name, o.total FROM users u
INNER JOIN orders o ON u.id = o.user_id;
-- Index join columns
CREATE INDEX idx_orders_user_id ON orders(user_id);
-- For large joins, ensure both columns are indexedPagination
La pagination OFFSET est O(n) — elle scanne toutes les lignes sautées. La pagination par keyset (curseur) est O(1) — elle utilise un index. Utilisez une comparaison de tuple pour un tri stable. Bien plus rapide pour la pagination profonde.
-- BAD: OFFSET scans all skipped rows
SELECT * FROM users ORDER BY id LIMIT 10 OFFSET 10000;
-- GOOD: keyset pagination
SELECT * FROM users WHERE id > 10000 ORDER BY id LIMIT 10;
-- Stable pagination with cursor
SELECT * FROM users WHERE (created_at, id) > ('2024-01-01', 100) ORDER BY created_at, id LIMIT 10;Vues matérialisées
Les vues matérialisées stockent physiquement les résultats de requête. Plus rapides que les vues pour les agrégations coûteuses. REFRESH met à jour les données (concurrently avec l'option CONCURRENTLY). Indexez-les pour des requêtes rapides.
CREATE MATERIALIZED VIEW sales_summary AS
SELECT product_id, SUM(quantity) AS total, AVG(price) AS avg_price
FROM sales GROUP BY product_id;
-- Refresh periodically
REFRESH MATERIALIZED VIEW sales_summary;
-- Create index on materialized view
CREATE INDEX ON sales_summary (total);JOINs avancés
Self join
Un self join interroge une table contre elle-même. Utilisez des alias pour distinguer. Courant pour les données hiérarchiques (employé-manager) et trouver des paires. L'astuce a.id < b.id évite les paires dupliquées.
SELECT e.name AS employee, m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;
-- Find pairs in same department
SELECT a.name, b.name FROM employees a, employees b
WHERE a.department = b.department AND a.id < b.id;Cross join
CROSS JOIN produit un produit cartésien : chaque ligne de A combinée avec chaque ligne de B. Utile pour générer des combinaisons. Attention : il peut produire d'énormes ensembles de résultats. Souvent utilisé implicitement avec la syntaxe par virgule.
-- Cartesian product: every combination
SELECT s.size, c.color FROM sizes s CROSS JOIN colors c;
-- Generate all size/color combinations
-- Useful for generating test data or matricesFULL OUTER JOIN
FULL OUTER JOIN renvoie toutes les lignes des deux tables. Des NULL remplissent les côtés non correspondants. Utile pour trouver des enregistrements non correspondants dans les deux directions. Non supporté dans MySQL (émulez avec UNION de LEFT et RIGHT joins).
SELECT u.name, o.order_id
FROM users u
FULL OUTER JOIN orders o ON u.id = o.user_id;
-- Returns all users and all orders
-- NULLs where there is no matchAnti-join
L'anti-join trouve les lignes de A qui ne correspondent pas à B. NOT EXISTS est gén éralement le plus clair et souvent le plus rapide. LEFT JOIN avec IS NULL est une alternative. Utilisez pour trouver les relations manquantes.
-- Users who have never ordered
SELECT u.* FROM users u
WHERE NOT EXISTS (
SELECT 1 FROM orders o WHERE o.user_id = u.id
);
-- Alternative: LEFT JOIN ... WHERE IS NULL
SELECT u.* FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL;Semi-join
Le semi-join renvoie les lignes de A qui correspondent à au moins une ligne de B. EXISTS est efficace car il s'arrête à la première correspondance. IN est équivalent mais peut performer différemment. Utilisez EXISTS pour les sous-requêtes corrélées.
-- Users who have at least one order
SELECT u.* FROM users u
WHERE EXISTS (
SELECT 1 FROM orders o WHERE o.user_id = u.id
);
-- Alternative: IN
SELECT * FROM users WHERE id IN (SELECT user_id FROM orders);Pièges courants
Comparaisons NULL
NULL est inconnu, pas une valeur. = NULL renvoie toujours NULL (traité comme false). Utilisez IS NULL et IS NOT NULL. NULL se propage à travers l'arithmétique. Utilisez COALESCE pour fournir des valeurs par défaut.
-- NULL = NULL is NULL (not true!)
SELECT * FROM users WHERE phone = NULL; -- Returns nothing
SELECT * FROM users WHERE phone IS NULL; -- Correct
SELECT * FROM users WHERE phone IS NOT NULL;
-- NULL in arithmetic yields NULL
SELECT 5 + NULL; -- NULLInjection SQL
L'injection SQL permet aux attaquants d'exécuter du SQL arbitraire. Ne concaténez jamais l'entrée utilisateur dans les requêtes. Utilisez toujours des requêtes paramétrées/prepared statements. Validez et assainissez toutes les entrées. Utilisez la liaison de paramètres ORM.
-- BAD: string concatenation
query = "SELECT * FROM users WHERE name = '" + input + "'"
-- GOOD: parameterized queries
SELECT * FROM users WHERE name = ?;
-- PostgreSQL: $1
-- MySQL: ?
-- Always use parameters, never concatenatePièges GROUP BY
Avec GROUP BY, toutes les colonnes non agrégées dans SELECT doivent être dans GROUP BY. Sinon, le résultat est ambigu. MySQL l'autorise (renvoie une valeur arbitraire) mais c'est incorrect. Suivez toujours le standard.
-- BAD: non-aggregated column not in GROUP BY
SELECT department, name, COUNT(*) FROM employees GROUP BY department;
-- Error: which name to show?
-- GOOD: aggregate or include in GROUP BY
SELECT department, COUNT(*) FROM employees GROUP BY department;
SELECT department, MAX(name) FROM employees GROUP BY department;Virgule flottante
FLOAT et DOUBLE sont des types approximatifs. Utilisez DECIMAL/NUMERIC pour une précision exacte (argent, mesures). DECIMAL(10,2) permet 10 chiffres avec 2 après la virgule. N'utilisez jamais FLOAT pour les données financières.
-- Floating point precision issues
SELECT 0.1 + 0.2; -- 0.30000000000000004
-- Use DECIMAL for money
CREATE TABLE accounts (balance DECIMAL(10, 2));
SELECT 0.10 + 0.20; -- 0.30 (exact)Conversion de type implicite
La conversion de type implicite peut désactiver les index et causer des scans complets de table. Comparez toujours des types correspondants. Si nécessaire, castez explicitement. Vérifiez les types de colonnes et assurez-vous que les paramètres de requête correspondent.
-- BAD: comparing string to number
SELECT * FROM users WHERE phone = 1234567890;
-- May cause full table scan due to type conversion
-- GOOD: compare same types
SELECT * FROM users WHERE phone = '1234567890';
-- Or cast explicitly
SELECT * FROM users WHERE CAST(phone AS BIGINT) = 1234567890;Snippets SQL associés
Copy-paste ready code for common tasks.
SELECT avec WHERE et ORDER BY
Filtrer, trier et limiter des lignes avec SELECT, WHERE et ORDER BY en SQL.
Requêtes JOIN
Requêtes de jointure multi-tables.
Sous-requêtes
Requêtes imbriquées.
Fonctions de fenêtre
Fonctions de fenêtre de classement et d'agrégation.
Fonctions d'agrégation
GROUP BY et HAVING.
CTE
Expressions de table communes.
Requêtes récursives
Interroger des données hiérarchiques avec CTE récursif.
Index
Créer et gérer des index.
Transactions
Contrôle de transaction et niveaux d'isolation.
Procédures stockées
Créer des procédures stockées et des fonctions.
Triggers
Triggers à exécution automatique.
Vues
Créer et gérer des vues.
Vues matérialisées
Vues matérialisées et rafraîchissement.
Tables partitionnées
Stratégies de partitionnement de table.
Sauvegarde et récupération
Sauvegarde, import et export de données.
Optimisation des performances
Analyse et optimisation des performances de requête.
Opérations JSON
Opérations PostgreSQL JSON/JSONB.
Recherche en texte intégral
Recherche en texte intégral PostgreSQL.
Pivot/Unpivot
PIVOT et Crosstab.
Requêtes de date
Opérations de date et d'heure.
Requêtes de pagination
Pagination LIMIT/OFFSET et par curseur.
Was this helpful?