04 — Intermediate SQL

Зачем эта тема: базовый SELECT/INSERT мы знаем. Теперь учимся соединять много таблиц, писать подзапросы, создавать views и constraints. Это уровень, на котором SQL становится мощным инструментом.

🎯 Практический квиз

30 вопросов с ответами и объяснениями для самопроверки → https://sdu.javazhan.tech/questions/7/categories/25


1. JOIN — все варианты

JOIN объединяет строки из 2+ таблиц по условию. Разница между JOIN-ами — в том, что делать со “несовпадающими” строками.

1.1 Сводная таблица JOIN-ов

ТипЧто возвращает
INNER JOINтолько совпадения в ОБЕИХ
LEFT JOINвсе из ЛЕВОЙ + совпадения, иначе NULL
RIGHT JOINвсе из ПРАВОЙ + совпадения, иначе NULL
FULL OUTER JOINвсе из ОБЕИХ, NULL где нет совпадений
CROSS JOINдекартово произведение (m × n)
NATURAL JOININNER JOIN по всем общим столбцам автоматически
SELF JOINтаблица с самой собой

1.2 Визуализация (таблицы)

employees:

emp_idnamedept_id
1Aigerim10
2Daulet20
3AselNULL

departments:

dept_iddept_name
10Sales
30HR

INNER JOIN (только пересечение):

namedept_name
AigerimSales

LEFT JOIN (все из employees):

namedept_name
AigerimSales
DauletNULL
AselNULL

RIGHT JOIN (все из departments):

namedept_name
AigerimSales
NULLHR

FULL OUTER JOIN (все из обеих):

namedept_name
AigerimSales
DauletNULL
AselNULL
NULLHR

Mental model

  • INNER = пересечение.
  • LEFT = всё из левой + что нашлось из правой.
  • RIGHT = всё из правой + что нашлось из левой.
  • FULL = всё из обеих, дыры заполнены NULL.

1.3 Синтаксис

-- INNER (можно опустить слово INNER)
SELECT e.name, d.dept_name
FROM   employees e
INNER JOIN departments d ON e.dept_id = d.dept_id;
 
-- LEFT
SELECT e.name, d.dept_name
FROM   employees e
LEFT JOIN departments d ON e.dept_id = d.dept_id;
 
-- FULL OUTER
SELECT e.name, d.dept_name
FROM   employees e
FULL OUTER JOIN departments d ON e.dept_id = d.dept_id;
 
-- CROSS (Cartesian)
SELECT e.name, d.dept_name FROM employees e CROSS JOIN departments d;
 
-- NATURAL JOIN — соединяет по всем общим столбцам
SELECT * FROM employees NATURAL JOIN departments;

INNER JOIN B ON cond sqlcommand — соединение, оставляет только совпадения.

LEFT JOIN B ON cond sqlcommand — все строки из левой таблицы.

FULL OUTER JOIN B ON cond sqlcommand — все строки из обеих.

CROSS JOIN B sqlcommand — декартово произведение.

NATURAL JOIN B sqlcommand — INNER по всем общим именам столбцов.

NATURAL JOIN опасен

Соединяет по ВСЕМ совпадающим именам — если в обеих таблицах есть created_at, то JOIN пойдёт и по нему тоже. Лучше явный JOIN ... ON ....

1.4 USING — упрощённый синтаксис

Если столбцы соединения называются одинаково:

SELECT name, dept_name
FROM employees JOIN departments USING (dept_id);

JOIN B USING (col) sqlcommand — упрощённый JOIN по одноимённым столбцам, при этом столбец col появляется один раз.


2. JOIN — типичные ловушки

2.1 LEFT JOIN + WHERE превращается в INNER

SELECT c.name, a.balance
FROM   customers c
LEFT JOIN accounts a ON a.customer_id = c.customer_id
WHERE  a.balance > 0;     -- ⚠️ убивает клиентов без счёта

a.balance > 0 отсекает строки, где a.balance = NULL, а это как раз “клиенты без счёта”. Эффект LEFT JOIN потерян.

Правильно: перенести условие в ON:

LEFT JOIN accounts a ON a.customer_id = c.customer_id AND a.balance > 0

Главный exam trap по JOIN

