SQL avance et PostgreSQL - 01 - SELECT, WHERE, ORDER BY : les fondamentaux solides

Les bases du SELECT en SQL qui font la différence : WHERE, ORDER BY, LIMIT, OFFSET, DISTINCT, aliases et les pièges courants.

01 - SELECT, WHERE, ORDER BY : les fondamentaux solides

Ce que tu vas apprendre

  • SELECT avec aliases, WHERE avec tous les opérateurs
  • ORDER BY, LIMIT, OFFSET (et pourquoi OFFSET est lent)
  • DISTINCT vs GROUP BY, CASE WHEN, type casting
  • Les pièges classiques avec NULL et LIKE

Prerequisites

Avoir PostgreSQL installe (ou un Docker postgres:16). Savoir ce qu'est une table et une colonne.


La table d'exemple

On va travailler avec une table products tout au long de cet article. Cree-la et inséré les donnees :

sqlCREATE TABLE products (
  id SERIAL PRIMARY KEY,
  name TEXT NOT NULL,
  category TEXT,
  price NUMERIC(10,2) NOT NULL,
  stock INT DEFAULT 0,
  created_at TIMESTAMP DEFAULT NOW()
);

INSERT INTO products (name, category, price, stock, created_at) VALUES
  ('Clavier mecanique', 'peripheriques', 89.99, 45, '2025-01-15'),
  ('Souris gaming', 'peripheriques', 59.99, 120, '2025-02-20'),
  ('Ecran 27 pouces', 'moniteurs', 349.00, 12, '2025-03-10'),
  ('Cable HDMI 2m', NULL, 12.50, 300, '2025-04-01'),
  ('Webcam HD', 'peripheriques', 45.00, 0, '2025-05-12'),
  ('SSD 1To', 'stockage', 79.99, 67, '2025-06-01'),
  ('RAM 16Go DDR5', 'composants', 65.00, 33, '2025-06-15'),
  ('Casque audio', NULL, 129.99, 8, '2025-07-20');

SELECT et aliases

Le AS pour renommer les colonnes dans le résultat :

sqlSELECT name AS produit, price AS prix_ht, price * 1.20 AS prix_ttc
FROM products;
      produit       | prix_ht | prix_ttc
--------------------+---------+----------
 Clavier mecanique  |   89.99 |  107.988
 Souris gaming      |   59.99 |   71.988
 Ecran 27 pouces    |  349.00 |  418.800
 Cable HDMI 2m      |   12.50 |   15.000
 ...

L'alias fonctionne aussi sur les tables (utile pour les JOINs, on verra dans l'article 02) :

sqlSELECT p.name, p.price FROM products p WHERE p.stock > 0;

WHERE : filtrer les lignes

Les opérateurs de base :

sql-- Egalite et inegalite
SELECT * FROM products WHERE category = 'peripheriques';
SELECT * FROM products WHERE price != 89.99;

-- Comparaisons
SELECT * FROM products WHERE price > 50 AND stock < 100;
SELECT * FROM products WHERE price BETWEEN 40 AND 100;
-- BETWEEN est inclusif : >= 40 AND <= 100

-- IN : plusieurs valeurs possibles
SELECT * FROM products WHERE category IN ('peripheriques', 'stockage');

-- LIKE et ILIKE (insensible a la casse, specifique PostgreSQL)
SELECT * FROM products WHERE name LIKE 'Clavier%';    -- commence par "Clavier"
SELECT * FROM products WHERE name ILIKE '%audio%';     -- contient "audio", case-insensitive

-- NULL : attention, syntaxe speciale
SELECT * FROM products WHERE category IS NULL;
SELECT * FROM products WHERE category IS NOT NULL;

Precedence des opérateurs : le piège AND/OR

Ca, c'est un classique. Regarde ces deux requêtes :

sql-- Requete 1 : ce que tu voulais probablement
SELECT * FROM products
WHERE (category = 'peripheriques' OR category = 'stockage')
AND price > 50;

-- Requete 2 : ce que tu as ecrit sans parentheses
SELECT * FROM products
WHERE category = 'peripheriques' OR category = 'stockage'
AND price > 50;

La requête 2 ne fait pas ce que tu crois. AND a une precedence plus haute que OR, donc ca se lit comme : category = 'peripheriques' OR (category = 'stockage' AND price > 50). Tu recuperes tous les périphériques quel que soit le prix, plus les produits stockage au-dessus de 50. Toujours mettre des parentheses quand tu melanges AND et OR.

ORDER BY

sql-- Tri simple
SELECT * FROM products ORDER BY price ASC;   -- du moins cher au plus cher
SELECT * FROM products ORDER BY price DESC;  -- du plus cher au moins cher

-- Tri sur plusieurs colonnes
SELECT * FROM products ORDER BY category ASC, price DESC;

-- NULLS FIRST / NULLS LAST (specifique PostgreSQL)
SELECT * FROM products ORDER BY category NULLS FIRST;
SELECT * FROM products ORDER BY category NULLS LAST;

Par défaut en PostgreSQL, les NULL arrivent en dernier pour ASC et en premier pour DESC. NULLS FIRST/LAST te permet de contrôler ca explicitement. La doc PostgreSQL sur ORDER BY explique bien le comportement.

LIMIT et OFFSET (et pourquoi OFFSET est lent)

sql-- Les 5 premiers produits
SELECT * FROM products ORDER BY created_at DESC LIMIT 5;

