🗄️ SQL
Полный справочник: типы, DDL и ограничения, DML и upsert, SELECT, JOIN, группировки, подзапросы, CTE, оконные функции, функции дат и строк, JSON, индексы, EXPLAIN, транзакции, нормализация и типовые задачи.
Шпаргалки · Данные · #sql #database #postgres #mysql #sqlite
Диалекты
SQL стандартизирован, но у каждой СУБД свои отличия: PostgreSQL, MySQL / MariaDB, SQLite, SQL Server (T-SQL), Oracle (PL/SQL). Здесь основа общая, различия помечены.
| Задача | PostgreSQL | MySQL | SQL Server | SQLite |
|---|---|---|---|---|
| Ограничить строки | LIMIT n OFFSET m |
LIMIT n OFFSET m |
TOP n или OFFSET m ROWS FETCH NEXT n ROWS ONLY |
LIMIT n OFFSET m |
| Авто-ключ | GENERATED ALWAYS AS IDENTITY, SERIAL |
AUTO_INCREMENT |
IDENTITY(1,1) |
INTEGER PRIMARY KEY |
| Текущее время | now() |
NOW() |
GETDATE(), SYSDATETIME() |
datetime('now') |
| Склейка | a || b, concat() |
CONCAT(a, b) |
a + b, CONCAT |
a || b |
| Кавычки имён | "name" |
`name` |
[name] |
"name" |
| Upsert | ON CONFLICT DO UPDATE |
ON DUPLICATE KEY UPDATE |
MERGE |
ON CONFLICT DO UPDATE |
Порядок выполнения запроса
FROM / JOIN → WHERE → GROUP BY → HAVING → SELECT → DISTINCT → ORDER BY → LIMIT. Поэтому псевдоним из SELECT нельзя использовать в WHERE (но можно в ORDER BY).
Типы данных
| Группа | Типы (PostgreSQL) |
|---|---|
| Целые | smallint, integer, bigint |
| Точные числа | numeric(12,2) (деньги), decimal |
| Приближённые | real, double precision |
| Текст | text, varchar(n), char(n) |
| Дата и время | date, time, timestamp, timestamptz (с часовым поясом), interval |
| Логический | boolean |
| Идентификаторы | uuid |
| Структуры | json, jsonb, array, enum |
| Двоичные | bytea |
Деньги храните в numeric, не в float. Время лучше в UTC (timestamptz).
DDL: схема
CREATE TABLE users (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
email VARCHAR(255) NOT NULL UNIQUE,
name TEXT NOT NULL,
age INT CHECK (age >= 0),
role TEXT NOT NULL DEFAULT 'user',
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);
CREATE TABLE orders (
id BIGINT GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
user_id BIGINT NOT NULL REFERENCES users(id) ON DELETE CASCADE,
total NUMERIC(12,2) NOT NULL CHECK (total >= 0),
status TEXT NOT NULL DEFAULT 'new',
UNIQUE (user_id, id)
);
ALTER TABLE users ADD COLUMN phone TEXT;
ALTER TABLE users ALTER COLUMN name SET NOT NULL;
ALTER TABLE users DROP COLUMN phone;
ALTER TABLE users RENAME COLUMN name TO full_name;
ALTER TABLE orders ADD CONSTRAINT fk_user FOREIGN KEY (user_id) REFERENCES users(id);
TRUNCATE TABLE logs; -- быстро очищает таблицу
DROP TABLE IF EXISTS tmp CASCADE;
CREATE TEMP TABLE t AS SELECT * FROM users WHERE age > 30;
| Ограничение | Смысл |
|---|---|
PRIMARY KEY |
уникальный идентификатор строки, не NULL |
FOREIGN KEY |
ссылка на другую таблицу; ON DELETE CASCADE / SET NULL / RESTRICT |
UNIQUE |
значения не повторяются (NULL допускается) |
NOT NULL |
значение обязательно |
CHECK |
произвольное условие |
DEFAULT |
значение по умолчанию |
DML: изменение данных
INSERT INTO users (email, name) VALUES ('a@x.ru', 'Аня'), ('b@x.ru', 'Борис');
INSERT INTO archive SELECT * FROM users WHERE created_at < '2020-01-01';
UPDATE users SET name = 'Анна', age = age + 1 WHERE id = 1;
UPDATE o SET status = 'paid' FROM payments p WHERE p.order_id = o.id; -- PostgreSQL: обновление по связи
DELETE FROM users WHERE id = 1;
INSERT INTO users (email, name) VALUES ('a@x.ru', 'Аня')
ON CONFLICT (email) DO UPDATE SET name = EXCLUDED.name; -- upsert (PostgreSQL, SQLite)
INSERT INTO users (id, name) VALUES (1, 'Аня') ON DUPLICATE KEY UPDATE name = VALUES(name); -- MySQL
INSERT ... RETURNING id; UPDATE ... RETURNING *; -- PostgreSQL: вернуть строки
Внимание:
UPDATEиDELETEбезWHEREзатронут все строки. Сначала выполнитеSELECTс тем же условием, работайте внутри транзакции.
SELECT
SELECT DISTINCT role, COUNT(*) AS n
FROM users u
WHERE age BETWEEN 18 AND 65
AND role IN ('user', 'admin')
AND name LIKE 'А%' -- % любая строка, _ один символ; ILIKE — без регистра (PostgreSQL)
AND phone IS NOT NULL
GROUP BY role
HAVING COUNT(*) > 5
ORDER BY n DESC, role
LIMIT 10 OFFSET 20;
Операторы: = <> != < > <= >=, AND OR NOT, BETWEEN, IN, LIKE, IS NULL, EXISTS, ANY / ALL. Сравнение с NULL через = NULL ничего не даёт: пишите IS NULL. Выражения: CASE WHEN age < 18 THEN 'minor' ELSE 'adult' END, COALESCE(a, b, 0), NULLIF(a, 0), CAST(x AS INT), x::int (PostgreSQL).
JOIN
| JOIN | Результат |
|---|---|
INNER JOIN |
только пары с совпадением |
LEFT [OUTER] JOIN |
все строки слева + совпадения справа (иначе NULL) |
RIGHT JOIN |
зеркально LEFT |
FULL OUTER JOIN |
все строки обеих таблиц |
CROSS JOIN |
все пары (декартово произведение) |
SELF JOIN |
таблица с самой собой (иерархии) |
SELECT u.name, o.total
FROM users u
LEFT JOIN orders o ON o.user_id = u.id AND o.status = 'paid' -- условие по правой таблице держите в ON
WHERE o.id IS NULL; -- пользователи без оплаченных заказов
SELECT e.name, m.name AS manager FROM employees e LEFT JOIN employees m ON m.id = e.manager_id;
Условие по правой таблице в WHERE превращает LEFT JOIN в INNER. USING (col) сокращает ON a.col = b.col.
Объединение результатов
SELECT id FROM a UNION SELECT id FROM b; -- уникальные
SELECT id FROM a UNION ALL SELECT id FROM b; -- с дублями (быстрее)
SELECT id FROM a INTERSECT SELECT id FROM b; -- пересечение
SELECT id FROM a EXCEPT SELECT id FROM b; -- разность (в Oracle MINUS)
Число и типы столбцов должны совпадать.
Агрегаты и группировки
SELECT status, COUNT(*) AS cnt, SUM(total) AS sum, AVG(total) AS avg, MIN(total), MAX(total),
COUNT(DISTINCT user_id) AS buyers,
COUNT(*) FILTER (WHERE total > 1000) AS big -- PostgreSQL; в других: SUM(CASE WHEN ... THEN 1 END)
FROM orders GROUP BY status;
SELECT region, product, SUM(sales) FROM s GROUP BY ROLLUP (region, product); -- подитоги; также CUBE, GROUPING SETS
SELECT string_agg(name, ', ' ORDER BY name) FROM users; -- склейка: GROUP_CONCAT (MySQL), STRING_AGG (SQL Server)
COUNT(*) считает строки, COUNT(col) только не-NULL. WHERE фильтрует до группировки, HAVING после.
Подзапросы и CTE
SELECT * FROM users WHERE id IN (SELECT user_id FROM orders WHERE total > 1000);
SELECT * FROM users u WHERE EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id); -- быстрее и безопаснее с NULL, чем IN
SELECT name, (SELECT COUNT(*) FROM orders o WHERE o.user_id = u.id) AS n FROM users u;
WITH paid AS (
SELECT user_id, SUM(total) AS total FROM orders WHERE status = 'paid' GROUP BY user_id
), top AS (
SELECT * FROM paid ORDER BY total DESC LIMIT 10
)
SELECT u.name, t.total FROM top t JOIN users u ON u.id = t.user_id;
WITH RECURSIVE tree AS ( -- иерархия
SELECT id, parent_id, name, 1 AS lvl FROM categories WHERE parent_id IS NULL
UNION ALL
SELECT c.id, c.parent_id, c.name, t.lvl + 1 FROM categories c JOIN tree t ON c.parent_id = t.id
)
SELECT * FROM tree;
SELECT * FROM users u, LATERAL (SELECT * FROM orders o WHERE o.user_id = u.id ORDER BY id DESC LIMIT 3) x; -- «последние N на каждого»
Оконные функции
SELECT name, dept, salary,
ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) AS rn,
RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS rnk, -- с пропусками при равенстве
DENSE_RANK() OVER (PARTITION BY dept ORDER BY salary DESC) AS drnk,
SUM(salary) OVER (PARTITION BY dept) AS dept_total,
AVG(salary) OVER () AS avg_all,
LAG(salary) OVER (ORDER BY id) AS prev, LEAD(salary) OVER (ORDER BY id) AS next,
SUM(salary) OVER (ORDER BY id ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running,
NTILE(4) OVER (ORDER BY salary) AS quartile,
FIRST_VALUE(name) OVER (PARTITION BY dept ORDER BY salary DESC) AS top_earner
FROM staff;
Окно не схлопывает строки, в отличие от GROUP BY. Фильтровать по оконной функции нужно через подзапрос или CTE.
Функции строк, чисел, дат
-- строки (PostgreSQL / MySQL)
LENGTH(s), UPPER(s), LOWER(s), TRIM(s), SUBSTRING(s FROM 2 FOR 3), REPLACE(s, 'a', 'b'), POSITION('x' IN s),
LEFT(s, 3), RIGHT(s, 3), LPAD(s, 5, '0'), SPLIT_PART(s, ',', 2), REGEXP_REPLACE(s, '\s+', ' ', 'g'), s ~ '^\d+$'
-- числа
ROUND(x, 2), CEIL(x), FLOOR(x), ABS(x), MOD(a, b), POWER(a, 2), SQRT(x), RANDOM()
-- даты (PostgreSQL)
now(), CURRENT_DATE, DATE_TRUNC('month', ts), EXTRACT(YEAR FROM ts), ts + INTERVAL '7 days', AGE(a, b),
TO_CHAR(ts, 'YYYY-MM-DD HH24:MI'), TO_DATE('2026-01-31', 'YYYY-MM-DD'), generate_series('2026-01-01'::date, '2026-01-31', '1 day')
-- MySQL: DATE_ADD(d, INTERVAL 7 DAY), DATEDIFF(a, b), DATE_FORMAT(d, '%Y-%m')
-- SQL Server: DATEADD(day, 7, d), DATEDIFF(day, a, b), FORMAT(d, 'yyyy-MM')
JSON и массивы (PostgreSQL)
SELECT data->>'name' AS name, data->'tags' AS tags, data #>> '{address,city}' AS city FROM events;
SELECT * FROM events WHERE data @> '{"type":"click"}';
SELECT jsonb_agg(row_to_json(u)) FROM users u; SELECT jsonb_each_text(data) FROM events;
SELECT * FROM t WHERE 'a' = ANY(tags); SELECT unnest(tags) FROM t;
MySQL: JSON_EXTRACT(col, '$.name'), col->>'$.name'. SQL Server: JSON_VALUE, OPENJSON.
Индексы
CREATE INDEX idx_orders_user ON orders (user_id);
CREATE UNIQUE INDEX uq_users_email ON users (lower(email));
CREATE INDEX idx_orders_user_status ON orders (user_id, status); -- составной: порядок важен
CREATE INDEX idx_paid ON orders (created_at) WHERE status = 'paid'; -- частичный (PostgreSQL)
CREATE INDEX CONCURRENTLY idx_x ON big (col); -- без блокировки записи (PostgreSQL)
CREATE INDEX idx_gin ON events USING gin (data); -- JSON, массивы, полнотекстовый поиск
DROP INDEX idx_orders_user;
Правила: индексируйте столбцы из WHERE, JOIN, ORDER BY; составной индекс (a, b) помогает запросам по a и по a, b, но не только по b; функция над столбцом (WHERE lower(email) = ..., WHERE col + 1 = 5) отключает обычный индекс; LIKE '%x' его не использует; индексы замедляют запись и занимают место. Типы: B-tree (по умолчанию), Hash, GIN, GiST, BRIN.
План выполнения
EXPLAIN SELECT * FROM orders WHERE user_id = 5;
EXPLAIN (ANALYZE, BUFFERS) SELECT ...; -- реальное выполнение (PostgreSQL)
Что искать: Seq Scan на больших таблицах (нужен индекс), большая разница оценки и факта строк (обновите статистику ANALYZE), Nested Loop на больших объёмах, сортировка на диске. VACUUM (ANALYZE) в PostgreSQL.
Транзакции
BEGIN;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT; -- или ROLLBACK;
SAVEPOINT sp1; ROLLBACK TO sp1;
SELECT * FROM accounts WHERE id = 1 FOR UPDATE; -- блокировка строки
SELECT * FROM jobs WHERE status = 'new' LIMIT 1 FOR UPDATE SKIP LOCKED; -- очередь задач
ACID: атомарность, согласованность, изолированность, долговечность.
| Уровень изоляции | Защищает от |
|---|---|
READ UNCOMMITTED |
почти ни от чего (грязное чтение) |
READ COMMITTED |
грязного чтения (по умолчанию в PostgreSQL, Oracle, SQL Server) |
REPEATABLE READ |
неповторяемого чтения (по умолчанию в MySQL InnoDB) |
SERIALIZABLE |
всех аномалий, включая фантомы |
Взаимная блокировка (deadlock): обновляйте строки в одном порядке, держите транзакции короткими.
Представления, функции, триггеры
CREATE VIEW active_users AS SELECT * FROM users WHERE active;
CREATE MATERIALIZED VIEW daily_sales AS SELECT date, SUM(total) FROM orders GROUP BY date;
REFRESH MATERIALIZED VIEW daily_sales;
CREATE FUNCTION add(a int, b int) RETURNS int LANGUAGE sql AS $$ SELECT a + b $$;
CREATE TRIGGER trg_updated BEFORE UPDATE ON users FOR EACH ROW EXECUTE FUNCTION set_updated_at();
Права
CREATE ROLE app LOGIN PASSWORD '...'; GRANT SELECT, INSERT, UPDATE ON users TO app; REVOKE DELETE ON users FROM app;
GRANT USAGE ON SCHEMA public TO app; ALTER DEFAULT PRIVILEGES IN SCHEMA public GRANT SELECT ON TABLES TO readonly;
Нормализация
| Форма | Правило |
|---|---|
| 1НФ | значения атомарны, нет повторяющихся групп |
| 2НФ | нет зависимости от части составного ключа |
| 3НФ | нет зависимости неключевых полей друг от друга |
Нормализуйте для целостности, денормализуйте осознанно для скорости чтения. Связи: один-к-одному, один-ко-многим (внешний ключ), многие-ко-многим (связующая таблица).
Пагинация
OFFSET на больших значениях медленный. Курсорная (keyset): WHERE (created_at, id) < (:last_ts, :last_id) ORDER BY created_at DESC, id DESC LIMIT 20.
Классические задачи
-- вторая по величине зарплата
SELECT DISTINCT salary FROM staff ORDER BY salary DESC OFFSET 1 LIMIT 1;
-- дубликаты
SELECT email, COUNT(*) FROM users GROUP BY email HAVING COUNT(*) > 1;
-- удалить дубликаты, оставив минимальный id
DELETE FROM users a USING users b WHERE a.email = b.email AND a.id > b.id;
-- топ-N в каждой группе
SELECT * FROM (SELECT *, ROW_NUMBER() OVER (PARTITION BY dept ORDER BY salary DESC) rn FROM staff) t WHERE rn <= 3;
-- накопительный итог
SELECT date, SUM(amount) OVER (ORDER BY date) AS running FROM payments;
-- разрывы в последовательности
SELECT id + 1 AS gap_start FROM t a WHERE NOT EXISTS (SELECT 1 FROM t b WHERE b.id = a.id + 1);
-- пользователи без заказов
SELECT u.* FROM users u LEFT JOIN orders o ON o.user_id = u.id WHERE o.id IS NULL;
-- процент от общего
SELECT dept, SUM(salary) * 100.0 / SUM(SUM(salary)) OVER () AS pct FROM staff GROUP BY dept;
Антипаттерны
SELECT *в приложениях: перечисляйте столбцы.- Склейка SQL из ввода пользователя: используйте параметры (
$1,?,:name). - Функции над индексируемыми столбцами в
WHERE. NOT INс подзапросом, где возможен NULL:NOT EXISTS.- Запросы в цикле (N+1):
JOINилиIN. - Хранение денег в
float, дат в строках, списков в одном поле. - Отсутствие индексов на внешних ключах.
Инструменты
psql, DBeaver, DataGrip, pgAdmin, MySQL Workbench, SSMS, Azure Data Studio; миграции Flyway, Liquibase, Alembic, Prisma Migrate; анализ запросов pg_stat_statements, EXPLAIN, Percona Toolkit.