SQL — язык структурированных запросов
SQL (Structured Query Language) — стандартный язык для работы с реляционными базами данных. С его помощью создаются таблицы, вставляются, обновляются и извлекаются данные. Навыки SQL востребованы у разработчиков, аналитиков и администраторов.
1. Что такое SQL и базы данных
База данных — это организованное хранилище информации. Реляционная база данных хранит данные в виде связанных таблиц: каждая таблица состоит из строк (записей) и столбцов (полей).
- Таблица (table) — набор строк с фиксированным набором столбцов.
- Строка (row / record) — одна запись в таблице.
- Столбец (column / field) — определённый атрибут записей (имя, цена, дата и т. д.).
Программа, которая управляет базой данных и понимает SQL, называется СУБД (система управления базами данных). Наиболее популярные СУБД:
| СУБД | Тип | Особенности |
|---|---|---|
| MySQL | Open Source | Простая настройка, широкое сообщество |
| PostgreSQL | Open Source | Расширенная типизация, JSON-поддержка, ACID |
| SQLite | Embedded | Встраивается в приложение, не требует сервера |
| SQL Server | Коммерческий | Интеграция с Microsoft-стеком |
| Oracle DB | Коммерческий | Крупныеenterprise-системы |
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
);Здесь:
PRIMARY KEY— уникальный идентификатор строки.NOT NULL— поле не может быть пустым.UNIQUE— все значения в столбце должны быть различны.DEFAULT— значение по умолчанию, если оно не указано при вставке.
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:
- Atomicity (атомарность) — либо все операции, либо ни одной.
- Consistency (согласованность) — после транзакции данные остаются в допустимом состоянии.
- Isolation (изоляция) — параллельные транзакции не мешают друг другу.
- Durability (долговечность) — результат зафиксированной транзакции сохраняется навсегда.
Нормализация баз данных
Нормализация — процесс организации таблиц для минимизации дублирования данных:
| Нормальная форма | Правило |
|---|---|
| 1НФ | Каждый столбец содержит только атомарные (неделимые) значения |
| 2НФ | Нет частичных зависимостей — каждый неключевой столбец зависит от всего первичного ключа |
| 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 удобен для рекурсии:
WITH RECURSIVEстроит дерево категорий; - подзапрос в
WHERE/HAVINGсравнивает поIN,EXISTS,ANY; - коррелированный подзапрос исполняется на каждую строку внешнего запроса — следите за производительностью.
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 |
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 ScanSeq Scanна больших таблицах — признак отсутствующего индекса;- функции в WHERE ломают индекс:
WHERE UPPER(name) = ...обходит idx — используйтеILIKEили выраженческие индексы; - индексов должно быть ровно столько, сколько нужно: каждый замедляет запись.
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, ... |
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', 'индекс:*')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 с откатами |
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-значение |
| ->> | достаёт текст |
| @> | содержит ли документ элемент |
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;
-- обновляйте по расписанию или после загрузки данных| Критерий | Обычный VIEW | Materialized |
|---|---|---|
| хранение | нет (запрос при чтении) | да, как таблица |
| актуальность | всегда | на момент последнего REFRESH |
| скорость чтения | как запрос | мгновенная |
CONCURRENTLY в PostgreSQL, чтобы не блокировать чтения при обновлении: REFRESH MATERIALIZED VIEW CONCURRENTLY ... требует уникального индекса.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;- для больших объёмов используйте COPY /
\copyвместо INSERT-циклов — десятки тысяч строк в секунду; - пачки по 500–1000 строк обычно оптимальны по задержке;
- без транзакции пакет не атомарен — оборачивайте в BEGIN/COMMIT.
BEGIN;
COPY shipments FROM '/tmp/ship.csv' (FORMAT csv);
COMMIT;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 | равномерное распределение без порядка |
Что дальше
Теперь ты знаешь основы SQL. Рекомендуемый следующий шаг — углублённое изучение одной из СУБД: MySQL или PostgreSQL.