Mirajv1.0
FR

36. PostgreSQL : Requêtes SELECT

Ce chapitre décrit l'instruction SELECT sur une instance MIRAJ en langage PostgreSQL : liste de sélection, FROM et jointures, WHERE, GROUP BY / HAVING, ORDER BY, LIMIT / OFFSET / FETCH FIRST, sous-requêtes, opérations ensemblistes, expressions de table (WITH), fonctions de fenêtrage, verrous de lecture et EXPLAIN. Il décrit le langage PostgreSQL tel que MIRAJ le parle : tous les exemples ont été exécutés sur une instance réelle et les résultats recopiés ; ce qui diffère de PostgreSQL est signalé au fil du texte et rassemblé en 36.15.

Les exemples s'appuient sur la base (le schéma) ventes, remplie une fois pour tout le chapitre :

CREATE SCHEMA ventes;
SET search_path TO ventes;

CREATE TABLE client (
    id      serial PRIMARY KEY,
    nom     varchar(50) NOT NULL,
    ville   varchar(50),
    email   varchar(80) UNIQUE,
    solde   numeric(10,2) NOT NULL DEFAULT 0,
    cree_le timestamp NOT NULL DEFAULT now()
);

CREATE TABLE article (
    id      serial PRIMARY KEY,
    code    varchar(20) NOT NULL UNIQUE,
    libelle text NOT NULL,
    prix    numeric(10,2) NOT NULL CHECK (prix >= 0),
    stock   integer NOT NULL DEFAULT 0,
    actif   boolean NOT NULL DEFAULT true
);

CREATE TABLE commande (
    id        serial PRIMARY KEY,
    client_id integer NOT NULL REFERENCES client (id),
    date_cmd  date NOT NULL DEFAULT current_date,
    etat      varchar(12) NOT NULL DEFAULT 'ouverte'
);

CREATE TABLE ligne_commande (
    commande_id integer NOT NULL REFERENCES commande (id) ON DELETE CASCADE,
    article_id  integer NOT NULL REFERENCES article (id),
    quantite    integer NOT NULL CHECK (quantite > 0),
    prix_unit   numeric(10,2) NOT NULL,
    total       numeric(12,2) GENERATED ALWAYS AS (quantite * prix_unit) STORED,
    PRIMARY KEY (commande_id, article_id)
);

INSERT INTO client (nom, ville, email, solde) VALUES
    ('Alice', 'Oran', '[email protected]', 100.50),
    ('Bob', 'Alger', '[email protected]', -20.00),
    ('Chloé', 'Oran', NULL, 0.00),
    ('David', 'Constantine', '[email protected]', 250.00),
    ('Émile', 'Alger', NULL, 75.25),
    ('Fatima', NULL, '[email protected]', 10.00);

INSERT INTO article (code, libelle, prix, stock, actif) VALUES
    ('ST-01', 'Stylo bille', 1.20, 500, true),
    ('CA-01', 'Cahier 96 pages', 3.50, 200, true),
    ('CL-01', 'Classeur', 6.90, 80, true),
    ('AG-01', 'Agrafeuse', 14.00, 0, false),
    ('GO-01', 'Gomme', 0.80, 150, true),
    ('TR-01', 'Trombones', 2.10, 40, true);

INSERT INTO commande (client_id, date_cmd, etat) VALUES
    (1, '2026-09-01', 'livrée'),
    (1, '2026-09-15', 'ouverte'),
    (2, '2026-09-20', 'livrée'),
    (3, '2026-10-02', 'ouverte'),
    (5, '2026-10-05', 'annulée');

INSERT INTO ligne_commande (commande_id, article_id, quantite, prix_unit) VALUES
    (1, 1, 10, 1.20), (1, 2, 2, 3.50),
    (2, 3, 1, 6.90),
    (3, 1, 100, 1.10), (3, 5, 20, 0.80),
    (4, 2, 5, 3.50), (4, 4, 1, 14.00);

Les clients 4 (David) et 6 (Fatima) n'ont pas de commande, la commande 5 n'a pas de ligne et l'article TR-01 n'a jamais été commandé : ces cas servent aux jointures externes.

Les résultats sont présentés à la manière de psql (NULL y est écrit en toutes lettres pour la lisibilité). Les fonctions et opérateurs cités sont décrits au chapitre 37. Fonctions SQL, les types au chapitre 33. Types de données.


36.1 Synopsis#

[WITH [RECURSIVE] nom [(colonne, ...)] AS [[NOT] MATERIALIZED] (requête) [, ...]]
SELECT [ALL | DISTINCT] élément [[AS] alias] [, ...]
    [FROM source [, ...]]
    [WHERE condition]
    [GROUP BY {expr | rang | ROLLUP (...) | CUBE (...) | GROUPING SETS (...)} [, ...]]
    [HAVING condition]
    [WINDOW nom AS (définition) [, ...]]
    [{UNION | INTERSECT | EXCEPT} [ALL | DISTINCT] requête]
    [ORDER BY expr [ASC | DESC] [NULLS {FIRST | LAST}] [, ...]]
    [LIMIT {n | ALL}] [OFFSET m [ROW | ROWS]]
    [FETCH {FIRST | NEXT} [n] {ROW | ROWS} {ONLY | WITH TIES}]
    [FOR {UPDATE | NO KEY UPDATE | SHARE | KEY SHARE} [OF table [, ...]] [NOWAIT | SKIP LOCKED]]

où source est :

    [ONLY] [schéma.]table [*] [[AS] alias [(colonne, ...)]]
  | (requête) [AS] alias [(colonne, ...)]
  | (VALUES (...), ...) [AS] alias [(colonne, ...)]
  | [LATERAL] fonction(arguments) [WITH ORDINALITY] [[AS] alias [(colonne, ...)]]
  | LATERAL (requête) [AS] alias [(colonne, ...)]
  | source [INNER | LEFT [OUTER] | RIGHT [OUTER] | FULL [OUTER]] JOIN source {ON condition | USING (colonne, ...)}
  | source NATURAL [INNER | LEFT | RIGHT | FULL] JOIN source
  | source CROSS JOIN source

Une requête rend son résultat puis l'étiquette SELECT n, où n est le nombre de lignes.


36.2 Liste de sélection#

