Товары и CIBlockElement::GetList
CIBlockElement::GetList
Как вы знаете, метод CIBlockElement::GetList модуля Информационные блоки может работать с данными товара (при наличии модуля Торговый каталог). Это подробно описано в документации и активно используется как в публичных компонентах, так и на административных страницах и скриптах. Однако архитектурные особенности реализации, равно как и неправильное использование этих возможностей, приводят к резкому падению производительности.
Давайте сделаем разные вызовы CIBlockElement::GetList и посмотрим, какие запросы будут выполнены в итоге.
- Сначала сделаем простую выборку товаров из инфоблока с ID = 2:
$iterator = \CIBlockElement::GetList(
array(),
array('IBLOCK_ID' => 2),
false,
false,
array('ID', 'NAME', 'IBLOCK_ID')
);
Запрос, который пойдет в базу:
SELECT BE.ID as ID,BE.NAME as NAME,BE.IBLOCK_ID as IBLOCK_ID
FROM b_iblock B
INNER JOIN b_lang L ON B.LID=L.LID
INNER JOIN b_iblock_element BE ON BE.IBLOCK_ID = B.ID
WHERE 1=1
AND (
((((BE.IBLOCK_ID = '2'))))
)
AND (((BE.WF_STATUS_ID=1 AND BE.WF_PARENT_ELEMENT_ID IS NULL)))
$iterator = \CIBlockElement::GetList(
array(),
array('IBLOCK_ID' => 2, 'CATALOG_AVAILABLE' => 'Y'),
false,
false,
array('ID', 'NAME', 'IBLOCK_ID')
);
Это приводит к запросу:
SELECT BE.ID as ID,BE.NAME as NAME,BE.IBLOCK_ID as IBLOCK_ID ,CAT_PR.QUANTITY as CATALOG_QUANTITY,
IF (CAT_PR.QUANTITY_TRACE = 'D', 'Y', CAT_PR.QUANTITY_TRACE) as CATALOG_QUANTITY_TRACE,
CAT_PR.QUANTITY_TRACE as CATALOG_QUANTITY_TRACE_ORIG, CAT_PR.WEIGHT as CATALOG_WEIGHT,
CAT_PR.VAT_ID as CATALOG_VAT_ID, CAT_PR.VAT_INCLUDED as CATALOG_VAT_INCLUDED,
IF (CAT_PR.CAN_BUY_ZERO = 'D', 'N', CAT_PR.CAN_BUY_ZERO) as CATALOG_CAN_BUY_ZERO,
CAT_PR.CAN_BUY_ZERO as CATALOG_CAN_BUY_ZERO_ORIG,
CAT_PR.PURCHASING_PRICE as CATALOG_PURCHASING_PRICE, CAT_PR.PURCHASING_CURRENCY as CATALOG_PURCHASING_CURRENCY,
CAT_PR.QUANTITY_RESERVED as CATALOG_QUANTITY_RESERVED,
IF (CAT_PR.SUBSCRIBE = 'D', 'Y', CAT_PR.SUBSCRIBE) as CATALOG_SUBSCRIBE, CAT_PR.SUBSCRIBE as CATALOG_SUBSCRIBE_ORIG,
CAT_PR.WIDTH as CATALOG_WIDTH, CAT_PR.LENGTH as CATALOG_LENGTH, CAT_PR.HEIGHT as CATALOG_HEIGHT,
CAT_PR.MEASURE as CATALOG_MEASURE, CAT_PR.TYPE as CATALOG_TYPE, CAT_PR.AVAILABLE as CATALOG_AVAILABLE,
CAT_PR.BUNDLE as CATALOG_BUNDLE, CAT_PR.PRICE_TYPE as CATALOG_PRICE_TYPE,
CAT_PR.RECUR_SCHEME_LENGTH as CATALOG_RECUR_SCHEME_LENGTH, CAT_PR.RECUR_SCHEME_TYPE as CATALOG_RECUR_SCHEME_TYPE,
CAT_PR.TRIAL_PRICE_ID as CATALOG_TRIAL_PRICE_ID, CAT_PR.WITHOUT_ORDER as CATALOG_WITHOUT_ORDER,
CAT_PR.SELECT_BEST_PRICE as CATALOG_SELECT_BEST_PRICE,
IF (CAT_PR.NEGATIVE_AMOUNT_TRACE = 'D', 'N', CAT_PR.NEGATIVE_AMOUNT_TRACE) as CATALOG_NEGATIVE_AMOUNT_TRACE,
CAT_PR.NEGATIVE_AMOUNT_TRACE as CATALOG_NEGATIVE_AMOUNT_TRACE_ORIG, CAT_VAT.RATE as CATALOG_VAT
FROM b_iblock B
INNER JOIN b_lang L ON B.LID=L.LID
INNER JOIN b_iblock_element BE ON BE.IBLOCK_ID = B.ID
left join b_catalog_product as CAT_PR on (CAT_PR.ID = BE.ID)
left join b_catalog_iblock as CAT_IB on ((CAT_PR.VAT_ID IS NULL or CAT_PR.VAT_ID = 0) and CAT_IB.IBLOCK_ID = BE.IBLOCK_ID)
left join b_catalog_vat as CAT_VAT on
(CAT_VAT.ID = IF((CAT_PR.VAT_ID IS NULL OR CAT_PR.VAT_ID = 0), CAT_IB.VAT_ID, CAT_PR.VAT_ID))
WHERE 1=1
AND (
((((BE.IBLOCK_ID = '2'))))
AND ((((CAT_PR.AVAILABLE='Y'))))
)
AND (((BE.WF_STATUS_ID=1 AND BE.WF_PARENT_ELEMENT_ID IS NULL)))
Таким образом, любое обращение к данным товара (фильтрация, сортировка, выборка одного из полей) приводит к join трех таблиц и выборке всех полей товара. 3. Уберем фильтрацию, но будем выбирать цену одного из типов (ID типа цены - 1):
$iterator = \CIBlockElement::GetList(
array(),
array('IBLOCK_ID' => 2),
false,
false,
array('ID', 'NAME', 'IBLOCK_ID', 'CATALOG_CATALOG_GROUP_ID_1')
);
Видим, что стало еще хуже:
SELECT BE.ID as ID,BE.NAME as NAME,BE.IBLOCK_ID as IBLOCK_ID,
CAT_P1.CATALOG_GROUP_ID as CATALOG_GROUP_ID_1, CAT_P1.ID as CATALOG_PRICE_ID_1,
CAT_P1.PRICE as CATALOG_PRICE_1, CAT_P1.CURRENCY as CATALOG_CURRENCY_1,
CAT_P1.QUANTITY_FROM as CATALOG_QUANTITY_FROM_1, CAT_P1.QUANTITY_TO as CATALOG_QUANTITY_TO_1,
CAT_P1.EXTRA_ID as CATALOG_EXTRA_ID_1,
'Базовая цена' as CATALOG_GROUP_NAME_1, 'Y' as CATALOG_CAN_ACCESS_1, 'Y' as CATALOG_CAN_BUY_1,
CAT_PR.QUANTITY as CATALOG_QUANTITY, IF (CAT_PR.QUANTITY_TRACE = 'D', 'Y', CAT_PR.QUANTITY_TRACE) as CATALOG_QUANTITY_TRACE,
CAT_PR.QUANTITY_TRACE as CATALOG_QUANTITY_TRACE_ORIG, CAT_PR.WEIGHT as CATALOG_WEIGHT,
CAT_PR.VAT_ID as CATALOG_VAT_ID, CAT_PR.VAT_INCLUDED as CATALOG_VAT_INCLUDED,
IF (CAT_PR.CAN_BUY_ZERO = 'D', 'N', CAT_PR.CAN_BUY_ZERO) as CATALOG_CAN_BUY_ZERO,
CAT_PR.CAN_BUY_ZERO as CATALOG_CAN_BUY_ZERO_ORIG, CAT_PR.PURCHASING_PRICE as CATALOG_PURCHASING_PRICE,
CAT_PR.PURCHASING_CURRENCY as CATALOG_PURCHASING_CURRENCY, CAT_PR.QUANTITY_RESERVED as CATALOG_QUANTITY_RESERVED,
IF (CAT_PR.SUBSCRIBE = 'D', 'Y', CAT_PR.SUBSCRIBE) as CATALOG_SUBSCRIBE, CAT_PR.SUBSCRIBE as CATALOG_SUBSCRIBE_ORIG,
CAT_PR.WIDTH as CATALOG_WIDTH, CAT_PR.LENGTH as CATALOG_LENGTH, CAT_PR.HEIGHT as CATALOG_HEIGHT,
CAT_PR.MEASURE as CATALOG_MEASURE, CAT_PR.TYPE as CATALOG_TYPE, CAT_PR.AVAILABLE as CATALOG_AVAILABLE,
CAT_PR.BUNDLE as CATALOG_BUNDLE, CAT_PR.PRICE_TYPE as CATALOG_PRICE_TYPE,
CAT_PR.RECUR_SCHEME_LENGTH as CATALOG_RECUR_SCHEME_LENGTH, CAT_PR.RECUR_SCHEME_TYPE as CATALOG_RECUR_SCHEME_TYPE,
CAT_PR.TRIAL_PRICE_ID as CATALOG_TRIAL_PRICE_ID, CAT_PR.WITHOUT_ORDER as CATALOG_WITHOUT_ORDER,
CAT_PR.SELECT_BEST_PRICE as CATALOG_SELECT_BEST_PRICE,
IF (CAT_PR.NEGATIVE_AMOUNT_TRACE = 'D', 'N', CAT_PR.NEGATIVE_AMOUNT_TRACE) as CATALOG_NEGATIVE_AMOUNT_TRACE,
CAT_PR.NEGATIVE_AMOUNT_TRACE as CATALOG_NEGATIVE_AMOUNT_TRACE_ORIG, CAT_VAT.RATE as CATALOG_VAT
FR OM b_iblock B
INNER JOIN b_lang L ON B.LID=L.LID
INNER JOIN b_iblock_element BE ON BE.IBLOCK_ID = B.ID
left join b_catalog_price as CAT_P1 on (CAT_P1.PRODUCT_ID = BE.ID and CAT_P1.CATALOG_GROUP_ID = 1)
left join b_catalog_product as CAT_PR on (CAT_PR.ID = BE.ID)
left join b_catalog_iblock as CAT_IB on ((CAT_PR.VAT_ID IS NULL or CAT_PR.VAT_ID = 0) and CAT_IB.IBLOCK_ID = BE.IBLOCK_ID)
left join b_catalog_vat as CAT_VAT on (CAT_VAT.ID = IF((CAT_PR.VAT_ID IS NULL OR CAT_PR.VAT_ID = 0), CAT_IB.VAT_ID, CAT_PR.VAT_ID))
WH ERE 1=1
AND (
((((BE.IBLOCK_ID = '2'))))
)
AND (((BE.WF_STATUS_ID=1 AND BE.WF_PARENT_ELEMENT_ID IS NULL)))
Если мы попробуем выбрать еще и данные по складам, то ситуация будет аналогичной.
Подведем промежуточные итоги:
- Выборки цен, количества на складах в CIBlockElement::GetList необходимо избегать категорически (особенно при наличии сортировки и неважно по каким полям). Эти данные нужно получать отдельными вызовами АПИ. К слову сказать, в штатных компонентах это было сделано еще в версии 17.0.
- Остается фильтрация и сортировка по полям товара, ценам, складам. Обращение к полям товара по ключам CATALOG_ дает дополнительный join трех таблиц. Обращение к N типам цен или складов - join N+3 таблиц. Мало того, что время запроса увеличивается, так еще можно получить ошибку MySql "Too many tables; MySQL can only use 61 tables in a join".
До выпуска связки обновлений catalog 18.6.100 + iblock 18.6.200 все вышеописанное относится, в том числе, к штатным компонентам и административным спискам модуля Информационные блоки (особенно в режиме совместного просмотра элементов и разделов, смотрите примечание ниже). После выхода данных обновлений в CIBlockElement::GetList доступны новые возможности работы с товарами.
Примечание: ранее для всех больших инфоблоков мы рекомендовали не использовать режим совместного просмотра по причине большого расхода памяти. С выходом версии 18.5.5 модуля Информационные блоки эта проблема решена - инфоблок со 100 тысячами элементов спокойно выводится в административном разделе в этом режиме (расход памяти снижен примерно в 20 раз).
С версий catalog 18.6.100 + iblock 18.6.200
С версии iblock 18.6.200 изменяются ключи метода. По всем ключам возможна фильтрация, сортировка, выборка.
| Поля товара |
|---|
| С версии catalog 20.0.200 добавились поля товара |
|---|
Теперь вызов метода с фильтрацией по доступности выглядит так:
$iterator = \CIBlockElement::GetList(
array(),
array('IBLOCK_ID' => 2, '=AVAILABLE' => 'Y'),
false,
false,
array('ID', 'NAME', 'IBLOCK_ID')
);
а в запросе только то, что просили, и join только один:
SELECT BE.ID as ID,BE.NAME as NAME,BE.IBLOCK_ID as IBLOCK_ID
FROM b_iblock B
INNER JOIN b_lang L ON B.LID=L.LID
INNER JOIN b_iblock_element BE ON BE.IBLOCK_ID = B.ID
left join b_catalog_product as PRD on (PRD.ID = BE.ID)
WHERE 1=1
AND (
((((BE.IBLOCK_ID = '2'))))
AND ((((PRD.AVAILABLE='Y'))))
)
AND (((BE.WF_STATUS_ID=1 AND BE.WF_PARENT_ELEMENT_ID IS NULL)))
Сделаем выборку размеров и веса доступных простых товаров:
$iterator = \CIBlockElement::GetList(
array(),
array('IBLOCK_ID' => 2, '=AVAILABLE' => 'Y', '=TYPE' => 1),
false,
false,
array('ID', 'NAME', 'IBLOCK_ID', 'WEIGHT', 'WIDTH', 'HEIGHT', 'LENGTH')
);
Запрос получается следующим:
SELECT BE.ID as ID,BE.NAME as NAME,BE.IBLOCK_ID as IBLOCK_ID ,
PRD.WEIGHT as WEIGHT, PRD.WIDTH as WIDTH, PRD.HEIGHT as HEIGHT, PRD.LENGTH as LENGTH
FROM b_iblock B
INNER JOIN b_lang L ON B.LID=L.LID
INNER JOIN b_iblock_element BE ON BE.IBLOCK_ID = B.ID
left join b_catalog_product as PRD on (PRD.ID = BE.ID)
WHERE 1=1
AND (
((((BE.IBLOCK_ID = '2'))))
AND ((((PRD.AVAILABLE='Y'))))
AND ((((PRD.TYPE = '1'))))
)
AND (((BE.WF_STATUS_ID=1 AND BE.WF_PARENT_ELEMENT_ID IS NULL)))
| Поля цен (без привязки к конкретному типу цены) |
|---|
| Поля цен (с указанием типа цены) |
|---|
| Поля складов |
|---|
Примеры выборок
Фильтрация по цене любого типа:
$iterator = \CIBlockElement::GetList(
array(),
array('IBLOCK_ID' => 2, '=AVAILABLE' => 'Y', '=TYPE' => 1, '>PRICE' => 500),
false,
false,
array('ID', 'NAME', 'IBLOCK_ID', 'WEIGHT', 'WIDTH', 'HEIGHT', 'LENGTH')
);
Фильтрация для типа цены с кодом 1 (обычно это базовая цена):
$iterator = \CIBlockElement::GetList(
array(),
array('IBLOCK_ID' => 2, '=AVAILABLE' => 'Y', '=TYPE' => 1, '>PRICE' => 500, '=PRICE_TYPE' => 1),
false,
false,
array('ID', 'NAME', 'IBLOCK_ID', 'WEIGHT', 'WIDTH', 'HEIGHT', 'LENGTH')
);
Или в таком варианте:
$iterator = \CIBlockElement::GetList(
array(),
array('IBLOCK_ID' => 2, '=AVAILABLE' => 'Y', '=TYPE' => 1, '>PRICE_1' => 500),
false,
false,
array('ID', 'NAME', 'IBLOCK_ID', 'WEIGHT', 'WIDTH', 'HEIGHT', 'LENGTH')
);
Теперь можно делать фильтры, ранее недоступные. Выбрать все товары, имеющие цены любого типа от 500 до 1000:
$iterator = \CIBlockElement::GetList(
array(),
array('IBLOCK_ID' => 2, '=AVAILABLE' => 'Y', '=TYPE' => 1, '>=PRICE' => 500, '<=PRICE' => 1000),
false,
false,
array('ID', 'NAME', 'IBLOCK_ID')
);
Или только цены типов с кодом 1,4,5:
$iterator = \CIBlockElement::GetList(
array(),
array('IBLOCK_ID' => 2, '=AVAILABLE' => 'Y', '=TYPE' => 1, '>=PRICE' => 500, '<=PRICE' => 1000, '@PRICE_TYPE' => [1,4,5]),
false,
false,
array('ID', 'NAME', 'IBLOCK_ID')
);
Выбрать все товары с ценами в любой валюте, эквивалентными диапазону от 100 до 200 USD:
$iterator = \CIBlockElement::GetList(
array(),
array('IBLOCK_ID' => 2, '=AVAILABLE' => 'Y', '=TYPE' => 1, '>=PRICE' => 100, '<=PRICE' => 200, 'CURRENCY_FOR_SCALE' => 'USD'),
false,
false,
array('ID', 'NAME', 'IBLOCK_ID')
);
Выбрать товары, которых на любом складе не больше 3:
$iterator = \CIBlockElement::GetList(
array(),
array('IBLOCK_ID' => 2, '<=STORE_AMOUNT' => 3),
false,
false,
array('ID', 'NAME', 'IBLOCK_ID')
);
Выбрать товары, которых на 17-м складе от 5 до 7:
$iterator = \CIBlockElement::GetList(
array(),
array('IBLOCK_ID' => 2, '<=STORE_AMOUNT' => 7, '>=STORE_AMOUNT' => 5, 'STORE_NUMBER' => 17),
false,
false,
array('ID', 'NAME', 'IBLOCK_ID')
);
Либо в таком виде:
$iterator = \CIBlockElement::GetList(
array(),
array('IBLOCK_ID' => 2, '<=STORE_AMOUNT_17' => 7, '>=STORE_AMOUNT_17' => 5),
false,
false,
array('ID', 'NAME', 'IBLOCK_ID')
);
Фильтрация товаров по наличию на определенных складах:
if(!empty($arParams['STORES'])){
$GLOBALS[$arParams['FILTER_NAME']]['@STORE_NUMBER'] => $arParams['STORES'];
$GLOBALS[$arParams['FILTER_NAME']]['>STORE_AMOUNT'] = 0;
}
Другой пример фильтрации товаров по наличию на определенных складах:
if(!empty($arParams['STORES'])){
$storesFilter = [
'LOGIC'=>'OR'
];
foreach ($arParams['STORES'] as $store_id){
$storesFilter[] = ['STORE_NUMBER' => intval($store_id),'>STORE_AMOUNT'=>0];
}
$GLOBALS[$arParams['FILTER_NAME']][] = $storesFilter;
}
| Заключение |
После установки обновлений catalog 18.6.100 + iblock 18.6.200 настоятельно рекомендуется перевести свои компоненты и скрипты на новые ключи. Увеличение производительности прямо пропорционально числу товаров в каталоге. Так, при тестах на разделе из 1,5 тысяч товаров прирост скорости выполнения составил порядка 30%. Штатные компоненты ( catalog.section
Компонент выводит список элементов раздела с указанным набором свойств.
Описание компонента «Элементы раздела» в пользовательской документации. , catalog.element
Компонент выводит детальную информацию по элементу каталога.
Описание компонента «Элемент каталога детально» в пользовательской документации. , catalog.top
Компонент выводит в таблице top элементов из всех разделов в соответствии с заданной сортировкой (используется как правило на главной странице сайта).
Описание компонента «top элементов каталога» в пользовательской документации. ), а также все компоненты наследники \Bitrix\Iblock\Component\Base переведены на новые фильтры.