Битрикс: очистка неиспользуемых свойств инфоблоков

SQL-запросы для поиска пустых свойств. Безопасное удаление, анализ заполненности, оптимизация базы данных.

Дмитрий Мещеряков
Дмитрий Мещеряков
📅 15 января 2020 г.📖 9 мин чтения

Инфоблок каталога, которому пять лет, — это археологический раскоп. Свойства TEST, TEST_2, NEW_PRICE_OLD, три варианта одного и того же поля от разных подрядчиков, и никто в команде не берётся сказать, какие из них ещё используются. Форма редактирования товара при этом прокручивается на два экрана.

Разберём, как найти реальных кандидатов на удаление и — что важнее — как не удалить лишнего. Сразу оговорю главное: «свойство пустое» и «свойство не используется» — разные утверждения, и вся сложность задачи именно в этом зазоре.

Почему в инфоблоке копятся лишние свойства

При активном развитии проекта накапливаются неиспользуемые свойства инфоблоков:

  • Свойства, которые перестали заполнять
  • Тестовые свойства, забытые после разработки
  • Устаревшие поля после редизайна

Это замедляет работу админки и увеличивает размер базы.

Сначала — про хранилище свойств

Прежде чем выполнять любой из запросов ниже, проверьте, как инфоблок хранит значения свойств. У Битрикса их два варианта:

  • Хранилище 1 (общее) — всё лежит в b_iblock_element_property;
  • Хранилище 2 (отдельные таблицы) — значения живут в b_iblock_element_prop_s{ID} (одиночные, колонка на свойство) и b_iblock_element_prop_m{ID} (множественные).
⚠️ Важно

Это не теоретическое замечание, а способ снести весь каталог. Если инфоблок переведён на второе хранилище, таблица b_iblock_element_property для него пуста целиком — и запрос «найди свойства без единого значения» честно вернёт вам все свойства инфоблока. Скрипт с флагом --execute после этого удалит их вместе с данными.

Проверить тип хранилища:

sql
SELECT ID, NAME, VERSION FROM b_iblock;  -- VERSION = 2 → отдельные таблицы

Для второго хранилища заполненность считается по колонкам PROPERTY_<ID> в b_iblock_element_prop_s{IBLOCK_ID}, а множественные — по b_iblock_element_prop_m{IBLOCK_ID}. Код в этой статье рассчитан на первое хранилище; для второго его нужно адаптировать, а до тех пор — не запускать.

SQL: поиск пустых свойств

Свойства без единого значения

sql
SELECT 
    p.ID,
    p.IBLOCK_ID,
    i.NAME AS IBLOCK_NAME,
    p.CODE,
    p.NAME,
    p.PROPERTY_TYPE,
    p.MULTIPLE
FROM b_iblock_property p
LEFT JOIN b_iblock i ON i.ID = p.IBLOCK_ID
LEFT JOIN b_iblock_element_property ep ON ep.IBLOCK_PROPERTY_ID = p.ID
WHERE ep.ID IS NULL
ORDER BY p.IBLOCK_ID, p.CODE;

Свойства с нулевой заполненностью

sql
SELECT 
    p.ID,
    p.IBLOCK_ID,
    i.NAME AS IBLOCK_NAME,
    p.CODE,
    p.NAME,
    (
        SELECT COUNT(DISTINCT e.ID)
        FROM b_iblock_element e
        WHERE e.IBLOCK_ID = p.IBLOCK_ID AND e.ACTIVE = 'Y'
    ) AS TOTAL_ELEMENTS,
    (
        SELECT COUNT(DISTINCT ep.IBLOCK_ELEMENT_ID)
        FROM b_iblock_element_property ep
        WHERE ep.IBLOCK_PROPERTY_ID = p.ID
          AND (ep.VALUE IS NOT NULL AND ep.VALUE != '')
    ) AS FILLED_ELEMENTS,
    ROUND(
        (
            SELECT COUNT(DISTINCT ep.IBLOCK_ELEMENT_ID)
            FROM b_iblock_element_property ep
            WHERE ep.IBLOCK_PROPERTY_ID = p.ID
              AND (ep.VALUE IS NOT NULL AND ep.VALUE != '')
        ) * 100 / NULLIF(
            (SELECT COUNT(DISTINCT e.ID)
             FROM b_iblock_element e
             WHERE e.IBLOCK_ID = p.IBLOCK_ID AND e.ACTIVE = 'Y'), 0
        ), 1
    ) AS FILL_PERCENT
FROM b_iblock_property p
LEFT JOIN b_iblock i ON i.ID = p.IBLOCK_ID
HAVING FILL_PERCENT = 0 OR FILL_PERCENT IS NULL
ORDER BY p.IBLOCK_ID, p.CODE;
💡 Совет

