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

Наша компанія займається розробкою, підтримкою та обслуговуванням рішень на Бітрікс та Бітрікс24 будь-якої складності. Від простих односторінкових сайтів до складних інтернет-магазинів, CRM систем з інтеграцією 1С та телефонії. Досвід розробників підтверджено сертифікатами від вендора.
Пропоновані послуги
Показано 1 з 1 послугУсі 1626 послуг
Оптимізація ORM-запитів D7 1С-Бітрікс
Середня
~1-2 тижні
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851
  • 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
    585
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Розробка на базі 1С Підприємство для компанії МИРСАНБЕЛ
    751
  • image_crm_dolbimby_434_0.webp
    Розробка сайту на CRM Бітрікс24 для компанії DOLBIMBY
    657
  • image_crm_technotorgcomplex_453_0.webp
    Розробка на базі Бітрікс24 для компанії ТЕХНОТОРГКОМПЛЕКС
    989

Оптимізація 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.