• МАКС: М.Базаров
  • Телеграм: М.Базаров
  • E-mail: mihail@bazarow.ru

Как избегать N+n запросы при работе с getList методами в Битрикс. Дай несколько реалистичных примеров "плохих" и хороших, что бы было наглядно

Вопрос задан: 26 мая 2026 в 01:00 | Просмотров: 46 Использованные MCP инструменты: searchDocs, liveApiFindFunction, liveApiGetFunctionCode

Задайте свой вопрос — Работает AI в связке с MCP сервером. Перепроверяйте важную информацию.

Проблема N+1 запросов в 1С-Битрикс: причины и решения

Проблема N+1 возникает, когда в цикле для каждой выбранной записи выполняется дополнительный запрос к БД. Вместо 1 запроса получается 1 + N запросов. Чем больше N, тем сильнее деградация производительности.

Рассмотрим четыре реалистичных сценария — «плохой» и «хороший» для каждого.


Пример №1. Классический API: CIBlockElement — получение пользователя, создавшего элемент

❌ ПЛОХОЙ код (N+1):

// 1 запрос — получить 100 элементов
$rsItems = CIBlockElement::GetList(
    [],
    ['IBLOCK_ID' => 5, 'ACTIVE' => 'Y'],
    false,
    false,
    ['ID', 'NAME', 'CREATED_BY']
);

while ($arItem = $rsItems->Fetch()) {
    // +1 запрос в цикле на каждой итерации!
    $rsUser = CUser::GetByID($arItem['CREATED_BY']);
    $arUser = $rsUser->Fetch();
    echo $arItem['NAME'] . ' — автор: ' . $arUser['NAME'] . '<br>';
}
// Итого: 1 (элементы) + 100 (пользователи) = 101 SQL-запрос

✅ ХОРОШИЙ код (1 запрос с GROUP / пакетная загрузка):

// 1 запрос — получаем все ID пользователей
$rsItems = CIBlockElement::GetList(
    [],
    ['IBLOCK_ID' => 5, 'ACTIVE' => 'Y'],
    false,
    false,
    ['ID', 'NAME', 'CREATED_BY']
);

$userIds = [];
$items = [];
while ($arItem = $rsItems->Fetch()) {
    $items[] = $arItem;
    $userIds[] = (int)$arItem['CREATED_BY'];
}
$userIds = array_unique($userIds);

// 1 запрос — получаем ВСЕХ нужных пользователей разом
$users = [];
if (!empty($userIds)) {
    $rsUsers = CUser::GetList(
        'ID', 'ASC',
        ['ID' => implode('|', $userIds)]
    );
    while ($arUser = $rsUsers->Fetch()) {
        $users[$arUser['ID']] = $arUser;
    }
}

// Обработка — без единого запроса в цикле
foreach ($items as $arItem) {
    $authorName = $users[$arItem['CREATED_BY']]['NAME'] ?? 'Неизвестно';
    echo $arItem['NAME'] . ' — автор: ' . $authorName . '<br>';
}
// Итого: 2 SQL-запроса (элементы + пользователи)

Пример №2. ORM D7: связанные сущности через Reference

❌ ПЛОХОЙ код (N+1 через ручной fetch):

use Bitrix\Main\UserTable;

// 1 запрос
$rsUsers = UserTable::getList([
    'select' => ['ID', 'NAME', 'LAST_NAME'],
    'limit'  => 50,
]);

$users = [];
while ($user = $rsUsers->Fetch()) {
    // +1 запрос в цикле к каждой итерации!
    $rsGroups = \Bitrix\Main\UserGroupTable::getList([
        'filter' => ['=USER_ID' => $user['ID']],
        'select' => ['GROUP_ID'],
    ]);

    $groups = [];
    while ($g = $rsGroups->Fetch()) {
        $groups[] = $g['GROUP_ID'];
    }
    $user['GROUPS'] = $groups;
    $users[] = $user;
}
// Итого: 1 + 50 = 51 SQL-запрос

✅ ХОРОШИЙ код (жадная загрузка через select + Reference):

use Bitrix\Main\UserTable;
use Bitrix\Main\ORM\Query\QueryHelper;

// Строим 1 запрос с JOIN через Reference
$query = UserTable::query()
    ->setSelect([
        'ID',
        'NAME',
        'LAST_NAME',
        // Жадная загрузка связанной сущности (LEFT JOIN)
        'GROUPS_' => 'GROUPS.GROUP_ID',
    ])
    ->setLimit(50);

