Есть такая типичная история, когда аналитик пишет запрос, всё работает, данные подтягиваются, но потом таблицы растут, и однажды утром отчёт просто перестаёт грузиться. Нажимаешь «обновить», идёшь за кофе, возвращаешься и всё ещё висит. Обсуждаешь с менеджером новый A/B-тест, возвращаешься, а там всё тот же крутящийся индикатор.

Знакомо? Рассказываю, как это обычно чинят и почему запросы тормозят. Диалект MS SQL Server (T-SQL).

Типичный запрос, который рано или поздно ломается

Обычная аналитическая задача: за месяц по пользователям собрать количество заказов, сумму и среднее время на сайте. Есть три таблицы: users (сотни тысяч записей), orders (миллионы), logs (десятки миллионов посекундных действий).

Вот как часто пишут в первом варианте (все так делали хоть раз):

SELECT 
    u.user_id,
    u.name,
    COUNT(o.order_id) AS orders_count,
    SUM(o.amount) AS total_amount,
    AVG(l.seconds_on_site) AS avg_time
FROM users u
LEFT JOIN orders o ON u.user_id = o.user_id
LEFT JOIN logs l ON u.user_id = l.user_id
WHERE u.registration_date > '2025-01-01'
    AND o.created_at > '2025-12-01'
GROUP BY u.user_id, u.name

Запрос работает, но чем больше данных, тем медленнее. На 500к пользователей, 2 млн заказов и 10 млн логов он легко уходит в 5 минут и дальше, разбираем по пунктам, что здесь не так.

Ошибка первая: индексов нет и не планировались

Часто аналитики рассуждают так: «я не DBA, моё дело запрос написать, а пусть база сама разбирается». База, конечно, разбирается, но без индексов она делает это больно и долго, сканируя таблицы целиком.

Минимальный набор индексов для такого запроса на MS SQL Server:

CREATE INDEX idx_orders_user_created ON orders(user_id, created_at);
CREATE INDEX idx_logs_user ON logs(user_id);

После их добавления время выполнения падает с 5 минут до 2-3, это не серебряная пуля и не лекарство от всех болезней, но первый и самый лёгкий шаг.

Ошибка вторая: JOIN с логами раздувает всё в гигантскую временную таблицу

У одного пользователя могут быть тысячи строк в логах. Когда запрос джойнит логи напрямую, каждая строка из заказов умножается на все строки логов этого пользователя. Получается временная таблица на миллиард записей, хотя нужна была всего одна строчка с агрегатом. Решение – агрегировать логи до JOIN (через обобщённое табличное выражение, CTE):

WITH logs_agg AS (
    SELECT 
        user_id,
        AVG(seconds_on_site) AS avg_time
    FROM logs
    WHERE date > '2025-12-01'
    GROUP BY user_id
)

А потом уже присоединять эту маленькую агрегированную таблицу, после этой правки 2-3 минуты превращаются в 30 секунд.

Ошибка третья: группировка по текстовому полю

GROUP BY u.user_id, u.name выглядит безобидно, но name – это текст, часто он бывает длинный. Группировка по тексту заставляет базу данных сравнивать строки на каждой итерации, а это дорого.

Решение простое: убрать текстовое поле из GROUP BY. Если нужно вывести имя пользователя, можно взять MIN(u.name) или MAX(u.name) для одного пользователя они одинаковы.
Это не влияет на результат, но заметно ускоряет запрос, 30 секунд превращаются в 12.

Ошибка четвёртая: фильтр стоит не на своём месте

В исходном запросе WHERE o.created_at > '2025-12-01' применяется после всех JOIN. Это значит, что база сначала джойнит все заказы за всё время (годы!), а только потом отбрасывает ненужные.

Фильтр нужно переносить прямо в JOIN:

LEFT JOIN orders o 
    ON u.user_id = o.user_id 
    AND o.created_at > '2025-12-01'

Тогда база отсекает ненужные данные на этапе соединения, а не после. Эффект особенно заметен, когда таблица заказов большая, 12 секунд превращается в 5 секунд.

Ошибка пятая: SELECT * вместо конкретных полей

Классика.
Лень перечислять поля, написал звёздочку, в результате база тащит все колонки таблицы, даже те, которые не нужны. На больших объёмах это лишний ввод-вывод и память.

Решение тривиальное: перечислить явно только те поля, которые реально нужны, не даст магического ускорения, но пару процентов выжмет и код сделает понятнее.

Что получается в итоге

WITH logs_agg AS (
    SELECT 
        user_id,
        AVG(seconds_on_site) AS avg_time
    FROM logs
    WHERE date > '2025-12-01'
    GROUP BY user_id
),
orders_agg AS (
    SELECT 
        user_id,
        COUNT(order_id) AS orders_count,
        SUM(amount) AS total_amount
    FROM orders
    WHERE created_at > '2025-12-01'
    GROUP BY user_id
)
SELECT 
    u.user_id,
    u.name,
    ISNULL(o.orders_count, 0) AS orders_count,
    ISNULL(o.total_amount, 0) AS total_amount,
    l.avg_time
