SQL (Structured Query Language) — это язык запросов к реляционным базам данных: на нём создают таблицы, добавляют и меняют строки, выбирают и считают данные. Запрос описывает, какой результат нужен, а СУБД сама решает, как его получить. Начинающему хватит шести операторов: SELECT, FROM, WHERE, GROUP BY, ORDER BY и JOIN.
Главное за 30 секунд
SQL — язык, на котором человек или программа обращается к базе данных. База хранит данные в таблицах: строка — одна запись, колонка — одно свойство записи.
Запросы выполняет СУБД: PostgreSQL, MySQL, SQLite, Microsoft SQL Server, ClickHouse. Основа языка описана в стандарте ISO/IEC 9075, но у каждой СУБД свой диалект. Например, первые строки выдачи в PostgreSQL отбирают через LIMIT, в SQL Server — через TOP.
Аналитики пишут на SQL отчёты: выручку по месяцам, средний чек, клиентов без покупок. Кому из них SQL нужен каждый день, мы разобрали в статье о видах аналитиков.
Запрос на выборку состоит из блоков-операторов, которые пишут в строгом порядке. Обязателен только SELECT, остальные блоки добавляют по задаче.
СУБД выполняет блоки запроса в другом порядке, чем их пишут: сначала берёт таблицу, потом фильтрует и группирует строки, и только после этого вычисляет SELECT. Физически СУБД может переставить операции, если результат не изменится.
Разберём запрос со всеми блоками: два клиента с самой большой оплаченной выручкой, у которых она выше 15 000 ₽.
SELECT client_id, SUM(amount) AS revenueFROM ordersWHERE status = 'paid'GROUP BY client_idHAVING SUM(amount) > 15000ORDER BY revenue DESCLIMIT 2;| client_id | revenue |
|---|---|
| 4 | 31200 |
| 2 | 23000 |
Псевдоним revenue работает в ORDER BY, потому что ORDER BY выполняется после SELECT. В HAVING мы повторили SUM(amount): по стандарту HAVING выполняется раньше SELECT. SQLite допускает псевдоним в HAVING. PostgreSQL вернёт ошибку: по его документации псевдонимы работают только в ORDER BY и GROUP BY.
Команды SQL делят на пять групп по назначению. В документации Oracle SELECT относится к DML, а DQL выделяют в учебниках, чтобы отделить чтение данных от их изменения.
| Группа | Расшифровка | Что делает | Команды |
|---|---|---|---|
| DDL | Data Definition Language | создаёт и меняет структуру: таблицы, индексы, представления | CREATE, ALTER, DROP, TRUNCATE |
| DML | Data Manipulation Language | добавляет, меняет и удаляет строки | INSERT, UPDATE, DELETE |
| DQL | Data Query Language | читает данные | SELECT |
| DCL | Data Control Language | выдаёт и отзывает права доступа | GRANT, REVOKE |
| TCL | Transaction Control Language | управляет транзакциями | BEGIN, COMMIT, ROLLBACK, SAVEPOINT |
Аналитик пишет в основном DQL. В SQLite команд GRANT и REVOKE нет: доступ к файлу базы регулирует операционная система.
Все запросы ниже выполнены в SQLite 3.51.0 через модуль sqlite3 из стандартной библиотеки Python. Таблицы под запросами — реальный вывод. База — две таблицы интернет-магазина: клиенты и их заказы. Структуру задаёт DDL-команда CREATE TABLE:
CREATE TABLE clients ( id INTEGER PRIMARY KEY, name TEXT NOT NULL, city TEXT, signup_date TEXT);CREATE TABLE orders ( id INTEGER PRIMARY KEY, client_id INTEGER REFERENCES clients(id), order_date TEXT, amount INTEGER, status TEXT);SELECT * FROM clients;| id | name | city | signup_date |
|---|---|---|---|
| 1 | Анна Котова | Москва | 2025-11-03 |
| 2 | Борис Лебедев | Казань | 2025-12-15 |
| 3 | Вера Соколова | Москва | 2026-01-20 |
| 4 | Глеб Орлов | Новосибирск | 2026-02-02 |
| 5 | Дарья Миронова | NULL | 2026-02-18 |
| 6 | Егор Волков | Казань | 2026-03-05 |
| 7 | Жанна Белова | Москва | 2026-03-27 |
У Дарьи Мироновой город не указан (NULL), у Жанны Беловой нет заказов.
SELECT * FROM orders;| id | client_id | order_date | amount | status |
|---|---|---|---|---|
| 101 | 1 | 2026-01-10 | 12500 | paid |
| 102 | 1 | 2026-02-14 | 8300 | paid |
| 103 | 2 | 2026-01-25 | 23000 | paid |
| 104 | 3 | 2026-02-03 | 4700 | cancelled |
| 105 | 3 | 2026-03-11 | 15600 | paid |
| 106 | 4 | 2026-03-02 | 31200 | paid |
| 107 | 5 | 2026-03-19 | 9900 | paid |
| 108 | 6 | 2026-04-05 | 6400 | new |
Колонка client_id ссылается на clients.id, по ней таблицы соединяют.
Оплаченные заказы от 10 000 ₽. Условия в WHERE соединяют через AND и OR.
SELECT id, client_id, amountFROM ordersWHERE status = 'paid' AND amount >= 10000;| id | client_id | amount |
|---|---|---|
| 101 | 1 | 12500 |
| 103 | 2 | 23000 |
| 105 | 3 | 15600 |
| 106 | 4 | 31200 |
Три самых крупных заказа. DESC сортирует по убыванию, без него сортировка идёт по возрастанию.
SELECT id, order_date, amountFROM ordersORDER BY amount DESCLIMIT 3;| id | order_date | amount |
|---|---|---|
| 106 | 2026-03-02 | 31200 |
| 103 | 2026-01-25 | 23000 |
| 105 | 2026-03-11 | 15600 |
Итоги по оплаченным заказам:
SELECT COUNT(*) AS orders_cnt, SUM(amount) AS revenue, ROUND(AVG(amount), 0) AS avg_check, MIN(amount) AS min_order, MAX(amount) AS max_orderFROM ordersWHERE status = 'paid';| orders_cnt | revenue | avg_check | min_order | max_order |
|---|---|---|---|---|
| 6 | 100500 | 16750 | 8300 | 31200 |
GROUP BY считает агрегаты отдельно для каждой группы, HAVING фильтрует уже посчитанные группы. Клиенты, у которых два и больше оплаченных заказа:
SELECT client_id, COUNT(*) AS cnt, SUM(amount) AS revenueFROM ordersWHERE status = 'paid'GROUP BY client_idHAVING COUNT(*) >= 2;| client_id | cnt | revenue |
|---|---|---|
| 1 | 2 | 20800 |
Условие на COUNT(*) ставят только в HAVING: в момент WHERE групп ещё нет.
INNER JOIN (можно писать JOIN) оставляет строки, у которых нашлась пара в обеих таблицах. Имя клиента рядом с каждым заказом:
SELECT c.name, o.id AS order_id, o.amountFROM clients AS cJOIN orders AS o ON o.client_id = c.idORDER BY o.id;| name | order_id | amount |
|---|---|---|
| Анна Котова | 101 | 12500 |
| Анна Котова | 102 | 8300 |
| Борис Лебедев | 103 | 23000 |
| Вера Соколова | 104 | 4700 |
| Вера Соколова | 105 | 15600 |
| Глеб Орлов | 106 | 31200 |
| Дарья Миронова | 107 | 9900 |
| Егор Волков | 108 | 6400 |
Жанны Беловой в выдаче нет: у неё нет заказов. LEFT JOIN сохраняет все строки левой таблицы и подставляет NULL, где пары нет:
SELECT c.name, COUNT(o.id) AS orders_cnt, COALESCE(SUM(o.amount), 0) AS totalFROM clients AS cLEFT JOIN orders AS o ON o.client_id = c.idGROUP BY c.id, c.nameORDER BY total DESC;| name | orders_cnt | total |
|---|---|---|
| Глеб Орлов | 1 | 31200 |
| Борис Лебедев | 1 | 23000 |
| Анна Котова | 2 | 20800 |
| Вера Соколова | 2 | 20300 |
| Дарья Миронова | 1 | 9900 |
| Егор Волков | 1 | 6400 |
| Жанна Белова | 0 | 0 |
COUNT(o.id) дал Жанне 0, потому что пропускает NULL. COALESCE заменил пустую сумму на 0. У Веры Соколовой в сумме есть отменённый заказ: фильтра по статусу нет.
| Вид JOIN | Что возвращает | Строк на нашей базе |
|---|---|---|
| INNER JOIN | только пары, которые нашлись в обеих таблицах | 8 |
| LEFT JOIN | все строки левой таблицы, справа NULL, если пары нет | 9 |
| RIGHT JOIN | все строки правой таблицы, слева NULL, если пары нет | 8 |
| FULL OUTER JOIN | все строки обеих таблиц | 9 |
| CROSS JOIN | каждую строку левой таблицы с каждой строкой правой | 56 (7 × 8) |
Число строк посчитано в SQLite для каждого вида JOIN. RIGHT и FULL OUTER JOIN появились в SQLite в версии 3.39.0.
Решите 150 задач на SQL в тренажёре
В модуле SQL программы SF Education 27 уроков, 2 бизнес-кейса и 150 задач в тренажёре. Первые 48 часов доступа бесплатны.
СУБД считает подзапрос в скобках и подставляет результат во внешний запрос. Заказы выше среднего чека (16 750 ₽):
SELECT id, client_id, amountFROM ordersWHERE status = 'paid' AND amount > (SELECT AVG(amount) FROM orders WHERE status = 'paid');| id | client_id | amount |
|---|---|---|
| 103 | 2 | 23000 |
| 106 | 4 | 31200 |
Подзапрос может вернуть и список: WHERE id IN (SELECT client_id FROM orders WHERE amount > 20000).
CTE (Common Table Expression) задаёт временную таблицу в начале запроса, на неё ссылаются по имени. Выручка и доля городов:
WITH paid AS ( SELECT c.city, o.amount FROM orders AS o JOIN clients AS c ON c.id = o.client_id WHERE o.status = 'paid')SELECT COALESCE(city, 'не указан') AS city, SUM(amount) AS revenue, ROUND(100.0 * SUM(amount) / (SELECT SUM(amount) FROM paid), 1) AS share_pctFROM paidGROUP BY cityORDER BY revenue DESC;| city | revenue | share_pct |
|---|---|---|
| Москва | 36400 | 36.2 |
| Новосибирск | 31200 | 31 |
| Казань | 23000 | 22.9 |
| не указан | 9900 | 9.9 |
Таблицу paid мы использовали дважды: для группировки и для суммы в знаменателе. Множитель 100.0 нужен для дробного результата: SQLite и PostgreSQL при делении целых отбрасывают дробную часть. Нарастающие итоги, ранги и сравнение с прошлым месяцем считают оконными функциями. Их мы разобрали в отдельной статье про оконные функции SQL.
Начинающему хватит пяти групп: агрегатные, строковые, даты, CASE и COALESCE. Сильнее всего между СУБД отличаются функции дат.
| Группа | Функции | Что делают | Пример |
|---|---|---|---|
| Агрегатные | COUNT, SUM, AVG, MIN, MAX | считают итог по группе | SUM(amount) |
| Строковые | UPPER, LOWER, LENGTH, SUBSTR, TRIM, REPLACE | меняют регистр, режут текст | SUBSTR(name, 1, 1) |
| Даты | SQLite: date, strftime; PostgreSQL: EXTRACT, date_trunc | достают из даты месяц или год | strftime('%m', signup_date) |
| CASE | CASE WHEN … THEN … ELSE … END | значение по условию, как ЕСЛИ в Excel | CASE WHEN amount > 10000 THEN 'крупный' END |
| COALESCE | COALESCE(a, b, …) | первый аргумент, отличный от NULL | COALESCE(city, 'не указан') |
Все пять групп в одном запросе:
SELECT name, UPPER(SUBSTR(name, 1, 1)) AS initial, COALESCE(city, 'не указан') AS city, strftime('%m', signup_date) AS month, CASE WHEN signup_date < '2026-01-01' THEN 'до 2026' ELSE '2026' END AS cohortFROM clientsORDER BY idLIMIT 5;| name | initial | city | month | cohort |
|---|---|---|---|---|
| Анна Котова | А | Москва | 11 | до 2026 |
| Борис Лебедев | Б | Казань | 12 | до 2026 |
| Вера Соколова | В | Москва | 01 | 2026 |
| Глеб Орлов | Г | Новосибирск | 02 | 2026 |
| Дарья Миронова | Д | не указан | 02 | 2026 |
Сравнение city = NULL не вернёт ни одной строки, NULL проверяют через IS NULL. Агрегаты по колонке пропускают NULL: в таблице clients 7 строк, а COUNT(city) возвращает 6.
Напишите запрос сами, потом откройте ответ с решением и результатом из SQLite.
SELECT name, signup_dateFROM clientsWHERE city = 'Москва'ORDER BY signup_date;| name | signup_date |
|---|---|
| Анна Котова | 2025-11-03 |
| Вера Соколова | 2026-01-20 |
| Жанна Белова | 2026-03-27 |
SELECT strftime('%Y-%m', order_date) AS month, COUNT(*) AS orders_cnt, SUM(amount) AS revenueFROM ordersWHERE status = 'paid'GROUP BY monthORDER BY month;| month | orders_cnt | revenue |
|---|---|---|
| 2026-01 | 2 | 35500 |
| 2026-02 | 1 | 8300 |
| 2026-03 | 3 | 56700 |
strftime('%Y-%m', …) оставляет от даты год и месяц. В PostgreSQL для этого пишут date_trunc('month', order_date).
SELECT c.nameFROM clients AS cLEFT JOIN orders AS o ON o.client_id = c.idWHERE o.id IS NULL;| name |
|---|
| Жанна Белова |
LEFT JOIN подставляет NULL в колонки заказа, если пары нет. Условие o.id IS NULL оставляет только такие строки.
SELECT COALESCE(c.city, 'не указан') AS city, ROUND(AVG(o.amount), 0) AS avg_checkFROM orders AS oJOIN clients AS c ON c.id = o.client_idWHERE o.status = 'paid'GROUP BY c.cityHAVING AVG(o.amount) > 10000ORDER BY avg_check DESC;| city | avg_check |
|---|---|
| Новосибирск | 31200 |
| Казань | 23000 |
| Москва | 12133 |
Условие на AVG ставят в HAVING. Строка без города выпала: её средний чек 9 900 ₽.
WITH top AS ( SELECT client_id, amount FROM orders WHERE status = 'paid' ORDER BY amount DESC LIMIT 1)SELECT c.name, top.amountFROM topJOIN clients AS c ON c.id = top.client_id;| name | amount |
|---|---|
| Глеб Орлов | 31200 |
Ту же задачу решает подзапрос: WHERE amount = (SELECT MAX(amount) FROM orders WHERE status = 'paid').
Как разбирать задачи на собеседовании, мы рассказали в статье о подготовке к интервью. Другие навыки, которые проверяют у кандидатов, — в материале о навыках бизнес-аналитика.
SQL — язык запросов. MySQL и PostgreSQL — СУБД, программы, которые хранят данные и выполняют запросы на SQL. Базовые SELECT, JOIN и GROUP BY работают в них одинаково, различаются функции дат, типы данных и отдельные операторы.
Подойдёт SQLite: база хранится в одном файле, сервер не нужен. Модуль sqlite3 входит в стандартную библиотеку Python. Скопируйте из статьи CREATE TABLE, добавьте строки через INSERT, и у вас будет та же база, что в примерах.
WHERE отбирает строки до группировки и не видит агрегатов. HAVING отбирает группы после GROUP BY и работает с SUM, COUNT, AVG. Условие на обычную колонку ставят в WHERE.
Проверьте, что можете объяснить порядок выполнения запроса, разницу WHERE и HAVING, виды JOIN, поведение NULL в агрегатах и разницу подзапроса и CTE. Отдельной темой идут оконные функции.
Пишите SQL-запросы для отчётов самостоятельно
Модуль SQL длится 1 месяц: 13 часов теории, 21 час практики, 150 задач и 2 бизнес-кейса. Выпускники получают удостоверение со сведениями в ФРДО.