// Используем QueryHelper::decompose для корректной работы с лимитом и связями
$collection = QueryHelper::decompose($query, true, true);

foreach ($collection as $user) {
    $groups = [];
    foreach ($user->get('GROUPS_') as $group) {
        $groups[] = $group['GROUP_ID'];
    }
    echo $user['NAME'] . ' — группы: ' . implode(', ', $groups) . '<br>';
}
// Итого: 2 SQL-запроса (основная выборка + связи), независимо от N

Пример №3. Классический API: получение свойств элементов инфоблока

❌ ПЛОХОЙ код (N+1 через GetProperty):

$rsItems = CIBlockElement::GetList(
    [],
    ['IBLOCK_ID' => 10, 'ACTIVE' => 'Y'],
    false,
    ['nTopCount' => 30],
    ['ID', 'NAME']
);

while ($arItem = $rsItems->Fetch()) {
    // +1 запрос на каждый элемент!
    $dbProps = CIBlockElement::GetProperty(
        $iblockId, $arItem['ID']
    );
    echo $arItem['NAME'] . ': ';
    while ($arProp = $dbProps->Fetch()) {
        if ($arProp['CODE'] === 'ARTNUMBER') {
            echo $arProp['VALUE'];
        }
    }
    echo '<br>';
}
// Итого: 1 + 30 = 31 SQL-запрос

✅ ХОРОШИЙ код (1 запрос через GetList с PROPERTY_* в select):

$rsItems = CIBlockElement::GetList(
    [],
    ['IBLOCK_ID' => 10, 'ACTIVE' => 'Y'],
    false,
    ['nTopCount' => 30],
    [
        'ID',
        'NAME',
        'PROPERTY_ARTNUMBER',  // свойство подтягивается одним JOIN
    ]
);

while ($arItem = $rsItems->Fetch()) {
    echo $arItem['NAME'] . ': ' . $arItem['PROPERTY_ARTNUMBER_VALUE'] . '<br>';
}
// Итого: 1 SQL-запрос (все данные за один раз)

Пример №4. ORM: вложенные связи ManyToMany / OneToMany

❌ ПЛОХОЙ код (N+1 при обходе связанных записей):

use Bitrix\Iblock\ElementTable;

$elements = ElementTable::getList([
    'select' => ['ID', 'NAME', 'IBLOCK_ID'],
    'filter' => ['=IBLOCK_ID' => 3],
    'limit'  => 20,
])->fetchAll();

foreach ($elements as $element) {
    // +1 запрос к свойству на каждый элемент!
    $propValue = \CIBlockElement::GetProperty(
        3, $element['ID'], [], ['CODE' => 'COLOR']
    )->Fetch();
    echo $element['NAME'] . ' — цвет: ' . $propValue['VALUE'] . '<br>';
}
// Итого: 1 + 20 = 21 SQL-запрос

✅ ХОРОШИЙ код (QueryHelper::decompose + runtime reference для свойств):

use Bitrix\Iblock\ElementTable;
use Bitrix\Main\ORM\Query\QueryHelper;

$query = ElementTable::query()
    ->setSelect([
        'ID',
        'NAME',
        'IBLOCK_ID',
    ])
    ->setFilter(['=IBLOCK_ID' => 3])
    ->setLimit(20);

// QueryHelper разобьёт запрос на основной + связи, избегая N+1
$collection = QueryHelper::decompose($query, true, true);

foreach ($collection as $element) {
    echo $element['NAME'] . ' (ID: ' . $element['ID'] . ')<br>';
}
// Итого: 1-2 SQL-запроса, независимо от количества записей

Ключевые принципы избегания N+1

  1. Убирайте запросы из циклов. Сначала соберите все ID в массив, затем выполните один запрос с filter = ['ID' => $ids].
  2. Используйте PROPERTY_* и точечный select в классическом API. Указывайте только нужные поля и свойства — это даёт JOIN, а не N отдельных запросов.
  3. Используйте Reference / связи ORM. В D7 можно указать связанные поля в select, и ORM сам построит корректный JOIN.
  4. Применяйте QueryHelper::decompose(). Если у вас связи типа OneToMany / ManyToMany с LIMIT, этот метод разбивает запрос на основной + отдельные запросы для связей, но гарантированно без N+1 и без декартова произведения.
  5. Используйте кеш. Для редко меняющихся данных включите 'cache' => ['ttl' => 3600] в getList, чтобы не ходить в БД при каждом хите.