Chaque élément est une expression, avec un alias facultatif (AS alias, ou l'alias seul) ; * désigne toutes les colonnes, table.* celles d'une table.

SELECT nom, solde AS balance, solde * 1.1 marge FROM client WHERE id <= 2 ORDER BY id;
 nom   | balance | marge
-------+---------+---------
 Alice | 100.50  | 110.550
 Bob   | -20.00  | -22.000
SELECT * FROM article WHERE id = 1;
SELECT c.*, 1 AS actif FROM client c WHERE c.id = 1;      -- toutes les colonnes de c, plus une
SELECT 1 AS a, 'x' AS b, NULL AS c, true AS d;            -- sans FROM : une ligne

Identifiants et alias#

  • Un identifiant s'écrit nu ou entre guillemets doubles ("Nom du client") ; les guillemets permettent les espaces, les signes et les mots réservés. Un alias placé après AS peut être un mot réservé (AS select).
  • Les noms sont insensibles à la casse, même entre guillemets : "Client", client et CLIENT désignent la même table, "ID" la colonne id (dans PostgreSQL, un nom entre guillemets est sensible à la casse). L'alias écrit est rendu tel quel au client.
  • Une colonne se qualifie par sa table ou l'alias de celle-ci (c.nom), une table par son schéma (ventes.client) ; public.client désigne la table de la base courante.
SELECT nom AS "Nom du client", solde AS "Solde (DA)" FROM client WHERE id = 1;
 Nom du client | Solde (DA)
---------------+------------
 Alice         | 100.50

Nom d'une colonne calculée#

Une expression sans alias porte le texte écrit dans la requête, là où PostgreSQL la nomme ?column? ou du nom de la fonction :

SELECT 1 + 1, 'a' || 'b', upper('x'), solde * 2, solde::int FROM client WHERE id = 1;
 1 + 1 | 'a' || 'b' | upper('x') | solde * 2 | solde::int
-------+------------+------------+-----------+------------
 2     | ab         | X          | 201.00    | 101

De même count(*) se nomme count(*) (PostgreSQL : count) et sum(solde) se nomme sum(solde). Donner un alias à toute colonne calculée que l'application lit par son nom : le code reste alors valable sur les deux serveurs.

Conditions dans la liste#

Une condition placée dans la liste est un booléen, rendu t / f (ou NULL) :

SELECT nom, solde > 0 AS crediteur, ville IS NULL AS sans_ville, ville = 'Oran' AS oran
FROM client ORDER BY id;
 nom    | crediteur | sans_ville | oran
--------+-----------+------------+------
 Alice  | t         | f          | t
 Bob    | f         | f          | f
 Chloé  | f         | f          | t
 David  | t         | f          | f
 Émile  | t         | f          | f
 Fatima | t         | t          | NULL

DISTINCT#

DISTINCT retire les lignes identiques sur tous les éléments de la liste ; deux NULL y sont considérés égaux.

SELECT DISTINCT ville FROM client ORDER BY ville;
 ville
-------------
 Alger
 Constantine
 Oran
 NULL
SELECT count(DISTINCT ville) AS villes, count(ville) AS renseignees, count(*) AS clients FROM client;
-- 3 | 5 | 6

Comme toute comparaison de texte, DISTINCT distingue la casse et les accents (36.4) : 'a' et 'A' sont deux valeurs.

SELECT DISTINCT x FROM (VALUES ('a'), ('A'), ('a')) AS v (x) ORDER BY x;     -- A, a

SELECT DISTINCT ON (expr, …) garde la première ligne de chaque groupe de valeurs des expressions, selon l'ORDER BY ; celui-ci doit commencer par ces expressions (dans un ordre quelconque), sinon 42P10 (SELECT DISTINCT ON expressions must match initial ORDER BY expressions). Une expression peut être un rang (DISTINCT ON (1)) ou l'alias d'une colonne de la liste ; le filtre vient après GROUP BY, HAVING et les fonctions de fenêtrage, avant LIMIT. Sans ORDER BY, la ligne gardée dans chaque groupe est quelconque et les lignes sortent dans l'ordre des expressions.

SELECT DISTINCT ON (ville) ville, nom, solde FROM client ORDER BY ville, solde DESC;
 ville       | nom    | solde
-------------+--------+--------
 Alger       | Émile  | 75.25
 Constantine | David  | 250.00
 Oran        | Alice  | 100.50
 NULL        | Fatima | 10.00

VALUES#

Une liste VALUES est une requête à part entière ; ses colonnes se nomment column1, column2… Dans un FROM, un alias de table et de colonnes les renomme :

VALUES (1, 'a'), (2, 'b');                                   -- colonnes column1, column2
SELECT * FROM (VALUES (1, 'un'), (2, 'deux')) AS v (n, mot) ORDER BY n;
 n | mot
---+------
 1 | un
 2 | deux

36.3 FROM et jointures#

Types de jointure#

SyntaxeSémantique
a, b ou a CROSS JOIN bproduit cartésien
a [INNER] JOIN b ON condles paires de lignes qui satisfont cond
a LEFT [OUTER] JOIN b ON condtoutes les lignes de a ; colonnes de b à NULL sans correspondance
a RIGHT [OUTER] JOIN b ON condtoutes les lignes de b ; colonnes de a à NULL sans correspondance
a FULL [OUTER] JOIN b ON condtoutes les lignes des deux côtés
a JOIN b USING (col, …)égalité sur les colonnes citées, fusionnées dans le résultat ; aussi avec LEFT, RIGHT, FULL
a NATURAL [LEFT | RIGHT | FULL] JOIN bUSING sur toutes les colonnes de même nom
a, LATERAL (requête) x, a {JOIN | LEFT JOIN | CROSS JOIN} LATERAL (requête) xsous-requête qui cite les colonnes de a, évaluée pour chaque ligne
SELECT c.nom, co.id AS commande, co.date_cmd
FROM client c JOIN commande co ON co.client_id = c.id
ORDER BY co.id;
 nom   | commande | date_cmd
-------+----------+------------
 Alice | 1        | 2026-09-01
 Alice | 2        | 2026-09-15
 Bob   | 3        | 2026-09-20
 Chloé | 4        | 2026-10-02
 Émile | 5        | 2026-10-05

Jointures externes#

SELECT c.nom, co.id AS commande
FROM client c LEFT JOIN commande co ON co.client_id = c.id
ORDER BY c.id, co.id;
 nom    | commande
--------+----------
 Alice  | 1
 Alice  | 2
 Bob    | 3
 Chloé  | 4
 David  | NULL
 Émile  | 5
 Fatima | NULL
-- compter sans perdre les clients sans commande
SELECT c.nom, count(co.id) AS commandes
FROM client c LEFT OUTER JOIN commande co ON co.client_id = c.id
GROUP BY c.id, c.nom ORDER BY c.id;
-- Alice 2, Bob 1, Chloé 1, David 0, Émile 1, Fatima 0

-- tous les articles, commandés ou non
SELECT a.code, l.commande_id
FROM ligne_commande l RIGHT JOIN article a ON a.id = l.article_id
ORDER BY a.id, l.commande_id;
-- … GO-01 | 3, TR-01 | NULL

Une condition sur la table optionnelle n'a pas le même effet dans ON et dans WHERE : dans ON elle restreint les correspondances (les lignes sans correspondance restent, complétées de NULL) ; dans WHERE elle élimine ces lignes, et la jointure externe se comporte comme une jointure interne.

-- clients sans commande livrée : la condition est dans ON, le WHERE cherche l'absence
SELECT c.id, c.nom
FROM client c LEFT JOIN commande co ON co.client_id = c.id AND co.etat = 'livrée'
WHERE co.id IS NULL AND c.id <= 3;                         -- 3 | Chloé

-- condition dans WHERE : seuls restent les clients qui ont une commande livrée
SELECT c.id, c.nom, co.id AS commande
FROM client c LEFT JOIN commande co ON co.client_id = c.id
WHERE co.etat = 'livrée' ORDER BY c.id;                    -- 1 | Alice | 1, 2 | Bob | 3

FULL JOIN garde les lignes sans correspondance des deux côtés :

SELECT c.nom, co.id AS commande
FROM client c FULL JOIN commande co ON co.client_id = c.id AND co.etat = 'livrée'
ORDER BY c.id NULLS LAST, co.id;
 nom    | commande
--------+----------
 Alice  | 1
 Bob    | 3
 Chloé  | NULL
 David  | NULL
 Émile  | NULL
 Fatima | NULL
 NULL   | 2
 NULL   | 4
 NULL   | 5

Produit cartésien et jointure par WHERE#

SELECT count(*) AS n FROM client CROSS JOIN article;      -- 36
SELECT count(*) AS n FROM client, article;                -- 36

SELECT c.nom, co.id
FROM client c, commande co
WHERE co.client_id = c.id AND co.etat = 'ouverte'
ORDER BY co.id;                                            -- Alice | 2, Chloé | 4

Plusieurs jointures#

SELECT c.nom, a.libelle, l.quantite, l.total
FROM client c
JOIN commande co ON co.client_id = c.id
JOIN ligne_commande l ON l.commande_id = co.id
JOIN article a ON a.id = l.article_id
WHERE co.etat = 'livrée'
ORDER BY c.nom, a.libelle;
 nom   | libelle         | quantite | total
-------+-----------------+----------+--------
 Alice | Cahier 96 pages | 2        | 7.00
 Alice | Stylo bille     | 10       | 12.00
 Bob   | Gomme           | 20       | 16.00
 Bob   | Stylo bille     | 100      | 110.00

Une colonne présente dans plusieurs tables doit être qualifiée : WHERE id = 1 dans une jointure de client et commande rend 42702 (Column 'id' in where clause is ambiguous).

USING et NATURAL : colonnes fusionnées#

CREATE TABLE remise (client_id integer PRIMARY KEY, taux numeric(4,2), id integer);
INSERT INTO remise VALUES (1, 0.10, 100), (4, 0.05, 101), (9, 0.50, 102);

SELECT * FROM commande JOIN remise USING (client_id) ORDER BY 2;
 client_id | id | date_cmd   | etat    | taux | id
-----------+----+------------+---------+------+-----
 1         | 1  | 2026-09-01 | livrée  | 0.10 | 100
 1         | 2  | 2026-09-15 | ouverte | 0.10 | 100
  • SELECT * rend la colonne de jointure une seule fois, en tête, puis les autres colonnes de gauche, puis celles de droite.
  • La colonne fusionnée se cite sans qualification (client_id) ; les colonnes homonymes qui ne sont pas de la jointure restent à qualifier (commande.id, remise.id).
  • Elle vaut la colonne du côté conservé : gauche pour JOIN et LEFT JOIN, droite pour RIGHT JOIN, et la valeur non NULL des deux pour FULL JOIN :
SELECT client_id, commande.id AS commande, taux
FROM commande FULL JOIN remise USING (client_id)
ORDER BY client_id, commande;
 client_id | commande | taux
-----------+----------+------
 1         | 1        | 0.10
 1         | 2        | 0.10
 2         | 3        | NULL
 3         | 4        | NULL
 4         | NULL     | 0.05
 5         | 5        | NULL
 9         | NULL     | 0.50

NATURAL JOIN joint sur toutes les colonnes de même nom :

CREATE TABLE ville_info (ville varchar(50) PRIMARY KEY, wilaya integer);
INSERT INTO ville_info VALUES ('Oran', 31), ('Alger', 16), ('Annaba', 23);

SELECT ville, nom, wilaya FROM client NATURAL LEFT JOIN ville_info ORDER BY id;
 ville       | nom    | wilaya
-------------+--------+--------
 Oran        | Alice  | 31
 Alger       | Bob    | 16
 Oran        | Chloé  | 31
 Constantine | David  | NULL
 Alger       | Émile  | 16
 NULL        | Fatima | NULL

NATURAL RIGHT JOIN et NATURAL FULL JOIN s'emploient de même (Annaba | NULL | 23 apparaît alors). NATURAL CROSS JOIN n'existe pas (42601).

Auto-jointure#

Une table se joint à elle-même sous deux alias :

SELECT a.nom, b.nom AS meme_ville
FROM client a JOIN client b ON a.ville = b.ville AND a.id < b.id
ORDER BY a.id;
 nom   | meme_ville
-------+------------
 Alice | Chloé
 Bob   | Émile

Table dérivée#

Une sous-requête dans FROM tient lieu de table. Elle doit porter un alias (PostgreSQL 16 le rend facultatif ; MIRAJ répond 42601, syntax error at end of input). Un alias de colonnes peut suivre.

SELECT t.ville, t.total
FROM (SELECT ville, sum(solde) AS total FROM client GROUP BY ville) AS t (ville, total)
WHERE total > 100
ORDER BY 1;
 ville       | total
-------------+--------
 Constantine | 250.00
 Oran        | 100.50

Alias de colonnes#

Un alias de colonnes AS x (a, b, …) s'écrit après toute source du FROM : table nommée, vue, expression WITH, table dérivée (même dont la liste est *), liste VALUES, fonction. Comme dans PostgreSQL, les premières colonnes sont renommées et les suivantes gardent leur nom :

SELECT * FROM article AS q (x, y) WHERE x = 1;
 x | y     | libelle     | prix | stock | actif
---+-------+-------------+------+-------+-------
 1 | ST-01 | Stylo bille | 1.20 | 500   | t
SELECT * FROM (SELECT * FROM remise) AS r (a, b) ORDER BY a;     -- colonnes a, b, id
SELECT * FROM remise AS r (a, b, c, d);
-- ERREUR 42P10 : table "r" has 3 columns available but 4 columns specified

LATERAL#

Une sous-requête précédée de LATERAL peut citer les colonnes des tables écrites avant elle ; elle est évaluée pour chaque ligne. Usage typique : « la dernière commande de chaque client ».

SELECT c.nom, d.id, d.date_cmd
FROM client c,
     LATERAL (SELECT id, date_cmd FROM commande co
              WHERE co.client_id = c.id ORDER BY date_cmd DESC LIMIT 1) d
ORDER BY c.id;
 nom   | id | date_cmd
-------+----+------------
 Alice | 2  | 2026-09-15
 Bob   | 3  | 2026-09-20
 Chloé | 4  | 2026-10-02
 Émile | 5  | 2026-10-05

Avec LEFT JOIN LATERAL (…) d ON true, les clients sans commande restent (David | NULL, Fatima | NULL). JOIN LATERAL … ON true et CROSS JOIN LATERAL sont équivalents à la virgule.

Limites : LATERAL ne précède qu'une sous-requête ; il n'est admis ni avec RIGHT ou FULL JOIN, ni avec USING ou NATURAL (0A000). La sous-requête étant replanifiée pour chaque ligne, réserver LATERAL à des tables de gauche de taille modérée ; pour un « premier par groupe » sur une grande table, préférer row_number() (36.10).

generate_series#

generate_series(début, fin [, pas]) rend une suite d'entiers, ou de dates et d'horodatages avec un pas interval :

SELECT n, n * n AS carre FROM generate_series(1, 4) AS s (n);
 n | carre
---+-------
 1 | 1
 2 | 4
 3 | 9
 4 | 16
SELECT g FROM generate_series(10, 1, -3) AS g;                               -- 10, 7, 4, 1
SELECT date '2026-10-01' + n AS jour FROM generate_series(0, 2) AS s (n);    -- trois dates
SELECT * FROM generate_series(date '2026-10-01', date '2026-10-03', interval '1 day') AS j;
-- 2026-10-01, 2026-10-02, 2026-10-03
SELECT * FROM generate_series(timestamp '2026-10-01 00:00', timestamp '2026-10-01 02:00',
                              interval '1 hour') AS j;
-- 2026-10-01 00:00:00, 2026-10-01 01:00:00, 2026-10-01 02:00:00

Une suite n'est pas bornée par cte_max_recursion_depth (la limite des requêtes récursives, voir 36.9) : generate_series(1, 2000) rend ses 2 000 valeurs. Sa propre limite est de 10 000 000 de valeurs (54000 : generate_series cannot return more than 10000000 rows). Un pas nul rend 22023 (step size cannot equal zero). Les bornes sont des constantes : elles ne citent pas une autre table du FROM (42703).

Fonctions ensemblistes dans la liste du SELECT#

generate_series, unnest, regexp_matches, regexp_split_to_table, json[b]_array_elements[_text] et json[b]_object_keys s'écrivent aussi dans la liste d'un SELECT, comme dans PostgreSQL 10 et suivants : la requête rend une ligne par valeur, chaque ligne source étant répétée. La colonne porte le nom de la fonction, ou son alias.

SELECT generate_series(1, 3);                                -- colonne generate_series : 1, 2, 3
SELECT generate_series(1, 3) * 10 AS dizaine;                -- 10, 20, 30
SELECT nom, generate_series(1, 2) AS tour FROM client WHERE id <= 2 ORDER BY nom, tour;
 nom   | tour
-------+------
 Alice | 1
 Alice | 2
 Bob   | 1
 Bob   | 2

Plusieurs fonctions d'une même liste avancent ensemble : la plus longue fixe le nombre de lignes, les autres sont complétées par NULL.

SELECT generate_series(1, 3) AS a, generate_series(1, 2) AS b;
 a | b
---+------
 1 | 1
 2 | 2
 3 | NULL

Les arguments de generate_series y sont des constantes (une colonne : 0A000, generate_series citing a column in the select list) ; unnest(ARRAY[…]) peut citer les colonnes de la ligne (SELECT nom, unnest(ARRAY[solde, solde * 2]) AS v FROM client). La forme vaut aussi dans INSERT … SELECT, une union ou une sous-requête ; elle n'est pas admise dans une liste regroupée, agrégée ou fenêtrée (0A000).

Fonction dans un FROM#

Une fonction — stockée ou fournie par le serveur — peut être appelée dans un FROM. Une fonction scalaire y rend une ligne d'une colonne, comme dans PostgreSQL ; c'est la forme qu'envoie cursor.callproc de psycopg2.

CREATE FUNCTION total_client(cid integer) RETURNS numeric(12,2) AS $$
  SELECT coalesce(sum(l.total), 0)
  FROM commande co JOIN ligne_commande l ON l.commande_id = co.id
  WHERE co.client_id = cid
$$ LANGUAGE sql;

SELECT * FROM total_client(3);                        -- colonne « total_client » : 31.50
SELECT * FROM total_client(3) AS t;                   -- colonne « t »
SELECT t.montant FROM total_client(3) AS t (montant); -- colonne « montant »
SELECT c.nom, t FROM client c, total_client(c.id) AS t ORDER BY c.nom;
 nom    | t
--------+--------
 Alice  | 25.90
 Bob    | 126.00
 Chloé  | 31.50
 David  | 0.00
 Fatima | 0.00
 Émile  | 0.00

La colonne porte le nom de l'alias, sinon celui de la fonction. Une fonction scalaire voit les tables qui la précèdent dans le FROM (dernier exemple), sans qu'il faille écrire LATERAL ; le mot est admis devant elle, en première position aussi (FROM LATERAL f(…)). Une liste de définitions de colonnes (AS t (a int, b text)) rend 0A000.

Fonctions ensemblistes stockées. Une fonction déclarée RETURNS TABLE (…), RETURNS SETOF type, RETURNS SETOF table (ou à paramètres OUT avec SETOF record) rend des lignes, lues comme celles d'une table (voir 40. Routines et déclencheurs pour RETURN NEXT et RETURN QUERY) :

CREATE FUNCTION commandes_de(cid integer) RETURNS TABLE (num integer, jour date, etat varchar) AS $$
  SELECT id, date_cmd, etat FROM commande WHERE client_id = cid ORDER BY id
$$ LANGUAGE sql;

SELECT * FROM commandes_de(1);
 num | jour       | etat
-----+------------+---------
 1   | 2026-09-01 | livrée
 2   | 2026-09-15 | ouverte
CREATE FUNCTION articles_actifs() RETURNS SETOF article AS $$
  SELECT * FROM article WHERE actif ORDER BY id
$$ LANGUAGE sql;

SELECT code, prix FROM articles_actifs() WHERE prix > 2;     -- CA-01 3.50, CL-01 6.90, TR-01 2.10

Une telle fonction s'appelle dans un FROM, avec des arguments constants : appelée dans la liste d'un SELECT (0A000 : set-valued function called in context that cannot accept a set), avec des arguments qui citent une autre table du FROM, ou suivie de WITH ORDINALITY, elle est refusée (0A000).

Fonctions du serveur qui rendent plusieurs lignes. Elles admettent WITH ORDINALITY, qui ajoute le rang de chaque ligne (colonne ordinality, ou le dernier nom de l'alias) :

FonctionColonnes
generate_series(début, fin [, pas])la valeur
unnest(tableau)un élément par ligne
regexp_split_to_table(texte, motif [, options])un morceau par ligne
regexp_matches(texte, motif [, options])un tableau des groupes par correspondance
json_each, jsonb_each, json_each_text, jsonb_each_textkey, value
json[b]_array_elements, json[b]_array_elements_textvalue
json[b]_object_keysla clé
SELECT * FROM unnest(ARRAY['rouge', 'vert']) WITH ORDINALITY AS c (couleur, rang);
-- rouge | 1 ; vert | 2
SELECT c.nom, v FROM client c, unnest(ARRAY[c.solde, c.solde * 2]) AS v
WHERE c.id <= 2 ORDER BY c.nom, v;
-- Alice 100.50, Alice 201.00, Bob -40.00, Bob -20.00
SELECT jour, n FROM generate_series(1, 3) WITH ORDINALITY AS g (jour, n);
SELECT * FROM regexp_split_to_table('a,b;c', '[,;]') WITH ORDINALITY AS t (morceau, n);
-- a | 1 ; b | 2 ; c | 3
SELECT * FROM jsonb_each('{"b": 2, "a": 1}') AS e;           -- a | 1 ; b | 2 (clés triées)
SELECT * FROM json_each_text('{"b": 2, "a": "x"}') AS e (k, v);   -- b | 2 ; a | x
SELECT c.nom, e.key FROM client c, jsonb_each(jsonb_build_object('id', c.id)) AS e
WHERE c.id = 1;                                               -- Alice | id

Les fonctions JSON sont latérales sur les tables qui les précèdent (LATERAL écrit ou non, LEFT JOIN LATERAL … ON true compris) ; un document NULL ne rend aucune ligne.

unnest déplie un tableau construit ARRAY[a, b, …], dont les éléments peuvent citer les colonnes des tables qui précèdent (converti ou non : ARRAY[…]::int[] convertit chaque élément ; ARRAY[]::int[] ne rend aucune ligne), et un tableau écrit en texte {a,b} : paramètre lié (unnest($1), unnest($1::int[]), la forme qu'envoient les pilotes pour une liste liée, en format texte) ou littéral (unnest('{1,2,NULL}'::int[])). Sans ::type[], les éléments d'un texte {…} restent du texte ; un élément NULL rend NULL, un tableau NULL ou {} aucune ligne. Le tableau d'une colonne (unnest(t.col)), ARRAY[] sans conversion et un paramètre tableau envoyé en binaire rendent 0A000.

FROM ONLY table et FROM table * sont acceptés : ONLY et * sont sans effet, MIRAJ n'ayant pas d'héritage de tables.


36.4 WHERE#

WHERE filtre les lignes produites par FROM, avant tout regroupement.

FormeExemple
comparaisons =, <>, !=, <, <=, >, >=solde >= 10 AND solde <> 75.25
AND, OR, NOTsolde != 0 AND NOT (ville = 'Oran')
[NOT] BETWEEN a AND b (bornes comprises)solde BETWEEN 0 AND 100
[NOT] IN (liste)ville IN ('Oran', 'Constantine')
= ANY (ARRAY[…]), <> ALL (ARRAY[…]), > ALL (ARRAY[…])ville = ANY (ARRAY['Oran', 'Constantine'])
IS [NOT] NULLemail IS NOT NULL
IS [NOT] DISTINCT FROMville IS DISTINCT FROM 'Oran'
[NOT] LIKE, [NOT] ILIKE, ESCAPEnom LIKE 'A%'
~, ~*, !~, !~* (expressions rationnelles)nom ~ '^[a-c]'
booléen seul, IS [NOT] TRUE, IS [NOT] FALSEWHERE actif, WHERE NOT actif
comparaison de lignes(ville, nom) IN (('Oran', 'Alice'), ('Alger', 'Bob')), (id, solde) > (3, 100)
sous-requêtes (36.7)id IN (SELECT …), EXISTS (SELECT …)
SELECT nom, solde FROM client WHERE solde >= 10 AND solde <> 75.25 ORDER BY id;
-- Alice 100.50, David 250.00, Fatima 10.00

SELECT libelle FROM article WHERE actif IS TRUE AND stock > 100 ORDER BY id;
-- Stylo bille, Cahier 96 pages, Gomme

SELECT id FROM commande WHERE date_cmd BETWEEN date '2026-09-01' AND date '2026-09-30' ORDER BY id;   -- 1, 2, 3
SELECT id FROM commande WHERE date_cmd > date '2026-10-05' - interval '1 month' ORDER BY id;          -- 2, 3, 4, 5
SELECT id FROM commande WHERE extract(month FROM date_cmd) = 9 ORDER BY id;                           -- 1, 2, 3

AND est prioritaire sur OR : solde > 0 OR ville = 'Oran' AND email IS NULL se lit solde > 0 OR (ville = 'Oran' AND email IS NULL). Un alias de la liste de sélection n'est pas visible dans WHERE (42703), comme dans PostgreSQL.

NULL et logique à trois valeurs#

Une comparaison avec NULL ne vaut ni vrai ni faux mais NULL, et WHERE ne garde que les lignes dont la condition est vraie :

SELECT NULL = NULL AS a, NULL IS NULL AS b, 1 = NULL AS c, NULL AND false AS d,
       NULL OR true AS e, NOT NULL AS f, 1 IN (2, NULL) AS g, 1 NOT IN (2, NULL) AS h;
 a    | b | c    | d | e | f    | g    | h
------+---+------+---+---+------+------+------
 NULL | t | NULL | f | t | NULL | NULL | NULL

Conséquences pratiques :

SELECT nom FROM client WHERE ville <> 'Oran' ORDER BY id;                -- Bob, David, Émile
SELECT nom FROM client WHERE ville IS DISTINCT FROM 'Oran' ORDER BY id;  -- Bob, David, Émile, Fatima
SELECT nom FROM client WHERE ville IS NULL;                              -- Fatima

Fatima, dont la ville est NULL, n'est rendue que par IS DISTINCT FROM, qui traite NULL comme une valeur. x NOT IN (sous-requête) ne rend rien dès que la sous-requête contient un NULL (36.7).

Texte : casse, accents, LIKE et expressions rationnelles#

Les textes sont comparés octet par octet (collation « C », comme PostgreSQL en C.UTF-8) : =, <, IN, LIKE, ~, DISTINCT, GROUP BY, ORDER BY, les jointures et les contraintes d'unicité distinguent la casse et les accents. Les espaces de fin sont ignorés par =. ILIKE et ~* ignorent la casse.

SELECT 'a' = 'A' AS casse, 'e' = 'é' AS accent, 'a' = 'a ' AS espace, 'a' < 'B' AS ordre;
 casse | accent | espace | ordre
-------+--------+--------+-------
 f     | f      | t      | f
SELECT nom FROM client WHERE nom = 'emile';                              -- aucune ligne
SELECT nom FROM client WHERE nom = 'Émile';                              -- Émile
OpérateurSur MIRAJ
LIKE, NOT LIKE (~~, !~~)% : toute suite de caractères, _ : un caractère ; sensible à la casse et aux accents
ILIKE, NOT ILIKE (~~*, !~~*)comme LIKE, casse et accents ignorés
~, !~expression rationnelle, sensible à la casse et aux accents
~*, !~*comme ~, casse ignorée (les accents restent distingués)
SELECT nom FROM client WHERE nom LIKE 'A%';                 -- Alice
SELECT nom FROM client WHERE nom LIKE 'a%';                 -- aucune ligne
SELECT nom FROM client WHERE nom LIKE '_o_';                -- Bob
SELECT nom FROM client WHERE nom ILIKE 'ALI%';              -- Alice
SELECT nom FROM client WHERE nom ILIKE 'emile';             -- Émile
SELECT nom FROM client WHERE nom ~ '^[A-C]' ORDER BY id;    -- Alice, Bob, Chloé
SELECT nom FROM client WHERE nom ~* '^[a-c]' ORDER BY id;   -- Alice, Bob, Chloé
SELECT nom FROM client WHERE nom ~* '^e';                   -- aucune ligne
SELECT nom FROM client WHERE nom !~ 'e$' ORDER BY id;       -- Bob, Chloé, David, Fatima
SELECT nom FROM client WHERE nom !~~* 'b%' ORDER BY id;     -- Alice, Chloé, David, Émile, Fatima

Le caractère d'échappement de LIKE est la barre oblique, ou celui donné par ESCAPE :

CREATE TABLE fichier (nom text);
INSERT INTO fichier VALUES ('rapport_2026.pdf'), ('rapport-2026.pdf'), ('100%.txt'), ('Rapport_final.PDF');

SELECT nom FROM fichier WHERE nom ILIKE 'rapport\_%' ORDER BY nom;           -- Rapport_final.PDF, rapport_2026.pdf
SELECT nom FROM fichier WHERE nom LIKE 'rapport#_%' ESCAPE '#' ORDER BY nom; -- rapport_2026.pdf
SELECT nom FROM fichier WHERE nom LIKE '100\%%';                             -- 100%.txt
SELECT nom FROM fichier WHERE nom ILIKE '%.pdf' ORDER BY nom;                -- 3 lignes, dont Rapport_final.PDF
SELECT nom FROM fichier WHERE nom LIKE '%.pdf' ORDER BY nom;                 -- rapport-2026.pdf, rapport_2026.pdf
SELECT nom FROM fichier WHERE nom ~ '^rapport' ORDER BY nom;                 -- rapport-2026.pdf, rapport_2026.pdf

Collations. COLLATE "C" (comme "POSIX" et ucs_basic, qualifiés ou non de pg_catalog) compare octet par octet, comme le défaut. COLLATE "default" et les autres collations de PostgreSQL ("fr_FR"…) sont acceptées et sans effet. Pour une comparaison qui ignore la casse et les accents, appliquer la collation utf8mb4_general_ci de MIRAJ, ou comparer lower(…) :

SELECT nom FROM client WHERE nom COLLATE "C" = 'alice';                     -- aucune ligne
SELECT nom FROM client WHERE nom = 'alice' COLLATE utf8mb4_general_ci;      -- Alice
SELECT nom FROM client WHERE lower(nom) = 'alice';                          -- Alice

Comparaison d'un nombre et d'un texte#

Un texte comparé à une colonne numérique est lu comme un nombre (WHERE id = '1', WHERE solde = '100.5'). Un texte qui n'est pas un nombre est une erreur, comme dans PostgreSQL :

SELECT nom FROM client WHERE solde = 'abc';
-- ERREUR 22P02 : invalid input syntax for type numeric: "abc"

36.5 GROUP BY et HAVING#

GROUP BY regroupe les lignes qui ont les mêmes valeurs pour les expressions citées ; les agrégats calculent une valeur par groupe. HAVING filtre les groupes, après le calcul.

SELECT ville, count(*) AS n, sum(solde) AS total, avg(solde) AS moyenne,
       min(solde) AS mini, max(solde) AS maxi
FROM client GROUP BY ville ORDER BY ville;
 ville       | n | total  | moyenne              | mini   | maxi
-------------+---+--------+----------------------+--------+--------
 Alger       | 2 | 55.25  | 27.6250000000000000  | -20.00 | 75.25
 Constantine | 1 | 250.00 | 250.0000000000000000 | 250.00 | 250.00
 Oran        | 2 | 100.50 | 50.2500000000000000  | 0.00   | 100.50
 NULL        | 1 | 10.00  | 10.0000000000000000  | 10.00  | 10.00

Les NULL forment un groupe. avg d'entiers ou de décimaux est un numeric à seize décimales (round(avg(prix), 2) pour l'affichage) ; sum d'un integer est un bigint.

SELECT ville, sum(solde) AS total FROM client
WHERE solde > 0                    -- filtre les lignes, avant le regroupement
GROUP BY ville
HAVING sum(solde) > 50             -- filtre les groupes
ORDER BY total DESC;
 ville       | total
-------------+--------
 Constantine | 250.00
 Oran        | 100.50
 Alger       | 75.25
-- chiffre d'affaires par client
SELECT c.nom, count(DISTINCT co.id) AS commandes, sum(l.total) AS montant
FROM client c
JOIN commande co ON co.client_id = c.id
JOIN ligne_commande l ON l.commande_id = co.id
GROUP BY c.id, c.nom
ORDER BY montant DESC;
 nom   | commandes | montant
-------+-----------+---------
 Bob   | 1         | 126.00
 Chloé | 1         | 31.50
 Alice | 2         | 25.90

Ce que GROUP BY accepte#

Une expression, un alias de la liste de sélection ou le rang d'un élément de la liste :

SELECT extract(month FROM date_cmd) AS mois, count(*) AS n
FROM commande GROUP BY extract(month FROM date_cmd) ORDER BY mois;      -- 9 | 3, 10 | 2

SELECT extract(month FROM date_cmd) AS mois, count(*) AS n
FROM commande GROUP BY mois ORDER BY mois;                              -- alias

SELECT to_char(date_cmd, 'YYYY-MM') AS mois, count(*) AS n
FROM commande GROUP BY 1 ORDER BY 1;                                    -- rang : 2026-09 | 3, 2026-10 | 2

Colonne hors GROUP BY#

Toute colonne de la liste qui n'est pas dans un agrégat doit figurer dans GROUP BY :

SELECT ville, nom FROM client GROUP BY ville;
-- ERREUR 42803 : 'nom' isn't in GROUP BY

La clé primaire dispense de citer les autres colonnes de sa table (dépendance fonctionnelle, comme dans PostgreSQL) : quand GROUP BY porte toutes les colonnes de la clé primaire d'une table, ses autres colonnes sont admises dans la liste, dans HAVING et dans ORDER BY, table.* compris.

SELECT c.id, c.nom, c.ville, count(o.id) AS commandes
FROM client c LEFT JOIN commande o ON o.client_id = c.id
GROUP BY c.id
ORDER BY c.nom;

Cela ne vaut que pour une table lue directement et sa clé primaire entière : les colonnes d'une autre table de la jointure, une clé primaire composée citée en partie, une contrainte UNIQUE, une vue ou une table dérivée restent soumises à la règle (42803).

Sans GROUP BY#

Un agrégat sans GROUP BY rend une seule ligne pour toute la table, même vide ; HAVING peut alors l'écarter :

SELECT count(*) AS c, sum(solde) AS s, avg(solde) AS a FROM client WHERE id > 100;   -- 0 | NULL | NULL
SELECT count(*) AS n FROM client HAVING count(*) > 100;                              -- aucune ligne

Agrégats#

AgrégatRôle
count(*), count(expr), count(DISTINCT expr)lignes, valeurs non NULL, valeurs distinctes
sum, avg, min, maxsum(integer) : bigint ; avg : numeric
stddev, variance, stddev_pop, var_pop, stddev_samp, var_sampécart-type et variance
string_agg(expr, séparateur [ORDER BY …])concaténation
array_agg(expr [ORDER BY …]), array_agg(DISTINCT expr)tableau des valeurs ({1,2,NULL}), NULL sur zéro ligne
bool_and, bool_or, everyconjonction, disjonction
json_agg, jsonb_agg, json_object_agg, jsonb_object_aggdocuments JSON
percentile_cont(f), percentile_disc(f), mode() suivis de WITHIN GROUP (ORDER BY …)percentile continu, discret, valeur la plus fréquente (22023 hors de 0..1)
rank(v), dense_rank(v), percent_rank(v), cume_dist(v) suivis de WITHIN GROUP (ORDER BY …)rang qu'aurait la valeur v
corr, covar_pop, covar_samp, regr_slope, regr_intercept, regr_r2, regr_count, regr_avgx, regr_avgy, regr_sxx, regr_syy, regr_sxystatistiques sur les paires non NULL
SELECT ville, string_agg(nom, ', ' ORDER BY nom) AS noms FROM client GROUP BY ville ORDER BY ville;
 ville       | noms
-------------+--------------
 Alger       | Bob, Émile
 Constantine | David
 Oran        | Alice, Chloé
 NULL        | Fatima

La clause FILTER (WHERE …) restreint les lignes vues par un agrégat :

SELECT ville,
       count(*) FILTER (WHERE solde > 0) AS crediteurs,
       sum(solde) FILTER (WHERE solde > 50) AS gros
FROM client GROUP BY ville ORDER BY ville;
 ville       | crediteurs | gros
-------------+------------+--------
 Alger       | 1          | 75.25
 Constantine | 1          | 250.00
 Oran        | 1          | 100.50
 NULL        | 1          | NULL
SELECT ville, array_agg(id ORDER BY id) AS ids FROM client GROUP BY ville ORDER BY ville;
 ville       | ids
-------------+-------
 Alger       | {2,5}
 Constantine | {4}
 Oran        | {1,3}
 NULL        | {6}
SELECT percentile_cont(0.5) WITHIN GROUP (ORDER BY solde) AS mediane,
       percentile_disc(0.5) WITHIN GROUP (ORDER BY solde) AS mediane_d,
       rank(100) WITHIN GROUP (ORDER BY solde DESC) AS rang_de_100
FROM client;
 mediane | mediane_d | rang_de_100
---------+-----------+-------------
 42.625  | 10.00     | 3

Le détail des agrégats est au chapitre 37. Fonctions SQL.

ROLLUP, CUBE, GROUPING SETS#

SELECT ville, etat, count(*) AS n
FROM client c JOIN commande co ON co.client_id = c.id
GROUP BY ROLLUP (ville, etat)
ORDER BY ville NULLS LAST, etat NULLS LAST;
 ville | etat    | n
-------+---------+---
 Alger | annulée | 1
 Alger | livrée  | 1
 Alger | NULL    | 2
 Oran  | livrée  | 1
 Oran  | ouverte | 2
 Oran  | NULL    | 3
 NULL  | NULL    | 5

ROLLUP (a, b) ajoute un sous-total par a et un total général ; CUBE (a, b) ajoute aussi les sous-totaux par b (dix lignes ici) ; GROUPING SETS ((ville), (etat), ()) ne rend que les regroupements demandés. La fonction grouping(col) vaut 1 sur une ligne de total où col est agrégée, ce qui la distingue d'un vrai NULL :

SELECT ville, count(*) AS n, grouping(ville) AS g FROM client GROUP BY ROLLUP (ville) ORDER BY g, ville;
 ville       | n | g
-------------+---+---
 Alger       | 2 | 0
 Constantine | 1 | 0
 Oran        | 2 | 0
 NULL        | 1 | 0
 NULL        | 6 | 1

36.6 ORDER BY, LIMIT, OFFSET, FETCH FIRST#

ORDER BY#

Sans ORDER BY, l'ordre des lignes n'est pas garanti. Une clé de tri est une expression, un alias de la liste ou le rang d'un élément ; ASC (défaut) ou DESC.

SELECT nom, solde FROM client ORDER BY solde DESC, nom;                   -- David, Alice, Émile, Fatima, Chloé, Bob
SELECT nom, solde * 2 AS double FROM client ORDER BY double DESC LIMIT 2; -- alias
SELECT nom, solde FROM client ORDER BY 2 DESC LIMIT 2;                    -- rang
SELECT nom FROM client ORDER BY length(nom), nom;                         -- expression
SELECT ville, count(*) AS n FROM client GROUP BY ville ORDER BY count(*) DESC, ville;   -- agrégat

Place des NULL. Comme dans PostgreSQL, les NULL sont derniers en ordre croissant et premiers en ordre décroissant ; NULLS FIRST et NULLS LAST imposent leur place.

SELECT nom, ville FROM client ORDER BY ville, nom;
 nom    | ville
--------+-------------
 Bob    | Alger
 Émile  | Alger
 David  | Constantine
 Alice  | Oran
 Chloé  | Oran
 Fatima | NULL
SELECT nom, ville FROM client ORDER BY ville NULLS FIRST, nom;        -- Fatima en tête
SELECT nom, ville FROM client ORDER BY ville DESC NULLS LAST, nom;    -- Alice, Chloé, David, Bob, Émile, Fatima

Ordre des textes. Le tri suit l'ordre des octets (collation « C », comme PostgreSQL en C.UTF-8) : majuscules avant minuscules, lettres accentuées après les lettres non accentuées.

SELECT nom FROM client ORDER BY nom;                     -- Alice, Bob, Chloé, David, Fatima, Émile
SELECT x FROM (VALUES ('b'), ('B'), ('a'), ('A'), ('é'), ('e')) AS v (x) ORDER BY x;
-- A, B, a, b, e, é

Pour un ordre qui ignore la casse et les accents, trier par x COLLATE utf8mb4_general_ci (collation de MIRAJ), puis par x pour départager : … ORDER BY x COLLATE utf8mb4_general_ci, x rend A, a, B, b, e, é.

LIMIT et OFFSET#

SELECT id FROM client ORDER BY id LIMIT 2;                -- 1, 2
SELECT id FROM client ORDER BY id LIMIT 2 OFFSET 3;       -- 4, 5
SELECT id FROM client ORDER BY id OFFSET 4;               -- 5, 6   (OFFSET seul)
SELECT id FROM client ORDER BY id OFFSET 1 LIMIT 2;       -- 2, 3   (OFFSET avant LIMIT)
SELECT id FROM client ORDER BY id LIMIT ALL OFFSET 5;     -- 6
SELECT id FROM client ORDER BY id LIMIT 0;                -- aucune ligne

LIMIT ALL et LIMIT NULL ne limitent pas. La valeur est un entier écrit ou un paramètre, pas une expression (LIMIT 1 + 1 : 42601) :

PREPARE page (int, int) AS SELECT id FROM client ORDER BY id LIMIT $1 OFFSET $2;
EXECUTE page(2, 1);                                       -- 2, 3

Pour une pagination stable, trier sur une clé unique (ORDER BY date_cmd, id).

FETCH FIRST#

Forme du standard, équivalente à LIMIT :

SELECT id FROM client ORDER BY id FETCH FIRST 2 ROWS ONLY;                    -- 1, 2
SELECT id FROM client ORDER BY id OFFSET 2 ROWS FETCH NEXT 2 ROWS ONLY;       -- 3, 4
SELECT id FROM client ORDER BY id FETCH FIRST ROW ONLY;                       -- 1

WITH TIES à la place de ONLY rend aussi les lignes ex æquo de la dernière, selon l'ORDER BY (obligatoire : sans lui, 42601 FETCH ... WITH TIES requires ORDER BY clause to be present) :

SELECT ville, nom FROM client ORDER BY ville FETCH FIRST 1 ROWS WITH TIES;
 ville | nom
-------+-------
 Alger | Bob
 Alger | Émile

36.7 Sous-requêtes#

FormeRôle
(SELECT …) à la place d'une valeursous-requête scalaire : une colonne, une ligne au plus ; NULL sans ligne
x [NOT] IN (SELECT …), (a, b) IN (SELECT …, …)appartenance
[NOT] EXISTS (SELECT …)existence d'au moins une ligne
x op ANY (SELECT …), x op SOME (SELECT …), x op ALL (SELECT …)comparaison à l'une ou à toutes les valeurs
(SELECT …) alias dans FROMtable dérivée (36.3)

Une sous-requête est corrélée quand elle cite une colonne de la requête qui l'entoure ; elle est alors évaluée pour chaque ligne.

-- scalaire, non corrélée
SELECT nom, solde FROM client WHERE solde > (SELECT avg(solde) FROM client) ORDER BY id;
-- Alice 100.50, David 250.00, Émile 75.25

-- scalaire corrélée, dans la liste
SELECT nom, (SELECT count(*) FROM commande co WHERE co.client_id = c.id) AS commandes,
            (SELECT max(date_cmd) FROM commande co WHERE co.client_id = c.id) AS derniere
FROM client c ORDER BY c.id;
 nom    | commandes | derniere
--------+-----------+------------
 Alice  | 2         | 2026-09-15
 Bob    | 1         | 2026-09-20
 Chloé  | 1         | 2026-10-02
 David  | 0         | NULL
 Émile  | 1         | 2026-10-05
 Fatima | 0         | NULL
SELECT nom FROM client WHERE id IN (SELECT client_id FROM commande WHERE etat = 'livrée') ORDER BY id;
-- Alice, Bob

SELECT nom FROM client c
WHERE EXISTS (SELECT 1 FROM commande co WHERE co.client_id = c.id AND co.etat = 'ouverte') ORDER BY id;
-- Alice, Chloé

SELECT nom FROM client c
WHERE NOT EXISTS (SELECT 1 FROM commande co WHERE co.client_id = c.id) ORDER BY id;
-- David, Fatima

SELECT libelle, prix FROM article WHERE prix >= ALL (SELECT prix FROM article);     -- Agrafeuse 14.00
SELECT libelle, prix FROM article
WHERE prix > ANY (SELECT prix_unit FROM ligne_commande WHERE commande_id = 4) ORDER BY id;
-- Classeur 6.90, Agrafeuse 14.00

SELECT nom FROM client WHERE (id, ville) IN (SELECT client_id, 'Oran' FROM commande) ORDER BY id;
-- Alice, Chloé

-- la plus grosse ligne de chaque article
SELECT a.libelle, l.quantite
FROM article a JOIN ligne_commande l ON l.article_id = a.id
WHERE l.quantite = (SELECT max(l2.quantite) FROM ligne_commande l2 WHERE l2.article_id = a.id)
ORDER BY a.id;

EXISTS placé dans la liste est un booléen (t / f). Une sous-requête peut porter ORDER BY et LIMIT (id IN (SELECT client_id FROM commande ORDER BY date_cmd DESC LIMIT 2)).

NOT IN et NULL. Si la sous-requête rend un NULL, x NOT IN (…) n'est jamais vrai et la requête ne rend aucune ligne (comme dans PostgreSQL). Préférer NOT EXISTS, qui n'a pas ce piège.

RequêteRéponse
sous-requête scalaire qui rend plusieurs lignes : SELECT (SELECT nom FROM client)21000 : Subquery returns more than 1 row
sous-requête scalaire à plusieurs colonnes : SELECT (SELECT nom, ville FROM client WHERE id = 1)42601 : Operand should contain 1 column(s)
ligne comparée à une sous-requête par = : (id, ville) = (SELECT client_id, 'Oran' FROM …)42601 : Operand should contain 1 column(s) ; écrire IN

36.8 Opérations ensemblistes : UNION, INTERSECT, EXCEPT#

requête {UNION | INTERSECT | EXCEPT} [ALL | DISTINCT] requête [...]
    [ORDER BY ...] [LIMIT n] [OFFSET m]
OpérateurRésultat
UNIONlignes de l'une ou l'autre requête, sans doublon
UNION ALLtoutes les lignes, doublons gardés
INTERSECTlignes présentes des deux côtés, sans doublon
INTERSECT ALLlignes présentes des deux côtés, autant de fois que du côté qui en a le moins
EXCEPTlignes de gauche absentes de droite, sans doublon
EXCEPT ALLlignes de gauche, moins une occurrence par ligne égale de droite

Avec client.ville (Oran ×2, Alger ×2, Constantine, NULL) et ville_info.ville (Oran, Alger, Annaba) :

SELECT ville FROM client UNION SELECT ville FROM ville_info ORDER BY ville;
-- Alger, Annaba, Constantine, Oran, NULL
SELECT ville FROM client INTERSECT SELECT ville FROM ville_info ORDER BY ville;
-- Alger, Oran
SELECT ville FROM client EXCEPT SELECT ville FROM ville_info ORDER BY ville;
-- Constantine, NULL
SELECT ville FROM client EXCEPT ALL SELECT ville FROM ville_info ORDER BY ville;
-- Alger, Constantine, Oran, NULL
  • Les requêtes doivent rendre le même nombre de colonnes (42601 : The used SELECT statements have a different number of columns) ; les noms sont ceux de la première.
  • Deux NULL sont égaux pour l'élimination des doublons ; les textes y sont comparés octet par octet (SELECT nom FROM client UNION SELECT 'alice' rend Alice et alice).
  • ORDER BY, LIMIT et OFFSET écrits à la fin portent sur le résultat entier ; la clé de tri est un nom de colonne, un rang ou une expression sur ces colonnes.
  • Priorité : INTERSECT est évalué avant UNION et EXCEPT, comme dans PostgreSQL ; les parenthèses imposent un autre ordre, et permettent un ORDER BY / LIMIT propre à une branche :
SELECT 1 AS n UNION ALL SELECT 2 INTERSECT SELECT 2;          -- 1, 2
(SELECT 1 AS n UNION ALL SELECT 2) INTERSECT SELECT 2;        -- 2

(SELECT nom FROM client ORDER BY solde DESC LIMIT 1)
UNION ALL
(SELECT nom FROM client ORDER BY solde LIMIT 1);              -- David, Bob

Types des colonnes. Un entier et un décimal donnent un décimal (SELECT 1 UNION SELECT 2.5 : 1.0, 2.5). Un texte écrit sans type ('2', '24:00:00') prend, comme dans PostgreSQL, le type des requêtes résolues avant lui, deux à deux de gauche à droite (celui de la requête qui le suit s'il est le premier) : entier (smallint, integer ou bigint, dont les bornes sont contrôlées), numeric, flottant, booléen, date, heure, timestamp, timestamptz, bytea, uuid, json, jsonb, interval ou énumération ; un texte illisible dans ce type est refusé, comme par une conversion ::type (22P02 ; 22003 hors des bornes d'un entier ; 22007 ou 22008 pour une date, une heure ou un intervalle). Il en va de même des lignes d'une liste VALUES et du terme récursif d'un WITH RECURSIVE, lu dans le type du terme de départ ; un terme de départ fait de textes sans type est un text, que le terme récursif ne peut changer en un autre type (42804).

SELECT 1 UNION SELECT '2';                                    -- 1, 2 (integer)
SELECT 1 UNION SELECT 'x';                                    -- 22P02
SELECT 1 UNION SELECT '99999999999';                          -- 22003 (integer)
SELECT '1.5' UNION SELECT 1 UNION SELECT 2.5;                 -- 22P02 ('1.5' lu en entier)
SELECT INTERVAL '1 day' UNION SELECT '24:00:00';              -- 1 day (interval)
VALUES (DATE '2026-01-02'), ('2026-03-04');                   -- deux dates

Un texte typé (colonne text, 'x'::text) ne correspond, comme dans PostgreSQL, qu'à un autre texte : face à un nombre, un booléen, une date, un bytea, un uuid, un document JSON, un intervalle ou une énumération, la requête est refusée (42804 : UNION types integer and text cannot be matched).


36.9 Expressions de table communes : WITH#

Une clause WITH nomme une ou plusieurs requêtes, utilisables comme des tables dans la requête principale et dans les expressions qui les suivent.

WITH total_cmd AS (
    SELECT commande_id, sum(total) AS montant FROM ligne_commande GROUP BY commande_id
), par_client AS (
    SELECT co.client_id, sum(t.montant) AS montant
    FROM commande co JOIN total_cmd t ON t.commande_id = co.id
    GROUP BY co.client_id
)
SELECT c.nom, p.montant
FROM client c JOIN par_client p ON p.client_id = c.id
ORDER BY p.montant DESC;
 nom   | montant
-------+---------
 Bob   | 126.00
 Chloé | 31.50
 Alice | 25.90
  • Une liste de colonnes renomme le résultat : WITH v (n, carre) AS (SELECT id, id * id FROM client).
  • Une expression peut être citée plusieurs fois (FROM x AS p JOIN x AS q ON …).
  • Une expression du nom d'une table existante la masque dans la requête (WITH client AS (SELECT 1 AS id) SELECT * FROM client rend une ligne).
  • AS MATERIALIZED et AS NOT MATERIALIZED sont acceptés.
  • WITH s'emploie dans une sous-requête, dans une table dérivée, dans la requête d'un INSERT … SELECT et dans une vue.

WITH s'écrit aussi en tête d'un INSERT, d'un UPDATE ou d'un DELETE, et une expression de la clause qui ouvre l'instruction peut être elle-même une écriture (WITH d AS (DELETE … RETURNING *) SELECT … FROM d) : elle s'exécute une fois, son nom désigne les lignes rendues. Voir 35. Langage DML.

Expressions récursives#

WITH RECURSIVE définit une expression qui se cite elle-même : un terme de départ, UNION ALL (ou UNION, qui écarte les lignes déjà produites), puis un terme récursif.

WITH RECURSIVE r (n) AS (
    SELECT 1
    UNION ALL
    SELECT n + 1 FROM r WHERE n < 5
)
SELECT n FROM r ORDER BY n;                                   -- 1, 2, 3, 4, 5

Parcours d'une hiérarchie :

CREATE TABLE categorie (
    id        integer PRIMARY KEY,
    parent_id integer REFERENCES categorie (id),
    nom       text NOT NULL
);
INSERT INTO categorie VALUES
    (1, NULL, 'Papeterie'), (2, 1, 'Écriture'), (3, 1, 'Classement'), (4, 2, 'Stylos'), (5, 2, 'Crayons');

WITH RECURSIVE arbre (id, nom, niveau, chemin) AS (
    SELECT id, nom, 0, nom FROM categorie WHERE parent_id IS NULL
    UNION ALL
    SELECT c.id, c.nom, a.niveau + 1, a.chemin || ' > ' || c.nom
    FROM categorie c JOIN arbre a ON c.parent_id = a.id
)
SELECT id, niveau, chemin FROM arbre ORDER BY chemin;
 id | niveau | chemin
----+--------+--------------------------------
 1  | 0      | Papeterie
 3  | 1      | Papeterie > Classement
 2  | 1      | Papeterie > Écriture
 5  | 2      | Papeterie > Écriture > Crayons
 4  | 2      | Papeterie > Écriture > Stylos

À connaître :

  • La récursion est bornée à 1 000 itérations par la variable cte_max_recursion_depth (SHOW cte_max_recursion_depth, SET cte_max_recursion_depth TO 100000). Au-delà :
    ERREUR 54000 : Recursive query aborted after 1001 iterations. Try increasing
    @@cte_max_recursion_depth to a larger value.
  • Une récursion sans condition d'arrêt n'est pas arrêtée par LIMIT : l'expression est calculée en entier avant la requête principale. WITH RECURSIVE r (n) AS (SELECT 1 UNION ALL SELECT n + 1 FROM r) SELECT n FROM r LIMIT 3 rend l'erreur ci-dessus (PostgreSQL rend trois lignes). Écrire la condition d'arrêt dans le terme récursif (WHERE n < 3).
  • Avec UNION, la récursion s'arrête d'elle-même quand plus aucune ligne nouvelle n'apparaît (détection de cycle simple).
  • Les clauses SEARCH et CYCLE ne sont pas reconnues (42601).
  • WITH RECURSIVE n'est pas admis dans une vue (0A000 : WITH RECURSIVE in a stored definition is not supported yet in the PostgreSQL language).

36.10 Fonctions de fenêtrage#

Une fonction de fenêtrage calcule, pour chaque ligne, une valeur sur un ensemble de lignes voisines (la fenêtre), sans regrouper le résultat.

fonction(...) [FILTER (WHERE condition)] OVER (
    [PARTITION BY expr [, ...]]
    [ORDER BY expr [ASC | DESC] [, ...]]
    [{ROWS | RANGE} BETWEEN borne AND borne]
)
fonction(...) OVER nom_de_fenêtre          -- avec WINDOW nom AS (...)
FonctionsRôle
row_number(), rank(), dense_rank()numéro, rang avec trous, rang sans trou
ntile(n), percent_rank(), cume_dist()répartition
lag(expr [, décalage [, défaut]]), lead(…)valeur d'une ligne précédente ou suivante
first_value(expr), last_value(expr), nth_value(expr, n)valeur d'une ligne du cadre
count, sum, avg, min, max, stddev, variance… avec OVERagrégat sur la fenêtre

Rangs#

SELECT nom, ville, solde,
       row_number() OVER (ORDER BY solde DESC) AS rang,
       rank() OVER (PARTITION BY ville ORDER BY solde DESC) AS rang_ville
FROM client ORDER BY rang;
 nom    | ville       | solde  | rang | rang_ville
--------+-------------+--------+------+------------
 David  | Constantine | 250.00 | 1    | 1
 Alice  | Oran        | 100.50 | 2    | 1
 Émile  | Alger       | 75.25  | 3    | 1
 Fatima | NULL        | 10.00  | 4    | 1
 Chloé  | Oran        | 0.00   | 5    | 2
 Bob    | Alger       | -20.00 | 6    | 2

Une fenêtre nommée par la clause WINDOW sert à plusieurs fonctions ; une fenêtre peut en compléter une autre (WINDOW w AS (PARTITION BY etat), w2 AS (w ORDER BY id)).

SELECT etat, id, rank() OVER w AS r, dense_rank() OVER w AS dr, row_number() OVER w AS rn
FROM commande WINDOW w AS (ORDER BY etat) ORDER BY id;
 etat    | id | r | dr | rn
---------+----+---+----+----
 livrée  | 1  | 2 | 2  | 2
 ouverte | 2  | 4 | 3  | 4
 livrée  | 3  | 2 | 2  | 3
 ouverte | 4  | 4 | 3  | 5
 annulée | 5  | 1 | 1  | 1

Agrégats sur une fenêtre#

Avec ORDER BY, le cadre par défaut va du début de la partition à la ligne courante : l'agrégat est un cumul. OVER () porte sur tout le résultat. ROWS BETWEEN … AND … fixe le cadre en nombre de lignes (UNBOUNDED PRECEDING, n PRECEDING, CURRENT ROW, n FOLLOWING, UNBOUNDED FOLLOWING).

SELECT co.id, t.montant,
       sum(t.montant) OVER (ORDER BY co.date_cmd) AS cumul,
       round(100 * t.montant / sum(t.montant) OVER (), 1) AS part
FROM commande co
JOIN (SELECT commande_id, sum(total) AS montant FROM ligne_commande GROUP BY commande_id) t
  ON t.commande_id = co.id
ORDER BY co.date_cmd;
 id | montant | cumul  | part
----+---------+--------+------
 1  | 19.00   | 19.00  | 10.4
 2  | 6.90    | 25.90  | 3.8
 3  | 126.00  | 151.90 | 68.7
 4  | 31.50   | 183.40 | 17.2
SELECT id, avg(id) OVER (ORDER BY date_cmd ROWS BETWEEN 1 PRECEDING AND 1 FOLLOWING) AS moy_glissante
FROM commande ORDER BY date_cmd;
-- 1.5, 2.0, 3.0, 4.0, 4.5 (à seize décimales)

SELECT id, count(*) FILTER (WHERE etat = 'livrée') OVER (ORDER BY id) AS livrees
FROM commande ORDER BY id;                                    -- 1, 1, 2, 2, 2

Une fonction de fenêtrage peut porter sur un agrégat d'un GROUP BY :

SELECT ville, count(*) AS n, rank() OVER (ORDER BY count(*) DESC) AS rang
FROM client GROUP BY ville ORDER BY rang, ville;
-- Alger 2 1, Oran 2 1, Constantine 1 3, NULL 1 3

Lignes voisines#

SELECT id, date_cmd,
       lag(date_cmd) OVER (ORDER BY date_cmd) AS precedente,
       date_cmd - lag(date_cmd) OVER (ORDER BY date_cmd) AS jours,
       lead(id, 1, 0) OVER (ORDER BY date_cmd) AS suivante
FROM commande ORDER BY date_cmd;
 id | date_cmd   | precedente | jours | suivante
----+------------+------------+-------+----------
 1  | 2026-09-01 | NULL       | NULL  | 2
 2  | 2026-09-15 | 2026-09-01 | 14    | 3
 3  | 2026-09-20 | 2026-09-15 | 5     | 4
 4  | 2026-10-02 | 2026-09-20 | 12    | 5
 5  | 2026-10-05 | 2026-10-02 | 3     | 0
SELECT code, prix,
       first_value(code) OVER w AS moins_cher,
       last_value(code) OVER (ORDER BY prix ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING) AS plus_cher,
       nth_value(code, 2) OVER w AS deuxieme
FROM article WINDOW w AS (ORDER BY prix) ORDER BY prix;
 code  | prix  | moins_cher | plus_cher | deuxieme
-------+-------+------------+-----------+----------
 GO-01 | 0.80  | GO-01      | AG-01     | NULL
 ST-01 | 1.20  | GO-01      | AG-01     | ST-01
 TR-01 | 2.10  | GO-01      | AG-01     | ST-01
 CA-01 | 3.50  | GO-01      | AG-01     | ST-01
 CL-01 | 6.90  | GO-01      | AG-01     | ST-01
 AG-01 | 14.00 | GO-01      | AG-01     | ST-01

(last_value exige un cadre étendu jusqu'à la fin, le cadre par défaut s'arrêtant à la ligne courante ; nth_value(code, 2) est NULL tant que le cadre n'a qu'une ligne.)

Premier de chaque groupe#

Une fonction de fenêtrage n'est pas admise dans WHERE (42803) : on filtre sur son résultat au travers d'une table dérivée (ou par SELECT DISTINCT ON, voir 36.2, pour le premier seul).

SELECT ville, nom, solde
FROM (SELECT ville, nom, solde,
             row_number() OVER (PARTITION BY ville ORDER BY solde DESC) AS rn
      FROM client) x
WHERE rn = 1
ORDER BY ville;
 ville       | nom    | solde
-------------+--------+--------
 Alger       | Émile  | 75.25
 Constantine | David  | 250.00
 Oran        | Alice  | 100.50
 NULL        | Fatima | 10.00

Avec rank() à la place de row_number(), les ex æquo de chaque groupe sont tous rendus.

Limites#

FormeRéponse
RANGE BETWEEN n PRECEDING AND … (cadre RANGE avec un décalage)0A000 : This version of Miraj doesn't yet support 'RANGE with an offset in a window frame'
cadre GROUPS, clause EXCLUDE42601
string_agg(…) OVER (…)0A000
fonction de fenêtrage dans WHERE42803 : Invalid use of group function

36.11 Verrous de lecture : FOR UPDATE, FOR SHARE#

Dans une transaction, une lecture verrouillante retient les lignes lues jusqu'au COMMIT ou au ROLLBACK, pour les modifier ensuite sans qu'une autre session ne les change entre-temps.

SELECT ... FOR {UPDATE | NO KEY UPDATE | SHARE | KEY SHARE} [OF table [, ...]] [NOWAIT | SKIP LOCKED]
BEGIN;
SELECT id, stock FROM article WHERE id = 1 FOR UPDATE;      -- 1 | 500 : la ligne est retenue
UPDATE article SET stock = stock - 10 WHERE id = 1;
COMMIT;
ClauseEffet
FOR UPDATE, FOR NO KEY UPDATEverrou exclusif : une autre session ne peut ni verrouiller ni modifier ces lignes
FOR SHARE, FOR KEY SHAREverrou partagé : d'autres sessions peuvent le prendre aussi (FOR SHARE), aucune ne peut prendre FOR UPDATE ni modifier
OF tabledans une jointure, ne verrouille que les lignes de la table citée (par son alias)
NOWAITéchoue tout de suite si une ligne est retenue par une autre session
SKIP LOCKEDsaute les lignes retenues par une autre session

Comportement constaté entre deux sessions, la session A tenant FOR UPDATE sur l'article 1 :

Session BRésultat
SELECT … WHERE id = 1 (lecture simple)rend la ligne aussitôt : une lecture ordinaire n'attend jamais
SELECT … WHERE id = 1 FOR UPDATE NOWAIT55P03 aussitôt : Lock wait timeout exceeded; try restarting transaction
SELECT … WHERE id = 1 FOR SHARE NOWAIT55P03 aussitôt
SELECT … WHERE id <= 3 ORDER BY id FOR UPDATE SKIP LOCKEDrend les lignes 2 et 3
SELECT … WHERE id = 1 FOR UPDATEattend la fin de la transaction de A, puis rend la ligne ; 55P03 au bout de lock_wait_timeout secondes
UPDATE article … WHERE id = 1attend de même ; 55P03 au bout de lock_wait_timeout

L'attente maximale se règle par session : SET lock_wait_timeout = 2 (en secondes). Le SQLSTATE 55P03 est celui de PostgreSQL pour NOWAIT ; il sert aussi ici à l'attente expirée.

SKIP LOCKED permet à plusieurs sessions de se partager une file de travail :

BEGIN;
SELECT id FROM commande WHERE etat = 'ouverte' ORDER BY id LIMIT 1 FOR UPDATE SKIP LOCKED;
-- traiter la commande rendue, puis :
UPDATE commande SET etat = 'livrée' WHERE id = 2;
COMMIT;

Hors d'un bloc de transaction, la clause est acceptée mais le verrou est rendu dès la fin de l'instruction : elle n'a d'intérêt qu'entre BEGIN et COMMIT. Le modèle de concurrence, les conflits de validation et les réessais sont décrits au chapitre 38. Transactions et session.


36.12 Lire une vue#

Une vue (voir 34. Langage DDL) se lit comme une table, dans toutes les formes de ce chapitre : jointures, regroupements, fenêtres, sous-requêtes.

CREATE VIEW v_ca AS
SELECT c.id, c.nom, sum(l.total) AS ca
FROM client c
JOIN commande co ON co.client_id = c.id
JOIN ligne_commande l ON l.commande_id = co.id
GROUP BY c.id, c.nom;

SELECT nom, ca, rank() OVER (ORDER BY ca DESC) AS rang FROM v_ca ORDER BY rang;
 nom   | ca     | rang
-------+--------+------
 Bob   | 126.00 | 1
 Chloé | 31.50  | 2
 Alice | 25.90  | 3

La requête d'une vue admet WITH (non récursif), ORDER BY et LIMIT. Elle n'admet pas FULL JOIN, LATERAL ni WITH RECURSIVE (0A000 : … in a stored definition is not supported yet in the PostgreSQL language), qui restent utilisables dans une requête directe.


36.13 Extensions MIRAJ dans un SELECT#

Ces formes ne sont pas du PostgreSQL ; elles viennent du langage MIRAJ et restent acceptées. Un script qui doit aussi s'exécuter sur un serveur PostgreSQL ne doit pas les employer.

FormeEffet
QUALIFY conditionfiltre sur le résultat d'une fonction de fenêtrage, sans table dérivée
GROUP BY a, b WITH ROLLUPéquivalent de GROUP BY ROLLUP (a, b)
alias de la liste dans HAVINGHAVING ecart > 0 où ecart est un alias (PostgreSQL le refuse)
x COLLATE utf8mb4_general_cicomparaison et tri sans casse ni accents (36.4)
MATCH (colonnes) AGAINST ('mots')recherche plein texte, sur un index créé par CREATE FULLTEXT INDEX (voir 9.22) ; les mots sont comparés selon la collation de la colonne, donc en respectant la casse
SELECT ville, nom, solde FROM client
QUALIFY row_number() OVER (PARTITION BY ville ORDER BY solde DESC) = 1
ORDER BY ville;
-- même résultat que « Premier de chaque groupe » (35.10)

CREATE FULLTEXT INDEX ft_lib ON article (libelle);
SELECT code, libelle FROM article WHERE MATCH (libelle) AGAINST ('pages');    -- CA-01 | Cahier 96 pages
SELECT code, libelle FROM article WHERE MATCH (libelle) AGAINST ('cahier');   -- aucune ligne : « Cahier »

36.14 EXPLAIN#

EXPLAIN affiche le plan d'exécution d'une requête sans l'exécuter. Le plan est celui de MIRAJ, dans sa propre présentation : un tableau d'une ligne par table lue, et non l'arbre de nœuds de PostgreSQL. Les outils qui analysent la sortie d'EXPLAIN de PostgreSQL ne la liront pas.

EXPLAIN SELECT c.nom, co.id FROM client c JOIN commande co ON co.client_id = c.id WHERE c.ville = 'Oran';
 id | select_type | table | type | possible_keys | key  | key_len | ref  | rows | Extra
----+-------------+-------+------+---------------+------+---------+------+------+-------------------------------
 1  | SIMPLE      | c     | ALL  | PRIMARY,email | NULL | NULL    | NULL | 6    | Using where
 1  | SIMPLE      | co    | ALL  | PRIMARY       | NULL | NULL    | NULL | 5    | Using join buffer (hash join)
EXPLAIN SELECT * FROM client WHERE id = 1;
 id | select_type | table  | type   | possible_keys | key     | key_len | ref   | rows | Extra
----+-------------+--------+--------+---------------+---------+---------+-------+------+-------
 1  | SIMPLE      | client | eq_ref | PRIMARY,email | PRIMARY | NULL    | const | 1    |

type indique le mode d'accès (ALL : lecture de toute la table ; eq_ref : une ligne par clé unique), key l'index retenu, rows l'estimation du nombre de lignes lues. La lecture détaillée d'un plan est décrite aux sections 9.20 et 9.21 du manuel général.

FormeRésultat
EXPLAIN requêtetableau ci-dessus
EXPLAIN (FORMAT JSON) requêtele même plan en un document JSON (une colonne EXPLAIN)
EXPLAIN ANALYZE requêteexécute la requête et rend l'arbre des opérations avec temps et lignes réels
EXPLAIN (ANALYZE, VERBOSE, COSTS off, BUFFERS) requêteoptions acceptées ; même sortie que EXPLAIN ANALYZE
EXPLAIN (FORMAT XML), EXPLAIN (FORMAT YAML)0A000 : EXPLAIN (FORMAT XML | YAML) is not supported yet in the PostgreSQL language
EXPLAIN ANALYZE SELECT * FROM client WHERE ville = 'Oran';
-> Project  (actual time=0.010..0.011 rows=2 loops=1)
    -> Table scan on client with pushed-down filter  (rows=6)  (actual time=0.009..0.009 rows=2 loops=1)

L'étiquette rendue est EXPLAIN. EXPLAIN s'applique aussi à UPDATE et DELETE (voir 35.9).


36.15 Non pris en charge et écarts#

Formes refusées#

Forme PostgreSQLRéponse de MIRAJÀ la place
LIMIT d'une expression (LIMIT 1 + 1), LIMIT -142601entier écrit ou paramètre
LIMIT 10, 5 (forme du langage MIRAJ)42601 : LIMIT offset, count is MIRAJ syntax, not accepted in the PostgreSQL languageLIMIT 5 OFFSET 10
a && b, a || b comme opérateurs logiques42601 pour && ; || concatèneAND, OR
table dérivée sans alias : FROM (SELECT …)42601 : syntax error at end of inputdonner un alias
alias d'une jointure : JOIN … USING (id) AS j42601
NATURAL CROSS JOIN42601
LATERAL avec RIGHT ou FULL JOIN, USING, NATURAL0A000 : … 'LATERAL with RIGHT or FULL JOIN, USING or NATURAL'
generate_series dont les bornes citent une autre table42703
unnest d'une colonne, ARRAY(SELECT …), colonnes de type tableau0A000array_agg, string_agg, json_agg
TABLE client42601SELECT * FROM client
SELECT … INTO nouvelle_table42000 : Undeclared variable: copieCREATE TABLE … AS SELECT …
TABLESAMPLE42601
SIMILAR TO42601~
ORDER BY … USING opérateur42601ASC, DESC
ORDER BY 3 NULLS LAST quand le rang désigne une colonne de *0A000 : NULLS FIRST | LAST on this column position is not supported yet in the PostgreSQL languagenommer la colonne
(a, b) = (SELECT …, …)42601 : Operand should contain 1 column(s)(a, b) IN (SELECT …, …)
WITH … SEARCH, WITH … CYCLE42601colonne de chemin calculée dans la récursion
WITH qui écrit (WITH x AS (INSERT … RETURNING …)) ailleurs qu'en tête de l'instruction0A000l'écrire en tête, ou deux instructions dans une transaction
WITH RECURSIVE, FULL JOIN, LATERAL dans une vue0A000requête directe
cadre RANGE avec décalage, GROUPS, EXCLUDE ; string_agg … OVER0A000, 42601 (36.10)cadre ROWS
EXPLAIN (FORMAT XML | YAML)0A000FORMAT JSON

Écarts de comportement#

SujetMIRAJPostgreSQL
identifiants entre guillemetsinsensibles à la cassesensibles
nom d'une colonne calculée sans aliastexte écrit (count(*), solde * 2)count, ?column?
alias de la liste dans HAVINGacceptérefusé
SELECT DISTINCT a … ORDER BY b (clé absente de la liste)acceptérefusé
WITH RECURSIVE sans condition d'arrêt, avec LIMITerreur à 1 000 itérations (cte_max_recursion_depth)s'arrête à LIMIT
generate_seriesau plus 10 000 000 de valeurs, erreur 54000 au-delàsans limite
FOR UPDATE hors transaction, ou sur une requête d'agrégatacceptésans effet utile, refusé avec un agrégat
attente d'un verrou expirée55P03 après lock_wait_timeoutattente sans limite par défaut
EXPLAINplan et présentation de MIRAJarbre de nœuds PostgreSQL
avg d'entiers ou de décimauxnumeric à seize décimaleséchelle variable selon la valeur

La liste condensée de ces points est tenue au chapitre 30 (section 30.2).

SQLSTATE rencontrés dans ce chapitre#

SQLSTATESensExemple
0A000non pris en chargeFULL JOIN dans une vue
21000sous-requête scalaire à plusieurs lignesSELECT (SELECT nom FROM client)
22023argument invalidepas nul de generate_series, percentile hors de 0..1
22P02écriture invalidetexte non numérique comparé à un nombre ; texte illisible dans le type d'une union
42601syntaxeformes non reconnues ci-dessus ; nombres de colonnes différents dans un UNION
42702colonne ambiguëWHERE id = 1 dans une jointure de deux tables qui ont id
42703colonne inconnuealias de la liste cité dans WHERE ; ORDER BY d'un rang hors de la liste
42803regroupementcolonne hors GROUP BY, fonction de fenêtrage dans WHERE
42P01table inconnue
42P10liste incohérentetrop d'alias de colonnes ; DISTINCT ON qui ne commence pas l'ORDER BY
54000limite atteinterécursion au-delà de cte_max_recursion_depth, generate_series de plus de 10 000 000 de valeurs
55P03verrou non obtenuNOWAIT, attente expirée