SQL — язык структурированных запросов

SQL (Structured Query Language) — стандартный язык для работы с реляционными базами данных. С его помощью создаются таблицы, вставляются, обновляются и извлекаются данные. Навыки SQL востребованы у разработчиков, аналитиков и администраторов.

Уровень: начальный 18 разделов ⏱ 2–3 часа

1. Что такое SQL и базы данных

База данных — это организованное хранилище информации. Реляционная база данных хранит данные в виде связанных таблиц: каждая таблица состоит из строк (записей) и столбцов (полей).

Программа, которая управляет базой данных и понимает SQL, называется СУБД (система управления базами данных). Наиболее популярные СУБД:

СУБДТипОсобенности
MySQLOpen SourceПростая настройка, широкое сообщество
PostgreSQLOpen SourceРасширенная типизация, JSON-поддержка, ACID
SQLiteEmbeddedВстраивается в приложение, не требует сервера
SQL ServerКоммерческийИнтеграция с Microsoft-стеком
Oracle DBКоммерческийКрупныеenterprise-системы
В дальнейшем примеры будут работать как в MySQL, так и в PostgreSQL — синтаксис почти идентичен на базовом уровне.

2. Типы данных

При создании столбца нужно указать его тип. Вот основные типы данных:

ТипОписаниеПример значения
INTЦелое число42, -7, 0
VARCHAR(n)Строка переменной длины (до n символов)'Иван', 'Привет'
TEXTДлинный текст без жёсткого ограниченияОписание товара
DECIMAL(p, s)Точное десятичное число (p — всего цифр, s — после запятой)199.99
DATEДата (год, месяц, день)2026-09-01
DATETIMEДата и время2026-09-01 14:30:00
BOOLEANЛогическое значение (TRUE / FALSE)TRUE

3. Создание и изменение таблиц

Для создания таблицы используется команда CREATE TABLE. Разберём на примере небольшого интернет-магазина:

CREATE TABLE users (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(100) NOT NULL,
    email VARCHAR(150) NOT NULL UNIQUE,
    age INT,
    created_at DATETIME DEFAULT CURRENT_TIMESTAMP
);

Здесь:

CREATE TABLE products (
    id INT PRIMARY KEY AUTO_INCREMENT,
    name VARCHAR(200) NOT NULL,
    price DECIMAL(10, 2) NOT NULL,
    stock INT DEFAULT 0,
    description TEXT
);
CREATE TABLE orders (
    id INT PRIMARY KEY AUTO_INCREMENT,
    user_id INT NOT NULL,
    product_id INT NOT NULL,
    quantity INT DEFAULT 1,
    order_date DATETIME DEFAULT CURRENT_TIMESTAMP,
    FOREIGN KEY (user_id) REFERENCES users(id),
    FOREIGN KEY (product_id) REFERENCES products(id)
);

Команда ALTER TABLE позволяет изменять структуру уже существующей таблицы:

ALTER TABLE users ADD phone VARCHAR(20);
ALTER TABLE users DROP COLUMN age;
ALTER TABLE users MODIFY phone VARCHAR(30);
Команда DROP TABLE безвозвратно удаляет таблицу вместе со всеми данными. Используйте её с осторожностью.

4. Вставка данных

Для добавления записей используется команда INSERT INTO.

INSERT INTO users (name, email, age)
VALUES ('Иван Петров', 'ivan@example.com', 28);

Массовая вставка нескольких строк за один запрос:

INSERT INTO products (name, price, stock) VALUES
('Ноутбук ASUS', 54990.00, 12),
('Наушники Sony', 7990.50, 35),
('Клавиатура Logitech', 3200.00, 50);

Явное указание NULL для столбцов без значения:

INSERT INTO users (name, email, age)
VALUES ('Мария Сидорова', 'maria@example.com', NULL);
Если вы перечислили все столбцы и вставляете данные во все из них, можно опустить список столбцов после имени таблицы. Но рекомендуется всегда указывать столбцы явно.

5. Выборка данных — SELECT

Команда SELECT — самая важная и часто используемая в SQL. Она извлекает данные из таблицы.

SELECT * FROM users;

Выбор определённых столбцов:

SELECT name, email FROM users;

Уникальные значения с помощью DISTINCT:

SELECT DISTINCT age FROM users;

Ограничение количества результатов:

SELECT * FROM products LIMIT 2;

Сортировка с помощью ORDER BY:

