Оптимизация SQL-запросов 1С-Битрикс
На нагруженном сайте Битрикс генерирует от 200 до 1000 SQL-запросов на страницу. Большинство из них — повторяющиеся, избыточные или с SELECT * вместо конкретных полей. Прежде чем покупать более дорогой сервер, есть смысл разобраться: какие именно запросы тормозят и почему.
Как найти проблемные запросы
Первый шаг — включить SQL-трекер Битрикса. В панели производительности (/bitrix/admin/perfmon_panel.php) переключите в режим профилирования. Либо программно:
\Bitrix\Main\Diag\SqlTracker::getInstance()->start();
// ... ваш код ...
$tracker = \Bitrix\Main\Diag\SqlTracker::getInstance();
$tracker->stop();
foreach ($tracker->getQueries() as $query) {
echo $query->getSql() . ' — ' . $query->getTime() . 'ms' . PHP_EOL;
}
Трекер покажет все запросы за запрос, их время и стек вызовов. Ищите:
- запросы дольше 50 мс — они тормозят страницу заметно
- дубли — один и тот же запрос по 10–50 раз на страницу
- запросы без
WHEREпо большим таблицам (b_iblock_element,b_sale_order_props)
Второй источник — slow_query_log MySQL/MariaDB. Включается через long_query_time = 0.5 в my.cnf. Даёт реальную картину под нагрузкой, не только при ручном тестировании.
Основные классы проблем
Запросы без индексов. b_iblock_element содержит от 10 000 до 1 000 000 строк. Запрос SELECT * FROM b_iblock_element WHERE IBLOCK_ID = 5 AND ACTIVE = 'Y' ORDER BY SORT без индекса по (IBLOCK_ID, ACTIVE, SORT) делает full table scan. Проверка: EXPLAIN SELECT ... — если в type видите ALL, индекса нет.
Битрикс создаёт индексы при установке, но не при добавлении пользовательских полей через b_uts_iblock_N_single (UTS-таблицы для пользовательских типов). Индексы по этим таблицам нужно добавлять вручную.
Избыточная выборка полей. CIBlockElement::GetList() по умолчанию джойнит несколько таблиц и возвращает десятки полей, включая DETAIL_TEXT весом в мегабайты. Если на странице нужны только ID, NAME и PREVIEW_PICTURE — задавайте $select явно:
CIBlockElement::GetList(
['SORT' => 'ASC'],
['IBLOCK_ID' => 5, 'ACTIVE' => 'Y'],
false,
['nPageSize' => 20],
['ID', 'NAME', 'PREVIEW_PICTURE'] // только нужные поля
);
Без $select запрос включает LEFT JOIN b_iblock_element_iprop и другие таблицы, которые не нужны.
N+1 проблема. Классика: загрузили 20 элементов, потом в цикле для каждого отдельным запросом загружаете свойства. Итог — 21 запрос вместо 1–2. В старом API решается через $arSelectFields с включением свойств. В D7 — через fetchCollection() с fill() или предварительную загрузку через select().
Повторные запросы одних и тех же данных. Настройки сайта, группы пользователей, разделы инфоблоков — запрашиваются на каждой странице повторно. Стандартный кеш Битрикса (BXCache) должен это закрывать, но если кеш выключен или тегированный кеш сбрасывается слишком часто — запросы идут в БД.
Работа с индексами
Добавление недостающих индексов — самое быстрое решение с наибольшим эффектом:
| Таблица | Рекомендуемый индекс | Когда нужен |
|---|---|---|
b_iblock_element |
(IBLOCK_ID, ACTIVE, SORT) |
Почти всегда |
b_iblock_element_property |
(IBLOCK_PROPERTY_ID, VALUE) |
При фильтрации по свойствам |
b_sale_order |
(USER_ID, STATUS_ID, DATE_INSERT) |
Личный кабинет покупателя |
b_sale_basket |
(ORDER_ID, FUSER_ID) |
Корзина, оформление заказа |
b_search_content_stem |
(PARAM2) |
Поиск по большому каталогу |
Индекс добавляется через SQL или Битрикс ORM:
$connection = \Bitrix\Main\Application::getConnection();
$connection->queryExecute(
"ALTER TABLE b_iblock_element_property ADD INDEX ix_prop_val (IBLOCK_PROPERTY_ID, VALUE(64))"
);
Осторожно с VALUE(64): строковые поля индексируются по префиксу. Для числовых значений, хранящихся как VARCHAR, рассмотрите виртуальный столбец с приведением типа.
Оптимизация через D7 ORM
D7 ORM даёт возможность точечно управлять запросом. Сравните:
// Плохо: загружает все поля, все свойства
$result = \Bitrix\Iblock\ElementTable::getList([
'filter' => ['IBLOCK_ID' => 5, 'ACTIVE' => 'Y'],
]);
// Лучше: только нужные поля, явный лимит
$result = \Bitrix\Iblock\ElementTable::getList([
'select' => ['ID', 'NAME', 'PREVIEW_PICTURE_ID'],
'filter' => ['IBLOCK_ID' => 5, 'ACTIVE' => 'Y'],
'order' => ['SORT' => 'ASC'],
'limit' => 20,
'offset' => 0,
'cache' => ['ttl' => 3600],
]);
Параметр cache в запросе ORM — встроенное кеширование на уровне запроса. Инвалидация — через тегированный кеш при изменении элемента инфоблока.
Кеш как замена лишним запросам
Если данные меняются раз в час — нет смысла ходить в БД на каждый запрос. Используйте \Bitrix\Main\Data\Cache:
$cache = \Bitrix\Main\Data\Cache::createInstance();
$cacheId = 'catalog_top_' . $iblockId;
$cachePath = '/catalog/top/';
if ($cache->initCache(3600, $cacheId, $cachePath)) {
$data = $cache->getVars();
} elseif ($cache->startDataCache()) {
$tagCache = new \Bitrix\Main\Data\TaggedCache();
$tagCache->startTagCache($cachePath);
$data = /* ваш запрос */;
$tagCache->registerTag('iblock_id_' . $iblockId);
$tagCache->endTagCache();
$cache->endDataCache($data);
}
При изменении любого элемента инфоблока тег iblock_id_N инвалидируется автоматически, и следующий запрос идёт в БД.
Сроки работ
| Задача | Объём работ | Ожидаемый эффект |
|---|---|---|
| Профилирование, выявление топ-10 запросов | 1 день | Понимание проблемы |
| Добавление недостающих индексов | 1–2 дня | Ускорение в 2–10 раз на конкретных запросах |
Оптимизация $select в компонентах |
2–3 дня | Снижение нагрузки на 20–40% |
| Устранение N+1, добавление кеша | 3–5 дней | Снижение числа запросов в 3–5 раз |
| Комплекс: индексы + выборка + кеш + ORM | 1–2 недели | Страница с 500+ запросов → 50–100 запросов |
Оптимизация SQL — не разовое действие. После изменений нужен регулярный мониторинг: slow query log раз в неделю, трекер при релизах новых компонентов. Проблемы накапливаются по мере роста каталога и объёма данных.