-- Pagination : page 2 avec 3 elements par page
SELECT * FROM products ORDER BY created_at DESC LIMIT 3 OFFSET 3;

OFFSET fonctionne, mais il a un problème fondamental : PostgreSQL doit quand meme lire et ignorer les N premières lignes. Sur une table de 1 million de lignes, OFFSET 999000 signifie que PostgreSQL lit 999000 lignes pour les jeter. C'est lineaire et ca devient tres lent.

L'alternative : la pagination par curseur (keyset pagination) :

sql-- Au lieu de OFFSET, utilise la derniere valeur vue
SELECT * FROM products
WHERE created_at < '2025-06-01'
ORDER BY created_at DESC
LIMIT 3;

Cette requête utilise un index sur created_at et saute directement au bon endroit. C'est le pattern utilise par les API de pagination "cursor-based" qu'on retrouve chez Stripe ou GitHub.

DISTINCT vs GROUP BY

sql-- DISTINCT : enleve les doublons
SELECT DISTINCT category FROM products;
-- Resultat : peripheriques, moniteurs, NULL, stockage, composants

-- GROUP BY fait la meme chose ici, mais permet d'ajouter des aggregations
SELECT category, COUNT(*) FROM products GROUP BY category;

La différence : DISTINCT enleve les doublons dans le résultat. GROUP BY regroupe les lignes pour appliquer des fonctions d'aggregation (COUNT, SUM, etc.). On en reparle en profondeur dans l'article 03.

CASE WHEN : la logique conditionnelle

sqlSELECT name, stock,
  CASE
    WHEN stock = 0 THEN 'Rupture'
    WHEN stock < 20 THEN 'Stock faible'
    WHEN stock < 100 THEN 'Disponible'
    ELSE 'Stock important'
  END AS disponibilite
FROM products;
       name        | stock |  disponibilite
-------------------+-------+-----------------
 Clavier mecanique |    45 | Disponible
 Souris gaming     |   120 | Stock important
 Ecran 27 pouces   |    12 | Stock faible
 Cable HDMI 2m     |   300 | Stock important
 Webcam HD         |     0 | Rupture
 SSD 1To           |    67 | Disponible
 RAM 16Go DDR5     |    33 | Disponible
 Casque audio      |     8 | Stock faible

CASE WHEN est aussi utilisable dans un WHERE, un ORDER BY ou meme un UPDATE. C'est l'équivalent d'un if/else en SQL.

Type casting

PostgreSQL est strict sur les types. Pour convertir :

sql-- Syntaxe PostgreSQL (raccourci ::)
SELECT '42'::INT;          -- text vers integer
SELECT 42::TEXT;            -- integer vers text
SELECT NOW()::DATE;         -- timestamp vers date
SELECT '3.14'::NUMERIC;    -- text vers numeric

-- Syntaxe standard SQL (fonctionne aussi)
SELECT CAST('42' AS INT);

Le :: est la syntaxe spécifique a PostgreSQL. Elle est plus courte et c'est celle que tout le monde utilise en pratique.

Les pièges avec NULL

C'est probablement la source de bugs la plus frequente en SQL. NULL n'est pas une valeur, c'est l'absence de valeur. Et ca a des consequences :

sql-- NULL = NULL n'est PAS TRUE
SELECT NULL = NULL;      -- Resultat : NULL (pas TRUE, pas FALSE)
SELECT NULL != NULL;     -- Resultat : NULL
SELECT NULL > 5;         -- Resultat : NULL

-- C'est pour ca qu'on utilise IS NULL / IS NOT NULL
SELECT * FROM products WHERE category IS NULL;     -- OK
SELECT * FROM products WHERE category = NULL;      -- ne retourne RIEN (piege !)

-- COALESCE : valeur par defaut si NULL
SELECT name, COALESCE(category, 'Non classe') AS category FROM products;

Le COALESCE prend le premier argument non-NULL. C'est l'équivalent du ?? en TypeScript. Garde-le en tête, tu en auras besoin dans quasiment chaque requête qui touche a des colonnes nullable.

LIKE avec % au début tue les index

Dernier piège, et pas des moindres :

sql-- Utilise un index (si tu en as un sur name)
SELECT * FROM products WHERE name LIKE 'Clavier%';

-- NE PEUT PAS utiliser un index B-tree
SELECT * FROM products WHERE name LIKE '%gaming%';

Quand le % est au début du pattern, PostgreSQL ne peut pas utiliser un index B-tree classique. Il doit faire un Seq Scan (parcourir toute la table). Sur une table de 10 millions de lignes, la différence entre les deux est énorme. Pour les recherches en plein texte, regarde plutot les index GIN avec pg_trgm ou le full-text search natif de PostgreSQL. On couvre les index dans l'article 05.


Résumé

  • WHERE avec AND/OR : toujours mettre des parentheses pour contrôler la precedence
  • OFFSET est lent sur les grosses tables, préféré la pagination par curseur
  • NULL = NULL retourne NULL, pas TRUE (utilise IS NULL)
  • LIKE '%pattern%' ne peut pas utiliser un index B-tree
  • CASE WHEN et COALESCE sont tes outils pour gerer la logique conditionnelle et les NULL

Article précédent : 00 - Introduction Article suivant : 02 - Les JOINs

Sources

Réservez un audit gratuit de 30 minutes. Je vous montre concrètement ce qu'on peut automatiser.