Расхождение в знаменателе. В запросе выше общее число элементов считается с условием ACTIVE = 'Y', а заполненные значения — без него. Для инфоблока, где половина элементов деактивирована, процент заполненности может получиться больше 100. Считайте обе части по одному и тому же множеству элементов — либо только по активным, либо по всем.

PHP-класс для анализа свойств

php
<?php

namespace Local\Iblock;

use Bitrix\Main\Loader;
use Bitrix\Main\Application;

class PropertyAnalyzer
{
    private $connection;

    public function __construct()
    {
        Loader::includeModule('iblock');
        $this->connection = Application::getConnection();
    }

    /**
     * Получить статистику по свойствам инфоблока
     */
    public function analyzeIblock(int $iblockId): array
    {
        $properties = [];
        
        // Общее количество активных элементов
        $totalElements = $this->getTotalElements($iblockId);

        // Получаем все свойства
        $propertyList = \CIBlockProperty::GetList(
            ['SORT' => 'ASC'],
            ['IBLOCK_ID' => $iblockId]
        );

        while ($property = $propertyList->Fetch()) {
            $filledCount = $this->getFilledCount($property['ID']);
            $fillPercent = $totalElements > 0 
                ? round($filledCount / $totalElements * 100, 1) 
                : 0;

            $properties[] = [
                'ID' => $property['ID'],
                'CODE' => $property['CODE'],
                'NAME' => $property['NAME'],
                'TYPE' => $property['PROPERTY_TYPE'],
                'MULTIPLE' => $property['MULTIPLE'],
                'TOTAL_ELEMENTS' => $totalElements,
                'FILLED_COUNT' => $filledCount,
                'FILL_PERCENT' => $fillPercent,
                'IS_EMPTY' => $filledCount === 0,
                'IS_LOW_USAGE' => $fillPercent < 5 && $fillPercent > 0,
            ];
        }

        return $properties;
    }

    /**
     * Получить только пустые свойства
     */
    public function getEmptyProperties(int $iblockId): array
    {
        $all = $this->analyzeIblock($iblockId);
        return array_filter($all, fn($p) => $p['IS_EMPTY']);
    }

    /**
     * Получить свойства с низкой, но ненулевой заполненностью
     * (пустые сюда не попадают — для них есть getEmptyProperties)
     */
    public function getLowUsageProperties(int $iblockId, float $threshold = 5.0): array
    {
        $all = $this->analyzeIblock($iblockId);

        return array_filter(
            $all,
            fn($p) => $p['FILL_PERCENT'] > 0 && $p['FILL_PERCENT'] < $threshold
        );
    }

    /**
     * Безопасное удаление свойства с проверкой
     */
    public function deleteProperty(int $propertyId, bool $force = false): array
    {
        $property = \CIBlockProperty::GetByID($propertyId)->Fetch();
        
        if (!$property) {
            return ['success' => false, 'error' => 'Свойство не найдено'];
        }

        $filledCount = $this->getFilledCount($propertyId);

        if ($filledCount > 0 && !$force) {
            return [
                'success' => false,
                'error' => "Свойство содержит {$filledCount} значений. Используйте force=true для удаления.",
                'filled_count' => $filledCount,
            ];
        }

        // Значения удалять отдельным запросом не нужно: CIBlockProperty::Delete
        // чистит их сам, причём с учётом того, какое у инфоблока хранилище.
        // Ручной DELETE по b_iblock_element_property на хранилище 2 просто
        // не найдёт строк и создаст ложное ощущение, что данные удалены
        $result = \CIBlockProperty::Delete($propertyId);

        return [
            'success' => $result,
            'deleted_values' => $filledCount,
            'property' => $property,
        ];
    }

    /**
     * Массовое удаление пустых свойств
     */
    public function deleteEmptyProperties(int $iblockId): array
    {
        $emptyProperties = $this->getEmptyProperties($iblockId);
        $results = [];

        foreach ($emptyProperties as $property) {
            $results[] = [
                'property' => $property,
                'result' => $this->deleteProperty($property['ID']),
            ];
        }

        return $results;
    }

    /**
     * Количество элементов с заполненным свойством
     */
    private function getFilledCount(int $propertyId): int
    {
        $sql = "SELECT COUNT(DISTINCT IBLOCK_ELEMENT_ID) as cnt 
                FROM b_iblock_element_property 
                WHERE IBLOCK_PROPERTY_ID = {$propertyId}
                  AND VALUE IS NOT NULL 
                  AND VALUE != ''";
        
        $result = $this->connection->query($sql)->fetch();
        return (int) ($result['cnt'] ?? 0);
    }