Если в WHERE есть условие на столбцы правой таблицы LEFT JOIN-а, JOIN превращается в INNER. Условия на правую таблицу должны быть в ON.

2.2 Дубликаты при JOIN на 1-many

Если у клиента 3 счёта, JOIN вернёт 3 строки на этого клиента. Это нормально, но забудешь — посчитаешь общую сумму неверно.

-- ОПАСНО: завышает сумму
SELECT SUM(c.balance + a.balance)
FROM customers c JOIN accounts a ON a.customer_id = c.customer_id;

3. Подзапросы (subqueries)

3.1 Скалярный подзапрос

Возвращает одно значение. Можно использовать как литерал.

SELECT name, balance,
       balance - (SELECT AVG(balance) FROM customers) AS diff_from_avg
FROM customers;

3.2 Подзапрос в WHERE — IN / NOT IN

-- Клиенты, у которых есть счёт
SELECT name FROM customers
WHERE customer_id IN (SELECT customer_id FROM accounts);
 
-- Клиенты без счетов
SELECT name FROM customers
WHERE customer_id NOT IN (SELECT customer_id FROM accounts);

NOT IN + NULL

Если подзапрос вернёт хоть один NULL, весь NOT IN вернёт пусто (трёхзначная логика). Используй NOT EXISTS для безопасности.

3.3 EXISTS / NOT EXISTS

EXISTS возвращает TRUE, если подзапрос вернул хоть одну строку.

-- Клиенты с хотя бы одним счётом
SELECT name FROM customers c
WHERE EXISTS (
    SELECT 1 FROM accounts a WHERE a.customer_id = c.customer_id
);
 
-- Клиенты без счетов
SELECT name FROM customers c
WHERE NOT EXISTS (
    SELECT 1 FROM accounts a WHERE a.customer_id = c.customer_id
);

EXISTS почти всегда лучше NOT IN

EXISTS работает корректно с NULL и часто быстрее на больших объёмах.

3.4 Сравнение с ALL / ANY / SOME

-- Зарплата выше всех в IT
SELECT name FROM employees
WHERE salary > ALL (SELECT salary FROM employees WHERE dept = 'IT');
 
-- Зарплата выше хотя бы одного в IT
SELECT name FROM employees
WHERE salary > ANY (SELECT salary FROM employees WHERE dept = 'IT');

ALL (subquery) sqlcommand — больше/меньше каждого.

ANY/SOME (subquery) sqlcommand — больше/меньше хотя бы одного.

3.5 Коррелированные подзапросы

Подзапрос ссылается на внешний запрос. Выполняется для каждой строки внешнего.

SELECT c.name,
       (SELECT COUNT(*) FROM accounts a WHERE a.customer_id = c.customer_id) AS num_acc
FROM customers c;

Корреляция = N+1

Подзапрос выполняется один раз на каждую строку. На больших таблицах — медленно. Часто переписывается через JOIN + GROUP BY.

3.6 Подзапрос в FROM (derived table)

SELECT city, AVG(num_acc) AS avg_accounts
FROM (
    SELECT c.city, COUNT(a.account_id) AS num_acc
    FROM customers c LEFT JOIN accounts a USING (customer_id)
    GROUP BY c.customer_id, c.city
) sub
GROUP BY city;

Алиас обязателен.


4. WITH — CTE (Common Table Expressions)

Именованный временный результат запроса. Читаемее вложенных подзапросов.

WITH city_stats AS (
    SELECT city, COUNT(*) AS num
    FROM customers
    GROUP BY city
),
big_cities AS (
    SELECT city FROM city_stats WHERE num > 10
)
SELECT * FROM customers WHERE city IN (SELECT city FROM big_cities);

WITH name AS (subquery), … sqlcommand — определяет CTE, доступную в основном запросе.

Когда CTE

  • Когда логика сложная и нужно разбить на шаги.
  • Когда один и тот же подзапрос нужен дважды.
  • Когда нужна рекурсивная структура (см. ch5).

5. Set operations — UNION / INTERSECT / EXCEPT

SELECT name FROM customers
UNION
SELECT name FROM employees;        -- объединение, без дубликатов
 
SELECT name FROM customers
UNION ALL
SELECT name FROM employees;        -- объединение, с дубликатами
 