FROM users u
LEFT JOIN orders_agg o ON u.user_id = o.user_id
LEFT JOIN logs_agg l ON u.user_id = l.user_id
WHERE u.registration_date > '2025-01-01'

Время выполнения на MS SQL Server: 2 секунды.

С пяти минут до двух секунд, никакого нового железа, только запрос переписали.

Коротко, о чём не стоит забывать

  • Индексы – это база, не надо быть DBA, но пару индексов поставить должен уметь каждый аналитик.

  • Агрегируй до JOIN, а не после, особенно на таблицах типа логов, где записей много на один ключ.

  • Группировка по тексту – боль, убирай ее при любой возможности

  • Фильтры в JOIN и WHERE работают в разном порядке, переноси фильтры в JOIN, если они относятся только к присоединяемой таблице.

  • SELECT * – вредно, лень потом выходит дороже.

Вопрос к тем, кто дочитал

Бывали у вас случаи, когда одна строчка кода превращала пятиминутный запрос в мгновенный? Или наоборот – сидели полдня, переписывали, а ускорения не получили?
Рассказывайте в комментариях, это полезно всем.



Я Таня, Аналитик данных с 5 летним стажем в анализе данных, еще больше интересного про будни и задачи аналитика в бигтехе в моем тг
?TanyaVSdannye?

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


  1. Akina
    02.04.2026 12:38

    Вот как часто пишут в первом варианте (все так делали хоть раз):

    Ага, и получают сервер колом и какие-то совершенно нереально-бешеные, само собой некорректные, значения в выводе. JOIN multiplying? не, не слышали... И очень бы хотелось узнать зачем там LEFT JOIN, когда логика требует INNER.

    Ошибка первая: индексов нет и не планировались

    Это ошибка проектирования БД. Редкий случай - аналитик не виноват.

    У одного пользователя могут быть тысячи строк в логах.

    Судя по вашему "500к пользователей, 2 млн заказов и 10 млн логов" - не тысячи, а в среднем по 20 записей на юзера, 5 записей на заказ.

    А если ещё учесть, что это были "десятки миллионов посекундных действий", то вообще получается, что юзер в среднем тратит 5 секунд на заказ. Офигеть скорострелы!

    Ошибка третья: группировка по текстовому полю

    Я ведь правильно понимаю, что users.user_id - это первичный индекс таблицы? Если так - вы только что объявили MS SQL круглым идиотом. Хотя на самом деле при наличии в выражении группировки первичного индекса никаких группировок по остальным полям той же таблицы сервер не должен делать. И если вы на самом деле увидели вдруг снижение времени выполнения с 30 секунд до 12, то лучше посмотрите в планы выполнения и найдите настоящую причину ускорения запроса.

    Время выполнения на MS SQL Server: 2 секунды.

    С пяти минут до двух секунд, никакого нового железа, только запрос переписали.

    Но главное - в том, что запрос вместо какой-то бредятины начал наконец возвращать правильные данные.

    Тип связывания, впрочем, за каким-то хреном так и остался неисправленным.

    В исходном запросе WHERE o.created_at > '2025-12-01' применяется после всех JOIN. Это значит, что база сначала джойнит все заказы за всё время (годы!), а только потом отбрасывает ненужные.

    Фильтры в JOIN и WHERE работают в разном порядке

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

    --------------

    Если коротко подвести итоги - вы слишком плохо выдумываете несуществующие ситуации, и неспособны довести их до уровня возможных на практике.


    1. falmer
      02.04.2026 12:38

      Вы слишком строги в рекламному посту. Самое главное в конце же было.


    1. TanyaVSdannye Автор
      02.04.2026 12:38

      всем, кроме вас, понятно, что пример специально утрирован для демонстрации возможных способов оптимизации

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


      1. Akina
        02.04.2026 12:38

        всем, кроме вас, понятно, что пример специально утрирован для демонстрации возможных способов оптимизации

        Ну так и демонстрируйте способы! А не выдумывайте невозможную на практике ситуацию.

        И особенно - не публикуйте сведений, которые являются заведомо неправильными/ошибочными. Ваши ошибки могут дорого обойтись тем, кто воспримет такие сведения за истинные.

        ограничиваетесь только комментариями с резкой критикой

        Поначалу я старался не выходить за рамки. Но всему есть пределы. У вас практически в каждой статье - одно и то же...

        Если доброе слово не помогает - приходится пистолетом.


        1. TanyaVSdannye Автор
          02.04.2026 12:38

          укажите ошибки тактично, если считаете, что они есть,
          в вашем комментарии выше только деструктивная критика


  1. Ivan22
    02.04.2026 12:38

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