Оптимизация JOIN-запросов в 1С-Битрикс
Битрикс активно использует JOIN в своём SQL: запрос к CIBlockElement::GetList() с выборкой свойств может объединять 8–12 таблиц. На малых объёмах это незаметно. Когда в b_iblock_element 500 000 строк, а в b_iblock_element_property — 10 миллионов, неоптимальный JOIN превращается в многосекундную операцию.
Как Битрикс строит JOIN-запросы
Когда вы передаёте свойства в $arSelectFields или фильтруете по ним, Битрикс добавляет JOIN к нескольким таблицам в зависимости от типа хранения свойства:
-
Обычные свойства —
LEFT JOIN b_iblock_element_property AS p1 ON p1.IBLOCK_ELEMENT_ID = be.ID AND p1.IBLOCK_PROPERTY_ID = N -
Множественные свойства — каждое значение отдельной строкой в
b_iblock_element_property, JOIN возвращает несколько строк на элемент -
UTS-свойства (пользовательские типы) — отдельная таблица
b_uts_iblock_N_singleилиb_uts_iblock_N_multiple, JOIN поIBLOCK_ELEMENT_ID -
Свойство-список (L) — дополнительный JOIN к
b_iblock_property_enumдля получения значений
Проблема: если запросить 10 свойств для 1000 элементов, Битрикс строит запрос с 10+ JOIN. MySQL выполняет вложенные циклы — для каждой строки из одной таблицы проходит по связанной. Без индексов по ключам JOIN это full scan на каждой итерации.
Главные причины медленных JOIN в Битрикс
1. Отсутствие индексов на колонках соединения.
Самый частый случай — JOIN по IBLOCK_ELEMENT_ID в b_iblock_element_property без индекса. Битрикс создаёт индекс ix1 по (IBLOCK_ELEMENT_ID) при установке, но с ростом объёма данных этого бывает недостаточно. EXPLAIN покажет type=ALL по таблице свойств.
Проверьте:
SHOW INDEX FROM b_iblock_element_property;
SHOW INDEX FROM b_uts_iblock_5_single; -- для инфоблока ID 5
Для UTS-таблиц индексы не создаются автоматически при добавлении свойств через административный интерфейс.
2. JOIN по строковому полю VALUE при числовых данных.
Поле VALUE в b_iblock_element_property имеет тип TEXT или VARCHAR(255). Фильтрация WHERE p.VALUE = '100' — это сравнение строк, индекс по TEXT-полю неэффективен, приведение типов при JOIN ломает использование индекса. Для свойств с числовыми значениями Битрикс дублирует данные в поле VALUE_NUM (FLOAT) — используйте его.
3. Запрос множественных свойств в одном JOIN.
Выборка 5 множественных свойств дублирует строки: если у элемента 3 значения свойства A и 4 значения свойства B — запрос вернёт 12 строк на элемент. MySQL обрабатывает декартово произведение, затем группирует. На 10 000 элементов промежуточная выборка может составить миллионы строк.
Оптимизация: разбить на два запроса — сначала получить основные данные, затем подгрузить значения множественных свойств отдельным запросом по массиву ID.
Оптимизация на уровне запросов
Вынести фильтрацию до JOIN. Если нужны элементы определённого раздела с конкретным свойством — сначала отфильтруйте по b_iblock_element (с индексом по IBLOCK_SECTION_ID), затем делайте JOIN. MySQL должен начать с наименьшей выборки.
Используйте STRAIGHT_JOIN чтобы зафиксировать порядок соединения, если оптимизатор выбирает неверный план:
$connection = \Bitrix\Main\Application::getConnection();
$result = $connection->query("
SELECT STRAIGHT_JOIN be.ID, be.NAME, p.VALUE
FROM b_iblock_element be
INNER JOIN b_iblock_element_property p
ON p.IBLOCK_ELEMENT_ID = be.ID AND p.IBLOCK_PROPERTY_ID = 42
WHERE be.IBLOCK_ID = 5
AND be.ACTIVE = 'Y'
AND p.VALUE_NUM BETWEEN 1000 AND 5000
ORDER BY be.SORT
LIMIT 20
");
Замена LEFT JOIN на подзапрос или EXISTS. Для проверки наличия связи (нужно ли значение свойства или только факт его наличия) EXISTS работает быстрее, чем LEFT JOIN с IS NOT NULL:
-- Медленнее: JOIN + группировка
SELECT be.* FROM b_iblock_element be
LEFT JOIN b_iblock_element_property p ON p.IBLOCK_ELEMENT_ID = be.ID AND p.IBLOCK_PROPERTY_ID = 10
WHERE p.ID IS NOT NULL;
-- Быстрее: EXISTS
SELECT be.* FROM b_iblock_element be
WHERE EXISTS (
SELECT 1 FROM b_iblock_element_property p
WHERE p.IBLOCK_ELEMENT_ID = be.ID AND p.IBLOCK_PROPERTY_ID = 10
);
Оптимизация через D7 ORM
D7 позволяет контролировать какие таблицы включаются в JOIN через параметр runtime и явную настройку relations. При работе с HL-инфоблоками (Highload) D7 генерирует более чистые запросы без лишних JOIN к b_iblock_element_property.
// Избегаем JOIN к таблице свойств, работаем напрямую с HL-таблицей
$result = \Bitrix\Highloadblock\HighloadBlockTable::compileEntity($hlBlock)
->getDataClass()::getList([
'select' => ['ID', 'UF_PRODUCT_ID', 'UF_PRICE'],
'filter' => ['>=UF_PRICE' => 1000, '<=UF_PRICE' => 5000],
'limit' => 100,
]);
HL-инфоблоки хранят данные в отдельной таблице без JOIN к b_iblock_element_property — для высоконагруженных свойств (технические характеристики, большие каталоги) это правильная архитектура.
Индексы для оптимизации JOIN
| Таблица | Индекс | Для чего |
|---|---|---|
b_iblock_element_property |
(IBLOCK_PROPERTY_ID, VALUE_NUM) |
Фильтр по числовым свойствам |
b_iblock_element_property |
(IBLOCK_ELEMENT_ID, IBLOCK_PROPERTY_ID) |
JOIN по элементу + свойству |
b_uts_iblock_N_single |
(UF_CUSTOM_FIELD) |
Фильтр по пользовательскому свойству |
b_catalog_price |
(PRODUCT_ID, CATALOG_GROUP_ID) |
JOIN цен к элементам |
b_iblock_section_element |
(IBLOCK_SECTION_ID) |
JOIN секций к элементам |
Сроки работ
| Задача | Срок | Эффект |
|---|---|---|
| EXPLAIN-анализ JOIN-запросов, добавление индексов | 2–3 дня | Ускорение 5–20x на проблемных запросах |
| Рефакторинг выборки свойств (разбиение на подзапросы) | 3–5 дней | Устранение декартова произведения |
| Перевод нагруженных свойств на HL-инфоблоки | 1–2 недели | Устранение JOIN к b_iblock_element_property |
| Комплексная оптимизация каталога | 2–3 недели | Страница каталога < 100 мс вместо 2–5 с |
JOIN-запросы в Битрикс — следствие архитектурного решения хранить все свойства в универсальной таблице. При небольших объёмах это работает. При росте данных нужно либо добавлять индексы, либо менять схему хранения на HL-инфоблоки или собственные таблицы.







