Оптимизация ORM-запросов D7 1С-Битрикс

Наша компания занимается разработкой, поддержкой и обслуживанием решений на Битрикс и Битрикс24 любой сложности. От простых одностраничных сайтов до сложных интернет магазинов, CRM систем с интеграцией 1С и телефонии. Опыт разработчиков подтвержден сертификатами от вендора.
Предлагаемые услуги
Показано 1 из 1 услугВсе 1626 услуг
Оптимизация ORM-запросов D7 1С-Битрикс
Средняя
~1-2 недели
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1173
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    811
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Разработка на базе Битрикс, Битрикс24, 1С для компании Development of an Online Appointment Booking Widget for a Medical Center
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Разработка на базе 1С Предприятие для компании МИРСАНБЕЛ
    745
  • image_crm_dolbimby_434_0.webp
    Разработка сайта на CRM Битрикс24 для компании DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Разработка на базе Битрикс24 для компании ТЕХНОТОРГКОМПЛЕКС
    976

Оптимизация ORM-запросов D7 1С-Битрикс

D7 ORM — объектно-реляционный маппер нового ядра Битрикс. Он удобен для разработки, но умеет генерировать неэффективные запросы, если не понимать его поведение. Запрос, написанный за 5 минут, может делать SELECT * с тремя лишними JOIN — и на большом каталоге это 500 мс вместо 10 мс.

Как D7 ORM строит SQL

ORM читает описание таблицы из метода getMap() класса-сущности. Отношения (references) описываются там же. Когда вы пишете:

\Bitrix\Iblock\ElementTable::getList([
    'select' => ['ID', 'NAME', 'IBLOCK.NAME'],
]);

ORM автоматически добавляет LEFT JOIN b_iblock ON b_iblock.ID = b_iblock_element.IBLOCK_ID — потому что вы запросили поле через точечную нотацию. Это удобно, но если вы не знаете IBLOCK_ID заранее и он всегда одинаковый — JOIN бессмысленный.

Посмотреть итоговый SQL можно через:

$query = \Bitrix\Iblock\ElementTable::query();
$query->setSelect(['ID', 'NAME']);
$query->setFilter(['IBLOCK_ID' => 5]);
echo $query->getQuery();  // выводит сформированный SQL

Главные источники проблем

Лишние поля в select. Если не передать select, ORM выбирает все поля из getMap(). Для ElementTable это 20+ полей включая DETAIL_TEXT (тип Text — потенциально мегабайты). На 100 элементах это мегабайты данных, которые PHP получает из MySQL и тут же выбрасывает.

Правило: всегда явно указывать select:

$result = \Bitrix\Iblock\ElementTable::getList([
    'select' => ['ID', 'NAME', 'PREVIEW_PICTURE_ID', 'DETAIL_PAGE_URL'],
    'filter' => ['=IBLOCK_ID' => 5, '=ACTIVE' => 'Y'],
    'order'  => ['SORT' => 'ASC'],
    'limit'  => 20,
]);

Автоматические JOIN через references. Когда в select присутствует поле через точечную нотацию (SECTION.NAME, IBLOCK.SORT) — ORM добавляет JOIN. Проверяйте через getQuery() — возможно, нужные данные уже есть в основной таблице или их можно получить отдельным запросом.

N+1 через fetchObject(). D7 поддерживает объектную модель (fetchObject()). При обращении к ленивым связям объект делает дополнительный запрос на каждое обращение:

// Это N+1 — каждый ->getSection() делает новый запрос
foreach ($elements->getIterator() as $element) {
    echo $element->getSection()->getName();  // запрос на каждый элемент!
}

// Правильно — загрузить секции сразу через select
$result = \Bitrix\Iblock\ElementTable::getList([
    'select' => ['ID', 'NAME', 'IBLOCK_SECTION_ID', 'SECTION_' => 'IBLOCK_SECTION.NAME'],
    'filter' => ['=IBLOCK_ID' => 5],
]);

Кеширование на уровне ORM-запроса