SELECT * FROM products ORDER BY price ASC;
SELECT * FROM products ORDER BY price DESC;

Фильтрация с помощью WHERE:

SELECT * FROM users WHERE age > 25;
SELECT * FROM products WHERE price <= 5000;
SELECT * FROM users WHERE name = 'Иван Петров';

6. Фильтрация и операторы

BETWEEN — диапазон значений

SELECT * FROM products
WHERE price BETWEEN 3000 AND 10000;

IN — проверка на вхождение в список

SELECT * FROM users
WHERE id IN (1, 3, 5);

LIKE — поиск по шаблону

Символ % заменяет любое количество символов, _ — один символ:

SELECT * FROM users WHERE name LIKE '%Петров%';
SELECT * FROM products WHERE name LIKE 'Ноут%';
SELECT * FROM users WHERE email LIKE '___@example.com';

IS NULL / IS NOT NULL

SELECT * FROM users WHERE age IS NULL;
SELECT * FROM users WHERE age IS NOT NULL;

Логические операторы AND / OR

SELECT * FROM products
WHERE price > 5000 AND stock > 10;

SELECT * FROM users
WHERE age > 30 OR name LIKE '%Сидорова%';

7. Группировка и агрегатные функции

Агрегатные функции подсчитывают значения по набору строк:

ФункцияНазначение
COUNT(*)Количество строк
SUM(column)Сумма значений
AVG(column)Среднее значение
MIN(column)Минимальное значение
MAX(column)Максимальное значение
SELECT COUNT(*) FROM orders;
SELECT SUM(price) FROM products;
SELECT AVG(price) FROM products;
SELECT MIN(price) FROM products;
SELECT MAX(price) FROM products;

Группировка с помощью GROUP BY позволяет считать агрегаты по группам строк:

SELECT user_id, COUNT(*) AS order_count
FROM orders
GROUP BY user_id;

Фильтрация после группировки — через HAVING:

SELECT user_id, COUNT(*) AS order_count
FROM orders
GROUP BY user_id
HAVING COUNT(*) >= 2;
Отличие WHERE от HAVING: WHERE фильтрует строки до группировки, а HAVING — после.

8. Соединение таблиц — JOIN

Реляционная модель хранит данные в разных таблицах. Чтобы получить данные из нескольких таблиц одновременно, используются соединения (JOIN).

INNER JOIN — только совпадающие записи

SELECT users.name, orders.id, orders.quantity
FROM users
INNER JOIN orders ON users.id = orders.user_id;

В результат попадут только те пользователи, которые сделали хотя бы один заказ, и только те заказы, у которых есть пользователь.

LEFT JOIN — все записи из левой таблицы

SELECT users.name, orders.id
FROM users
LEFT JOIN orders ON users.id = orders.user_id;

Покажет всех пользователей, даже если у них нет заказов. Столбцы из orders будут NULL.

RIGHT JOIN — все записи из правой таблицы

SELECT users.name, orders.id
FROM users
RIGHT JOIN orders ON users.id = orders.user_id;

Покажет все заказы, включая те, у которых user_id ссылается на несуществующего пользователя.

Тройное соединение

SELECT users.name, products.name, orders.quantity
FROM orders
INNER JOIN users ON orders.user_id = users.id
INNER JOIN products ON orders.product_id = products.id;
Всегда указывайте INNER JOIN, если хотите видеть только полные совпадения. Неуказанный JOIN в некоторых СУБД интерпретируется как INNER JOIN.

9. Обновление и удаление

UPDATE — изменение данных

UPDATE users SET age = 29 WHERE id = 1;
UPDATE products SET price = price * 1.10;
Внимание! Запрос UPDATE без WHERE изменит все строки в таблице. Всегда проверяйте условие перед выполнением.

DELETE — удаление записей

DELETE FROM orders WHERE id = 5;
DELETE FROM users WHERE name = 'Мария Сидорова';
DELETE FROM orders (без WHERE) удалит все заказы. Это действие необратимо!

TRUNCATE — быстрое удаление всех строк

TRUNCATE TABLE orders;

Команда TRUNCATE удаляет все строки, но работает быстрее DELETE, так как не логирует каждое удаление. Таблица при этом сохраняется.

10. Индексы

Индекс — это дополнительная структура данных, которая ускоряет поиск по столбцу (или группе столбцов). Аналогия — оглавление в книге: вместо листания всех страниц вы сразу переходите к нужному разделу.

CREATE INDEX idx_users_email ON users(email);
CREATE INDEX idx_products_price ON products(price);

