Оптимизация highload-блоков для больших объемов данных 1С-Битрикс

Наша компания занимается разработкой, поддержкой и обслуживанием решений на Битрикс и Битрикс24 любой сложности. От простых одностраничных сайтов до сложных интернет магазинов, CRM систем с интеграцией 1С и телефонии. Опыт разработчиков подтвержден сертификатами от вендора.
Предлагаемые услуги
Показано 1 из 1 услугВсе 1626 услуг
Оптимизация highload-блоков для больших объемов данных 1С-Битрикс
Средняя
~1-2 недели
Часто задаваемые вопросы
Наши компетенции:
Этапы разработки
Последние работы
  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1173
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    811
  • image_bitrix-bitrix-24-1c_development_of_an_online_appointment_booking_widget_for_a_medical_center_594_0.webp
    Разработка на базе Битрикс, Битрикс24, 1С для компании Development of an Online Appointment Booking Widget for a Medical Center
    564
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Разработка на базе 1С Предприятие для компании МИРСАНБЕЛ
    745
  • image_crm_dolbimby_434_0.webp
    Разработка сайта на CRM Битрикс24 для компании DOLBIMBY
    655
  • image_crm_technotorgcomplex_453_0.webp
    Разработка на базе Битрикс24 для компании ТЕХНОТОРГКОМПЛЕКС
    976

Оптимизация Highload-блоков для больших объёмов данных 1С-Битрикс

Highload-блоки (модуль highloadblock) — механизм Битрикс для хранения произвольных данных в отдельных таблицах вне инфоблоковой архитектуры. Популярные применения: журналы событий, каталоги товаров с нестандартной структурой, пользовательские профили, накопительные данные (история заказов, аналитика, очереди). Пока строк меньше 50–100 тысяч — всё работает нормально. При 1–10 миллионах строк начинаются проблемы: ORM Битрикс генерирует неоптимальные запросы, индексы не покрывают реальные выборки, JOINы тормозят.

Где ломается производительность

Основные антипаттерны при работе с Highload:

1. Отсутствие нужных индексов. Highload-блок создаёт таблицу с первичным ключом ID и автоинкрементом. Пользовательские поля типа UF_* не индексируются автоматически. Выборка getList(['filter' => ['UF_PRODUCT_ID' => 123]]) при миллионе строк — это table scan.

*2. SELECT -подобные запросы. ORM Битрикс по умолчанию выбирает все поля. Если у записи 30 UF-полей, включая TEXT и FILE, это дорогой запрос даже при малом result set.

3. Неограниченные выборки без пагинации. DataManager::getList() без limit вернёт все записи в память PHP.

4. Связанные таблицы через Reference. Если Highload связан с другим Highload или инфоблоком через Reference-поля — ORM строит JOIN, который без правильных индексов убивает производительность.

5. Частые UPDATE по полям без индекса. Типично для статусных полей, счётчиков.

Диагностика: что тормозит

Включаем лог медленных запросов MySQL:

[mysqld]
slow_query_log        = 1
slow_query_log_file   = /var/log/mysql/slow.log
long_query_time       = 1
log_queries_not_using_indexes = 1

Включаем профилирование в Битрикс (только в dev-среде):

define('BX_SECURITY_SHOW_MESSAGE', true);
\Bitrix\Main\Diag\SqlTracker::start();

// ... ваш код

$tracker = \Bitrix\Main\Diag\SqlTracker::getInstance();
foreach ($tracker->getQueries() as $query) {
    if ($query->getTime() > 0.1) {
        echo $query->getSql() . ' — ' . round($query->getTime() * 1000) . 'ms' . PHP_EOL;
    }
}

Оптимизация 1: правильные индексы

Для Highload-таблицы добавляем индексы через прямой SQL — в агенте при установке или в migration-скрипте:

// Определяем имя таблицы Highload-блока
$hlBlock = \Bitrix\Highloadblock\HighloadBlockTable::getList([
    'filter' => ['NAME' => 'ProductCatalog'],
])->fetch();

$hlEntity = \Bitrix\Highloadblock\HighloadBlockTable::compileEntity($hlBlock);
$tableName = $hlEntity->getDBTableName();

$connection = \Bitrix\Main\Application::getConnection();

// Индекс по полю, которое используется в фильтрах
$connection->queryExecute(
    "CREATE INDEX IF NOT EXISTS idx_product_id ON {$tableName} (UF_PRODUCT_ID)"
);

// Составной индекс для типичного запроса: status + date
$connection->queryExecute(
    "CREATE INDEX IF NOT EXISTS idx_status_date ON {$tableName} (UF_STATUS, UF_DATE_CREATE)"
);

// Индекс для полнотекстового поиска
$connection->queryExecute(
    "CREATE FULLTEXT INDEX IF NOT EXISTS ft_name ON {$tableName} (UF_NAME) WITH PARSER ngram"
);

Оптимизация 2: явный SELECT нужных полей

Никогда не запрашиваем select: ['*'] или пустой массив select:

// Плохо — выбирает все поля
$result = ProductCatalogTable::getList([
    'filter' => ['UF_CATEGORY_ID' => $categoryId],
]);

// Хорошо — только то, что нужно
$result = ProductCatalogTable::getList([
    'select' => ['ID', 'UF_NAME', 'UF_PRICE', 'UF_ACTIVE'],
    'filter' => ['UF_CATEGORY_ID' => $categoryId, 'UF_ACTIVE' => 1],
    'order'  => ['UF_SORT' => 'ASC'],
    'limit'  => 50,
    'offset' => ($page - 1) * 50,
]);

