Setting up bulk deletion of 1C-Bitrix products

Our company is engaged in the development, support and maintenance of Bitrix and Bitrix24 solutions of any complexity. From simple one-page sites to complex online stores, CRM systems with 1C and telephony integration. The experience of developers is confirmed by certificates from the vendor.
Our competencies:
Development stages
Latest works
  • image_website-b2b-advance_0.png
    B2B ADVANCE company website development
    1212
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Website development for FIXPER company
    815
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Development based on Bitrix, Bitrix24, 1C for the company Development of an Online Appointment Booking Widget for a Medical Center
    565
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Development based on 1C Enterprise for MIRSANBEL
    747
  • image_crm_dolbimby_434_0.webp
    Website development on CRM Bitrix24 for DOLBIMBY
    657
  • image_crm_technotorgcomplex_453_0.webp
    Development based on Bitrix24 for the company TECHNOTORGKOMPLEKS
    980

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 from b_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).