Покрытие запросов — ситуация, когда все запрошенные столбцы уже есть в индексе, и СУБД не нужно обращаться к самой таблице. Это значительно ускоряет выполнение.

SELECT email FROM users WHERE email = 'ivan@example.com';

Запрос выше будет очень быстрым, если на столбец email создан индекс.

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

11. Транзакции и нормализация

Транзакции

Транзакция — это группа операций, которые выполняются как единое целое. Либо все применяются, либо ни одна.

BEGIN;

UPDATE accounts SET balance = balance - 5000 WHERE id = 1;
UPDATE accounts SET balance = balance + 5000 WHERE id = 2;

COMMIT;

Если что-то пошло не так:

ROLLBACK;

Транзакции должны соответствовать принципам ACID:

Нормализация баз данных

Нормализация — процесс организации таблиц для минимизации дублирования данных:

Нормальная формаПравило
1НФКаждый столбец содержит только атомарные (неделимые) значения
2НФНет частичных зависимостей — каждый неключевой столбец зависит от всего первичного ключа
3НФНет транзитивных зависимостей — неключевые столбцы зависят только от первичного ключа

Правило 3НФ на практике означает: если столбец зависит не от ключа напрямую, а через другой столбец, его стоит вынести в отдельную таблицу.

Не стоит чрезмерно нормализовать — иногда допускается дублирование данных ради производительности (денормализация). Но для начала придерживайтесь 3НФ.

12. Подзапросы и CTE

Один запрос может вычисляться на основе другого. Подзапросы возвращают значение, набор строк или таблицу; CTE (WITH) делают сложные запросы читаемыми.

-- среднее по категории внутри общего контекста
SELECT product, price,
       (SELECT AVG(price) FROM products p2
        WHERE p2.category = p.category) AS avg_cat
FROM products p;
-- CTE: сперва агрегат, затем фильтр
WITH sales_per_product AS (
  SELECT product_id, SUM(qty * price) AS revenue
  FROM order_items
  GROUP BY product_id
)
SELECT p.name, s.revenue
FROM sales_per_product s
JOIN products p ON p.id = s.product_id
ORDER BY s.revenue DESC
LIMIT 5;
Разбивайте тяжёлый запрос на шаги с CTE: читаемость растёт, а оптимизатор строит один план без потери скорости.

13. Оконные функции

Оконные функции считают агрегаты по окну строк, не сворачивая их в одну: каждая строка сохраняется и получает дополнительное значение. Без них «топ по каждой категории» превращается в многословие.

SELECT product, category, price,
       RANK()       OVER (PARTITION BY category ORDER BY price DESC) AS rnk,
       ROW_NUMBER() OVER (ORDER BY price DESC) AS rn
FROM products;
-- корректно: топ-3 дорогих товара в каждой категории
WITH ranked AS (
  SELECT p.*,
         RANK() OVER (PARTITION BY category ORDER BY price DESC) AS rnk
  FROM products p
)
SELECT name, category, price
FROM ranked
WHERE rnk <= 3;
ФункцияСмыслКлючевое слово
ROW_NUMBER()порядковый номерORDER BY
RANK() / DENSE_RANK()ранг с пропусками / безPARTITION BY
SUM() OVER (...) накопленный итогROWS BETWEEN
LAG() / LEAD()значение соседней строкиROW
WHERE работает до оконной функции: фильтровать по результату RANK() можно только во внешнем запросе или CTE, как в примере выше.

14. Планы запросов и индексы

Медленный запрос почти всегда лечится индексом или переписыванием. Инструмент — EXPLAIN: он показывает, как БД собирается исполнять запрос.

EXPLAIN SELECT name FROM users WHERE email = 'a@b.ru';
EXPLAIN ANALYZE SELECT ...   -- реальное время
CREATE INDEX idx_users_email ON users(email);
-- после индекса: Index Scan вместо Seq Scan
Проверяйте план на реальном объёме данных: на тестовой таблице из 100 строк оптимизатор выбирает не то, что в проде. Пример из жизни — статьи Алгоритмы и индексы.

15. Ограничения целостности: FOREIGN KEY и CHECK

Целостность данных защищает база, а не «дисциплина разработчиков». Ограничения FOREIGN KEY, CHECK, UNIQUE и NOT NULL не позволяют сохранить противоречивые данные.

