Учимся читать SQL
March 15, 2025
Введение
Я отчётливо помню, как сидел на втором курсе на лабах по БД и методом научного тыка подбирал порядок слов SQL-запросе с GROUP BY который вернёт мне нужный результат. Потому что я не понимал как работает SQL, хотя был прилежным (на программистских курсах) студентах, ходил на лекции и делал лабы за себя и "того парня".
20 лет спустя, когда я стал был семинаристом на лабах по БД я столкнулся с той же самой проблемой у своих студентов. И так как за 20 лет я всё-таки понял как работает SQL я придумал для своих студентов способ объяснения, который работает очень хорошо.
И если вам пока что сложно понять что делает этот запрос:
SELECT e.id,
e.name,
sum(training_budget) as sum,
array_agg(d.name || ' - ' || de.training_budget) as details
FROM employees e
JOIN department_employees de on e.id = de.employee_id
JOIN departments d on de.department_id = d.id
WHERE d.location = 'Новосибирск'
GROUP BY e.id
HAVING sum(de.training_budget) > 80000
ORDER BY sum(training_budget) desc
OFFSET 1 LIMIT 1;
- в этом посте я дам вам инструмент, который поможет вам уверенно интерпретировать в голове SQL-запросы.
Warning:
Та модель, которая последует дальше - это не то, как настоящие СУБД исполняют SQL-запросы. Это упрощённая и не полная логическая модель, с которой проще работать человеку.
Намного более подробный разбор семантики SELECT-а есть в посте Лукаса Эдара https://blog.jooq.org/a-beginners-guide-to-the-true-order-of-sql-operations/, а про то как СУБД реально выполянют запросы можно почитать в книге PostgreSQL 17 изнутри от ребят из Postgres Professional.
Но прежде чем переходить непосредственно к инструменту - рассмотрим небольшую схему БД, на которой я буду его иллюстрировать.
Схема БД
В этом посте я буду использовать следующую схему БД:
CREATE TABLE employees
(
id BIGINT GENERATED ALWAYS AS IDENTITY
PRIMARY KEY,
passport NUMERIC(10) NOT NULL
UNIQUE,
name VARCHAR(256) NOT NULL
CHECK (length((name)) > 3)
);
CREATE TABLE departments
(
id BIGINT GENERATED ALWAYS AS IDENTITY
PRIMARY KEY,
name VARCHAR,
location VARCHAR NOT NULL
);
CREATE TABLE department_employees
(
department_id BIGINT REFERENCES departments,
employee_id BIGINT REFERENCES employees,
training_budget NUMERIC(8, 2),
PRIMARY KEY (department_id, employee_id)
);
Тут у нас есть некие сотрудники и отделы, связанные отношением многие ко многим. Кроме того, на каждого сотрудника в рамках каждого отдела выделен собственный бюджет на обучение, а сами отделы привязаны ко определённому местоположению.
Теперь можно переходить непосредственно в логической модели SELECT-запроса.
SELECT-запрос как конвейер, по которому едут таблицы
Представьте, что SELECT-запрос - это конвейер, по которому едут таблицы, и "постами" которого являются SQL-операции, которые эти таблицы как-то трансформируют.
Но, вопреки синтаксической структуре SELECT-а, этот конвейер начинается не с операции SELECT, а с оператора FROM.
FROM
Оператор FROM, пожалуй является самым простым - он передаёт дальше по конвейеру всю таблицу целиком, без каких либо модификаций.
JOIN
Следующим постом конвейера является оператор JOIN. У JOIN-а есть несколько вариаций, основными из которых являются CROSS, INNER, LEFT, RIGHT, FULL.
Но так как это не вводный пост в SQL SELECT в целом, а пост про логическую модель его работы, я рассмотрю только необходимый мне минимум - CROSS JOIN и INNER JOIN.