Приложение В: Шпаргалка по SQL — запросы, JOIN, агрегация, индексы
📌 О чём это приложение
Эта шпаргалка — твой карманный справочник по SQL. Здесь собраны самые частые запросы, синтаксис, примеры и паттерны, которые ты будешь использовать каждый день. Держи её под рукой во время работы.
Структура:
- ✅ Быстрый синтаксис без лишних слов
- 💡 Реальные примеры из практики
- ⚠️ Частые ошибки и как их избежать
- 🎯 Когда использовать тот или иной подход
1️⃣ БАЗОВЫЕ ЗАПРОСЫ (CRUD)
SELECT — Выборка данных
sql
-- Все столбцы
SELECT * FROM users;
-- Конкретные столбцы
SELECT id, name, email FROM users;
-- С алиасами
SELECT
id AS user_id,
CONCAT(first_name, ' ', last_name) AS full_name,
created_at AS registered_at
FROM users;
-- DISTINCT — уникальные значения
SELECT DISTINCT city FROM users;WHERE — Фильтрация
sql
-- Основные операторы
SELECT * FROM products WHERE price > 1000;
SELECT * FROM products WHERE price BETWEEN 500 AND 2000;
SELECT * FROM products WHERE category_id IN (1, 3, 5);
SELECT * FROM products WHERE name LIKE '%phone%'; -- содержит "phone"
SELECT * FROM products WHERE name LIKE 'iPhone%'; -- начинается с "iPhone"
SELECT * FROM products WHERE description IS NULL;
SELECT * FROM products WHERE description IS NOT NULL;
-- Логические операторы
SELECT * FROM products
WHERE price > 1000
AND category_id = 2
AND in_stock = 1;
SELECT * FROM products
WHERE category_id = 1 OR category_id = 2;
-- NOT
SELECT * FROM users WHERE NOT city = 'Moscow';
SELECT * FROM users WHERE email NOT LIKE '%@gmail.com';ORDER BY — Сортировка
sql
-- По одному полю
SELECT * FROM products ORDER BY price ASC; -- по возрастанию
SELECT * FROM products ORDER BY price DESC; -- по убыванию
-- По нескольким полям
SELECT * FROM products
ORDER BY category_id ASC, price DESC;
-- NULL в начале или конце
SELECT * FROM products
ORDER BY discount IS NULL, discount DESC;LIMIT и OFFSET — Пагинация
sql
-- Первые 10 записей
SELECT * FROM products LIMIT 10;
-- Записи с 11 по 20 (страница 2)
SELECT * FROM products LIMIT 10 OFFSET 10;
-- Формула для пагинации
-- LIMIT = items_per_page
-- OFFSET = (page - 1) * items_per_page
-- Страница 3, по 20 товаров
SELECT * FROM products LIMIT 20 OFFSET 40;INSERT — Вставка данных
sql
-- Одна запись
INSERT INTO users (name, email, password)
VALUES ('Ivan', 'ivan@mail.ru', 'hashed_password');
-- Несколько записей
INSERT INTO users (name, email, password) VALUES
('Ivan', 'ivan@mail.ru', 'hash1'),
('Maria', 'maria@mail.ru', 'hash2'),
('Petr', 'petr@mail.ru', 'hash3');
-- Получить ID вставленной записи (в PHP)
$pdo->lastInsertId();
-- Игнорировать дубликаты
INSERT IGNORE INTO users (email, name)
VALUES ('ivan@mail.ru', 'Ivan');
-- Обновить при дубликате
INSERT INTO users (email, name, login_count)
VALUES ('ivan@mail.ru', 'Ivan', 1)
ON DUPLICATE KEY UPDATE
login_count = login_count + 1;UPDATE — Обновление данных
sql
-- Обновить одну запись
UPDATE users
SET email = 'newemail@mail.ru'
WHERE id = 1;
-- Обновить несколько полей
UPDATE users
SET
email = 'newemail@mail.ru',
updated_at = NOW()
WHERE id = 1;
-- Обновить по условию
UPDATE products
SET price = price * 0.9
WHERE category_id = 3;
-- ⚠️ БЕЗ WHERE обновит ВСЕ записи!
UPDATE users SET role = 'admin'; -- ОПАСНО!DELETE — Удаление данных
sql
-- Удалить одну запись
DELETE FROM users WHERE id = 1;
-- Удалить по условию
DELETE FROM products WHERE price < 100;
-- ⚠️ БЕЗ WHERE удалит ВСЕ записи!
DELETE FROM users; -- ОПАСНО!
-- Очистить таблицу полностью (быстрее DELETE)
TRUNCATE TABLE logs;2️⃣ JOIN — Объединение таблиц
Типы JOIN
INNER JOIN — только совпадающие записи
LEFT JOIN — все из левой + совпадающие из правой
RIGHT JOIN — все из правой + совпадающие из левой
CROSS JOIN — все комбинации (декартово произведение)INNER JOIN — Пересечение
sql
-- Товары с названиями категорий
SELECT
products.id,
products.name AS product_name,
categories.name AS category_name,
products.price
FROM products
INNER JOIN categories ON products.category_id = categories.id;
-- Алиасы таблиц (короче и читаемей)
SELECT
p.id,
p.name AS product_name,
c.name AS category_name,
p.price
FROM products p
INNER JOIN categories c ON p.category_id = c.id;
-- Несколько JOIN
SELECT
o.id AS order_id,
u.name AS customer_name,
p.name AS product_name,
oi.quantity,
oi.price
FROM orders o
INNER JOIN users u ON o.user_id = u.id
INNER JOIN order_items oi ON oi.order_id = o.id
INNER JOIN products p ON oi.product_id = p.id;LEFT JOIN — Все из левой таблицы
sql
-- Все пользователи + их заказы (даже если заказов нет)
SELECT
u.id,
u.name,
COUNT(o.id) AS orders_count
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
GROUP BY u.id, u.name;
-- Найти пользователей БЕЗ заказов
SELECT u.*
FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.id IS NULL;
-- Категории и количество товаров (даже пустые категории)
SELECT
c.name AS category,
COUNT(p.id) AS products_count
FROM categories c
LEFT JOIN products p ON p.category_id = c.id
GROUP BY c.id, c.name;RIGHT JOIN — Все из правой таблицы
sql
-- То же самое, что LEFT JOIN, но таблицы наоборот
SELECT
c.name AS category,
COUNT(p.id) AS products_count
FROM products p
RIGHT JOIN categories c ON p.category_id = c.id
GROUP BY c.id, c.name;
-- 💡 RIGHT JOIN используется редко, обычно просто меняют таблицы местами в LEFT JOINCROSS JOIN — Декартово произведение
sql
-- Все комбинации цветов и размеров
SELECT
colors.name AS color,
sizes.name AS size
FROM colors
CROSS JOIN sizes;
-- Результат:
-- Red, S
-- Red, M
-- Red, L
-- Blue, S
-- Blue, M
-- Blue, LSelf JOIN — Таблица сама с собой
sql
-- Сотрудники и их руководители
SELECT
e.name AS employee,
m.name AS manager
FROM employees e
LEFT JOIN employees m ON e.manager_id = m.id;
-- Категории и подкатегории
SELECT
c1.name AS category,
c2.name AS subcategory
FROM categories c1
LEFT JOIN categories c2 ON c2.parent_id = c1.id
WHERE c1.parent_id IS NULL;3️⃣ Агрегатные функции
Основные функции
sql
-- COUNT — подсчёт строк
SELECT COUNT(*) FROM users; -- все строки
SELECT COUNT(id) FROM users; -- не NULL значения
SELECT COUNT(DISTINCT city) FROM users; -- уникальные значения
-- SUM — сумма
SELECT SUM(price) FROM products;
SELECT SUM(quantity * price) FROM order_items;
-- AVG — среднее
SELECT AVG(price) FROM products;
SELECT AVG(rating) FROM reviews;
-- MIN / MAX — минимум и максимум
SELECT MIN(price) FROM products;
SELECT MAX(created_at) FROM orders;
-- Несколько функций одновременно
SELECT
COUNT(*) AS total_products,
AVG(price) AS avg_price,
MIN(price) AS min_price,
MAX(price) AS max_price,
SUM(in_stock) AS total_in_stock
FROM products;GROUP BY — Группировка
sql
-- Количество товаров в каждой категории
SELECT
category_id,
COUNT(*) AS products_count
FROM products
GROUP BY category_id;
-- С названием категории (JOIN)
SELECT
c.name AS category,
COUNT(p.id) AS products_count,
AVG(p.price) AS avg_price
FROM categories c
LEFT JOIN products p ON p.category_id = c.id
GROUP BY c.id, c.name;
-- Группировка по нескольким полям
SELECT
category_id,
in_stock,
COUNT(*) AS count
FROM products
GROUP BY category_id, in_stock;
-- Продажи по датам
SELECT
DATE(created_at) AS date,
COUNT(*) AS orders_count,
SUM(total) AS revenue
FROM orders
GROUP BY DATE(created_at)
ORDER BY date DESC;HAVING — Фильтрация после группировки
sql
-- WHERE фильтрует ДО группировки
-- HAVING фильтрует ПОСЛЕ группировки
-- Категории с более чем 10 товарами
SELECT
category_id,
COUNT(*) AS count
FROM products
GROUP BY category_id
HAVING COUNT(*) > 10;
-- Пользователи с заказами на сумму > 10000
SELECT
user_id,
SUM(total) AS total_spent
FROM orders
GROUP BY user_id
HAVING SUM(total) > 10000;
-- WHERE + HAVING
SELECT
category_id,
AVG(price) AS avg_price
FROM products
WHERE in_stock = 1 -- фильтр ДО группировки
GROUP BY category_id
HAVING AVG(price) > 1000 -- фильтр ПОСЛЕ группировки
ORDER BY avg_price DESC;4️⃣ Подзапросы (Subqueries)
В WHERE
sql
-- Товары дороже средней цены
SELECT * FROM products
WHERE price > (SELECT AVG(price) FROM products);
-- Пользователи с заказами
SELECT * FROM users
WHERE id IN (SELECT DISTINCT user_id FROM orders);
-- Пользователи БЕЗ заказов
SELECT * FROM users
WHERE id NOT IN (SELECT DISTINCT user_id FROM orders);
-- ⚠️ Проблема с NULL в NOT IN:
-- Если в подзапросе есть NULL, NOT IN вернёт пустой результат!
-- Лучше использовать NOT EXISTS или LEFT JOINВ SELECT
sql
-- Количество заказов для каждого пользователя
SELECT
u.name,
u.email,
(SELECT COUNT(*) FROM orders WHERE user_id = u.id) AS orders_count
FROM users u;
-- Последний заказ пользователя
SELECT
u.name,
(SELECT MAX(created_at) FROM orders WHERE user_id = u.id) AS last_order
FROM users u;В FROM (производная таблица)
sql
-- Топ-3 самых дорогих товара в каждой категории
SELECT category_id, name, price
FROM (
SELECT
category_id,
name,
price,
ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY price DESC) AS rn
FROM products
) ranked
WHERE rn <= 3;
-- Средняя цена по категориям
SELECT
cat_stats.category_id,
cat_stats.avg_price,
c.name
FROM (
SELECT
category_id,
AVG(price) AS avg_price
FROM products
GROUP BY category_id
) cat_stats
JOIN categories c ON c.id = cat_stats.category_id;EXISTS / NOT EXISTS
sql
-- Категории, в которых есть товары
SELECT * FROM categories c
WHERE EXISTS (
SELECT 1 FROM products p WHERE p.category_id = c.id
);
-- Категории без товаров
SELECT * FROM categories c
WHERE NOT EXISTS (
SELECT 1 FROM products p WHERE p.category_id = c.id
);
-- 💡 EXISTS быстрее IN для больших таблиц
-- EXISTS останавливается при первом совпадении5️⃣ Функции и операторы
Строковые функции
sql
-- CONCAT — склеивание строк
SELECT CONCAT(first_name, ' ', last_name) AS full_name FROM users;
SELECT CONCAT_WS(' - ', code, name) FROM products; -- с разделителем
-- LENGTH — длина строки
SELECT name, LENGTH(name) FROM users;
-- UPPER / LOWER — регистр
SELECT UPPER(name) FROM categories;
SELECT LOWER(email) FROM users;
-- SUBSTRING — подстрока
SELECT SUBSTRING(description, 1, 100) FROM products; -- первые 100 символов
SELECT SUBSTRING(phone, -4) FROM users; -- последние 4 символа
-- REPLACE — замена
SELECT REPLACE(description, 'old', 'new') FROM products;
-- TRIM — удаление пробелов
SELECT TRIM(name) FROM users; -- с обоих концов
SELECT LTRIM(name) FROM users; -- слева
SELECT RTRIM(name) FROM users; -- справаЧисловые функции
sql
-- ROUND — округление
SELECT ROUND(price, 2) FROM products; -- до 2 знаков
SELECT ROUND(avg_rating, 1) FROM products; -- до 1 знака
-- CEIL / FLOOR — округление вверх/вниз
SELECT CEIL(price) FROM products; -- 12.1 → 13
SELECT FLOOR(price) FROM products; -- 12.9 → 12
-- ABS — модуль числа
SELECT ABS(balance) FROM accounts;
-- RAND — случайное число
SELECT * FROM products ORDER BY RAND() LIMIT 5; -- 5 случайных товаровФункции даты и времени
sql
-- NOW — текущая дата и время
SELECT NOW(); -- 2024-03-15 14:30:25
-- CURDATE / CURTIME — отдельно дата и время
SELECT CURDATE(); -- 2024-03-15
SELECT CURTIME(); -- 14:30:25
-- DATE / TIME — извлечение из datetime
SELECT DATE(created_at) FROM orders;
SELECT TIME(created_at) FROM orders;
-- YEAR / MONTH / DAY — компоненты даты
SELECT YEAR(created_at) FROM orders;
SELECT MONTH(created_at) FROM orders;
SELECT DAY(created_at) FROM orders;
-- DATE_FORMAT — форматирование
SELECT DATE_FORMAT(created_at, '%d.%m.%Y') FROM orders; -- 15.03.2024
SELECT DATE_FORMAT(created_at, '%Y-%m') FROM orders; -- 2024-03
-- DATEDIFF — разница в днях
SELECT DATEDIFF(NOW(), created_at) AS days_ago FROM orders;
-- DATE_ADD / DATE_SUB — прибавить/вычесть
SELECT DATE_ADD(NOW(), INTERVAL 7 DAY); -- через неделю
SELECT DATE_SUB(NOW(), INTERVAL 1 MONTH); -- месяц назад
-- Заказы за последние 30 дней
SELECT * FROM orders
WHERE created_at >= DATE_SUB(NOW(), INTERVAL 30 DAY);CASE — Условная логика
sql
-- Простой CASE
SELECT
name,
price,
CASE
WHEN price < 500 THEN 'Дешёвый'
WHEN price < 2000 THEN 'Средний'
ELSE 'Дорогой'
END AS price_category
FROM products;
-- С агрегацией
SELECT
category_id,
SUM(CASE WHEN in_stock = 1 THEN 1 ELSE 0 END) AS in_stock_count,
SUM(CASE WHEN in_stock = 0 THEN 1 ELSE 0 END) AS out_of_stock_count
FROM products
GROUP BY category_id;
-- NULLIF — вернуть NULL при совпадении
SELECT NULLIF(discount, 0) FROM products; -- 0 → NULL
-- COALESCE — первое не-NULL значение
SELECT COALESCE(phone, email, 'Нет контакта') FROM users;IF / IFNULL
sql
-- IF — тернарный оператор
SELECT
name,
IF(in_stock = 1, 'В наличии', 'Нет в наличии') AS availability
FROM products;
-- IFNULL — замена NULL
SELECT IFNULL(discount, 0) FROM products;
-- Разница с COALESCE:
-- IFNULL принимает 2 аргумента
-- COALESCE принимает любое количество6️⃣ Индексы
Создание индексов
sql
-- Обычный индекс
CREATE INDEX idx_email ON users(email);
CREATE INDEX idx_category ON products(category_id);
-- Уникальный индекс
CREATE UNIQUE INDEX idx_unique_email ON users(email);
-- Составной индекс (несколько полей)
CREATE INDEX idx_category_price ON products(category_id, price);
-- Полнотекстовый индекс (для поиска)
CREATE FULLTEXT INDEX idx_description ON products(description);
-- Удаление индекса
DROP INDEX idx_email ON users;
-- Просмотр индексов таблицы
SHOW INDEX FROM users;Когда использовать индексы
sql
-- ✅ Индексируй:
-- - PRIMARY KEY (создаётся автоматически)
-- - FOREIGN KEY (category_id, user_id и т.д.)
-- - Поля в WHERE (email, status, created_at)
-- - Поля в ORDER BY (если часто сортируешь)
-- - Поля в JOIN
-- ❌ Не индексируй:
-- - Маленькие таблицы (<1000 строк)
-- - Поля с низкой селективностью (например, пол: M/F)
-- - Поля, которые часто обновляются
-- - Очень длинные текстовые поля
-- 💡 Составные индексы:
-- Порядок полей важен!
-- INDEX (category_id, price) работает для:
-- WHERE category_id = 1
-- WHERE category_id = 1 AND price > 100
-- Но НЕ работает для:
-- WHERE price > 100 (только второе поле)
-- 💡 Проверь использование индекса:
EXPLAIN SELECT * FROM products WHERE category_id = 1;EXPLAIN — Анализ запроса
sql
EXPLAIN SELECT * FROM orders
WHERE user_id = 123
ORDER BY created_at DESC;
-- Смотри на:
-- type:
-- const/eq_ref - отлично
-- ref - хорошо
-- range - нормально
-- ALL - плохо (полное сканирование таблицы)
-- possible_keys: какие индексы могут быть использованы
-- key: какой индекс реально используется
-- rows: сколько строк будет просканировано
-- EXPLAIN ANALYZE (MySQL 8.0+) — с реальным временем выполнения
EXPLAIN ANALYZE
SELECT * FROM products WHERE price > 1000;7️⃣ Транзакции
Основы
sql
-- Начать транзакцию
START TRANSACTION;
-- или
BEGIN;
-- Выполнить запросы
UPDATE accounts SET balance = balance - 1000 WHERE id = 1;
UPDATE accounts SET balance = balance + 1000 WHERE id = 2;
-- Подтвердить изменения
COMMIT;
-- Или откатить
ROLLBACK;В PHP с PDO
php
try {
$pdo->beginTransaction();
// Списать со счёта отправителя
$stmt = $pdo->prepare("UPDATE accounts SET balance = balance - ? WHERE id = ?");
$stmt->execute([1000, $fromAccountId]);
// Зачислить на счёт получателя
$stmt = $pdo->prepare("UPDATE accounts SET balance = balance + ? WHERE id = ?");
$stmt->execute([1000, $toAccountId]);
// Записать в историю
$stmt = $pdo->prepare("INSERT INTO transactions (from_id, to_id, amount) VALUES (?, ?, ?)");
$stmt->execute([$fromAccountId, $toAccountId, 1000]);
$pdo->commit();
} catch (Exception $e) {
$pdo->rollBack();
throw $e;
}ACID свойства
Atomicity (Атомарность) — всё или ничего
Consistency (Согласованность) — БД всегда в корректном состоянии
Isolation (Изолированность) — транзакции не влияют друг на друга
Durability (Долговечность) — данные сохраняются после commit8️⃣ Продвинутые запросы
UNION — Объединение результатов
sql
-- Все email из users и из admins
SELECT email FROM users
UNION
SELECT email FROM admins;
-- UNION убирает дубликаты
-- UNION ALL оставляет дубликаты (быстрее)
SELECT name FROM products WHERE category_id = 1
UNION ALL
SELECT name FROM products WHERE category_id = 2;
-- Количество столбцов должно совпадать!
-- Типы данных должны быть совместимыWindow Functions (MySQL 8.0+)
sql
-- ROW_NUMBER — нумерация строк
SELECT
name,
price,
ROW_NUMBER() OVER (ORDER BY price DESC) AS row_num
FROM products;
-- RANK — ранг с пропусками
SELECT
name,
price,
RANK() OVER (ORDER BY price DESC) AS rank
FROM products;
-- Если цены: 100, 100, 90
-- Ранги: 1, 1, 3 (не 1, 1, 2!)
-- DENSE_RANK — ранг без пропусков
-- Ранги: 1, 1, 2
-- PARTITION BY — группировка внутри окна
SELECT
category_id,
name,
price,
ROW_NUMBER() OVER (PARTITION BY category_id ORDER BY price DESC) AS rank_in_category
FROM products;
-- OVER() без PARTITION BY — по всей таблице
SELECT
name,
price,
AVG(price) OVER() AS avg_price_overall
FROM products;WITH (CTE) — Общие табличные выражения
sql
-- Вместо подзапросов используй CTE для читаемости
WITH expensive_products AS (
SELECT * FROM products WHERE price > 5000
),
cheap_products AS (
SELECT * FROM products WHERE price < 1000
)
SELECT
(SELECT COUNT(*) FROM expensive_products) AS expensive_count,
(SELECT COUNT(*) FROM cheap_products) AS cheap_count;
-- Рекурсивные CTE — для иерархий
WITH RECURSIVE category_tree AS (
-- Базовый случай
SELECT id, name, parent_id, 1 AS level
FROM categories
WHERE parent_id IS NULL
UNION ALL
-- Рекурсивный случай
SELECT c.id, c.name, c.parent_id, ct.level + 1
FROM categories c
INNER JOIN category_tree ct ON c.parent_id = ct.id
)
SELECT * FROM category_tree ORDER BY level, id;9️⃣ Оптимизация запросов
Частые проблемы
sql
-- ❌ SELECT * — лишние данные
SELECT * FROM products;
-- ✅ Выбирай только нужные поля
SELECT id, name, price FROM products;
-- ❌ Отсутствие индексов
SELECT * FROM orders WHERE user_id = 123;
-- ✅ Создай индекс
CREATE INDEX idx_user_id ON orders(user_id);
-- ❌ Функции в WHERE (индекс не работает)
SELECT * FROM users WHERE YEAR(created_at) = 2024;
-- ✅ Используй диапазон
SELECT * FROM users
WHERE created_at >= '2024-01-01' AND created_at < '2025-01-01';
-- ❌ OR вместо IN
SELECT * FROM products WHERE id = 1 OR id = 2 OR id = 3;
-- ✅ IN быстрее
SELECT * FROM products WHERE id IN (1, 2, 3);
-- ❌ NOT IN с подзапросом (медленно + проблемы с NULL)
SELECT * FROM users WHERE id NOT IN (SELECT user_id FROM orders);
-- ✅ LEFT JOIN с IS NULL
SELECT u.* FROM users u
LEFT JOIN orders o ON o.user_id = u.id
WHERE o.id IS NULL;Проблема N+1
sql
-- ❌ В коде: запрос на каждую запись
-- SELECT * FROM orders;
-- foreach order:
-- SELECT * FROM users WHERE id = order.user_id;
-- Итого: 1 + N запросов
-- ✅ Один запрос с JOIN
SELECT
o.*,
u.name AS user_name,
u.email AS user_email
FROM orders o
INNER JOIN users u ON u.id = o.user_id;Покрывающий индекс
sql
-- Индекс содержит ВСЕ нужные поля
CREATE INDEX idx_covering ON products(category_id, price, name);
-- Этот запрос вообще не обращается к таблице!
SELECT name, price FROM products
WHERE category_id = 1
ORDER BY price;
-- Всё берётся из индекса → SUPER FAST🔟 Типы данных — Шпаргалка
Числовые
sql
TINYINT -- -128 до 127 (1 байт)
SMALLINT -- -32768 до 32767 (2 байта)
INT -- -2млрд до 2млрд (4 байта)
BIGINT -- огромные числа (8 байт)
DECIMAL(10,2) -- точные числа (цены, деньги)
FLOAT -- числа с плавающей точкой
DOUBLE -- больше precision
-- 💡 Для денег ВСЕГДА используй DECIMAL!
price DECIMAL(10, 2) -- 12345678.90Строковые
sql
CHAR(10) -- фиксированная длина, дополняется пробелами
VARCHAR(255) -- переменная длина, до 255 символов
TEXT -- длинный текст, до 65KB
MEDIUMTEXT -- до 16MB
LONGTEXT -- до 4GB
-- 💡 Для email, phone — VARCHAR
-- 💡 Для статей, описаний — TEXTДата и время
sql
DATE -- 2024-03-15
TIME -- 14:30:25
DATETIME -- 2024-03-15 14:30:25
TIMESTAMP -- автообновление, часовой пояс
YEAR -- 2024
-- 💡 created_at / updated_at — TIMESTAMP
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMPДругие
sql
BOOLEAN -- (на самом деле TINYINT(1))
ENUM('draft', 'published', 'deleted') -- список значений
JSON -- для хранения JSON (MySQL 5.7+)1️⃣1️⃣ Создание таблиц — Best Practices
sql
CREATE TABLE users (
-- Primary Key
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
-- Основные поля
name VARCHAR(100) NOT NULL,
email VARCHAR(255) UNIQUE NOT NULL,
password VARCHAR(255) NOT NULL,
-- Enum для статусов
status ENUM('active', 'inactive', 'banned') DEFAULT 'active',
-- Timestamps
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
updated_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
-- Индексы
INDEX idx_email (email),
INDEX idx_status (status),
INDEX idx_created (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Foreign Keys
CREATE TABLE orders (
id BIGINT UNSIGNED AUTO_INCREMENT PRIMARY KEY,
user_id BIGINT UNSIGNED NOT NULL,
total DECIMAL(10, 2) NOT NULL,
status ENUM('pending', 'paid', 'cancelled') DEFAULT 'pending',
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
-- Foreign Key с каскадным удалением
FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE,
INDEX idx_user (user_id),
INDEX idx_status (status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-- Связь многие-ко-многим
CREATE TABLE product_tag (
product_id BIGINT UNSIGNED NOT NULL,
tag_id BIGINT UNSIGNED NOT NULL,
PRIMARY KEY (product_id, tag_id),
FOREIGN KEY (product_id) REFERENCES products(id) ON DELETE CASCADE,
FOREIGN KEY (tag_id) REFERENCES tags(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;1️⃣2️⃣ Частые паттерны
Мягкое удаление (Soft Delete)
sql
ALTER TABLE users ADD COLUMN deleted_at TIMESTAMP NULL;
-- Вместо DELETE
UPDATE users SET deleted_at = NOW() WHERE id = 1;
-- Выборка только активных
SELECT * FROM users WHERE deleted_at IS NULL;
-- Восстановление
UPDATE users SET deleted_at = NULL WHERE id = 1;Пагинация с курсором
sql
-- Вместо OFFSET (медленно на больших страницах)
SELECT * FROM products
WHERE id > last_seen_id
ORDER BY id
LIMIT 20;
-- last_seen_id — это id последнего элемента предыдущей страницыСчётчики
sql
-- ❌ Медленно (пересчёт каждый раз)
SELECT COUNT(*) FROM orders WHERE user_id = 1;
-- ✅ Счётчик в таблице users
ALTER TABLE users ADD COLUMN orders_count INT DEFAULT 0;
-- Обновление триггером или в коде
UPDATE users SET orders_count = orders_count + 1 WHERE id = 1;Денормализация для производительности
sql
-- Вместо JOIN каждый раз
SELECT o.*, u.name AS user_name
FROM orders o
JOIN users u ON u.id = o.user_id;
-- Храни имя в orders
ALTER TABLE orders ADD COLUMN user_name VARCHAR(100);
-- Обновляй при изменении
UPDATE orders SET user_name = 'New Name' WHERE user_id = 1;
-- ⚠️ Жертвуем нормализацией ради скорости
-- Применяй только если действительно нужно1️⃣3️⃣ Безопасность
SQL-инъекции — КАК ЗАЩИТИТЬСЯ
php
// ❌ ОПАСНО! Никогда так не делай!
$query = "SELECT * FROM users WHERE email = '$email'";
$pdo->query($query);
// ✅ Prepared Statements
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = ?");
$stmt->execute([$email]);
// или named parameters
$stmt = $pdo->prepare("SELECT * FROM users WHERE email = :email");
$stmt->execute(['email' => $email]);Права доступа
sql
-- Создай отдельного пользователя для приложения
CREATE USER 'app_user'@'localhost' IDENTIFIED BY 'strong_password';
-- Дай минимальные права
GRANT SELECT, INSERT, UPDATE, DELETE ON mydb.* TO 'app_user'@'localhost';
-- ❌ Не давай DROP, CREATE, ALTER для production пользователя!🎯 Быстрые команды для терминала
bash
# Вход в MySQL
mysql -u root -p
# Выбор базы данных
USE database_name;
# Показать таблицы
SHOW TABLES;
# Структура таблицы
DESCRIBE users;
# или
SHOW CREATE TABLE users;
# Экспорт базы данных
mysqldump -u root -p database_name > backup.sql
# Импорт базы данных
mysql -u root -p database_name < backup.sql
# Экспорт только структуры
mysqldump -u root -p --no-data database_name > structure.sql
# Экспорт только данных
mysqldump -u root -p --no-create-info database_name > data.sql✅ Чеклист перед запуском запроса в production
- [ ] Используешь ли prepared statements?
- [ ] Есть ли WHERE в UPDATE/DELETE? (если нет — обновятся ВСЕ записи!)
- [ ] Создал ли индексы для полей в WHERE и JOIN?
- [ ] Проверил ли запрос через EXPLAIN?
- [ ] Тестировал ли на копии данных?
- [ ] Есть ли LIMIT для больших выборок?
- [ ] Используешь ли транзакции для критичных операций?
- [ ] Нужна ли пагинация для результата?
🎓 Что дальше?
Эта шпаргалка покрывает 90% повседневных задач с SQL. Для углубления:
- Изучи EXPLAIN — понимание того, как MySQL выполняет запросы
- Практикуй оптимизацию — медленные запросы на больших данных
- Изучи репликацию и шардинг — для высоконагруженных проектов
- NoSQL — когда SQL не подходит (Redis, MongoDB)
💾 Сохрани эту шпаргалку!
Держи её под рукой во время работы. SQL — это навык, который нарабатывается практикой. Чем больше пишешь запросы, тем лучше понимаешь, как работает БД.
Совет: Создай свою базу данных для экспериментов и пробуй все примеры вживую. Ломать свою тестовую БД — лучший способ научиться! 🚀