SELECT name FROM customers
INTERSECT
SELECT name FROM employees;        -- общие
 
SELECT name FROM customers
EXCEPT
SELECT name FROM employees;        -- в первой, но не во второй
ОператорЧто делаетДубликаты
UNIONобъединениеубирает
UNION ALLобъединениеоставляет
INTERSECTпересечениеубирает
EXCEPTразность (Postgres) / MINUS (Oracle)убирает

Условия set operations

  1. Одинаковое количество столбцов.
  2. Совместимые типы данных по позициям.
  3. Имена столбцов берутся из первого SELECT.

UNION vs UNION ALL

UNION дороже — нужна сортировка/хеширование для удаления дубликатов. Если знаешь, что дубликатов нет — используй UNION ALL.


6. CASE — условная логика

SELECT name, balance,
       CASE
           WHEN balance >= 100000 THEN 'VIP'
           WHEN balance >= 10000  THEN 'Regular'
           ELSE 'Basic'
       END AS tier
FROM customers;

Короткая форма (CASE на равенство):

SELECT name,
       CASE country
           WHEN 'KZ' THEN 'Kazakhstan'
           WHEN 'KG' THEN 'Kyrgyzstan'
           ELSE 'Other'
       END
FROM customers;

CASE WHEN cond THEN val … ELSE val END sqlcommand — условное выражение в SQL.

CASE в агрегатах = pivot

SELECT
  SUM(CASE WHEN status='OK'   THEN 1 ELSE 0 END) AS ok_cnt,
  SUM(CASE WHEN status='FAIL' THEN 1 ELSE 0 END) AS fail_cnt
FROM transactions;

Превращает строки в столбцы.


7. NULL в SQL

7.1 Функции для NULL

-- Заменить NULL на значение
SELECT name, COALESCE(phone, 'no phone') FROM customers;
 
-- Если a = b, вернуть NULL
SELECT NULLIF(division, 0) FROM stats;

COALESCE(a, b, c) sqlcommand — возвращает первое не-NULL значение.

NULLIF(a, b) sqlcommand — возвращает NULL, если a = b, иначе a.

7.2 NULL в агрегатах

  • COUNT(*) считает все строки.
  • COUNT(col), SUM, AVG пропускают NULL.
  • Если ВСЕ значения NULL → SUM = NULL, AVG = NULL, COUNT(col) = 0.

Среднее с NULL

AVG(balance) игнорирует NULL. Если хочешь считать NULL как 0: AVG(COALESCE(balance, 0)).


8. Views — виртуальные таблицы

view — сохранённый SELECT-запрос, к которому можно обращаться как к таблице. dbterm

CREATE VIEW vip_customers AS
SELECT customer_id, name, balance
FROM   customers
WHERE  balance >= 100000;
 
SELECT * FROM vip_customers WHERE name LIKE 'A%';
DROP VIEW vip_customers;

CREATE VIEW name AS SELECT …; sqlcommand — создаёт view.

DROP VIEW name; sqlcommand — удаляет view.

8.1 Зачем views

  • Безопасность — пользователь видит только нужные столбцы/строки.
  • Упрощение — сложный JOIN прячется за простым именем.
  • Логическая независимость — если поменялась схема, можно адаптировать view, оставив запросы клиентов прежними.

8.2 Updatable views

Через view можно делать INSERT/UPDATE/DELETE, если:

  • view над одной таблицей,
  • нет агрегатов, DISTINCT, GROUP BY,
  • все обязательные столбцы базовой таблицы доступны.

Иначе — read-only.

8.3 Materialized view

CREATE MATERIALIZED VIEW account_summary AS
SELECT customer_id, SUM(balance) AS total
FROM accounts GROUP BY customer_id;
 
REFRESH MATERIALIZED VIEW account_summary;

Хранит результат физически. Быстрый чтение, но устаревает — нужно REFRESH. Используется для аналитики.

ViewMaterialized view
Хранениенетесть, на диске
Скорость SELECTкак у запросабыстро
Свежестьвсегда актуальнаустаревает
Refreshне нуженвручную/по расписанию

Materialized view ≠ кеш

Не “обновляется автоматически” в большинстве СУБД. Нужно REFRESH явно.


