Setting Up Bulk Product Deletion in 1C-Bitrix
Old collection discontinued — 1,200 SKUs need to be removed from the catalog. Or after a price list import, duplicates were found — 300 extra records. Deleting through the standard interface 20 at a time would take an hour. Plus, each deletion triggers a cascade of operations that easily crashes the site if done without understanding the architecture.
What Happens When Deleting a Product
CIBlockElement::Delete($id) removes:
- Record from
b_iblock_element - Properties from
b_iblock_element_property - Section bindings from
b_iblock_section_element - Prices from
b_catalog_price - Catalog data from
b_catalog_product - Barcodes from
b_catalog_product_barcode - Files via
CFile::Delete()— physically from disk and fromb_file - Element cache
Each deletion triggers OnBeforeIBlockElementDelete and OnAfterIBlockElementDelete events. If modules like CRM, search, or other systems subscribe to these — each deletion is processed by these handlers.
Bulk Deletion Without Server Overload
Deleting 1,000 elements in one request creates a load spike. Correct approach — batch deletion with pauses:
$toDelete = [1001, 1002, /* ... 1000 ids */];
$batchSize = 20;
foreach (array_chunk($toDelete, $batchSize) as $batch) {
foreach ($batch as $id) {
\CIBlockElement::Delete($id);
}
sleep(1); // Pause between batches
}
For very large volumes (10,000+), operation runs as an agent with progress saving:
// Agent saves remaining IDs in b_option and restarts itself
$remaining = unserialize(\Bitrix\Main\Config\Option::get('mymodule', 'delete_queue'));
$batch = array_splice($remaining, 0, 20);
foreach ($batch as $id) {
\CIBlockElement::Delete($id);
}
\Bitrix\Main\Config\Option::set('mymodule', 'delete_queue', serialize($remaining));
Deactivation Instead of Deletion
Physical deletion is irreversible. For products that might return (seasonal, temporarily removed), deactivation is better — ACTIVE = 'N'. Deletion is justified only for duplicates or erroneously created records.
Before deleting, check if there are active orders for the product. If the product exists in b_sale_basket or b_sale_order_basket — deletion breaks historical integrity:
SELECT COUNT(*)
FROM b_sale_order_basket sob
WHERE sob.PRODUCT_ID IN (1001, 1002, 1003)
AND sob.ORDER_ID IN (
SELECT ID FROM b_sale_order WHERE STATUS_ID NOT IN ('F', 'C')
);
If the query returns non-zero — don't delete these products, only deactivate.
Deleting Trade Offers (SKU)
For products with trade offers (SEO-type S) delete all offers (b_iblock_element from the offers iblock) first, then the main product. Order matters: when deleting a product, Bitrix doesn't automatically delete linked offers — they remain orphaned.
// Get product offers
$offers = \CCatalogSKU::getOffersList(
[$productId],
$catalogIblockId,
[],
['ID'],
[]
);
if (!empty($offers[$productId])) {
foreach ($offers[$productId] as $offer) {
\CIBlockElement::Delete($offer['ID']);
}
}
// Delete main product
\CIBlockElement::Delete($productId);
File Cleanup
After bulk deletion via direct SQL (if someone bypassed CIBlockElement::Delete()), files remain in /upload/. To clean them, find b_file IDs not referenced from b_iblock_element_property:
SELECT f.ID, f.SUBDIR, f.FILE_NAME
FROM b_file f
LEFT JOIN b_iblock_element_property p ON p.VALUE = CAST(f.ID AS CHAR)
WHERE p.ID IS NULL
AND f.MODULE_ID = 'iblock'
AND f.DATE_CREATE < NOW() - INTERVAL '7 days';
Files from results safely delete via \CFile::Delete($fileId).







