Если ваш сайт на PHP начал тормозить, скорее всего, дело не в хостинге и не в кривых руках разработчика, а в запросах к базе данных. База данных — это сердце любого динамического сайта, и если она работает медленно, весь сайт превращается в черепаху. В этой статье я расскажу, как найти проблемные запросы, ускорить их и не сойти с ума при этом. Без сложных терминов, только практика.

Digital-студия WNDER

Почему запросы тормозят: основные причины

Представьте, что база данных — это библиотека, а запрос — это запрос библиотекарю найти книгу. Если библиотекарь бегает по всем полкам без системы, поиск займет вечность. Так и с БД: если запрос написан неправильно или нет индексов, серверу приходится сканировать всю таблицу, чтобы найти нужные данные.

Вот главные виновники медленных запросов:

  • Отсутствие индексов — как отсутствие каталога в библиотеке.
  • Лишние данные — вы запрашиваете все колонки, хотя нужна одна.
  • Запросы в цикле — N+1 проблема, когда вы делаете запрос для каждой записи.
  • Неправильное использование JOIN — слишком много соединений или соединения без индексов.

Самое обидное, что часто эти проблемы можно исправить за пару минут, но из-за невнимательности мы теряем часы.

Индексы: ваш главный друг

Индекс — это структура, которая позволяет базе данных быстро находить нужные строки. Без индекса БД сканирует всю таблицу, а с индексом — ищет по отсортированной структуре, как по алфавиту в книге.

Допустим, у вас есть таблица пользователей:

CREATE TABLE users (
    id INT PRIMARY KEY,
    email VARCHAR(255),
    name VARCHAR(100),
    created_at TIMESTAMP
);

Если вы часто ищете по email, добавьте индекс:

CREATE INDEX idx_email ON users(email);

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

Правило: индексируйте только те поля, которые часто используются в WHERE, JOIN или ORDER BY. Не плодите индексы ради индексов.

Подготовленные запросы и кеширование

Подготовленные запросы (prepared statements) в PHP — это не только защита от SQL-инъекций, но и способ ускорить выполнение повторяющихся запросов. Когда вы используете PDO или MySQLi с подготовленными выражениями, база данных кеширует план выполнения запроса, что снижает нагрузку.

Пример на PDO:

$pdo = new PDO('mysql:host=localhost;dbname=test', 'user', 'pass');
$stmt = $pdo->prepare('SELECT * FROM users WHERE email = ?');
$stmt->execute(['user@example.com']);
$user = $stmt->fetch();

А вот с кешированием результатов — можно использовать Memcached или Redis. Например, если у вас популярная страница, которая редко меняется, закешируйте результат запроса:

$cacheKey = 'user_profile_' . $userId;
$cached = $redis->get($cacheKey);
if ($cached === false) {
    $stmt = $pdo->prepare('SELECT * FROM users WHERE id = ?');
    $stmt->execute([$userId]);
    $user = $stmt->fetch();
    $redis->set($cacheKey, json_encode($user), 3600); // на 1 час
} else {
    $user = json_decode($cached, true);
}

Это как приготовить еду заранее и разогреть, когда нужно, вместо того чтобы готовить каждый раз с нуля.

Избегаем N+1 проблему

Классическая ошибка — запрос в цикле. Например, вы выводите список статей и для каждой статьи подтягиваете автора:

$articles = $pdo->query('SELECT * FROM articles')->fetchAll();
foreach ($articles as $article) {
    $stmt = $pdo->prepare('SELECT name FROM users WHERE id = ?');
    $stmt->execute([$article['author_id']]);
    $author = $stmt->fetch();
    echo $article['title'] . ' by ' . $author['name'];
}

Если статей 100, будет 101 запрос. Это медленно. Решение — один запрос с JOIN:

$stmt = $pdo->query('SELECT articles.title, users.name FROM articles JOIN users ON users.id = articles.author_id');
$rows = $stmt->fetchAll();
foreach ($rows as $row) {
    echo $row['title'] . ' by ' . $row['name'];
}

Или, если JOIN не подходит, можно собрать все ID и сделать один запрос с IN:

$ids = array_column($articles, 'author_id');
$placeholders = implode(',', array_fill(0, count($ids), '?'));
$stmt = $pdo->prepare("SELECT id, name FROM users WHERE id IN ($placeholders)");
$stmt->execute($ids);
$users = $stmt->fetchAll();

Анализ медленных запросов: включаем логи

MySQL умеет записывать все медленные запросы в специальный лог. Включите его, чтобы увидеть, какие запросы тормозят. В конфигурации MySQL (my.cnf) добавьте:

slow_query_log = 1
slow_query_log_file = /var/log/mysql-slow.log
long_query_time = 2

Теперь все запросы, которые выполняются дольше 2 секунд, попадут в лог. Периодически проверяйте его и оптимизируйте проблемные места.

ИнструментЧто делает
EXPLAINПоказывает план выполнения запроса, помогает увидеть, используется ли индекс
mysqltunerСкрипт, который анализирует конфигурацию MySQL и дает советы
Percona ToolkitНабор инструментов для анализа и оптимизации запросов
Лайфхак: используйте команду EXPLAIN перед сложным запросом. Если в колонке type стоит ALL — это плохо, значит, БД сканирует всю таблицу. Ищите index или ref.

Что в итоге?

Оптимизация запросов — это не магия, а системный подход. Начните с малого: включите логи медленных запросов, найдите проблемные места, добавьте индексы, перепишите циклы с N+1 на JOIN. Через неделю вы заметите, как сайт летает.

И помните: не оптимизируйте то, что и так работает быстро. Сначала измерьте, потом режьте.

Студия WNDER