7. SELECT Queries
This chapter describes the SELECT statement: select list, FROM and joins, WHERE, GROUP BY / HAVING, ORDER BY, LIMIT / OFFSET, subqueries and UNION. Sections 7.12 to 7.20 complete this foundation: full grammar, joins in practice, common table expressions, window functions, locks, optimizer hints and reading EXPLAIN. Every statement in this chapter was verified in the parser, in the executor and by execution; what does not exist is marked not supported.
The examples rely on two tables, populated once for the whole chapter:
CREATE TABLE clients (
id INT AUTO_INCREMENT PRIMARY KEY,
nom VARCHAR(50) NOT NULL,
ville VARCHAR(50),
solde DECIMAL(10,2)
);
INSERT INTO clients (nom, ville, solde) VALUES
('Alice', 'Oran', 100.50),
('Bob', 'Alger', -20.00),
('Chloé', 'Oran', 0.00),
('David', 'Constantine', NULL),
('Émile', 'Alger', 75.25),
('Fatima', NULL, 10.00);
CREATE TABLE commandes (
id INT PRIMARY KEY,
client_id INT,
montant DECIMAL(10,2)
);
INSERT INTO commandes VALUES
(1, 1, 10.00), (2, 1, 20.50), (3, 2, 5.00), (4, 9, 1.00), (5, 3, NULL);7.1 Synopsis#
SELECT [DISTINCT] item [, item ...]
[FROM table [jointure ...]]
[WHERE condition]
[GROUP BY expr [, expr ...]]
[HAVING condition]
[ORDER BY expr [ASC|DESC] [, ...]]
[LIMIT n [OFFSET m] | LIMIT m, n]This synopsis is the basic form; the full grammar (WITH, UNION, locks, OVER, etc.) is given in §7.12.
7.2 Select list#
Each item is an expression, with an optional alias (AS alias, or an alias without AS):
SELECT nom, solde AS balance, solde * 1.1 marge FROM clients;
SELECT * FROM clients; -- all columns
SELECT clients.* , 1 AS actif FROM clients; -- all columns of a named table, plus an expressionDISTINCT#
DISTINCT eliminates result rows that are identical on all selected items (a NULL value is considered equal to another NULL for this elimination, unlike = in a WHERE):
SELECT DISTINCT ville FROM clients ORDER BY ville;
SELECT DISTINCT ville, solde FROM clients WHERE ville IS NOT NULL ORDER BY ville, solde;COUNT(DISTINCT expr) counts distinct values rather than rows:
SELECT COUNT(DISTINCT ville) FROM clients;7.3 FROM and joins#
Supported join types#
| Syntax | Semantics |
|---|---|
FROM a, b or a CROSS JOIN b | Cartesian product: each row of a with each row of b. |
a [INNER] JOIN b ON cond | Keeps only the pairs of rows that satisfy cond. |
a LEFT [OUTER] JOIN b ON cond | All rows of a; the columns of b are NULL when no row of b satisfies cond. |
a RIGHT [OUTER] JOIN b ON cond | Mirror of LEFT JOIN: all rows of b, columns of a set to NULL when there is no match. |
a JOIN b USING (col, ...) | Equivalent to ON a.col = b.col AND .... The column named in USING can be written without a qualifier in the rest of the query, but SELECT * returns it once per table (unlike MariaDB, which merges it). |
a STRAIGHT_JOIN b | Treated as a JOIN b (inner join); the join order is not imposed on the planner. |
INNER JOIN, LEFT JOIN, RIGHT JOIN and CROSS JOIN can all carry an alias on each table, and chain several joins in a single FROM.
SELECT c.nom, o.montant FROM clients c JOIN commandes o ON o.client_id = c.id;
SELECT c.nom, COUNT(o.id) FROM clients c LEFT JOIN commandes o ON o.client_id = c.id
GROUP BY c.id, c.nom ORDER BY c.id;
SELECT c.nom, o.id, o.montant FROM clients c RIGHT JOIN commandes o ON o.client_id = c.id
ORDER BY o.id; -- all orders, including those without an existing client (id = 9)
SELECT nom, montant FROM clients JOIN commandes USING (id) ORDER BY id;Self-join#
A table can be joined to itself; each occurrence needs a distinct alias:
SELECT a.nom, b.nom
FROM clients a
JOIN clients b ON a.ville = b.ville AND a.id < b.id;Derived table (subquery in FROM)#
A subquery can replace a table in FROM or in a join; it needs an alias:
SELECT t.ville, t.total
FROM (
SELECT ville, SUM(solde) AS total
FROM clients
GROUP BY ville
) t
WHERE t.total > 0;Not supported#
NATURAL JOIN(in any form) is rejected by the parser (error 1235): join explicitly withONorUSINGinstead.- There is no
FULL [OUTER] JOIN; an equivalent can be built with aLEFT JOINand aRIGHT JOINcombined byUNION(see §7.9):SELECT c.nom, o.id FROM clients c LEFT JOIN commandes o ON o.client_id = c.id UNION SELECT c.nom, o.id FROM clients c RIGHT JOIN commandes o ON o.client_id = c.id;
7.4 WHERE#
WHERE filters the rows produced by FROM before any grouping. Available operators:
| Category | Operators |
|---|---|
| Comparison | =, <> (or !=), <, <=, >, >= |
| Logic | AND, OR, NOT |
| Sets | [NOT] IN (value, ...), [NOT] IN (subquery) |
| Range | [NOT] BETWEEN value AND value |
| Text pattern | [NOT] LIKE pattern (%: any sequence of characters, _: one character), [NOT] REGEXP / RLIKE pattern (regular expression) |
| Absence of value | IS [NOT] NULL |
| Existence | [NOT] EXISTS (subquery) |
SELECT nom FROM clients WHERE solde BETWEEN 0 AND 100 AND ville IN ('Oran', 'Alger');
SELECT nom FROM clients WHERE nom LIKE '_h%' OR nom REGEXP '^[A-E]';
SELECT nom FROM clients WHERE ville IS NULL;Three-valued logic and NULL#
A comparison with NULL (including NULL = NULL) is never TRUE or FALSE, but UNKNOWN; a row is retained by WHERE only if its condition is TRUE (UNKNOWN, like FALSE, excludes the row). Testing for the absence of a value therefore requires IS NULL / IS NOT NULL, never = NULL:
SELECT nom FROM clients WHERE solde = NULL; -- never returns anything
SELECT nom FROM clients WHERE solde IS NULL; -- DavidChapter 9 (§9.5, "Three-valued logic") details this behavior and its usual pitfalls (NOT IN with a NULL in the list, NOT on an UNKNOWN condition, aggregates that ignore NULLs).
7.5 GROUP BY and HAVING#
GROUP BY groups the rows that share the same values of the listed expressions; the aggregate functions (COUNT, SUM, AVG, MIN, MAX, GROUP_CONCAT, ...) are then computed per group. HAVING filters groups, just as WHERE filters rows (and can therefore apply to an aggregate, which WHERE cannot).
SELECT ville, COUNT(*) FROM clients GROUP BY ville ORDER BY ville;
SELECT ville, COUNT(*) c FROM clients GROUP BY ville HAVING c > 1 ORDER BY ville;Column referenced outside GROUP BY#
MIRAJ's SQL mode does not, by default, include the equivalent of the ONLY_FULL_GROUP_BY restriction: a column of the select list that is neither an aggregate nor named in GROUP BY is accepted, and returns a value from one of the rows of the group (which one is not guaranteed; enabling it in sql_mode restores error 1055). Example:
SELECT ville, nom, COUNT(*) FROM clients GROUP BY ville;Here nom is neither aggregated nor in GROUP BY: for the 'Oran' group (Alice and Chloé), the value of nom returned is that of one of the two rows of the group, with no guarantee as to which. Use this form only when the column is in fact constant within each group (for example a column functionally dependent on the grouped column), otherwise the result depends on the internal order of the rows.
Name resolution in GROUP BY, HAVING and ORDER BY#
A name without a table qualifier (ville, not clients.ville) can designate either a column of a table in the FROM or an alias from the SELECT list. The resolution rule differs depending on the clause:
GROUP BY: a real column of a table being read takes precedence over an alias of the same name, as long as it exists unambiguously among the tables of theFROM. The alias is used only if no column of that name exists, or to resolve an ambiguity between several tables that each have a column of that name.HAVINGandORDER BY: conversely, an alias from theSELECTlist takes precedence over a table column of the same name.- In all cases, a qualified name (
t.col) is always a table column, never an alias.
-- the alias `nom` hides the real column `ville` for ORDER BY, but GROUP BY groups on the
-- real column `nom` of clients: each client has a distinct name, hence one group per row,
-- not one group per city as grouping on the alias would give
SELECT UPPER(ville) AS nom, COUNT(*) FROM clients WHERE ville IS NOT NULL GROUP BY nom ORDER BY nom;A name carried by several tables of the FROM is ambiguous (error 1052) unless it unambiguously designates an alias from the SELECT list, or is qualified.
7.6 GROUP_CONCAT#
GROUP_CONCAT([DISTINCT] expr [, expr ...] [ORDER BY clé [ASC|DESC] [, ...]] [SEPARATOR 'chaîne'])Concatenates, for each group, the values of expr (NULL values are ignored), in the order given by the ORDER BY clause internal to GROUP_CONCAT (independent of any ORDER BY of the query), separated by SEPARATOR (, by default). DISTINCT eliminates repeated values before concatenation.
SELECT GROUP_CONCAT(nom) FROM clients WHERE ville = 'Oran';
-- Alice,Chloé
SELECT GROUP_CONCAT(nom ORDER BY nom DESC) FROM clients WHERE ville = 'Oran';
-- Chloé,Alice
SELECT ville, GROUP_CONCAT(DISTINCT id ORDER BY id DESC SEPARATOR ' / ') FROM clients
GROUP BY ville ORDER BY ville;7.7 ORDER BY, LIMIT and OFFSET#
ORDER BY sorts the result by one or more expressions, each ASC (default) or DESC; a numeric position (ORDER BY 1) designates the corresponding item of the select list.
SELECT nom, solde FROM clients ORDER BY solde DESC, nom;LIMIT bounds the number of rows returned; OFFSET (or the form LIMIT offset, count) skips the first rows of the sorted result:
SELECT nom FROM clients ORDER BY solde DESC LIMIT 3;
SELECT nom FROM clients ORDER BY solde DESC LIMIT 1 OFFSET 4;
SELECT nom FROM clients ORDER BY solde DESC LIMIT 4, 1; -- equivalent: LIMIT 1 OFFSET 47.8 Subqueries#
A subquery (a parenthesized SELECT) can appear:
- as a scalar value, anywhere an expression is expected (it must return at most one row and one column; more than one row at execution time is the "single-row subquery" error);
- in an
[NOT] IN (subquery)test; - in an
[NOT] EXISTS (subquery)test; - as a derived table in
FROM(see §7.3).
A subquery can be correlated: it references a column of a table of the outer query and is then re-evaluated for each row of the latter.
-- scalar, correlated
SELECT nom, solde FROM clients c WHERE solde = (
SELECT MAX(solde) FROM clients v WHERE v.ville = c.ville
);
-- EXISTS / NOT EXISTS, correlated
SELECT id FROM commandes o WHERE NOT EXISTS (
SELECT 1 FROM clients c WHERE c.id = o.client_id
);
-- IN with subquery
DELETE FROM commandes WHERE client_id NOT IN (SELECT id FROM clients);
-- subquery in the select list
SELECT c.nom, (SELECT COUNT(*) FROM commandes o WHERE o.client_id = c.id) AS nb
FROM clients c ORDER BY c.id;7.9 UNION#
requête_select UNION [ALL] requête_select [UNION [ALL] requête_select ...]
[ORDER BY ...] [LIMIT ...]UNION combines the results of several SELECTs with the same number of columns; UNION (without ALL) eliminates duplicate rows from the combined result, UNION ALL keeps them all.
SELECT ville FROM clients WHERE id < 4
UNION
SELECT ville FROM clients WHERE id > 3
ORDER BY ville;
SELECT client_id FROM commandes
UNION ALL
SELECT id FROM clients WHERE id > 5
ORDER BY 1 DESC LIMIT 3;A final ORDER BY or LIMIT, after the last member, applies to the combined result of the entire union, not just to the last SELECT; it can reference a column of a member qualified by its original table name (ORDER BY clients.id) as well as by its position or its alias. Each member can be parenthesized to give it its own ORDER BY / LIMIT, applied before the combination:
(SELECT nom FROM clients ORDER BY id DESC LIMIT 1)
UNION
(SELECT nom FROM clients ORDER BY id LIMIT 1);An ORDER BY or LIMIT placed on a non-parenthesized member followed by a UNION is rejected by the grammar (syntax error 1064): parenthesize the member to resolve the ambiguity between "sort of this member" and "sort of the entire union".
7.10 Using a view#
A view is read exactly like a table, in FROM, a join or a subquery:
SELECT * FROM v_clients_actifs WHERE ville = 'Oran';The creation, modification and refresh of views (ordinary view or CACHED) are described in chapter 5, "DDL Language".
7.11 Features not available in this version#
Based on a check of the parser and executor code (the project's readme.txt file is out of date on several of these points):
| Feature | Status |
|---|---|
Subqueries (scalar, IN, EXISTS, correlated, derived table) | Available. |
UNION / UNION ALL | Available, with ORDER BY / LIMIT per member or for the entire union (§7.9). |
RIGHT JOIN | Available, like INNER JOIN and LEFT JOIN (§7.3). |
Window functions (OVER (PARTITION BY ... ORDER BY ...)) | Available. |
Common table expressions (WITH name AS (...)) | Available, except for WITH RECURSIVE (not supported). |
NATURAL JOIN | Not supported (error 1235); write the condition with ON or USING. |
FULL [OUTER] JOIN | Not supported; combine a LEFT JOIN and a RIGHT JOIN with UNION (§7.3 and §7.13). |
WITH RECURSIVE | Not supported (error 1235, §7.15). |
INTERSECT, EXCEPT, MINUS | Not supported (syntax error 1064, §7.14). |
GROUP BY ... WITH ROLLUP, ROLLUP(), CUBE(), GROUPING SETS | Not supported (WITH ROLLUP: error 1235; ROLLUP(...) and CUBE(...): error 1305 "FUNCTION ... does not exist"; GROUPING SETS: error 1064, §7.17). |
LATERAL | Not supported (syntax error 1064, §7.13). |
x op ANY / SOME / ALL (subquery) | Not supported (syntax error 1064, §7.16). |
Row constructor (a, b) IN (...) | Not supported (syntax error 1064, §7.16). |
WINDOW name AS (...) clause, OVER name | Not supported (syntax error 1064, §7.18). |
Index hints USE / FORCE / IGNORE INDEX | Not supported (syntax error 1064, §7.19). |
VALUES (...) as a table in FROM | Not supported (syntax error 1064); VALUES is accepted as the body of a common table expression (§7.15). |
Non-unique secondary indexes on an ordinary column, full EXPLAIN | Partial support depending on the version: see chapter 5 (DDL) and roadmap.md for the precise status. |
This table reflects the state of the code at the time of writing; refer to chapter 16 ("Known limitations") for the up-to-date list of features outside DML/SELECT (transactions, privileges, networking, etc.).
7.12 Full grammar#
SELECT grammar as accepted by the parser ([ ]: optional, |: alternative, { }: at least one element):
requête ::=
[ WITH nom [(col, ...)] AS ( select | VALUES (expr, ...), ... ) [, ...] ]
membre [ UNION [ALL | DISTINCT] membre ... ]
[ ORDER BY expr [ASC|DESC] [, ...] ] -- after the last member: applies to the union
[ LIMIT ... ] -- same
membre ::= select | ( select )
select ::=
SELECT [ /*+ indications */ ] [ DISTINCT | DISTINCTROW | ALL ]
[ SQL_CALC_FOUND_ROWS | SQL_NO_CACHE | SQL_CACHE | HIGH_PRIORITY | STRAIGHT_JOIN ] ...
item [, item ...]
[ INTO @variable [, ...] | INTO OUTFILE 'file' ... | INTO DUMPFILE 'file' ]
[ FROM DUAL | source [ jointure ... ] ]
[ WHERE condition ]
[ GROUP BY expr [ASC|DESC] [, ...] ]
[ HAVING condition ]
[ ORDER BY expr [ASC|DESC] [, ...] ]
[ LIMIT { n | décalage, n | n OFFSET décalage } [ROWS EXAMINED m] ]
[ FOR UPDATE | FOR SHARE [OF table, ...] [NOWAIT | WAIT s | SKIP LOCKED]
| LOCK IN SHARE MODE [NOWAIT | WAIT s | SKIP LOCKED] ]
item ::= * | table.* | base.table.* | expr [[AS] alias]
source ::= table [PARTITION (p, ...)] [[AS] alias] | ( select ) [AS] alias | ( source jointure ... )
jointure ::=
, source
| [INNER | CROSS] JOIN source [ON condition | USING (col, ...)]
| STRAIGHT_JOIN source [ON condition | USING (col, ...)]
| LEFT [OUTER] JOIN source { ON condition | USING (col, ...) }
| RIGHT [OUTER] JOIN source { ON condition | USING (col, ...) }Details verified in the parser:
- A
JOINwithoutONorUSINGis a Cartesian product; aLEFTorRIGHT JOINwithoutONorUSINGis a syntax error (1064). SELECTwithoutFROM(orFROM DUAL) is allowed; the wordDUALis recognized only in place of the first table.SQL_CALC_FOUND_ROWS,SQL_NO_CACHE,SQL_CACHEandHIGH_PRIORITYare accepted and have no effect on the result;DISTINCTROWis equivalent toDISTINCT.- A parenthesized group of joins at the start of
FROM(FROM (a LEFT JOIN b ON ...)) is accepted; it is flattened, the parentheses do not change the meaning. GROUP BY expr ASC|DESCis accepted but imposes no ordering: write anORDER BY.LIMITaccepts an integer, a?parameter of a prepared statement or a local variable of a routine;LIMIT 18446744073709551615(the "all rows" idiom) is accepted.LIMIT n ROWS EXAMINED mbounds the number of rows examined by the whole statement.INTO(variables, file) is allowed only in the firstSELECTof the statement, never in a table expression or a subquery.
All the forms below assume the clients and commandes tables from the chapter header.
7.13 Joins in practice#
Summary of join types#
| Need | Form | Supported |
|---|---|---|
| Only the rows that match | INNER JOIN ... ON | yes |
| All rows on the left, with or without a match | LEFT JOIN ... ON | yes |
| All rows on the right | RIGHT JOIN ... ON | yes |
| All rows on both sides | FULL [OUTER] JOIN | no (1064); emulation with UNION |
| All combinations | CROSS JOIN, FROM a, b, JOIN without ON | yes |
| Columns with the same name | JOIN ... USING (col) | yes |
| Columns with the same name, detected automatically | NATURAL JOIN | no (1235) |
| Join of a table with itself | distinct aliases | yes |
| Join with a derived table or table expression | JOIN (SELECT ...) t ON ... | yes |
Subquery that references the left table in the FROM | JOIN LATERAL (...) | no (1064) |
| Join with a view | like a table (§7.10) | yes |
Examples and results#
INNER JOIN: clients without orders and orders without a client disappear.
SELECT c.nom, o.id FROM clients c JOIN commandes o ON o.client_id = c.id ORDER BY o.id;+-------+----+
| nom | id |
+-------+----+
| Alice | 1 |
| Alice | 2 |
| Bob | 3 |
| Chloé | 5 |
+-------+----+LEFT JOIN: all clients; NULL where there is no order.
SELECT c.nom, o.id FROM clients c LEFT JOIN commandes o ON o.client_id = c.id ORDER BY c.id, o.id;+--------+------+
| nom | id |
+--------+------+
| Alice | 1 |
| Alice | 2 |
| Bob | 3 |
| Chloé | 5 |
| David | NULL |
| Émile | NULL |
| Fatima | NULL |
+--------+------+RIGHT JOIN: all orders; order 4 references a nonexistent client (id 9).
SELECT c.nom, o.id FROM clients c RIGHT JOIN commandes o ON o.client_id = c.id ORDER BY o.id;+-------+----+
| nom | id |
+-------+----+
| Alice | 1 |
| Alice | 2 |
| Bob | 3 |
| NULL | 4 |
| Chloé | 5 |
+-------+----+CROSS JOIN: the Cartesian product of 6 clients by 5 orders gives 30 rows; reserve it for intended cases (parameter tables, generating combinations).
Anti-join (clients without any order): LEFT JOIN ... IS NULL or NOT EXISTS, which give the same result.
SELECT c.nom FROM clients c LEFT JOIN commandes o ON o.client_id = c.id WHERE o.id IS NULL ORDER BY c.id;
SELECT nom FROM clients c WHERE NOT EXISTS (SELECT 1 FROM commandes o WHERE o.client_id = c.id) ORDER BY id;+--------+
| nom |
+--------+
| David |
| Émile |
| Fatima |
+--------+Join with a derived table (aggregate per client, then join); switching to LEFT JOIN keeps the clients without orders:
SELECT c.nom, t.total
FROM clients c
LEFT JOIN (SELECT client_id, SUM(montant) AS total FROM commandes GROUP BY client_id) t
ON t.client_id = c.id
ORDER BY c.id;+--------+-------+
| nom | total |
+--------+-------+
| Alice | 30.50 |
| Bob | 5.00 |
| Chloé | NULL |
| David | NULL |
| Émile | NULL |
| Fatima | NULL |
+--------+-------+Multiple joins: they chain from left to right in the same FROM, with a distinct alias for each occurrence of a table (without a distinct alias: error 1066); the types can be mixed (JOIN, LEFT JOIN, RIGHT JOIN). An ON condition can reference the tables placed before it:
SELECT c.nom, o.id AS cmd, x.id AS suivante
FROM clients c
JOIN commandes o ON o.client_id = c.id
LEFT JOIN commandes x ON x.client_id = c.id AND x.id > o.id
ORDER BY o.id;+-------+-----+----------+
| nom | cmd | suivante |
+-------+-----+----------+
| Alice | 1 | 2 |
| Alice | 2 | NULL |
| Bob | 3 | NULL |
| Chloé | 5 | NULL |
+-------+-----+----------+Self-join: the pairs of clients in the same city (see §7.3); the alias is required to tell the two occurrences apart. a.id < b.id avoids symmetric pairs and pairs of a client with itself:
SELECT a.nom, b.nom FROM clients a JOIN clients b ON a.ville = b.ville AND a.id < b.id;+-------+-------+
| nom | nom |
+-------+-------+
| Alice | Chloé |
| Bob | Émile |
+-------+-------+Emulated FULL OUTER JOIN: LEFT JOIN then RIGHT JOIN combined by UNION (which eliminates duplicates; the final ORDER BY applies to the entire union):
SELECT c.nom, o.id FROM clients c LEFT JOIN commandes o ON o.client_id = c.id
UNION
SELECT c.nom, o.id FROM clients c RIGHT JOIN commandes o ON o.client_id = c.id
ORDER BY 1, 2;+--------+------+
| nom | id |
+--------+------+
| NULL | 4 |
| Alice | 1 |
| Alice | 2 |
| Bob | 3 |
| Chloé | 5 |
| David | NULL |
| Émile | NULL |
| Fatima | NULL |
+--------+------+With UNION (rather than UNION ALL), two truly identical rows from both sides are merged: if duplicate rows are significant, add an identity column (here o.id, c.id) or build the union with UNION ALL and a WHERE o.id IS NULL on the right-hand part.
USING: equality condition on columns with the same name; the column can be named without a qualifier. Note: SELECT * returns the column once per table (two ville columns below).
SELECT * FROM clients c1 JOIN clients c2 USING (ville) LIMIT 1;+----+-------+-------+--------+----+-------+-------+-------+
| id | nom | ville | solde | id | nom | ville | solde |
+----+-------+-------+--------+----+-------+-------+-------+
| 1 | Alice | Oran | 100.50 | 3 | Chloé | Oran | 0.00 |
+----+-------+-------+--------+----+-------+-------+-------+Which join type to choose#
- Should the rows of a table that have no match stay in the result? No:
INNER JOIN. Yes, those of the main table:LEFT JOIN(write the main table on the left;RIGHT JOINis its mirror and adds nothing more). - To find the rows without a match:
LEFT JOIN ... WHERE right.key IS NULLorNOT EXISTS;NOT IN (subquery)is tricky withNULL(see below). - To simply test for the existence of a match without duplicating the left rows:
WHERE EXISTS (...)orIN (subquery)rather than a join, which multiplies the left row by the number of matches (Alice appears twice in the first example). - If you need aggregated values from the other table: aggregate first in a derived table, then join, which avoids multiplying rows before the aggregate.
Behavior of NULL in joins#
- An
ON a.x = b.ycondition is never true when either side isNULL: twoNULLs do not join each other. Fatima (villeNULL) joins no client in the join onvilleabove, not even herself. In a
LEFT JOIN, the right-side columns without a match areNULL. AWHEREcondition on a column of the right table (WHERE o.montant > 10) eliminates these rows (a comparison withNULLis never true) and in effect turns theLEFT JOINinto anINNER JOIN. To filter the right table while keeping all the left rows, put the condition in theON:SELECT c.nom, o.id FROM clients c LEFT JOIN commandes o ON o.client_id = c.id AND o.montant > 10 ORDER BY c.id;+--------+------+ | nom | id | +--------+------+ | Alice | 2 | | Bob | NULL | | Chloé | NULL | | David | NULL | | Émile | NULL | | Fatima | NULL | +--------+------+With
WHERE o.montant > 10instead, only Alice's row remains.- Counting matches:
COUNT(o.id)ignoresNULLs (0 for a client without orders),COUNT(*)counts the "no match" row itself (1):SELECT c.nom, COUNT(o.id) AS nb, COUNT(*) AS lignes FROM clients c LEFT JOIN commandes o ON o.client_id = c.id GROUP BY c.id, c.nom ORDER BY c.id;+--------+----+--------+ | nom | nb | lignes | +--------+----+--------+ | Alice | 2 | 2 | | Bob | 1 | 1 | | Chloé | 1 | 1 | | David | 0 | 1 | | Émile | 0 | 1 | | Fatima | 0 | 1 | +--------+----+--------+
NOT IN (subquery)returns zero rows as soon as the subquery produces aNULL(see §7.16);NOT EXISTSdoes not have this pitfall.- A column of a table on the right side of a
LEFT JOINbecomes nullable for the rest of the query, even if it isNOT NULLin the table (likewise for the left side of aRIGHT JOIN).
Engine-specific performance notes#
- Join algorithm. When the
ON(orUSING) condition contains at least one equality between a column on each side (o.client_id = c.id, possibly combined byANDwith other conditions), the engine uses a hash join. Otherwise (ON a.solde > b.solde,OR, expression, Cartesian product) it uses a nested loop, whose cost is the product of the sizes. The equality must involve columns of the same type family (number with number, string with string): comparing a string to a number rules out the hash join. - Join order. Joins are planned in the order written in the
FROM, with no reordering based on statistics;STRAIGHT_JOINadds nothing (it is treated asJOIN). Write first the table that filters the most. - Indexes. Indexes (primary key,
UNIQUE, secondary) serve table reads by equality with a constant (WHERE id = 1,WHERE ville = 'Oran', see §7.20). In the tests carried out,EXPLAINof a join shows anALLaccess for each table, the matching being done by the hash join: an index on the join column does not replace this mechanism. A read through a non-unique index on a constant applies only to equality; ranges (id > 2) scan the table. - Unnecessary outer joins. A
LEFT JOINfrom which no column is read and which cannot multiply rows (join on a unique key, or aDISTINCTquery) is removed from the plan. This optimization does not apply if the joined table is referenced by*,t.*, a qualified column or a subquery, nor for a view or a derived table. - Expensive queries.
max_join_size(withsql_big_selects) rejects aSELECTblock whose estimated number of examined rows is too large (error 1104), before any execution. Ranges andORs do not reduce this estimate;EXPLAINdoes not apply the check. - Views and derived tables. A view is read as its query specialized by the caller's conditions; a common table expression is replaced by its query (§7.15).
7.14 Set operations: UNION, INTERSECT, EXCEPT#
UNION [ALL | DISTINCT]: supported (§7.9).DISTINCTafterUNIONis the default behavior, identical to no keyword. The number of columns of the members must be identical (error 1222 otherwise); the column names of the result are those of the first member.INTERSECTandEXCEPT(as well asMINUS, and theirALLvariants): not supported, the parser responds with syntax error 1064. The equivalents are written withIN/NOT INorEXISTS/NOT EXISTS:-- INTERSECT: identifiers present in clients AND in commandes.client_id SELECT DISTINCT id FROM clients WHERE id IN (SELECT client_id FROM commandes) ORDER BY id; -- EXCEPT: identifiers of clients without orders (NOT EXISTS, safe with NULL) SELECT id FROM clients c WHERE NOT EXISTS (SELECT 1 FROM commandes o WHERE o.client_id = c.id) ORDER BY id;Result of the first (1, 2, 3) and of the second (4, 5, 6). Unlike the standard
INTERSECTandEXCEPT, these rewrites compare with equality, and therefore do not match twoNULLvalues.
7.15 Common table expressions (WITH)#
WITH nom [(colonne, ...)] AS ( SELECT ... | VALUES (expr, ...), ... ) [, nom2 AS (...)]
SELECT ... ;A common table expression (CTE) gives a name to a query that can be reused in the rest of the statement, including several times and in a join with itself. Verified:
- several CTEs separated by commas, a CTE being able to reference those that precede it;
- a column list after the name renames the columns (the number must match, error 1222; incompatible with a
SELECT *body, error 1235); - a
VALUES (...), (...)body is accepted; without a column list, the column names are those of the first row (1,'a'): provide a column list; - the
WITHis written at the start of a query or of a parenthesized member, and applies to the whole union; WITH RECURSIVEis not supported (error 1235): no recursive queries (tree traversal, number series); hierarchy traversals are done with fixed-depth self-joins or on the application side.
The engine replaces each reference to a CTE by its query (like a derived table), and limits the total number of these expansions to bound the size of a query: a CTE referenced at many nested levels may be rejected with syntax error 1064.
WITH c(v, n) AS (SELECT ville, COUNT(*) FROM clients GROUP BY ville),
m AS (SELECT MAX(n) AS mx FROM c)
SELECT c.v FROM c, m WHERE c.n = m.mx ORDER BY c.v;+-------+
| v |
+-------+
| Alger |
| Oran |
+-------+WITH t(a, b) AS (VALUES (1, 'a'), (2, 'b')) SELECT * FROM t;+---+---+
| a | b |
+---+---+
| 1 | a |
| 2 | b |
+---+---+7.16 Subqueries: additional notes#
Supported forms (§7.8): scalar, IN, EXISTS, correlated, derived table, and subquery in the select list, including inside an expression (EXISTS (...) as a 0/1 value).
- Scalar subquery: zero rows gives
NULL; more than one row is error 1242 ("Subquery returns more than 1 row").SELECT nom FROM clients WHERE id = (SELECT client_id FROM commandes WHERE id = 99); -- 0 rows (NULL) SELECT (SELECT id FROM clients); -- error 1242 - Correlated: can reference a column of the outer query (by alias or by table name, in
WHERE, in the select list, inIN, inEXISTS), including in a comparison test:SELECT nom FROM clients WHERE id IN (SELECT client_id FROM commandes WHERE montant > clients.solde)returns Bob. ANY,SOME,ALL:x > ANY (subquery),x = SOME (...),x > ALL (...)are not supported (syntax error 1064). Equivalents:x = ANY (S)isx IN (S);x > ANY (S)isEXISTS (SELECT 1 FROM ... WHERE x > column)orx > (SELECT MIN(col) ...);x > ALL (S)isx > (SELECT MAX(col) ...)(beware ofNULLs: filter withcol IS NOT NULL).- Row constructor:
(a, b) IN ((1, 'x'), ...)and(a, b) = (SELECT ...)are not supported (1064). Writea = 1 AND b = 'x'or a correlatedEXISTS. LIMITin anINsubquery: accepted by this engine.SELECT nom FROM clients WHERE id IN (SELECT client_id FROM commandes ORDER BY id LIMIT 2);Returns Alice (the first two orders are those of client 1).
INandNULL:x IN (S)isTRUEif a value matches; otherwiseUNKNOWN(notFALSE) ifScontains aNULL. The pitfall is inNOT IN: as soon asScontains aNULL, the test is neverTRUE, and the query returns no rows.SELECT nom FROM clients WHERE id NOT IN (SELECT client_id FROM commandes UNION ALL SELECT NULL); -- 0 rows SELECT nom FROM clients c WHERE NOT EXISTS (SELECT 1 FROM commandes o WHERE o.client_id = c.id) ORDER BY id; -- David, Émile, FatimaPrefer
NOT EXISTS, or addWHERE col IS NOT NULLin theNOT INsubquery.- Uncorrelated subquery: it depends on no outer column; the result is the same as a constant value (
WHERE solde > (SELECT AVG(solde) FROM clients)returns Alice and Émile). - Derived table: the alias is required (§7.3).
LATERAL(a derived table that references another table of the sameFROM) is not supported (1064); use a correlated subquery in the select list, or an aggregated derived table then joined.
7.17 GROUP BY, HAVING and groupings: additional notes#
GROUP BYaccepts expressions, positions (GROUP BY 1) and aliases (resolution rules in §7.5).NULLvalues form a single group.- Aggregate functions supported in
SELECT ... GROUP BY:COUNT,SUM,AVG,MIN,MAX,GROUP_CONCAT(§7.6),STD/STDDEV/STDDEV_POP,VARIANCE(details in chapter 8).COUNT(DISTINCT expr)andSUM(DISTINCT expr)filter on distinct values. - Without
GROUP BY, an aggregate function always produces one row, even on an empty table (COUNT(*)returns 0, the othersNULL); withGROUP BYon zero rows, the result is empty. HAVINGcan reference an alias from the select list (§7.5); a condition on a non-aggregated column belongs rather inWHERE, which is more efficient because it filters before grouping.WITH ROLLUP: not supported (error 1235).ROLLUP(...)andCUBE(...)are understood as function calls and fail (error 1305);GROUPING SETSis a syntax error (1064). Produce subtotals with aUNION ALLquery of oneGROUP BYper level:SELECT ville, COUNT(*) AS n FROM clients WHERE ville IS NOT NULL GROUP BY ville UNION ALL SELECT NULL, COUNT(*) FROM clients WHERE ville IS NOT NULL ORDER BY ville IS NULL, ville;(one row per city, then a total row whose
villeisNULL).DISTINCTapplies to the entire row;SELECT DISTINCT ville, soldedistinguishes pairs (§7.2),NULLincluded.ORDER BYsortsNULLs first in ascending order and last in descending order:SELECT id, montant FROM commandes ORDER BY montant;+----+---------+ | id | montant | +----+---------+ | 5 | NULL | | 4 | 1.00 | | 3 | 5.00 | | 1 | 10.00 | | 2 | 20.50 | +----+---------+
7.18 Window functions#
Syntax: function(...) OVER ([PARTITION BY expr, ...] [ORDER BY expr [ASC|DESC], ...] [frame]), where the frame is ROWS | RANGE followed by bound or BETWEEN bound AND bound, a bound being UNBOUNDED PRECEDING, n PRECEDING, CURRENT ROW, n FOLLOWING or UNBOUNDED FOLLOWING.
Supported functions:
| Category | Functions |
|---|---|
| Ranking | ROW_NUMBER(), RANK(), DENSE_RANK(), PERCENT_RANK(), CUME_DIST(), NTILE(n) |
| Offset | LAG(expr [, offset [, default]]), LEAD(expr [, offset [, default]]) |
| Value | FIRST_VALUE(expr), LAST_VALUE(expr), NTH_VALUE(expr, n) |
| Window aggregate | COUNT(expr), COUNT(*), SUM, AVG, MIN, MAX, STD, STDDEV, STDDEV_POP, VARIANCE |
Not supported (error 1235 unless stated otherwise): COUNT(DISTINCT ...), SUM(DISTINCT ...) and GROUP_CONCAT with OVER; RANGE with a numeric offset (RANGE BETWEEN 1 PRECEDING ...; RANGE UNBOUNDED ... and RANGE CURRENT ROW remain allowed, as does ROWS with an offset); any other function used with OVER; the named WINDOW w AS (...) clause and OVER w (syntax error 1064): write the full specification at each OVER. The offset of LAG/LEAD and the rank of NTILE / NTH_VALUE must be integer constants (NTILE and NTH_VALUE: at least 1). FILTER (WHERE ...) and the GROUPS frame are not supported.
Default frame: without ORDER BY, the whole partition; with ORDER BY, from the first row of the partition to the current row and its peers (RANGE UNBOUNDED PRECEDING to CURRENT ROW). This is why LAST_VALUE(...) without an explicit frame returns the current row and not the last of the partition: write ROWS BETWEEN UNBOUNDED PRECEDING AND UNBOUNDED FOLLOWING.
A window function goes in the select list or in the ORDER BY; in WHERE it is rejected (error 1111). To filter on its result, wrap it in a derived table or a CTE.
The top of each city (top n per group):
SELECT nom, ville, solde FROM (
SELECT nom, ville, solde,
ROW_NUMBER() OVER (PARTITION BY ville ORDER BY solde DESC) AS rn
FROM clients WHERE ville IS NOT NULL
) t WHERE rn = 1 ORDER BY ville;+-------+-------------+--------+
| nom | ville | solde |
+-------+-------------+--------+
| Émile | Alger | 75.25 |
| David | Constantine | NULL |
| Alice | Oran | 100.50 |
+-------+-------------+--------+David is first in Constantine: in a descending sort, the NULL value is ranked last, and he is alone in his group.
Running total (cumulative sum):
SELECT id, montant, SUM(montant) OVER (ORDER BY id) AS cumul FROM commandes ORDER BY id;+----+---------+-------+
| id | montant | cumul |
+----+---------+-------+
| 1 | 10.00 | 10.00 |
| 2 | 20.50 | 30.50 |
| 3 | 5.00 | 35.50 |
| 4 | 1.00 | 36.50 |
| 5 | NULL | 36.50 |
+----+---------+-------+Sum per partition, rank and previous value:
SELECT id, nom, SUM(solde) OVER (PARTITION BY ville) AS tot_ville FROM clients WHERE ville IS NOT NULL ORDER BY id;
SELECT ville, RANK() OVER (ORDER BY COUNT(*) DESC) AS r, COUNT(*) FROM clients GROUP BY ville ORDER BY r, ville;
SELECT id, nom, solde - LAG(solde) OVER (ORDER BY id) AS diff FROM clients ORDER BY id;The second query shows that a window function can operate on an aggregate: Alger and Oran (2 clients) are tied at rank 1, NULL and Constantine at rank 3. Window functions are evaluated after WHERE, GROUP BY and HAVING.
Sliding window (ROWS 1 PRECEDING: previous row and current row):
SELECT nom, SUM(solde) OVER (ORDER BY id ROWS 1 PRECEDING) AS s FROM clients ORDER BY id;+--------+--------+
| nom | s |
+--------+--------+
| Alice | 100.50 |
| Bob | 80.50 |
| Chloé | -20.00 |
| David | 0.00 |
| Émile | 75.25 |
| Fatima | 85.25 |
+--------+--------+7.19 Read locks and optimizer hints#
Locking reads#
SELECT ... FOR UPDATE [OF table, ...] [NOWAIT | WAIT secondes | SKIP LOCKED]
SELECT ... FOR SHARE [OF table, ...] [NOWAIT | WAIT secondes | SKIP LOCKED]
SELECT ... LOCK IN SHARE MODE [NOWAIT | WAIT secondes | SKIP LOCKED]The clause is written last in the block (after LIMIT) and does not accept OF with LOCK IN SHARE MODE. It applies to the tables of the block, or to those in OF. In the default model (multi-version optimistic concurrency, MV-OCC), nobody waits for a lock. A row held by another transaction (or modified since the transaction's snapshot) gives, depending on the clause:
| Clause | Conflicting row |
|---|---|
| default | conflict (error 1213): the client's transaction is rolled back (MV-OCC) |
NOWAIT, WAIT n | error 1205, only the statement fails (no waiting takes place) |
SKIP LOCKED | the row is skipped from the result |
Outside a transaction (autocommit), only the check is performed: the lock is not kept after the statement. The lock intents of an explicit transaction last until COMMIT or ROLLBACK. See chapter 9 ("Transactions and concurrency") for the locking models.
START TRANSACTION;
SELECT nom, solde FROM clients WHERE id = 1 FOR UPDATE;
UPDATE clients SET solde = solde - 10 WHERE id = 1;
COMMIT;Optimizer hints /*+ ... */#
A hint comment right after SELECT is read. Only MAX_EXECUTION_TIME(n) has an effect: maximum execution time in milliseconds (1 to 2,147,483,647); beyond that, the statement fails with error 1969. The other recognized hints are read and then ignored; a syntax error in the comment causes it to be ignored entirely with a warning, without the query failing.
SELECT /*+ MAX_EXECUTION_TIME(1000) */ nom FROM clients WHERE ville = 'Oran';The index hints USE INDEX, FORCE INDEX and IGNORE INDEX (after a table name) are not supported (syntax error 1064): the engine chooses the index on its own. No join hint changes the join algorithm or order.
LIMIT n ROWS EXAMINED m is another execution bound: the statement stops after examining m rows and returns the partial result with a warning ("The query exceeded LIMIT ROWS EXAMINED m. The query result may be incomplete"), without an error.
7.20 Reading a plan with EXPLAIN#
EXPLAIN applies to SELECT, UPDATE, DELETE and INSERT ... SELECT (the statement is planned, never executed); EXPLAIN INSERT ... VALUES, EXPLAIN REPLACE ... VALUES and EXPLAIN LOAD DATA respond with error 1235. EXPLAIN table_name is equivalent to DESCRIBE. The format is limited to the tabular output: no FORMAT=JSON, no EXPLAIN ANALYZE.
Output columns: id, select_type, table, partitions, type, possible_keys, key, key_len, ref, rows, Extra. key_len is always NULL (indexes are in memory, with no serialized key length).
| Column | Observed values |
|---|---|
select_type | SIMPLE, PRIMARY (first member of a union), UNION (subsequent members), DERIVED (derived table), DEPENDENT SUBQUERY (correlated subquery in the select list) |
type | eq_ref (primary key or UNIQUE index by equality with a constant: one row), ref (secondary index by equality with a constant), ALL (full table scan), index (read through the vector index), - (no table) |
possible_keys | indexes of the table (primary key, UNIQUE, secondary), by name; NULL if there are none |
key | index chosen; NULL with ALL |
rows | eq_ref: 1; ref: number of rows for the key; ALL: number of live rows in the table |
Extra | Using where, Using filesort, Using temporary, Using join buffer (hash join), No tables used |
Examples (with an index idx_ville on clients(ville) and idx_cli on commandes(client_id)):
EXPLAIN SELECT * FROM clients WHERE id = 1;| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | Extra |
| 1 | SIMPLE | clients | NULL | eq_ref | PRIMARY | PRIMARY | NULL | const | 1 | |EXPLAIN SELECT * FROM clients WHERE ville = 'Oran';| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | Extra |
| 1 | SIMPLE | clients | NULL | ref | PRIMARY,idx_ville | idx_ville | NULL | const | 2 | |EXPLAIN SELECT c.nom, o.montant FROM clients c JOIN commandes o ON o.client_id = c.id;| id | select_type | table | partitions | type | possible_keys | key | key_len | ref | rows | Extra |
| 1 | SIMPLE | c | NULL | ALL | PRIMARY,idx_ville | NULL | NULL | NULL | 6 | |
| 1 | SIMPLE | o | NULL | ALL | PRIMARY,idx_cli | NULL | NULL | NULL | 5 | Using join buffer (hash join) |Reading: table c is scanned in full, table o is loaded into a join buffer and matched by hashing. A join without equality between columns (ON a.solde > b.solde) gives two ALL rows with no Using join buffer: nested loop. Using filesort indicates a sort (ORDER BY), Using temporary a grouping (GROUP BY) or a deduplicated union.
Limits of this version's EXPLAIN, to be aware of before drawing conclusions:
- It describes table reads, not each operator: an
IN (SELECT ...)subquery does not add its own row; a correlated subquery in the select list appears asDEPENDENT SUBQUERY(one row per table read,Using where); a derived table appears through the reads of its content (DERIVED). - Range reads (
id > 2,BETWEEN) andORDER BY id LIMIT nare reported asALL(withUsing filesortfor the sort): only equality with a constant uses an index. - With a join, a
WHERE c.id = 1condition on one of the tables does not switch its read toeq_refin the output (ALLread, condition inUsing where). - The
key_lenandfilteredcolumns are not populated (filtereddoes not exist).
What to do for a slow query: read type (ALL on a large table?), add a UNIQUE or secondary index on the column compared to a constant, check type equality between the two columns of a join, aggregate before joining, and set MAX_EXECUTION_TIME or max_join_size to bound exploratory queries.