Учебная база: две таблицы для примеров
Все SQL запросы в статье работают на одной маленькой базе из двух таблиц: клиенты и заказы. Код подходит для PostgreSQL, MySQL и SQLite. Самый быстрый способ попробовать без установки сервера - SQLite: он встроен в Python, а еще есть онлайн-песочницы вроде sqliteonline и db-fiddle.
CREATE TABLE users (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
city TEXT,
age INTEGER
);
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
user_id INTEGER REFERENCES users(id),
amount INTEGER,
created DATE
);
INSERT INTO users (id, name, city, age) VALUES
(1, 'Анна', 'Москва', 24),
(2, 'Борис', 'Казань', 31),
(3, 'Вера', 'Москва', 19),
(4, 'Глеб', NULL, 42);
INSERT INTO orders (id, user_id, amount, created) VALUES
(1, 1, 1500, '2026-09-01'),
(2, 1, 700, '2026-09-15'),
(3, 2, 3200, '2026-10-02'),
(4, 3, 450, '2026-10-10');
Обратите внимание: у Глеба нет ни одного заказа и не указан город. Эти пробелы специально оставлены, на них хорошо видна разница между видами JOIN и поведение NULL.
SELECT: выборка данных и фильтр WHERE
SELECT - главная команда SQL. Она говорит базе, какие столбцы вернуть и из какой таблицы.
SELECT name, city FROM users;
SELECT * FROM users; -- все столбцы
SELECT name, age + 1 AS next_age FROM users;
Звездочка удобна, чтобы быстро посмотреть таблицу, но в коде приложения лучше перечислять столбцы явно: запрос не сломается, если в таблицу добавят новое поле, и база не будет гонять лишние данные. AS задает псевдоним, под ним столбец придет в результате.
Фильтрация строк
WHERE оставляет только строки, которые подходят под условие.
SELECT name FROM users WHERE city = 'Москва';
SELECT name FROM users WHERE age BETWEEN 20 AND 35;
SELECT name FROM users WHERE city IN ('Москва', 'Казань');
SELECT name FROM users WHERE name LIKE 'А%';
SELECT name FROM users WHERE city IS NULL;
Последняя строка - частая ловушка. Условие city = NULL не вернет ничего, потому что NULL означает "неизвестно" и не равен даже самому себе. Для проверки пустоты есть только IS NULL и IS NOT NULL.
| Оператор | Что делает | Пример |
|---|---|---|
=, <> | Равно, не равно | city <> 'Казань' |
>, <= | Сравнение | age >= 18 |
AND, OR, NOT | Логика | age > 20 AND city = 'Москва' |
BETWEEN | Диапазон включительно | amount BETWEEN 500 AND 2000 |
IN | Совпадение со списком | id IN (1, 3) |
LIKE | Шаблон: % любые символы, _ один символ | name LIKE '_ера' |
IS NULL | Проверка на пустое значение | city IS NULL |
ORDER BY, LIMIT и DISTINCT
Строки в таблице не имеют гарантированного порядка. Если порядок важен, задайте его явно.
SELECT name, age FROM users ORDER BY age DESC;
SELECT name, age FROM users ORDER BY city, age;
SELECT * FROM orders ORDER BY amount DESC LIMIT 2;
SELECT DISTINCT city FROM users;
DESC сортирует по убыванию, ASC (по умолчанию) по возрастанию. Сортировать можно по нескольким столбцам: сначала по первому, внутри одинаковых значений по второму. LIMIT ограничивает число строк, вместе с ORDER BY это классический способ получить топ-N. В MS SQL Server вместо LIMIT пишут SELECT TOP 2, в PostgreSQL работает и стандартный вариант FETCH FIRST 2 ROWS ONLY. DISTINCT убирает повторы.
JOIN: как объединить таблицы
Данные о клиенте и его заказах лежат в разных таблицах, связь между ними - столбец orders.user_id. JOIN склеивает строки по такому условию.
SELECT u.name, o.amount, o.created
FROM users u
JOIN orders o ON o.user_id = u.id;
Буквы u и o - короткие псевдонимы таблиц, чтобы не писать имена целиком. Просто JOIN означает INNER JOIN: в результат попадут только клиенты, у которых есть заказы. Глеба там не будет.
SELECT u.name, o.amount
FROM users u
LEFT JOIN orders o ON o.user_id = u.id;
LEFT JOIN возвращает всех клиентов из левой таблицы. Для Глеба столбец amount будет NULL. На этом построен популярный вопрос с собеседований "найди клиентов без заказов":
SELECT u.name
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.id IS NULL;
| Тип JOIN | Что вернет | Когда нужен |
|---|---|---|
| INNER JOIN | Только строки с совпадением в обеих таблицах | Клиенты и их заказы |
| LEFT JOIN | Все строки левой таблицы и совпадения справа или NULL | Все клиенты, даже без заказов |
| RIGHT JOIN | Все строки правой таблицы | Редко, обычно переписывают через LEFT |
| FULL JOIN | Все строки обеих таблиц | Сверка двух списков |
| CROSS JOIN | Каждая строка с каждой | Генерация комбинаций |
FULL JOIN поддерживается в PostgreSQL и в SQLite начиная с версии 3.39, а в MySQL его нет, там объединяют LEFT и RIGHT JOIN через UNION. Если забыть условие ON или ошибиться в нем, получится декартово произведение: 4 клиента на 4 заказа дадут 16 строк вместо 4. Это одна из самых частых ошибок новичков.
GROUP BY и агрегатные функции
Агрегатные функции считают одно значение по множеству строк: COUNT, SUM, AVG, MIN, MAX. Без группировки они работают по всей таблице:
SELECT COUNT(*) AS total, SUM(amount) AS revenue FROM orders;
GROUP BY делит строки на группы и считает агрегат для каждой. Например, сколько клиентов в каждом городе:
SELECT city, COUNT(*) AS users_count
FROM users
GROUP BY city;
Строки с пустым городом соберутся в отдельную группу NULL. А вот сумма заказов по каждому клиенту, где пригодятся сразу JOIN и GROUP BY:
SELECT u.name,
COUNT(o.id) AS orders_count,
SUM(o.amount) AS total
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.name
ORDER BY total DESC;
Правило, на котором спотыкаются все: в SELECT вместе с агрегатами можно указывать только столбцы из GROUP BY. Запрос SELECT name, city, COUNT(*) FROM users GROUP BY city PostgreSQL отклонит с ошибкой, потому что непонятно, чье имя показать для группы.
Обратите внимание на COUNT(o.id) вместо COUNT(*). Для Глеба LEFT JOIN дает одну строку с NULL, и COUNT(*) насчитал бы ему один заказ. COUNT(столбец) пропускает NULL и вернет честный ноль.
HAVING: фильтр после группировки
WHERE фильтрует строки до группировки, HAVING - группы после нее. Поэтому условие на агрегат пишется только в HAVING.
SELECT user_id, SUM(amount) AS total
FROM orders
WHERE created >= '2026-09-01'
GROUP BY user_id
HAVING SUM(amount) > 1000;
Здесь сначала отбираются заказы с сентября, потом они группируются по клиенту, и в конце остаются клиенты с суммой больше 1000. Чтобы не путаться, держите в голове порядок, в котором база выполняет запрос. Он отличается от порядка, в котором мы его пишем.
| Шаг | Часть запроса | Что происходит |
|---|---|---|
| 1 | FROM, JOIN | Собираются исходные строки |
| 2 | WHERE | Отбрасываются лишние строки |
| 3 | GROUP BY | Строки объединяются в группы |
| 4 | HAVING | Отбрасываются лишние группы |
| 5 | SELECT | Вычисляются столбцы и псевдонимы |
| 6 | ORDER BY | Сортировка |
| 7 | LIMIT | Обрезка результата |
Отсюда понятно, почему псевдоним из SELECT нельзя использовать в WHERE: на шаге 2 его еще нет. А в ORDER BY можно, сортировка идет после SELECT.
INSERT, UPDATE и DELETE: изменение данных
Кроме чтения, SQL умеет добавлять, менять и удалять строки.
INSERT INTO users (id, name, city, age) VALUES (5, 'Дина', 'Пермь', 27);
UPDATE users SET city = 'Самара' WHERE id = 4;
DELETE FROM orders WHERE amount < 500;
Перед UPDATE и DELETE сначала выполните SELECT с тем же WHERE и посмотрите, какие строки попадут под изменение. UPDATE или DELETE без WHERE затронет всю таблицу, и откатить это можно только из бэкапа или внутри транзакции. В рабочих базах оборачивайте опасные изменения в BEGIN; ... COMMIT;, а если результат не понравился, выполняйте ROLLBACK;.
Как тренироваться и где SQL пригодится
- Запустите учебную базу из первого раздела в SQLite или онлайн-песочнице.
- Решите задачи: средний возраст клиентов по городам; клиент с самой большой суммой заказов; число заказов по месяцам; города, где больше одного клиента.
- Переходите к тренажерам с готовыми базами: sql-ex.ru, задачи SQL на LeetCode и Stepik-курсы с автопроверкой.
- Свяжите SQL с языком программирования: в Python база SQLite подключается модулем
sqlite3из стандартной библиотеки.
import sqlite3
con = sqlite3.connect("shop.db")
rows = con.execute(
"SELECT name FROM users WHERE city = ?", ("Москва",)
).fetchall()
print(rows)
Знак вопроса - параметр запроса. Никогда не подставляйте пользовательский ввод в SQL через f-строки: так появляются SQL-инъекции. SQL спрашивают почти на любом собеседовании, в том числе на позицию Python junior, а начать изучать сам Python поможет гайд Python с нуля. Если работаете с API, пригодится и статья о формате JSON. Все материалы по базам данных - в теге SQL.