    /**
     * Общее количество активных элементов
     */
    private function getTotalElements(int $iblockId): int
    {
        $sql = "SELECT COUNT(*) as cnt 
                FROM b_iblock_element 
                WHERE IBLOCK_ID = {$iblockId} AND ACTIVE = 'Y'";
        
        $result = $this->connection->query($sql)->fetch();
        return (int) ($result['cnt'] ?? 0);
    }
}

Консольный скрипт анализа

php
<?php
// /local/cli/analyze_properties.php

require_once $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php';

use Local\Iblock\PropertyAnalyzer;

$iblockId = (int) ($argv[1] ?? 0);

if ($iblockId <= 0) {
    echo "Usage: php analyze_properties.php <IBLOCK_ID>\n";
    exit(1);
}

$analyzer = new PropertyAnalyzer();
$properties = $analyzer->analyzeIblock($iblockId);

// Заголовок таблицы
printf(
    "%-6s %-25s %-30s %8s %8s %8s\n",
    'ID', 'CODE', 'NAME', 'FILLED', 'TOTAL', 'PERCENT'
);
echo str_repeat('-', 95) . "\n";

foreach ($properties as $prop) {
    $status = '';
    if ($prop['IS_EMPTY']) {
        $status = ' [EMPTY]';
    } elseif ($prop['IS_LOW_USAGE']) {
        $status = ' [LOW]';
    }
    
    printf(
        "%-6d %-25s %-30s %8d %8d %7.1f%%%s\n",
        $prop['ID'],
        substr($prop['CODE'], 0, 25),
        substr($prop['NAME'], 0, 30),
        $prop['FILLED_COUNT'],
        $prop['TOTAL_ELEMENTS'],
        $prop['FILL_PERCENT'],
        $status
    );
}

// Итоги
$emptyCount = count(array_filter($properties, fn($p) => $p['IS_EMPTY']));
$lowCount = count(array_filter($properties, fn($p) => $p['IS_LOW_USAGE']));

echo str_repeat('-', 95) . "\n";
echo "Total properties: " . count($properties) . "\n";
echo "Empty properties: {$emptyCount}\n";
echo "Low usage (<5%): {$lowCount}\n";

Запуск

bash
php /home/bitrix/www/local/cli/analyze_properties.php 2

# Вывод:
# ID     CODE                      NAME                          FILLED    TOTAL  PERCENT
# -----------------------------------------------------------------------------------------------
# 45     OLD_PRICE                 Старая цена                       0     1250     0.0% [EMPTY]
# 46     TEST_PROPERTY             Тестовое свойство                 0     1250     0.0% [EMPTY]
# 47     RARELY_USED               Редко используемое               12     1250     1.0% [LOW]

Скрипт очистки

php
<?php
// /local/cli/cleanup_properties.php

require_once $_SERVER['DOCUMENT_ROOT'] . '/bitrix/modules/main/include/prolog_before.php';

use Local\Iblock\PropertyAnalyzer;

$iblockId = (int) ($argv[1] ?? 0);
$dryRun = !in_array('--execute', $argv);

if ($iblockId <= 0) {
    echo "Usage: php cleanup_properties.php <IBLOCK_ID> [--execute]\n";
    echo "  Without --execute: dry run (shows what would be deleted)\n";
    echo "  With --execute: actually deletes empty properties\n";
    exit(1);
}

$analyzer = new PropertyAnalyzer();
$emptyProperties = $analyzer->getEmptyProperties($iblockId);

if (empty($emptyProperties)) {
    echo "No empty properties found in iblock #{$iblockId}\n";
    exit(0);
}

echo "Found " . count($emptyProperties) . " empty properties:\n\n";

foreach ($emptyProperties as $prop) {
    echo "  [{$prop['ID']}] {$prop['CODE']} - {$prop['NAME']}\n";
}

if ($dryRun) {
    echo "\nDry run mode. Add --execute to delete these properties.\n";
    exit(0);
}

echo "\nDeleting...\n";

foreach ($emptyProperties as $prop) {
    $result = $analyzer->deleteProperty($prop['ID']);
    
    if ($result['success']) {
        echo "  [OK] Deleted: {$prop['CODE']}\n";
    } else {
        echo "  [ERROR] {$prop['CODE']}: {$result['error']}\n";
    }
}

echo "\nDone!\n";

Почему пустое свойство не значит неиспользуемое

Это ключевая мысль статьи, и её стоит проговорить отдельно, потому что скрипт выше даёт очень убедительный список, которому хочется довериться.

