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

Наша компанія займається розробкою, підтримкою та обслуговуванням рішень на Бітрікс та Бітрікс24 будь-якої складності. Від простих односторінкових сайтів до складних інтернет-магазинів, CRM систем з інтеграцією 1С та телефонії. Досвід розробників підтверджено сертифікатами від вендора.
Пропоновані послуги
Показано 1 з 1 послугУсі 1626 послуг
Оптимізація highload-блоків для великих обсягів даних 1С-Бітрікс
Середня
~1-2 тижні
Часті питання

Наші компетенції:

Етапи розробки

Останні роботи

  • image_website-b2b-advance_0.png
    Розробка сайту компанії B2B ADVANCE
    1262
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Розробка веб-сайту для компанії ФІКСПЕР
    851
  • 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
    585
  • image_bitrix-bitrix-24-1c_mirsanbel_458_0.webp
    Розробка на базі 1С Підприємство для компанії МИРСАНБЕЛ
    751
  • image_crm_dolbimby_434_0.webp
    Розробка сайту на CRM Бітрікс24 для компанії DOLBIMBY
    657
  • image_crm_technotorgcomplex_453_0.webp
    Розробка на базі Бітрікс24 для компанії ТЕХНОТОРГКОМПЛЕКС
    989

Оптимізація 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, ліміти, пагінація
  • Впровадження тегованого кешу для важких вибірок
  • (За необхідності) Партиціонування хронологічних таблиць
  • Навантажувальне тестування з A/B-порівнянням до/після

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