Учимся читать 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.

Но прежде чем переходить непосредственно к инструменту - рассмотрим небольшую схему БД, на которой я буду его иллюстрировать.

Схема БД

В этом посте я буду использовать следующую схему БД:

data model.drawio
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.

SELECT

Приземляем логическую модель на код

SQL-запрос, как конвейер, по которому едут строки