Свойство может не иметь ни одного значения и при этом быть жизненно необходимым:

  • Оно заполняется обменом с 1С. Сегодня пусто, потому что выгрузка не приходила неделю; удалите — и следующий обмен свалится с ошибкой или молча потеряет данные.
  • Оно участвует в умном фильтре или в настройках компонентов. Настройки хранятся по ID свойства; удаление оставляет висящие ссылки, и фильтр начинает вести себя странно.
  • Оно упоминается в коде. Шаблоны, обработчики событий, интеграции. Проверяется одной командой, и проверять нужно обязательно:
bash
grep -rn "PROPERTY_TEST_FIELD\|'TEST_FIELD'" /home/bitrix/www/local/ /home/bitrix/www/bitrix/templates/
  • Оно новое. Свойство добавили вчера под задачу, которая ещё в работе.

Поэтому рабочий порядок такой: скрипт даёт список кандидатов, дальше по каждому — grep по коду, проверка в настройках умного фильтра и вопрос контент-менеджеру. Только после этого удаление.

💡 Совет

Дешёвая альтернатива удалению. Если уверенности нет, свойство можно просто скрыть из формы редактирования (снять активность или убрать из настроек отображения). Админка станет чище, данные останутся, а решение будет обратимым. Реальную экономию места даёт удаление свойств с миллионами значений, а не десятка пустых — так что чаще всего задача в удобстве, а не в размере базы, и радикальное решение просто не нужно.

Итоги

  1. Проверьте VERSION инфоблока перед запуском чего бы то ни было. Для хранилища 2 запросы из этой статьи дают ложный результат, а не ошибку — это худший вид неверного ответа.
  2. Бэкап базы — до, а не «если что-то пойдёт не так». Удаление свойства необратимо, вместе с ним уходят все значения.
  3. Dry run по умолчанию. Скрипт очистки выше требует явного --execute, и это правильный дизайн для любого инструмента, который удаляет данные.
  4. Каждого кандидата проверяйте grep-ом по коду и в настройках умного фильтра.
  5. Не удаляйте на боевом сервере в рабочее время. Удаление свойства с большим числом значений — это долгий DELETE, то есть блокировки и просевший сайт.
  6. Сомневаетесь — скройте, а не удаляйте. Обратимое решение почти всегда лучше необратимого при одинаковом внешнем эффекте.

Частые вопросы

Как найти неиспользуемые свойства инфоблока?

SQL-запросом, который ищет свойства без единого значения в таблице значений, и подсчётом заполненности в процентах. Но перед любым запросом проверьте вариант хранения: SELECT VERSION FROM b_iblock. Код для первого хранилища на инфоблоке со вторым даёт ложный результат, а не ошибку, — это худший вид неверного ответа.

Чем отличаются варианты хранения свойств и почему это критично?

При первом варианте все значения лежат в общей таблице b_iblock_element_property. При втором — в отдельных таблицах инфоблока: b_iblock_element_prop_s{ID} для одиночных значений, где на каждое свойство своя колонка, и b_iblock_element_prop_m{ID} для множественных. Критично это потому, что для инфоблока со вторым хранилищем общая таблица пуста целиком, и запрос «свойства без единого значения» вернёт все свойства инфоблока, а скрипт очистки удалит их вместе с данными.

Почему пустое свойство нельзя сразу удалять?

Потому что «пустое» и «неиспользуемое» — разные вещи. Свойство может заполняться обменом с 1С и быть пустым просто потому, что выгрузка не приходила неделю. Оно может участвовать в умном фильтре или настройках компонентов, которые хранят ссылки по ID свойства, — удаление оставит висящие ссылки, и фильтр начнёт вести себя странно. Оно может упоминаться в шаблонах, обработчиках и интеграциях. И наконец, оно может быть добавлено вчера под задачу, которая ещё в работе.

Как проверить кандидата на удаление?

Тремя проверками. Grep по коду проекта на код свойства — шаблоны, обработчики событий, интеграции. Просмотр настроек умного фильтра и параметров компонентов, где свойства прописаны по ID. И вопрос контент-менеджеру: он знает про сезонные поля и про то, что заполняется раз в год. Только после всех трёх — удаление.

Что делать, если уверенности в удалении нет?

Скрыть свойство из формы редактирования — снять активность или убрать из настроек отображения. Админка станет чище, данные останутся, решение будет обратимым. Реальную экономию места даёт удаление свойств с миллионами значений, а не десятка пустых, поэтому чаще всего задача в удобстве, а не в размере базы, и радикальное решение просто не нужно.

Что учесть при массовом удалении свойств?

Бэкап базы до операции, а не «если что-то пойдёт не так»: удаление свойства необратимо, вместе с ним уходят все значения. Режим dry run по умолчанию, чтобы удаление требовало явного флага. И не запускать на боевом сервере в рабочее время: удаление свойства с большим числом значений — это долгий DELETE, то есть блокировки и просевший сайт.