What SQL Is
A declarative language for relational data: you state the result you want, and MySQL's optimizer decides how to produce it.
SELECT select_expr [, ...] [FROM table_references] [WHERE ...] [GROUP BY ...] [HAVING ...] [ORDER BY ...] [LIMIT ...]SQL is a declarative language for relational data. A statement says what the result should contain — which columns, from which tables, matching which conditions, in which order — and the server's optimizer decides how to produce it: which index to read, which join order to use, whether it can stream rows or must sort them first. You never write the loop.
Why the relational model held
Every SQL operation takes tables and returns a table. That closure is what lets one query become the input to another, and it is the reason the language outlived the object databases, the query builders and the key-value stores that were each going to replace it. Data outlives the application that wrote it, and a declarative query keeps working when the indexes, the optimizer or the hardware underneath it change.
Which MySQL this is
This course documents MySQL 8.4 LTS, generally available since 30 April 2024. Oracle ships two tracks: Innovation releases, which add features and may change behaviour or remove deprecated functionality, and LTS releases, which get bug and security fixes only, with no removals inside the series. Pinning to 8.4 means the behaviour described here does not move under you.
A dialect, not the standard
MySQL both extends standard SQL and omits parts of it. HAVING may refer to columns in the select list, which the standard forbids; NULLS FIRST does not exist at all. Portable SQL is something you choose deliberately, not something you get by default.
Example
-- One statement, no loops. The clauses have a fixed logical order, but the
-- optimizer may execute them any way that produces the same answer.
SELECT c.name,
COUNT(*) AS order_count -- an aggregate, one value per group
FROM customers AS c
JOIN orders AS o ON o.customer_id = c.id
WHERE o.placed_at >= '2026-01-01' -- filters rows, before grouping
GROUP BY c.id, c.name
HAVING COUNT(*) > 3 -- filters groups, after grouping
ORDER BY order_count DESC, c.name -- second key breaks ties in the first
LIMIT 10;When to use it
- Deciding whether a reporting question belongs in the database or in application code.
- Reading a colleague's query and knowing which clause filters what.
- Confirming a feature exists in 8.4 before a production deploy depends on it.
More examples
Find out which server you are talking to
An 8.4 LTS server and an Innovation-track server accept different SQL, and two 8.4 servers with different sql_mode settings treat the same GROUP BY or the same out-of-range INSERT differently. Three cheap queries save a long debugging session.
-- Run these before trusting any feature list: syntax available to you depends
-- on the version, and behaviour depends on the SQL mode.
SELECT VERSION(); -- e.g. 8.4.4
SELECT @@sql_mode; -- e.g. ONLY_FULL_GROUP_BY,STRICT_TRANS_TABLES,...
SELECT @@default_storage_engine; -- InnoDB on a stock 8.4 serverAsk the optimizer what it intends to do
Declarative means you do not choose the plan, so the only way to know what will happen is to ask. EXPLAIN is the first tool to reach for when a query is slow, before rewriting anything.
EXPLAIN
SELECT id, email FROM customers WHERE email = '[email protected]';
-- key: uq_customers_email, rows: 1 -> the optimizer found and used the index
-- key: NULL, rows: 92000 -> full scan: no usable index for this filterThe same question asked two ways
Two spellings of one question. Which runs faster is a property of the data, the indexes and the optimizer, not of the text you typed — so measure with EXPLAIN rather than arguing from style.
-- Customers who have at least one order over 500, as a join...
SELECT DISTINCT c.name
FROM customers AS c
JOIN orders AS o ON o.customer_id = c.id
WHERE o.total > 500;
-- ...and as an existence test. Same result set.
SELECT c.name
FROM customers AS c
WHERE EXISTS (SELECT 1 FROM orders AS o
WHERE o.customer_id = c.id AND o.total > 500);
Discussion