← Назад к вопросам

Были ли задачи оптимизации больших запросов к базе данных на работе?

1.6 Junior🔥 252 комментариев
#Опыт и карьера

Комментарии (2)

🐱
deepseek-v3.2PrepBro AI5 апр. 2026 г.(ред.)

Ответ сгенерирован нейросетью и может содержать ошибки

Да, оптимизация запросов к базе данных — одна из ключевых задач в моей практике

С работой с объёмными данными (миллионы и миллиарды записей) я сталкивался многократно, особенно в высоконагруженных проектах: финансовых аналитических системах, маркетплейсах и социальных платформах. Задачи оптимизации — это не разовая акция, а постоянный процесс мониторинга, анализа и тонкой настройки. Я поделюсь несколькими типичными кейсами и подходами, которые применял.

Кейс 1: Оптимизация медленного отчетного запроса с агрегацией

Ситуация: В системе аналитики е-commerce запрос для построения отчета по продажам за год (с группировкой по месяцам и категориям, подсчетом сумм, среднего чека) выполнялся более 25 секунд при 10+ миллионах заказов.

Проблемы и решение:

  1. Анализ плана выполнения (EXPLAIN): Первый и обязательный шаг. Запрос делал полное сканирование таблицы (FULL TABLE SCAN) и использовал временную таблицу на диске (Using temporary; Using filesort) из-за сложной GROUP BY и ORDER BY.

    -- Исходный проблемный запрос (упрощенно)
    EXPLAIN SELECT
        DATE_FORMAT(o.created_at, '%Y-%m') as month,
        c.name as category,
        COUNT(o.id) as order_count,
        SUM(o.total) as total_sum
    FROM orders o
    JOIN order_items oi ON o.id = oi.order_id
    JOIN products p ON oi.product_id = p.id
    JOIN categories c ON p.category_id = c.id
    WHERE o.status = 'completed' AND o.created_at >= '2023-01-01'
    GROUP BY month, c.id
    ORDER BY month DESC, total_sum DESC;
    
    План показал отсутствие подходящих индексов на `o.created_at`, `o.status` и `p.category_id`.

  1. Создание составных индексов: Вместо отдельных индексов создал составные, покрывающие условия фильтрации и джойнов.

    ALTER TABLE orders ADD INDEX idx_status_created (status, created_at);
    ALTER TABLE products ADD INDEX idx_category_id (category_id);
    
  2. Денормализация и материализованное представление: Для отчетов, где критична скорость, а данные могут быть не самыми свежими, предложил и реализовал материализованное представление (Materialized View). Ежедневно ночью агрегированные данные за прошедший день вычислялись и сохранялись в отдельную оптимизированную таблицу sales_daily_aggregated. Отчетный запрос стал обращаться к ней, сократив время до 200-300 мс.

    -- Таблица для агрегированных данных
    CREATE TABLE sales_daily_aggregated (
        date DATE NOT NULL,
        category_id INT NOT NULL,
        order_count INT DEFAULT 0,
        total_sum DECIMAL(15,2) DEFAULT 0,
        PRIMARY KEY (date, category_id),
        INDEX idx_date (date)
    ) ENGINE=InnoDB;
    

Кейс 2: Борьба с N+1 проблемой в ORM (Eloquent/Doctrine)

Ситуация: В API пользовательской ленты активности (как в социальной сети) при загрузке списка постов с авторами и комментариями генерировалось несколько сотен запросов. Страница "лагала".

Решение:

  • Жадная загрузка (Eager Loading): Вместо ленивой загрузки в цикле использовал with() (в Laravel/Eloquent) или JOIN в DQL (Doctrine), чтобы загрузить все связанные данные за 2-3 запроса.

    // ПЛОХО: N+1 запросов
    $posts = Post::all();
    foreach ($posts as $post) {
        echo $post->author->name; // Отдельный запрос на каждом шаге
    }
    
    // ХОРОШО: Всего 2 запроса (на посты и на авторов)
    $posts = Post::with(['author', 'comments.user'])->limit(100)->get();
    
  • Селективность выборки: Использовал select() для выборки только нужных полей, уменьшая объем передаваемых данных и нагрузку на сеть/память.

    $posts = Post::select(['id', 'title', 'author_id'])
                 ->with(['author:id,name,avatar'])
                 ->paginate(50);
    

Кейс 3: Оптимизация пагинации на глубоких страницах

Проблема: Классический LIMIT 10000, 20 на таблице с 5 млн записей выполнялся крайне медленно, так как СУБД фактически должна была отсчитать и отбросить первые 10000 записей.

Решение:

  • Курсорная пагинация (Keyset Pagination): Вместо OFFSET использовал условие на WHERE с уникальным, проиндексированным полем (обычно id или created_at).

    // Запрос для следующей страницы
    $lastId = $request->input('last_id');
    $posts = Post::where('id', '>', $lastId)
                 ->orderBy('id')
                 ->limit(20)
                 ->get();
    
    Это позволяет использовать индекс для быстрого поиска точки старта.

Общий алгоритм и инструменты, которые я применяю:

  1. Мониторинг и выявление: Постоянный анализ slow query log (например, с помощью Percona Monitoring and Management (PPM) или Laravel Telescope для framework-проектов). Использование EXPLAIN ANALYZE в MySQL 8+ для детального понимания стоимости операций.
  2. Проектирование индексов: Не просто добавление индексов на все поля, а осмысленное создание составных покрывающих индексов (covering indexes), учитывающих порядок полей в WHERE, ORDER BY и JOIN. Понимание разницы между BTREE и HASH индексами.
  3. Анализ и рефакторинг запроса:
    *   Устранение `SELECT *`.
    *   Минимизация подзапросов в `SELECT`, замена на `JOIN` где возможно.
    *   Избегание функций в условиях `WHERE` (например, `YEAR(created_at) = 2023`), которые мешают использованию индексов.
    *   Разбивка сложных запросов на несколько простых, если это выгодно (например, кэширование промежуточных результатов).
  1. Архитектурные изменения:
    *   **Шардирование (горизонтальное partitioning)** таблиц по дате или диапазону ключа.
    *   **Репликация:** Направление тяжелых отчетных запросов на read-only реплику, разгружая основную мастер-базу.
    *   **Внедрение кэширования результатов** сложных вычислений (Redis, Memcached) с инвалидацией по событиям.

Вывод: Оптимизация больших запросов — это комплексная дисциплина, лежащая на стыке глубокого понимания SQL, внутреннего устройства СУБД (InnoDB) и бизнес-логики приложения. Нет серебряной пули, но есть методология: измерять, анализировать, вносить точечные изменения и снова измерять. Часто самый большой прирост даёт не добавление индекса, а изменение структуры запроса или данных.

Были ли задачи оптимизации больших запросов к базе данных на работе? | PrepBro