Медленный Битрикс на проде почти всегда упирается не в «магию ядра», а в лишние SQL: N+1 в циклах, SELECT *, отсутствие индексов и кеш без инвалидации. D7 ORM удобен, но сам по себе не спасает — он лишь делает дорогие ошибки аккуратнее. Ниже — практический минимум: как увидеть запросы, убрать N+1, сузить select, поставить индекс и закешировать выборку с тегами.
Базовый контракт ORM (DataManager, getList, filter, Reference) разобран в статье Bitrix D7 ORM: DataManager, Entity, getList. Кеш — в Cache и TaggedCache. Здесь фокус именно на скорости запросов.
Включите профилировщик SQL
На dev (никогда на проде для всех пользователей) можно включить статистику запросов:
// local/php_interface/init.php — только на dev!
define('SHOW_SQL_STAT', true);
Внизу страницы появится список SQL с временем. Ищите повторяющиеся однотипные SELECT в цикле и запросы без WHERE по индексу. Альтернатива — медленный лог MySQL / EXPLAIN на конкретный запрос.
N+1 — главная проблема ORM
Классика: выбрали список сущностей, а внутри цикла снова ходите в другую таблицу.
use Bitrix\Main\Loader;
use Bitrix\Main\ORM\Fields\Relations\Reference;
use Bitrix\Main\ORM\Query\Join;
// ПЛОХО: N+1
$items = ElementTable::getList(['select' => ['ID']])->fetchAll();
foreach ($items as $item) {
$props = PropertyTable::getList([
'filter' => ['=ELEMENT_ID' => $item['ID']],
])->fetchAll();
}
// ХОРОШО: один запрос с JOIN (runtime Reference)
$items = ElementTable::getList([
'select' => ['ID', 'NAME', 'PROP_VALUE' => 'PROPERTY.VALUE'],
'runtime' => [
new Reference(
'PROPERTY',
PropertyTable::class,
Join::on('this.ID', 'ref.ELEMENT_ID')
),
],
])->fetchAll();
// ХОРОШО (альтернатива): два запроса + map в PHP
$items = ElementTable::getList(['select' => ['ID', 'NAME']])->fetchAll();
$ids = array_column($items, 'ID');
$propsByElement = [];
if ($ids) {
$propRes = PropertyTable::getList([
'filter' => ['@ELEMENT_ID' => $ids],
'select' => ['ELEMENT_ID', 'VALUE'],
]);
while ($p = $propRes->fetch()) {
$propsByElement[(int)$p['ELEMENT_ID']][] = $p['VALUE'];
}
}
Если связь постоянная — опишите Reference в getMap() сущности и берите поля через USER.LOGIN в select. Подробности карты полей и filter — в гайде по ORM.
Индексы: быстрая проверка
EXPLAIN SELECT * FROM b_iblock_element
WHERE IBLOCK_ID = 5 AND ACTIVE = 'Y';
Если в EXPLAIN видите type: ALL на большой таблице — подходящего индекса нет. Добавляйте составной индекс под реальный filter (порядок колонок важен):
ALTER TABLE b_iblock_element
ADD INDEX idx_iblock_active (IBLOCK_ID, ACTIVE);
Не плодите индексы «на всякий случай»: каждый тормозит INSERT/UPDATE. Смотрите на фильтры, которые реально горят в SHOW_SQL_STAT.
Узкий select и пагинация
select => ['*'] тянет TEXT/BLOB и лишние колонки. Для списков оставляйте только нужное + limit/offset:
$page = max(1, (int)$page);
$limit = 20;
$result = ProductTable::getList([
'select' => ['ID', 'NAME', 'PRICE'],
'filter' => ['=ACTIVE' => 'Y'],
'order' => ['ID' => 'DESC'],
'limit' => $limit,
'offset' => ($page - 1) * $limit,
'count_total' => true,
]);
$total = $result->getCount();
$rows = $result->fetchAll();
Кэширование результатов getList
Файловый/управляемый кеш без тегов быстро отдаёт устаревшие списки. Минимум — TaggedCache и сброс тега на add/update/delete. Развёрнуто — в статье про Cache и TaggedCache.
use Bitrix\Main\Application;
use Bitrix\Main\Data\Cache;
$cache = Cache::createInstance();
$cacheId = 'orders_list_' . md5(serialize($filter));
$cacheDir = '/vendor/orders/';
$tagged = Application::getInstance()->getTaggedCache();
if ($cache->initCache(3600, $cacheId, $cacheDir)) {
$data = $cache->getVars();
} elseif ($cache->startDataCache()) {
$tagged->startTagCache($cacheDir);
$tagged->registerTag('vendor_order');
$data = OrderTable::getList([
'filter' => $filter,
'select' => ['ID', 'CODE', 'STATUS'],
'limit' => 50,
])->fetchAll();
$tagged->endTagCache();
$cache->endDataCache($data);
}
// в add/update/delete заказа:
Application::getInstance()->getTaggedCache()->clearByTag('vendor_order');
Чеклист перед продом
- Нет getList/getById внутри foreach по большому списку.
- В select только нужные поля; есть limit.
- Filter совпадает с индексом; EXPLAIN без полного скана на горячих запросах.
- Даты в filter — объекты Type\DateTime, не «почти правильные» строки.
- Кеш списка — с тегом и сбросом при записи.
Оптимизация SQL в Битрикс — это дисциплина запросов, а не отдельный «турбо-модуль». ORM даёт удобный API; скорость появляется, когда вы явно управляете JOIN, select, индексами и кешем. Если ещё нет каркаса таблицы — начните с DataManager и getList, затем вернитесь к этому чеклисту.
