Задача - сформировать фид для выгрузки.
Дано:
кол-во товаров более 50 000
Товары выгружаются с пользовательскими свойствами
Результат эксперимента
Довольно частая задача - это формирование фидов, что подразумевает под собой выгрузку большого кол-ва элементов.
Битрикс часто ругают за то - что при простых операциях - происходит огромное ко-во запросов к БД, я видел на проектах , как для отображения простой страницы каталога, битрикс делал более 5000 запросов к БД. Именно из- за этого проекты на Битрикс иногда очень требовательны к железу на хостинге.
Современное ядро D7 позволяет значительно снизить нагрузку на БД и сильно ускоряет все процессы связанные с обращением к БД.
Долгое время на одном проекте работал скрипт по формированию фида для загрузки на сторонние сервисы. Скрипт работал, никаких сбоев не выдавал, и все всех устраивало, но вот пришло время вносить изменения и в него.
Первое что бросилось в глаза это то. как в этом скрипте происходила выборка товаров из каталога и добавление к ним пользовательских свойств
\Bitrix\Main\Diag\Debug::startTimeLabel('f_time1');
$obj_items=\CIBlockElement::GetList([] ,$filter, false, false, $select);
while ($objItem = $obj_items->GetNextElement()) {
$arItem=$objItem->GetFields();
$result[$arItem['ID']] = $arItem;
$result[$arItem['ID']]['PROPERTIES'] = $objItem->GetProperties();
}
\Bitrix\Main\Diag\Debug::endTimeLabel('f_time1');
$f_time=\Bitrix\Main\Diag\Debug::getTimeLabels();
$str_res= "Кол-во эл-в выборки Старое ядро: ".$obj_items->SelectedRowsCount().PHP_EOL;
$str_res.= "Время выполонения выборки Старое ядро: ".round($f_time['f_time1']['time'], 8, PHP_ROUND_HALF_UP).PHP_EOL;
echo $str_res;
Первое что пришло на ум это изменить добавление свойств к элементу
$obj_items=\CIBlockElement::GetList([] ,$filter, false, false, $select);
while ($arItem = $obj_items->fetch()) {
$result[$arItem['ID']] = $arItem;
$result[$arItem['ID']]['PROPERTIES'] = [];
$ids[] = $arItem['ID'];
}
$chunks = array_chunk($ids, 1000);
foreach ($chunks as $key => $chunk) {
\CIBlockElement::GetPropertyValuesArray(
$result,
2,
['ID' => $chunk],
['CODE' => $properties],
['GET_RAW_DATA' => 'Y']
);
}
\Bitrix\Main\Diag\Debug::endTimeLabel('f_time1');
$f_time=\Bitrix\Main\Diag\Debug::getTimeLabels();
$str_res= "Кол-во эл-в выборки Старое ядро: ".$obj_items->SelectedRowsCount().PHP_EOL;
$str_res.= "Время выполонения выборки Старое ядро: ".round($f_time['f_time1']['time'], 8, PHP_ROUND_HALF_UP).PHP_EOL;
echo $str_res;
Что дало уже приемлемый результат:
Но, обращаться к БД в цикле - это точно нехорошо.
Поэтому решил попробовать Переписать этот код с использованием возможностей ядра D7
//список свойств
//CODE свойств изменены
$properties = [
'PROP_1',
'PROP_2',
'PROP_3',
'PROP_4',
'PROP_5',
'PROP_6',
'PROP_7',
'PROP_8',
'PROP_9',
'PROP_10',
'PROP_11',
'PROP_12',
'PROP_13',
'PROP_14',
'PROP_15',
'PROP_16',
'PROP_17',
'PROP_18',
'PROP_19',
'PROP_20',
'PROP_21',
'PROP_22',
'PROP_23',
'PROP_24',
'PROP_25',
];
//ищем IBLOCK_ID по CODE
$CatalogiblockId = Helper::getIblockIdByCode("catalog");
foreach ( $properties as $property_code) {
//ID свойства по CODE
$PROP_ARTICLE_ID = Helper::getIblockPropIDByCode($property_code, $CatalogiblockId);
$props['PROPERTY_'.$PROP_ARTICLE_ID ]= ['data_type' => 'string'];
}
$props['IBLOCK_ELEMENT_ID']= ['data_type' => 'integer'];
$entityProps = Bitrix\Main\Entity\Base::compileEntity(
'PROPS',
$props,
[
'table_name' => sprintf('b_iblock_element_prop_s%s', $CatalogiblockId),
]
);
$select = [
'ID',
'IBLOCK_ID',
'NAME',
'SORT',
'IBLOCK_SECTION_ID',
'DETAIL_PICTURE',
'PROPS'
];
$result = \Bitrix\Iblock\ElementTable::getList([
'select' => $select,
'filter' => [
'IBLOCK_ID' => $CatalogiblockId,
],
'runtime' => [
'PROPS' => [
'data_type' => $entityProps->getDataClass(),
'reference' => [
'=this.ID' => 'ref.IBLOCK_ELEMENT_ID',
],
],
],
]);
\Bitrix\Main\Diag\Debug::endTimeLabel('f_time1');
$f_time=\Bitrix\Main\Diag\Debug::getTimeLabels();
echo "Кол-во эл-в выборки D7: ".$result->getSelectedRowsCount().PHP_EOL;
echo "Время выполонения выборки D7: ".round($f_time['f_time1']['time'], 4, PHP_ROUND_HALF_UP).PHP_EOL;
И результат который очень порадовал
Т.к. получить свойства элементов напрямую нельзя, то сначала создаем сущность $entityProps которая содержит поля из таблицы b_iblock_element_prop_s.IBLOCK_ID где наименование столбцов свойств являются PROPERTY_.ID (ID свойства, поэтому надо их сначала получить по CODE, или добавить посмотрев в свойствах элементов)
Затем мы делаем выборку присоединив сущность к выборке.
Данный пример показывает не только как ускорить выполнение выборки на больших массивах, но и может помочь существенно снизить нагрузку на БД даже в обычных каталогах. Где на первый взгляд время выборки не столь существенно. Ведь на выборке в 30 товаров из каталога разница во времени будет не существенной, и на скорость загрузки это будет влиять минимально.
Но если взять во внимание существенное снижение количества запросов к БД - это может существенно снизить нагрузку на БД. И проведя простую оптимизацию - вы добьетесь потрясающих результатов.
Итог:
Добились ускорения выполнения скрипта с 1333 сек до 0,552, т.е. почти в 700 раз.
Снижение количества запросов к БД с более 50 000. до ОДНОГО.
Комментарии (9)
Aleks_ja
10.05.2022 00:20+5[offtop]
А что это за жуткий микс $snake_case, $camelCase, $StudlyCase, а потом ещё $SNAKE_CASE_CAPS. В Битрикс так принято?
[/offtop]
iMedved2009
10.05.2022 02:29+7Никоим образом не хочу умолять заслуги автора, но каждый раз когда я вижу попытки сделать битрикс лучше мне почему то вспоминается анекдот:
Идёт девушка – видит - парень косит траву в противогазе:
- Ты что, с ума сошёл - зачем противогаз надел?
- Я комсомолец - не могу без трудностей...
- Кончай фигней страдать, пошли лучше переспим.
- Хорошо - но только в гамаке и стоя...
mixtyraa
11.05.2022 10:26+2D7 ORM в большинстве случаев всегда повышает производительность, потому что заставляет подумать разработчика о том, как и какие данные он получает из БД.
Пару узких мест, которые я вижу в вашем решении:
1) Как я понял, вы используете иб 2 версии, соответсвенно, на уровне бд есть две таблицы для значений свойств b_iblock_element_prop_s и b_iblock_element_prop_m , в первой хранятся свойства с одиночными значениям, в последней множественные значения свойства. Обдумывали решение для свойств с множественными значениями?
2) Helper::getIblockPropIDByCode($property_code, $CatalogiblockId); -- вижу как запрос в бд, тоже в цикле. В условиях отсутствия кеша - это вызывает проблему N+1
3) Bitrix\Main\Entity\Base::compileEntity -- я могу ошибаться, но метод достаточно прожорлив, для использования в скрипте в фоне, можно пренебречь, но для выполнения кода на хите, я бы подумал в преимуществе такого подхода. Рассматривали ли альтернативы? Почему отказались от обычного join? (при условии, что вы используете только одиночные свойства)
4) Я бы посмотрел в строну пагинации, потому что объем данных, думаю, будет у вас расти. Вероятно у вас есть фильтры для выборки, которые тоже оказывают влияние на производительность запроса.
Real_Fermer Автор
11.05.2022 21:18+1не знаю насколько это кошерно....
while ($arItem = $obItems->fetch()) { $result[$arItem['ID']] = getItemRes($arItem); } function getItemRes($item) { $res = []; foreach ($item as $index => $row) { if (!empty($row)) { $tmp = unserialize($row); if ($tmp != false) { if (!empty($tmp['VALUE'])) { $res[$index] = $tmp['VALUE']; } } else { $res[$index] = $row; } } } return $res; }
Это просто получить ID свойств по CODE. их в принципе можно и руками прописать. просто привык не пользоваться ID, т.к. на dev и на prod - ID могут различаться
Т.к. скрипт отрабатывает в фоне, альтернативу не рассматривал. Целей оптимизации этого скрипта добился. В качестве академического интереса, можно подумать
Опять же это уже больше исследовательский момент. Но так же можно будет попробовать. Можно и на чистом MySqQL написать.
Спасибо за содержательный комментарий
vooft
Заменить 50к запросов на один - это хорошо, конечно, но как оно в базе выглядит? Может, он там full table scan делает теперь на каждом запросе и под нагрузкой все ляжет.
m03r
И на промежуточный вариант тоже было бы интересно посмотреть.