Оптимизация 3: кеширование результатов

Highload-данные хорошо кешируются через Bitrix\Main\Data\Cache:

class CachedProductCatalog
{
    private const CACHE_TAG = 'hl_product_catalog';
    private const CACHE_TTL = 3600;

    public function getByCategory(int $categoryId): array
    {
        $cache    = \Bitrix\Main\Data\Cache::createInstance();
        $cacheKey = 'hl_catalog_cat_' . $categoryId;

        if ($cache->initCache(self::CACHE_TTL, $cacheKey, '/hl/catalog/')) {
            return $cache->getVars();
        }

        $cache->startDataCache();

        $result = $this->fetchFromDb($categoryId);

        // Тегированный кеш — инвалидируется при изменении любого элемента
        $tagCache = new \Bitrix\Main\Data\TaggedCache();
        $tagCache->startTagCache('/hl/catalog/');
        $tagCache->registerTag(self::CACHE_TAG . '_' . $categoryId);
        $tagCache->endTagCache();

        $cache->endDataCache($result);

        return $result;
    }

    // Инвалидация при изменении данных
    public static function clearCache(int $categoryId): void
    {
        $tagCache = new \Bitrix\Main\Data\TaggedCache();
        $tagCache->clearByTag(self::CACHE_TAG . '_' . $categoryId);
    }
}

Инвалидация кеша из обработчика событий Highload:

\Bitrix\Main\EventManager::getInstance()->addEventHandler(
    'highloadblock',
    'ProductCatalogOnAfterUpdate',
    function (\Bitrix\Main\Event $event) {
        $fields = $event->getParameter('fields');
        if (isset($fields['UF_CATEGORY_ID'])) {
            CachedProductCatalog::clearCache((int)$fields['UF_CATEGORY_ID']);
        }
    }
);

Оптимизация 4: прямые SQL-запросы для агрегации

ORM Битрикс не всегда генерирует эффективный SQL для агрегатных запросов. Для COUNT, SUM, GROUP BY с большими таблицами — идём напрямую:

class HlStatistics
{
    public function getOrderCountByStatus(string $tableName): array
    {
        $connection = \Bitrix\Main\Application::getConnection();
        $tableName  = $connection->getSqlHelper()->forSql($tableName);

        $result = $connection->query(
            "SELECT UF_STATUS, COUNT(*) as cnt, SUM(UF_AMOUNT) as total
             FROM {$tableName}
             WHERE UF_DATE_CREATE >= DATE_SUB(NOW(), INTERVAL 30 DAY)
             GROUP BY UF_STATUS
             ORDER BY cnt DESC"
        );

        $rows = [];
        while ($row = $result->fetch()) {
            $rows[$row['UF_STATUS']] = [
                'count' => (int)$row['cnt'],
                'total' => (float)$row['total'],
            ];
        }

        return $rows;
    }
}

Оптимизация 5: партиционирование для хронологических данных

Если Highload хранит логи или события с датой — партиционирование по диапазону дат резко ускоряет выборки за период:

ALTER TABLE b_hl_event_log
    PARTITION BY RANGE (YEAR(UF_DATE_CREATE) * 100 + MONTH(UF_DATE_CREATE)) (
        PARTITION p_2024_01 VALUES LESS THAN (202402),
        PARTITION p_2024_02 VALUES LESS THAN (202403),
        -- ...
        PARTITION p_future VALUES LESS THAN MAXVALUE
    );

Партиции создаются в init-скрипте или агенте; новые партиции добавляются заранее через агент, запускаемый в начале каждого месяца.

Оптимизация 6: Redis для счётчиков и очередей

Если Highload используется как очередь задач или хранилище счётчиков — частые UPDATE одной строки создают lock contention. Выносим счётчики в Redis:

class HlCounter
{
    public function increment(string $key, int $amount = 1): void
    {
        $redis = \Bitrix\Main\Data\Cache::createInstance();
        // Или напрямую через \Local\Redis\Client
        $redis->increment('hl_counter_' . $key, $amount);
    }

    // Агент сбрасывает Redis-счётчики в Highload раз в минуту
    public function flushToDb(): void
    {
        $keys = $this->redis->keys('hl_counter_*');
        foreach ($keys as $key) {
            $value  = $this->redis->get($key);
            $hlKey  = str_replace('hl_counter_', '', $key);
            $this->updateHighloadRecord($hlKey, $value);
            $this->redis->del($key);
        }
    }
}

Бенчмарки: что даёт каждая оптимизация

Оптимизация Таблица 1М строк Таблица 10М строк
Добавление индекса по фильтруемому полю 4000 ms → 5 ms 40000 ms → 8 ms
SELECT только нужных полей 800 ms → 120 ms
Кеш результата (попадание) 120 ms → 0.5 ms
Прямой SQL вместо ORM (агрегация) 350 ms → 45 ms 3000 ms → 80 ms
Партиционирование по дате 3000 ms → 60 ms

Состав работ

  • Аудит Highload-блоков: структура, объём данных, типичные запросы
  • Анализ slow query log: топ тормозящих запросов
  • Добавление индексов (одиночных и составных) под реальные паттерны фильтрации
  • Рефакторинг кода: явный select, лимиты, пагинация
  • Внедрение тегированного кеша для тяжёлых выборок
  • (По необходимости) Партиционирование хронологических таблиц
  • Нагрузочное тестирование с AB-сравнением до/после

Сроки: аудит + индексы + кеш — 2–3 недели. Полная оптимизация с партиционированием и рефакторингом кода — 4–8 недель.