SQL для начинающих: первые запросы SELECT, WHERE и JOIN на учебной базе

SQL для начинающих: первые запросы SELECT, WHERE и JOIN на учебной базе Полезное

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 откатить уже нельзя.

Как тренироваться дальше

  1. Соберите учебную базу из этой статьи и добейтесь того же вывода.
  2. Напишите 10 своих вопросов к данным («кто купил больше всех в сентябре», «города без заказов») и ответьте на каждый одним запросом.
  3. Добавьте третью таблицу, например products, и соединение трех таблиц.
  4. Перенесите схему в 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 она обязательна, иначе клиент ждет продолжения запроса. Во многих программах и библиотеках один запрос можно отправить и без нее.

OTUS Журнал
Скидка 5% 12-19 октября на курсы (popup)
Бесплатные открытые уроки (поп-ап)