Setting Up Bulk Product Movement Between Sections in 1C-Bitrix
Catalog restructuring: the "Smartphones" section splits into "Apple Smartphones", "Samsung Smartphones", "Xiaomi Smartphones". 800 products need to be distributed to new sections. Or the supplier changed category structure, and 400 items need moving from one section to another.
Section Structure and Product Links
Iblock sections are stored in b_iblock_section. Product-to-section binding is the IBLOCK_SECTION_ID field in b_iblock_element (primary section). Additional binding to multiple sections is the b_iblock_section_element table: IBLOCK_ELEMENT_ID, IBLOCK_SECTION_ID, ADDITIONAL_PROPERTY_ID.
When moving a product, update both:
-
IBLOCK_SECTION_IDinb_iblock_element— primary section. - Record in
b_iblock_section_element— for proper filter functionality.
Moving via CIBlockElement::Update
Standard update method with section change:
\CIBlockElement::Update($elementId, false, [
'IBLOCK_SECTION_ID' => $newSectionId,
]);
After Update(), Bitrix automatically updates b_iblock_section_element. But the method is slow for bulk operations — each call goes through events, cache, and permissions.
Direct SQL update for speed:
global $DB;
$elementIds = implode(',', array_map('intval', $productIds));
$newSection = (int)$newSectionId;
$DB->Query("
UPDATE b_iblock_element
SET IBLOCK_SECTION_ID = {$newSection}
WHERE ID IN ({$elementIds})
");
$DB->Query("
DELETE FROM b_iblock_section_element
WHERE IBLOCK_ELEMENT_ID IN ({$elementIds})
AND ADDITIONAL_PROPERTY_ID IS NULL
");
foreach ($productIds as $id) {
$DB->Query("
INSERT INTO b_iblock_section_element
(IBLOCK_ELEMENT_ID, IBLOCK_SECTION_ID)
VALUES
({$id}, {$newSection})
");
}
Direct SQL works 50-100 times faster than CIBlockElement::Update() for bulk operations, but requires manual cache clearing.
Multi-section Binding
If a product appears in multiple sections simultaneously (e.g., "Smartphones" and "Sales"), b_iblock_section_element contains multiple rows for one IBLOCK_ELEMENT_ID:
// Delete old section bindings
\Bitrix\Iblock\SectionElementTable::deleteByFilter([
'IBLOCK_ELEMENT_ID' => $elementId,
'ADDITIONAL_PROPERTY_ID' => false,
]);
// Add new ones
foreach ($sectionIds as $secId) {
\Bitrix\Iblock\SectionElementTable::add([
'IBLOCK_ELEMENT_ID' => $elementId,
'IBLOCK_SECTION_ID' => $secId,
]);
}
The primary section (IBLOCK_SECTION_ID in b_iblock_element) remains one — the one considered "main" for breadcrumbs and URLs.
Cache Clearing After Moving
After bulk moving, component cache doesn't expire immediately. Force clearing:
\Bitrix\Main\Application::getInstance()->getTaggedCache()->clearByTag('iblock_id_' . $iblockId);
// For specific sections
foreach (array_unique(array_merge($oldSectionIds, [$newSectionId])) as $secId) {
\Bitrix\Main\Application::getInstance()->getTaggedCache()
->clearByTag('iblock_section_' . $secId);
}
Conditional Moving
To automatically distribute products to sections based on properties — for example, by brand:
$brandSectionMap = [
'Apple' => 125,
'Samsung' => 126,
'Xiaomi' => 127,
];
$res = \CIBlockElement::GetList(
[],
['IBLOCK_ID' => $iblockId, 'IBLOCK_SECTION_ID' => $sourceSection],
false,
false,
['ID', 'PROPERTY_BRAND']
);
while ($item = $res->GetNext()) {
$brand = $item['PROPERTY_BRAND_VALUE'];
$targetId = $brandSectionMap[$brand] ?? null;
if ($targetId) {
\CIBlockElement::Update($item['ID'], false, ['IBLOCK_SECTION_ID' => $targetId]);
}
}
For large volumes, this script runs as a Bitrix agent in batches of 100-200 items with progress saved in b_option.