D7 поддерживает встроенный кеш через параметр cache:

$result = \Bitrix\Iblock\ElementTable::getList([
    'select' => ['ID', 'NAME', 'PREVIEW_PICTURE_ID'],
    'filter' => ['=IBLOCK_ID' => 5, '=ACTIVE' => 'Y'],
    'cache'  => [
        'ttl'   => 3600,   // время жизни в секундах
        'cache_joins' => true,  // кешировать с JOIN-ами
    ],
]);

Кеш хранится в файловой системе (или memcache/redis если настроен). Инвалидируется автоматически при изменении данных через ORM — если данные меняются прямым SQL, кеш не сбрасывается.

Важно: параметр cache_joins => true нужен если в select есть поля из связанных таблиц. Без него кеш работает некорректно при наличии JOIN.

Runtime-поля и подзапросы

D7 позволяет добавлять вычисляемые поля в запрос через runtime:

use Bitrix\Main\Entity;

$result = \Bitrix\Iblock\ElementTable::getList([
    'select' => ['ID', 'NAME', 'PRICE_VALUE'],
    'runtime' => [
        new Entity\ReferenceField(
            'PRICE',
            \Bitrix\Catalog\PriceTable::class,
            ['=this.ID' => 'ref.PRODUCT_ID', '=ref.CATALOG_GROUP_ID' => new Entity\ExpressionField('PTYPE', '1')],
            ['join_type' => 'LEFT']
        ),
        new Entity\ExpressionField('PRICE_VALUE', '%s', ['PRICE.PRICE']),
    ],
    'filter' => ['=IBLOCK_ID' => 5],
]);

Через ExpressionField можно вычислять значения на стороне MySQL без PHP-обработки: ROUND(%s * 1.2, 2) для расчёта цены с наценкой.

Работа с большими выборками

При обработке большого числа записей (импорт, экспорт, массовое обновление) не загружайте всё в память:

// Плохо: все 100 000 строк в памяти PHP
$result = SomeTable::getList(['select' => ['ID', 'NAME']]);
$all = $result->fetchAll();

// Хорошо: обработка пакетами по 500
$offset = 0;
$limit  = 500;
do {
    $result = SomeTable::getList([
        'select' => ['ID', 'NAME'],
        'limit'  => $limit,
        'offset' => $offset,
        'order'  => ['ID' => 'ASC'],  // стабильная сортировка для пагинации
    ]);
    $rows = $result->fetchAll();
    foreach ($rows as $row) {
        // обработка
    }
    $offset += $limit;
} while (count($rows) === $limit);

Альтернатива — курсорная пагинация по ID: filter => ['>ID' => $lastId], что эффективнее OFFSET при больших значениях.

Диагностика через SqlTracker

Для анализа ORM-запросов в реальном контексте:

$tracker = \Bitrix\Main\Diag\SqlTracker::getInstance();
$tracker->start();

// ... ваш код с ORM-запросами ...

$tracker->stop();
$queries = $tracker->getQueries();
usort($queries, fn($a, $b) => $b->getTime() <=> $a->getTime());

foreach (array_slice($queries, 0, 10) as $q) {
    echo round($q->getTime() * 1000, 1) . ' ms: ' . substr($q->getSql(), 0, 200) . PHP_EOL;
}

Сроки работ

Задача Срок Эффект
Аудит ORM-запросов, выявление лишних select/JOIN 1–2 дня Понимание картины
Оптимизация select, устранение N+1 2–4 дня Снижение нагрузки на 30–60%
Добавление кеша к медленным запросам 1–2 дня Устранение повторных запросов
Рефакторинг сложных запросов (runtime, подзапросы) 3–5 дней Перенос вычислений на сторону БД
Комплексная оптимизация ORM-слоя 1.5–2 недели Стабильная работа под нагрузкой

D7 ORM — инструмент баланса между удобством разработки и производительностью. Для читаемых запросов без критических требований к скорости — хорошо. Для горячих путей с тысячами вызовов в минуту — нужно контролировать каждый генерируемый SQL.