9. Integrity constraints

Правила, которые DBMS гарантирует на уровне таблицы.

ConstraintСмысл
NOT NULLзначение обязательно
UNIQUEзначения уникальны (NULL допускаются)
PRIMARY KEYуникальный ID, NOT NULL
FOREIGN KEYссылка на PK другой таблицы
CHECK (cond)условие на значение
DEFAULT valзначение по умолчанию
CREATE TABLE accounts (
    account_id  INT PRIMARY KEY,
    customer_id INT NOT NULL REFERENCES customers(customer_id) ON DELETE CASCADE,
    type        VARCHAR(20) CHECK (type IN ('checking','savings','loan')),
    balance     DECIMAL(15,2) DEFAULT 0 CHECK (balance >= 0),
    UNIQUE(customer_id, type)
);

9.1 Именование constraints

ALTER TABLE accounts
    ADD CONSTRAINT chk_balance_nonneg CHECK (balance >= 0);
 
ALTER TABLE accounts DROP CONSTRAINT chk_balance_nonneg;

Зачем имя

Без имени СУБД даст автоимя типа accounts_balance_check_2. Через год не вспомнишь, что это. Всегда давай осмысленные имена.

9.2 Deferrable constraints

CREATE TABLE … (
    …,
    FOREIGN KEY (parent_id) REFERENCES parents(id) DEFERRABLE INITIALLY DEFERRED
);

DEFERRABLE — проверка откладывается до конца транзакции. Полезно при циклических зависимостях.


10. Авторизация — DCL

-- Дать право
GRANT SELECT, INSERT ON customers TO clerk_role;
GRANT ALL PRIVILEGES ON accounts TO admin_role;
 
-- Отозвать
REVOKE INSERT ON customers FROM clerk_role;
 
-- С правом передачи дальше
GRANT SELECT ON customers TO analyst WITH GRANT OPTION;

GRANT priv ON table TO user; sqlcommand — выдать право.

REVOKE priv ON table FROM user; sqlcommand — отозвать право.

PrivilegeДействие
SELECTчитать
INSERTвставлять
UPDATE [(cols)]менять
DELETEудалять
REFERENCESиспользовать как FK
ALL PRIVILEGESвсё сразу

Roles

На практике дают права не пользователю, а роли, потом назначают роль пользователю. Так проще управлять командой.


11. Транзакции — кратко (полное в ch17)

BEGIN;
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
UPDATE accounts SET balance = balance + 1000 WHERE id = 2;
COMMIT;       -- или ROLLBACK;

BEGIN / START TRANSACTION sqlcommand — начало транзакции.

COMMIT sqlcommand — зафиксировать изменения.

ROLLBACK sqlcommand — откатить всё, что было после BEGIN.

Главное правило

Все изменения, которые должны произойти вместе или никак, оборачивай в одну транзакцию.


12. Часто на экзамене

Exam traps

  1. LEFT JOIN + WHERE на правую таблицу = INNER JOIN. Условие должно идти в ON.
  2. NOT IN + NULL = пусто. Используй NOT EXISTS.
  3. UNION vs UNION ALLUNION убирает дубликаты, ALL нет.
  4. View vs Materialized View — view виртуальна, MV хранится.
  5. CHECK не может ссылаться на другие таблицы в большинстве СУБД (используй триггер).
  6. GRANT на столбец возможен для UPDATE: GRANT UPDATE(balance) ON accounts TO ….
  7. Foreign key ON DELETE — варианты CASCADE/SET NULL/RESTRICT — знать все.

13. Mini-quiz

  1. Чем LEFT JOIN + WHERE B.col IS NULL хорош для поиска “сирот”?
  2. Почему NOT IN (SELECT … WITH NULL) опасен?
  3. Какая разница между UNION и UNION ALL по производительности?
  4. Можно ли делать INSERT через view? При каких условиях?
  5. Что произойдёт при ON DELETE CASCADE, если удалить родительскую строку?

Если понял эту главу

Ты владеешь всеми типами JOIN-ов, умеешь писать подзапросы (включая EXISTS и коррелированные), работаешь с CTE, set-операциями, views и constraints. Теперь можно идти в advanced SQL: триггеры, процедуры, оконные функции.