← К кейсам

Кейсы

Почему операция в Битрикс24 занимала 39 секунд: диагностика таблицы на 9,5 млн строк

Разбор медленной операции в коробочном Битрикс24: профилирование, поиск одного SQL-запроса на 35 секунд, EXPLAIN, таблица b_utm_calendar_event на 9,5 млн строк и подбор подходящего индекса.

Проблема

На одном из коробочных порталов заметно замедлилась операция объединения сущностей CRM. Пользователь запускал объединение и почти минуту ждал завершения.

Первой гипотезой мог быть кастомный PHP-код, большое количество связанных объектов или сама логика объединения. Но вместо оптимизации наугад я сначала разделил время выполнения PHP и базы данных.

Сначала отделил PHP от базы

Замеры показали, что основная задержка возникает не в PHP, а внутри SQL.

Вся операция 39,15 с
SQL-запросы суммарно 37,61 с
Количество SQL-запросов 100
Один самый медленный запрос 35,55 с

Почти всё время SQL приходилось на один SELECT. Это сразу сузило область поиска: оптимизировать весь процесс объединения не требовалось — сначала нужно было разобраться именно с этим запросом.

Проблемный штатный запрос

Узким местом оказался штатный запрос Битрикс к таблице:

b_utm_calendar_event

Упрощённо условие выглядело так:

WHERE FIELD_ID = 17
  AND VALUE = 'D_378252'

Запрос искал связанные записи календарных событий. Результат был совсем небольшим — всего 113 строк, но получение этих строк занимало около 35,55 секунды.

На момент диагностики в таблице было:

9 479 572 строк

Сам по себе большой объём ещё не является проблемой. MySQL нормально работает и с более крупными таблицами, если запрос может быстро найти нужные строки по подходящему индексу.

Что показал EXPLAIN

MySQL использовал существующий индекс, но он не соответствовал условию запроса полностью.

В таблице были индексы, в том числе по:

VALUE_ID

и составной индекс:

FIELD_ID, VALUE_ID, VALUE_INT

При этом запрос фильтровал по:

FIELD_ID
VALUE

Поле VALUE имело тип TEXT и подходящего индекса для этой комбинации не было.

В результате MySQL мог использовать FIELD_ID, но после этого всё равно должен был проверить очень большой набор строк по VALUE.

EXPLAIN оценивал объём просмотра примерно в:

4 739 786 строк

То есть для получения 113 результатов база фактически работала примерно с половиной таблицы.

9 479 572 строк в таблице
          ↓
индекс по FIELD_ID
          ↓
~4 739 786 потенциальных строк
          ↓
проверка VALUE = 'D_378252'
          ↓
113 нужных строк

Почему проблема проявилась со временем

На небольшом объёме такой запрос может работать незаметно быстро. Если после фильтрации по FIELD_ID остаётся несколько тысяч строк, дополнительная проверка по VALUE почти не ощущается.

Но на старой коробке таблица постепенно растёт:

десятки тысяч
→ сотни тысяч
→ миллионы строк

Код запроса при этом может вообще не меняться. Поэтому проблема выглядит как внезапное "Битрикс стал тормозить", хотя фактически узкое место росло вместе с объёмом данных.

Это важный момент при диагностике старых коробок: один и тот же код на данных трёхлетней давности и на текущем объёме может иметь совершенно разные характеристики.

Какое исправление предложил

Для конкретного условия нужен индекс, позволяющий отбирать записи не только по FIELD_ID, но и по VALUE.

В ходе диагностики я предложил отдельный индекс:

CREATE INDEX ix_utm_calendar_event_crm_value
ON b_utm_calendar_event (
    VALUE(32),
    FIELD_ID,
    VALUE_ID
);

Поскольку VALUE имеет тип TEXT, используется префиксный индекс VALUE(32). Для значения вида D_378252 такого префикса достаточно, чтобы в индекс попало всё фактическое значение.

Цель такого индекса — перейти от:

FIELD_ID
↓
миллионы кандидатов
↓
фильтрация VALUE

к гораздо более узкому поиску:

VALUE + FIELD_ID
↓
небольшой диапазон подходящих записей

Результат диагностики

В результате удалось точно локализовать узкое место: штатный запрос Битрикс к b_utm_calendar_event фильтровал данные по FIELD_ID и неиндексированному VALUE. На таблице примерно с 9,48 млн строк существующий индекс сокращал выборку недостаточно, и MySQL оценивал просмотр примерно 4,74 млн строк для получения 113 результатов.

Причина была не просто в "большой таблице" и не в общем количестве данных, а в сочетании трёх факторов:

большой объём данных
+
конкретный WHERE
+
неподходящая структура существующих индексов

В этом случае одного профилирования вместе с EXPLAIN оказалось достаточно, чтобы объяснить почти 36 секунд из 39 секунд всей операции.