CRUD и запросы
CRUD
| CRUD | SQL |
|---|---|
| Create | INSERT |
| Read | SELECT |
| Update | UPDATE |
| Delete | DELETE |
INSERT
Одна строка:
INSERT INTO users (email, name) VALUES ('a@b.com', 'Алиса');
Несколько:
INSERT INTO users (email, name) VALUES
('a@b.com', 'Алиса'),
('c@d.com', 'Боб'),
('e@f.com', 'Катя');
Что даёт: один запрос вместо цикла. Быстрее в разы.
Вернуть id:
INSERT INTO users (email, name) VALUES ('a@b.com', 'Алиса');
SELECT LAST_INSERT_ID();
⚠️ MySQL не имеет RETURNING как PostgreSQL. LAST_INSERT_ID() работает только для AUTO_INCREMENT.
INSERT ... SELECT:
INSERT INTO users_backup (email, name)
SELECT email, name FROM users WHERE created_at < '2025-01-01';
ON DUPLICATE KEY UPDATE (upsert)
MySQL-специфично:
INSERT INTO users (email, name)
VALUES ('a@b.com', 'Алиса')
ON DUPLICATE KEY UPDATE
name = VALUES(name),
updated_at = NOW();
Что даёт: insert или update при конфликте PRIMARY KEY / UNIQUE.
VALUES(col) — то, что пытались вставить. В MySQL 8.0.20+ лучше:
INSERT INTO users (email, name)
VALUES ('a@b.com', 'Алиса') AS new
ON DUPLICATE KEY UPDATE
name = new.name,
updated_at = NOW();
⚠️ ON DUPLICATE KEY UPDATE срабатывает на любой UNIQUE-конфликт, не только на PK.
REPLACE INTO
REPLACE INTO users (id, email, name) VALUES (1, 'a@b.com', 'Алиса');
Что делает: DELETE + INSERT (или INSERT при отсутствии).
⚠️ Удаляет старую строку и создаёт новую. AUTO_INCREMENT другой, ON DELETE CASCADE сработает.
Правило: почти всегда — INSERT ... ON DUPLICATE KEY UPDATE, не REPLACE.
INSERT IGNORE
INSERT IGNORE INTO users (email, name) VALUES ('a@b.com', 'Алиса');
Что делает: при ошибке (дубликат, NULL, etc.) — пропускает.
⚠️ Игнорирует все ошибки, не только дубликаты. Опасно.
Правило: лучше ON DUPLICATE KEY UPDATE.
SELECT
SELECT * FROM users;
SELECT id, email FROM users;
SELECT email AS user_email FROM users;
SELECT DISTINCT status FROM orders;
SELECT * FROM users LIMIT 10 OFFSET 20;
⚠️ SELECT * в проде — плохо (тянет лишнее, ломается при ALTER).
WHERE
WHERE age = 25
WHERE age != 25
WHERE age <> 25
WHERE age > 18 AND country = 'RU'
WHERE status = 'paid' OR status = 'shipped'
WHERE status IN ('pending', 'paid')
WHERE age BETWEEN 18 AND 65
WHERE email LIKE '%@gmail.com' -- регистрозависимо в utf8mb4_general_ci? НЕТ, зависит от коллации
WHERE email IS NULL
WHERE email IS NOT NULL
⚠️ LIKE регистронезависим в дефолтных коллациях (_ci). Для регистрозависимого — BINARY или _bin.
ORDER BY
ORDER BY created_at DESC
ORDER BY name ASC, age DESC
ORDER BY id LIMIT 10
ORDER BY created_at DESC NULLS LAST -- в MySQL через IS NULL
NULLS LAST (эмуляция):
ORDER BY col IS NULL, col ASC;
GROUP BY
SELECT country, COUNT(*) AS cnt
FROM users
GROUP BY country
ORDER BY cnt DESC;
⚠️ В MySQL по умолчанию ONLY_FULL_GROUP_BY включён (с 5.7). Правило: все неагрегированные колонки в GROUP BY.
HAVING
SELECT country, COUNT(*) AS cnt
FROM users
GROUP BY country
HAVING COUNT(*) > 10;
Правило:
WHERE— до группировки.HAVING— после.
UPDATE
UPDATE users SET name = 'Алиса' WHERE id = 1;
UPDATE users
SET name = 'Алиса',
email = 'alice@b.com',
updated_at = NOW()
WHERE id = 1;
Из другой таблицы (JOIN):
UPDATE orders o
JOIN (
SELECT order_id, SUM(price * quantity) AS total
FROM order_items
GROUP BY order_id
) s ON s.order_id = o.id
SET o.total = s.total;
Что даёт: UPDATE ... JOIN — MySQL-специфичный синтаксис. В PostgreSQL — UPDATE FROM.
⚠️ Забыли WHERE — обновит всю таблицу.
Найти обновлённые строки:
SELECT ROW_COUNT();
DELETE
DELETE FROM users WHERE id = 1;
DELETE FROM orders
WHERE status = 'cancelled'
AND created_at < NOW() - INTERVAL 1 YEAR;
С JOIN:
DELETE o FROM orders o
JOIN users u ON u.id = o.user_id
WHERE u.email = 'spam@example.com';
Multi-table delete:
DELETE o, oi
FROM orders o
JOIN order_items oi ON oi.order_id = o.id
WHERE o.status = 'cancelled';
⚠️ DELETE без WHERE — снесёт всё. Проверяйте через SELECT сначала.
LIMIT
SELECT * FROM users LIMIT 10;
SELECT * FROM users LIMIT 20, 10; -- OFFSET 20, LIMIT 10
SELECT * FROM users LIMIT 10 OFFSET 20;
Что даёт: пагинация.
⚠️ Большой OFFSET — медленно. Для больших таблиц — cursor pagination:
SELECT * FROM users WHERE id > 100000 ORDER BY id LIMIT 10;
Агрегаты
SELECT
COUNT(*) AS total,
COUNT(bio) AS with_bio,
COUNT(DISTINCT country) AS countries,
AVG(age) AS avg_age,
MIN(created_at) AS first_user,
MAX(created_at) AS last_user,
SUM(total) AS revenue
FROM users;
⚠️ COUNT(col) игнорирует NULL.
CASE
SELECT
email,
CASE
WHEN age < 18 THEN 'ребёнок'
WHEN age < 65 THEN 'взрослый'
ELSE 'пенсионер'
END AS category
FROM users;
IF / IFNULL / COALESCE
SELECT IF(age >= 18, 'adult', 'minor') FROM users;
SELECT IFNULL(bio, 'нет описания') FROM users;
SELECT COALESCE(phone, email, 'нет контакта') FROM users;
SELECT NULLIF(country, '') FROM users;
Разница:
IF— тернарный оператор.IFNULL(a, b)— если a NULL, вернуть b.COALESCE(a, b, c, ...)— первое не-NULL.NULLIF(a, b)— NULL, еслиa = b.
Транзакции
START TRANSACTION;
UPDATE accounts SET balance = balance - 100 WHERE id = 1;
UPDATE accounts SET balance = balance + 100 WHERE id = 2;
COMMIT;
-- или ROLLBACK;
Autocommit:
SELECT @@autocommit; -- 1 по умолчанию
SET autocommit = 0; -- не рекомендуется
SAVEPOINT
START TRANSACTION;
INSERT INTO users (email) VALUES ('a@b.com');
SAVEPOINT sp1;
INSERT INTO users (email) VALUES ('a@b.com'); -- ошибка UNIQUE
ROLLBACK TO SAVEPOINT sp1;
COMMIT;
Параметризация
Node.js (mysql2):
// ❌
await pool.query(`SELECT * FROM users WHERE email = '${email}'`);
// ✅
await pool.query('SELECT * FROM users WHERE email = ?', [email]);
PHP (PDO):
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?');
$stmt->execute([$email]);
Laravel:
User::where('email', $email)->first();
DB::select('SELECT * FROM users WHERE email = ?', [$email]);
⚠️ Никогда не конкатенируйте SQL.
Пример: полный цикл
-- 1. INSERT
INSERT INTO users (email, name) VALUES ('alice@example.com', 'Алиса');
SET @user_id = LAST_INSERT_ID();
-- 2. SELECT
SELECT id, email, name, created_at FROM users WHERE id = @user_id;
-- 3. UPDATE
UPDATE users SET name = 'Алиса П.' WHERE id = @user_id;
-- 4. Upsert
INSERT INTO users (email, name)
VALUES ('alice@example.com', 'Алиса Петрова')
ON DUPLICATE KEY UPDATE name = VALUES(name);
-- 5. DELETE
DELETE FROM users WHERE id = @user_id;
Частые ошибки
1. UPDATE / DELETE без WHERE:
Обновит/удалит всё
Решение: сначала SELECT.
2. INSERT в цикле:
foreach ($rows as $r) {
$pdo->exec("INSERT ..."); // 1000 запросов
}
Решение: batch INSERT с несколькими VALUES.
3. SELECT *:
Лишний трафик, ломкость
Решение: явный список.
4. LIKE '%abc%' без индекса:
Full scan
Решение: FULLTEXT или pg_trgm (в MariaDB есть).
5. REPLACE INTO вместо upsert:
Сносит связанные данные через CASCADE
Решение: ON DUPLICATE KEY UPDATE.
6. Неявное приведение типов:
WHERE varchar_col = 123 -- ломает индекс
Решение: одинаковые типы.
7. Транзакция с HTTP внутри:
Блокировки надолго
Решение: HTTP — вне транзакции.
8. LAST_INSERT_ID при batch INSERT:
Возвращает id первой строки, не последней
Решение: знать о поведении.
Практика
1. Создайте таблицу и вставьте 3 строки одним INSERT.
2. Сделайте upsert через ON DUPLICATE KEY UPDATE.
3. Обновите с JOIN.
4. Удалите с JOIN.
5. Транзакция: переведите 100 между счетами и откатите.
6. Параметризованный запрос в своём приложении.
Итог
- INSERT: batch,
ON DUPLICATE KEY UPDATE,INSERT IGNORE. - SELECT:
WHERE,ORDER BY,LIMIT. - UPDATE: с
WHERE, поддержкаJOIN. - DELETE: сначала
SELECT, поддержкаJOIN. - Транзакции:
START TRANSACTION/COMMIT/ROLLBACK. - Параметризация:
?или:name. LAST_INSERT_ID()— id после INSERT.
Дальше — JOIN и связи.