CREATE TABLE orders (
    id INT PRIMARY KEY,
    user_id INT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
    status VARCHAR(20) NOT NULL DEFAULT 'new'
        CHECK (status IN ('new', 'paid', 'shipped', 'cancelled')),
    qty INT CHECK (qty > 0),
    email VARCHAR(150) CHECK (email LIKE '%@%')
);
ОграничениеЗащищает от
FOREIGN KEYссылок на несуществующие записи
CHECKзначений вне допустимого диапазона
UNIQUEдублей по ключевому полю
NOT NULLотсутствия обязательного значения

Поведение при удалении родителя задаёт ON DELETE: CASCADE удаляет дочерние строки, SET NULL обнуляет ссылку, RESTRICT запрещает удаление.

Слишком много жёстких связей усложняют изменение схемы. Проверяйте: каскадное удаление — действительно то, что нужно, или данные ценнее.

16. SQL-инъекции и параметризация

Инъекция — это внедрение куска SQL через пользовательский ввод, склеенный с запросом строкой. Безопасный способ — параметризованные запросы: СУБД гарантирует, что переданное значение — данные, а не команда.

-- уязвимо: ввод подставляется напрямую
SELECT * FROM users WHERE login = '" + login + "' AND pass = '" + pass + "'

-- взлом: login = admin' -- 
SELECT * FROM users WHERE login = 'admin' -- ' AND pass = ''

-- безопасно (Python, psycopg2):
cursor.execute(
    "SELECT * FROM users WHERE login = %s AND pass = %s",
    (login, pass),
)
-- PostgreSQL: место значения обозначается $1, $2
SELECT * FROM users WHERE login = $1 AND pass = $2
ЯзыкПлейсхолдер
Python (psycopg2)%s / $1
Java (JDBC)? (PreparedStatement)
PHP (PDO):named или ?
JS (node-postgres)$1, $2, ...
Правило одно: никакая строка от пользователя не попадает в SQL иначе как через плейсхолдер. Даже числа — только через белые списки.

17. Полнотекстовый поиск: tsvector и GIN

При LIKE '%слово%' база сканирует каждую строку — на больших данных это секунды. Полнотекстовый поиск PostgreSQL индексирует слова и находит их с учётом морфологии за миллисекунды.

CREATE INDEX articles_fts ON articles
    USING GIN (to_tsvector('russian', title || ' ' || body));

SELECT id, title
FROM articles
WHERE to_tsvector('russian', title || ' ' || body)
      @@ to_tsquery('russian', 'postgresql:&индексы');

Операторы запроса: & (и), | (или), ! (не). Ранжировать результаты помогает ts_rank():

SELECT ts_rank(tsv, to_tsquery('russian', 'индексы')) AS r
FROM articles
WHERE tsv @@ to_tsquery('russian', 'индексы')
ORDER BY r DESC LIMIT 10;
-- поиск по префиксу: «индекс*»
to_tsquery('russian', 'индекс:*')
Держите tsvector отдельным столбцом (сгенерированным), чтобы не писать выражение в каждом запросе. Стоп-слова отсекаются автоматически, но большие тексты экономично индексировать частично.

18. Бэкапы и миграции

Данные без бэкапа — «дом без страховки». Логический дамп pg_dump выгружает схему и данные в SQL-файл, а восстановление через psql проверяет копию: бэкап, который никогда не восстанавливали, бэкапом не считается.

# PostgreSQL
pg_dump -U app -d shop -f shop_$(date +%F).sql
# восстановление на чистой машине
psql -U app -d shop_new -f shop_2026-01-01.sql

# MySQL
mysqldump -u app -p shop > shop.sql

Миграции (Flyway, Liquibase) версионируют изменения схемы: каждый файл V1__init.sql, V2__add_status.sql применяется один раз и только по порядку. «Править БД руками» заменяется воспроизводимым процессом при деплое.

ИнструментЗадача
pg_dump / mysqldumpбэкап и перенос данных
Flywayверсии миграций в SQL
Liquibaseмиграции в XML/YAML с откатами
Храните дампы вне машины с базой (правило 3-2-1) и заносите проверку восстановления в календарь: пустая резервная копия спасает меньше, чем кажется.

19. JSON в реляционной БД: PostgreSQL jsonb

Не всё стоит превращать в отдельные таблицы: для вложенных документов (настройки, метаданные, формы) PostgreSQL умеет хранить JSON с индексацией. Тип jsonb хранит данные в бинарном виде и позволяет запросы внутрь документа.

