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

URL: https://dmeshcheryakov.ru/blog/bitrix-cleanup-unused-properties/
Раздел: 1С-Битрикс
Теги: 1С-Битрикс, SQL, Оптимизация, Инфоблоки
Опубликовано: 2020-01-15
Обновлено: 2026-09-07
Автор: Дмитрий Мещеряков (https://dmeshcheryakov.ru)

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

Инфоблок каталога, которому пять лет, — это археологический раскоп. Свойства `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. **Сомневаетесь — скройте, а не удаляйте.** Обратимое решение почти всегда лучше необратимого при одинаковом внешнем эффекте.