SQL (Structured Query Language) — декларативный язык запросов к реляционным базам данных: вы описываете, какие данные нужны, а система управления базой данных (СУБД) сама решает, как их достать. Начинающему достаточно освоить четыре вещи: создать таблицы, выбрать строки (SELECT + WHERE), посчитать итоги (GROUP BY) и связать таблицы (JOIN).
Содержание
Ниже — практический маршрут: учебная база в SQLite, которую можно собрать за пять минут, и первые запросы к ней с реальным выводом. Примеры проверены 24.09.2026 в SQLite 3.51.0 (свежий релиз на эту дату — 3.53.4, для запросов статьи разницы нет); где PostgreSQL ведет себя иначе, это сказано рядом.
Три слова, которые путают новички
- SQL — язык. Есть стандарт ISO/IEC 9075, но каждая СУБД реализует его со своими расширениями.
- СУБД — программа, которая хранит данные и выполняет запросы: SQLite, PostgreSQL, MySQL, SQL Server.
- Диалект — вариант SQL конкретной СУБД.
SELECT ... WHEREодинаков почти везде, а постраничный вывод, типы дат и автоинкремент различаются.
Поэтому фраза «выучить SQL» на практике означает «выучить общее ядро языка и один диалект». Для старта удобен SQLite: это библиотека и утилита sqlite3 без сервера, база — один файл на диске.
Шаг 1. Учебная база: клиенты и заказы
Установите sqlite3 (в macOS он обычно уже есть, в Ubuntu — пакет sqlite3, для Windows — архив sqlite-tools с официального сайта SQLite). Сохраните скрипт в файл schema.sql:
PRAGMA foreign_keys = ON;
CREATE TABLE clients (
id INTEGER PRIMARY KEY,
name TEXT NOT NULL,
city TEXT,
age INTEGER CHECK (age BETWEEN 14 AND 120)
) STRICT;
CREATE TABLE orders (
id INTEGER PRIMARY KEY,
client_id INTEGER NOT NULL REFERENCES clients(id),
amount INTEGER NOT NULL CHECK (amount > 0), -- в рублях, целым числом
created TEXT NOT NULL -- дата в формате ГГГГ-ММ-ДД
) STRICT;
INSERT INTO clients (id, name, city, age) VALUES
(1, 'Анна', 'Москва', 34),
(2, 'Борис', 'Казань', 19),
(3, 'Вера', 'Москва', 27),
(4, 'Глеб', NULL, 45),
(5, 'Дарья', 'Казань', 22);
INSERT INTO orders (id, client_id, amount, created) VALUES
(1, 1, 1200, '2026-09-01'),
(2, 1, 800, '2026-09-10'),
(3, 2, 450, '2026-09-03'),
(4, 3, 3100, '2026-09-05'),
(5, 5, 990, '2026-09-12'),
(6, 3, 700, '2026-09-15');
Создайте базу и откройте ее в режиме таблиц с заголовками:
sqlite3 shop.db < schema.sql
sqlite3 -header -column shop.db
Что здесь важно понять:
PRIMARY KEY— первичный ключ, уникальный номер строки.REFERENCES clients(id)— внешний ключ: заказ ссылается на существующего клиента.NOT NULLиCHECK— ограничения. Они отклоняют неверные данные на уровне базы, а не на честном слове программиста.STRICT(SQLite 3.37+) включает строгую проверку типов. Без него SQLite применяет «родство типов» и может сохранить текст в числовой столбец.- Сумма хранится целым числом рублей: в SQLite нет точного десятичного типа,
DECIMALтам фактически становится числом с плавающей точкой или целым.
Проверим, что ограничения работают:
INSERT INTO clients (name, city, age) VALUES ('Ева', 'Москва', 7);
INSERT INTO clients (name, city, age) VALUES ('Жанна', 'Москва', 'двадцать');
Runtime error: CHECK constraint failed: age BETWEEN 14 AND 120 (19)
Runtime error: cannot store TEXT value in INTEGER column clients.age (19)
Так ошибки выглядят в интерактивной консоли sqlite3. Если подать команды файлом через <, префикс будет Runtime error near line 1:, суть сообщения та же.
Одна оговорка про SQLite: проверка внешних ключей по умолчанию выключена и включается командой PRAGMA foreign_keys = ON; в каждом новом подключении. Строка в скрипте действует только на время его выполнения. Если открыть базу заново и вставить заказ несуществующего клиента client_id = 99, без этой команды вставка пройдет молча, а с ней — упадет с FOREIGN KEY constraint failed. В PostgreSQL и MySQL (InnoDB) внешние ключи проверяются по умолчанию, отдельно включать их не нужно (в MySQL проверку можно временно выключить SET foreign_key_checks = 0, чем пользуются при загрузке дампов).
Шаг 2. SELECT и WHERE: выбрать нужные строки
Минимальный запрос — какие столбцы и из какой таблицы:
SELECT name, age FROM clients;
name age
----- ---
Анна 34
Борис 19
Вера 27
Глеб 45
Дарья 22
WHERE оставляет только строки, для которых условие истинно. Условия объединяются через AND, OR, NOT; полезны также IN (...), BETWEEN и LIKE:
SELECT name, city, age
FROM clients
WHERE age > 20 AND city = 'Москва';
name city age
---- ------ ---
Анна Москва 34
Вера Москва 27
Порядок строк без ORDER BY не гарантирован: база вправе вернуть их в любом порядке. Нужна сортировка — пишите ее явно, LIMIT ограничивает число строк (в SQL Server вместо него TOP, в стандарте — FETCH FIRST):
SELECT name, age FROM clients ORDER BY age DESC LIMIT 3;
name age
---- ---
Глеб 45
Анна 34
Вера 27
Ловушка с NULL
NULL означает «значение неизвестно», а не пустую строку и не ноль. Любое сравнение с ним дает не «истину», а «неизвестно», поэтому такой запрос возвращает ноль строк без всякой ошибки:
SELECT name, city FROM clients WHERE city = NULL;
Исправление — специальный оператор IS NULL:
SELECT name, city FROM clients WHERE city IS NULL;
name city
---- ----
Глеб
Второе следствие: условие city <> 'Москва' вернет только Бориса и Дарью, Глеба с неизвестным городом в выборке не будет. Если он нужен, добавьте OR city IS NULL.
Шаг 3. GROUP BY и HAVING: посчитать итоги
Агрегатные функции COUNT, SUM, AVG, MIN, MAX сворачивают группу строк в одно значение. GROUP BY задает, по какому столбцу делить строки на группы:
SELECT city, COUNT(*) AS clients_cnt, AVG(age) AS avg_age
FROM clients
GROUP BY city;
city clients_cnt avg_age
------ ----------- -------
1 45.0
Казань 2 20.5
Москва 2 30.5
Строки с NULL в city попали в отдельную группу — первая строка вывода.
WHERE фильтрует строки до группировки, HAVING — группы после нее. Поэтому агрегат в WHERE писать нельзя:
SELECT city, COUNT(*) FROM clients WHERE COUNT(*) > 1 GROUP BY city;
Parse error: misuse of aggregate: COUNT()
SELECT city, COUNT(*) FROM clients WHERE COUNT(*) > 1 GROUP BY city;
error here ---^
Правильно — перенести условие на группы: ... GROUP BY city HAVING COUNT(*) > 1;. В PostgreSQL 18 текст ошибки другой: aggregate functions are not allowed in WHERE.
Шаг 4. JOIN: связать клиентов и заказы
Данные разложены по двум таблицам, чтобы имя клиента не повторялось в каждом заказе. JOIN ... ON собирает их обратно по ключу:
SELECT c.name, o.amount, o.created
FROM orders AS o
JOIN clients AS c ON c.id = o.client_id
ORDER BY o.created;
name amount created
----- ------ ----------
Анна 1200 2026-09-01
Борис 450 2026-09-03
Вера 3100 2026-09-05
Анна 800 2026-09-10
Дарья 990 2026-09-12
Вера 700 2026-09-15
c и o — псевдонимы таблиц, они сокращают запись и снимают неоднозначность, если столбцы называются одинаково.
| Вид соединения | Что возвращает | Когда брать |
|---|---|---|
INNER JOIN (просто JOIN) |
только пары, у которых нашлось совпадение | заказы с данными клиента |
LEFT JOIN |
все строки левой таблицы, недостающие поля правой = NULL |
все клиенты, включая тех, у кого нет заказов |
без условия ON |
каждую строку с каждой (декартово произведение) | почти никогда в отчетах |
Итог по всем клиентам, включая тех, кто ничего не купил:
SELECT c.name,
COUNT(o.id) AS orders_cnt,
COALESCE(SUM(o.amount), 0) AS total
FROM clients AS c
LEFT JOIN orders AS o ON o.client_id = c.id
GROUP BY c.id, c.name
ORDER BY total DESC;
name orders_cnt total
----- ---------- -----
Вера 2 3800
Анна 2 2000
Дарья 1 990
Борис 1 450
Глеб 0 0
COUNT(o.id) не считает NULL, поэтому у Глеба 0, а COUNT(*) дал бы 1. COALESCE заменяет NULL-сумму на ноль.
Частая ошибка с LEFT JOIN — поставить условие на правую таблицу в WHERE. Запрос «сколько у каждого клиента заказов дороже 1000» с WHERE o.amount > 1000 вернет только Анну и Веру: строки с NULL отсеялись, и LEFT JOIN превратился во внутренний. Если перенести условие в ON o.client_id = c.id AND o.amount > 1000, в выводе будут все пятеро, у троих — 0.
Типичные ошибки новичка
Столбец вне группировки. Запрос SELECT name, SUM(amount) FROM clients JOIN orders ON orders.client_id = clients.id; в SQLite выполнится и вернет Анна | 7240: 7240 — сумма всех заказов, а имя взято из произвольной строки. PostgreSQL 18 такой запрос отклоняет:
ERROR: column "clients.name" must appear in the GROUP BY clause or be used in an aggregate function
Исправление: каждый неагрегированный столбец из SELECT вынести в GROUP BY (GROUP BY c.id, c.name, как выше). Правило полезно соблюдать и там, где СУБД прощает.
Пропущено условие соединения. SELECT COUNT(*) FROM clients, orders; возвращает 30 — это 5 клиентов умножить на 6 заказов. Если число строк после соединения подозрительно выросло, проверьте ON.
UPDATE или DELETE без WHERE. Команда меняет все строки таблицы. Пока учитесь, запускайте изменения внутри транзакции и смотрите число затронутых строк до фиксации:
BEGIN;
UPDATE clients SET city = 'Казань'; -- забыли WHERE
SELECT changes() AS updated; -- 5: изменены все клиенты
ROLLBACK; -- отменяем
После ROLLBACK города остаются прежними. changes() — функция SQLite; в других СУБД число строк сообщает клиент (UPDATE 5 в psql). На рабочей базе транзакция не заменяет резервную копию: после COMMIT откатить уже нельзя.
Как тренироваться дальше
- Соберите учебную базу из этой статьи и добейтесь того же вывода.
- Напишите 10 своих вопросов к данным («кто купил больше всех в сентябре», «города без заказов») и ответьте на каждый одним запросом.
- Добавьте третью таблицу, например
products, и соединение трех таблиц. - Перенесите схему в PostgreSQL и посмотрите, что пришлось поменять.
Итоговый артефакт — файл queries.sql с вопросами в комментариях и ответами-запросами. С ним удобно прийти на собеседование или в учебный проект.
Если не получилось:
| Симптом | Вероятная причина |
|---|---|
no such table: clients |
открыт другой файл базы или скрипт не выполнился |
| запрос вернул ноль строк без ошибки | сравнение = NULL, лишний пробел или регистр в строке |
LIKE 'а%' не находит «Анна» |
в SQLite LIKE игнорирует регистр только для латиницы |
строк после JOIN слишком много |
нет условия ON или соединение не по ключу |
Когда основы уложатся, пригодится обзор теории: введение в SQL для новичка.
Что поменяется в другой СУБД
| В SQLite | В PostgreSQL / MySQL |
|---|---|
база — один файл, CREATE DATABASE нет |
база создается командой, нужен сервер |
STRICT для строгих типов |
типы проверяются всегда |
внешние ключи — через PRAGMA |
проверяются по умолчанию (MySQL — в InnoDB) |
| нет точного десятичного типа | NUMERIC / DECIMAL с точной арифметикой |
столбец вне GROUP BY допустим |
PostgreSQL — ошибка; MySQL 8 — ошибка при режиме ONLY_FULL_GROUP_BY (включен по умолчанию) |
Выводы
- SQL — язык запросов, СУБД — программа, диалект — вариант языка в конкретной СУБД; ядро
SELECT/WHERE/GROUP BY/JOINпереносится почти без изменений. - Для первых шагов хватает SQLite: база — один файл, ограничения
CHECK,NOT NULLиSTRICTсразу показывают, как база защищает данные. NULLсравнивают черезIS NULL, аWHEREфильтрует строки до группировки,HAVING— группы после.LEFT JOINсохраняет строки без пары, но условие на правую таблицу вWHEREпревращает его во внутреннее соединение.- Изменения данных учебно запускайте в транзакции и проверяйте число затронутых строк до
COMMIT.
Где применяется / связь с практикой
Освойте тему на практике
Запросы из этой статьи — ежедневная работа аналитика, бэкенд-разработчика и тестировщика: выборка для отчета, проверка данных после релиза, поиск «потерянных» записей через LEFT JOIN. Следующий уровень — оконные функции, подзапросы, индексы и чтение плана запроса. Их системно разбирают на курсе Otus по SQL, а попробовать формат можно на бесплатных открытых уроках Otus.
FAQ
Можно ли написать на SQL целое приложение?
Обычно нет: SQL работает с данными, а интерфейс и логику пишут на Python, Java, PHP и других языках, которые отправляют запросы в базу. Хранимые процедуры в PostgreSQL или SQL Server позволяют вынести часть логики в базу, но это дополнение, а не замена.
Важен ли регистр в SQL?
Ключевые слова (select и SELECT) не чувствительны к регистру. Сравнение строк в данных зависит от СУБД и настроек сортировки (collation), поэтому 'Москва' = 'москва' в разных базах может дать разный результат.
Нужна ли точка с запятой в конце запроса?
В консоли sqlite3 и psql она обязательна, иначе клиент ждет продолжения запроса. Во многих программах и библиотеках один запрос можно отправить и без нее.