CREATE TABLE shop_products (
  id      BIGSERIAL PRIMARY KEY,
  name    TEXT NOT NULL,
  meta    JSONB NOT NULL DEFAULT '{}'
);

-- запрос внутрь JSON
SELECT name FROM shop_products
WHERE meta->>'color' = 'blue'
  AND (meta->>'weight')::int > 2;

Для быстрых фильтров по полям внутри JSON создают GIN-индекс:

CREATE INDEX idx_products_meta ON shop_products USING gin (meta);
ОператорЧто делает
->достаёт jsonb-значение
->>достаёт текст
@>содержит ли документ элемент
JSON — не замена нормализации. Для фильтруемых и сортируемых полей лучше обычные колонки; JSON подходит для гибкого, редко запрашиваемого «хвоста» (см. курс MySQL).
Предпочитайте jsonb типу json: бинарный формат без дублируемых пробелов и с поддержкой индексов.

20. Материализованные представления

Тяжёлые агрегаты (итоги продаж за месяц, сводки) считать на каждом запросе дорого. Materialized view — результат запроса, сохранённый как таблица: обновление по расписанию, а чтение — мгновенное.

CREATE MATERIALIZED VIEW sales_by_month AS
  SELECT date_trunc('month', created_at) AS month,
         sum(amount) AS total, count(*) AS orders
  FROM orders GROUP BY 1;

REFRESH MATERIALIZED VIEW sales_by_month;
-- обновляйте по расписанию или после загрузки данных
КритерийОбычный VIEWMaterialized
хранениенет (запрос при чтении)да, как таблица
актуальностьвсегдана момент последнего REFRESH
скорость чтениякак запросмгновенная
Используйте CONCURRENTLY в PostgreSQL, чтобы не блокировать чтения при обновлении: REFRESH MATERIALIZED VIEW CONCURRENTLY ... требует уникального индекса.
Данные всегда чуть «старые». Для интерактивных отчётов планируйте refresh в рабочем окне, иначе пользователи увидят устаревшие цифры.

21. Пакетные операции: bulk и UPSERT

По одной строке вставлять сотни тысяч записей медленно. Пакетная вставка сокращает обмен с БД, а конфликты ключей решает upsert — вставь или обнови, не задваивая строку.

-- вставка нескольких строк разом
INSERT INTO products (sku, price) VALUES
  ('A-1', 100), ('A-2', 200), ('A-3', 300);

-- upsert: обновить при совпадении ключа
INSERT INTO inventory (sku, qty) VALUES ('A-1', 5)
ON CONFLICT (sku) DO UPDATE SET qty = inventory.qty + EXCLUDED.qty;
BEGIN;
COPY shipments FROM '/tmp/ship.csv' (FORMAT csv);
COMMIT;
Сравнивайте время: один большой batch против N маленьких. На тестовых данных видно, где искать «бутылочное горлышко».
Upsert без индекса на ключе конфликта не работает: ON CONFLICT требует уникального ограничения или индекса по указанным колонкам.

22. Разделение больших таблиц: partitioning

Таблица с миллиардами строк — медленные запросы и тяжёлый VACUUM. Разделение (partitioning) делит логически одну таблицу на физические фрагменты: условие WHERE отбрасывает лишние сразу.

CREATE TABLE events (
  id BIGSERIAL, happened_at DATE, payload jsonb
) PARTITION BY RANGE (happened_at);

CREATE TABLE events_2026_09 PARTITION OF events
  FOR VALUES FROM ('2026-09-01') TO ('2026-10-01');
CREATE TABLE events_2026_10 PARTITION OF events
  FOR VALUES FROM ('2026-10-01') TO ('2026-11-01');

Запросы по месяцу задевают только нужный партишен. Для больших старых данных партишен можно открепить и архивировать без LOCK на всю таблицу.

СпособКогда
RANGE (по дате)события, логи, транзакции
LIST (по значению)регионы, категории справочников
HASHравномерное распределение без порядка
Индексы и внешние ключи по-разному ведут себя в партициях. Создавайте индекс на самой партиции и следите за связями: внешние ключи на партиционированных таблицах в PostgreSQL официально ограничены.
Начните с партиционирования, когда таблица перевалила за сотни гигабайт или VACUUM/индексы стали бутылочным горлышком — это не решение «на всякий случай».

Что дальше

Теперь ты знаешь основы SQL. Рекомендуемый следующий шаг — углублённое изучение одной из СУБД: MySQL или PostgreSQL.

← Вернуться к курсам