Highload-блок на объёме - индексы, заливка, выкладка структуры
Highload-блок на сотнях тысяч записей: индексы под фильтры, быстрая заливка, порционное чтение и перенос структуры между стендами.
Механика
Highload-блок - это отдельная таблица базы и набор пользовательских полей к ней. Запись о самом блоке хранит только имя сущности и имя таблицы, всё остальное описывают поля.
Структура полей живёт не в этой таблице, а в общем хранилище полей платформы. Поэтому перенос блока на другой стенд - это перенос двух вещей: записи о блоке и описаний его полей.
Класс для запросов собирается на лету по идентификатору блока. Готового имени класса в коде нет, и обращение к справочнику всегда начинается со сборки сущности.
Таблицу создаёт сама платформа, а индексы - нет. Кроме первичного ключа, в таблице не будет ничего, и первый же фильтр по внешнему коду начнёт читать её целиком.
Множественное поле хранится не в основной таблице, а в служебной рядом с ней. Выборка по такому полю дороже обычной, и на большом объёме это заметно по времени ответа.
Добавление записи через сущность делает по одному запросу на строку. Сто тысяч строк в таком режиме заливаются часами, хотя те же данные вставляются пачками за минуты.
Событий у highload-блока почти нет, и в этом его сила. Он не тянет за собой пересчёт цен, переиндексацию поиска и сброс тегированного кэша, как инфоблок.
Отсюда типовая ошибка планирования: справочник заводят как инфоблок и удивляются скорости. Highload-блок берут именно тогда, когда записей много, они однотипные и разделы им не нужны.
Обратная сторона - минимум готовых механизмов вокруг. Ни фасетного индекса, ни поиска по сайту, ни прав на отдельные записи: всё, что нужно сверх выборок, пишут сами.
Шаги
- Оценить объём таблицы и понять, какие поля участвуют в фильтрах.
- Поставить индексы на поля фильтра и на внешний код записи.
- Заливать данные пачками, а не по одной записи через сущность.
- Читать большие выборки порциями по идентификатору, а не целиком.
- Перенести структуру блока на боевой стенд кодом миграции.
- Отдельно продумать полную перезаливку справочника и её безопасность.
Код
Смотрим объём и структуру таблицы:
$entity = \Bitrix\Highloadblock\HighloadBlockTable::compileEntity( \Bitrix\Highloadblock\HighloadBlockTable::getById($hlId)->fetch());$table = $entity->getDBTableName();$conn = \Bitrix\Main\Application::getConnection();print_r($conn->query("SHOW INDEX FROM {$table}")->fetchAll()); // какие индексы естьprintf("строк: %d\n", $conn->queryScalar("SELECT COUNT(*) FROM {$table}"));Список индексов почти всегда состоит из одного первичного ключа. Это нормальное состояние свежего блока, и именно с него начинается разбор медленных выборок по справочнику.
Ставим индексы под свои фильтры:
ALTER TABLE vendor_brands ADD INDEX ix_xml_id (UF_XML_ID);ALTER TABLE vendor_brands ADD INDEX ix_active_sort (UF_ACTIVE, UF_SORT);-- индекс ставят на поля фильтра, а не на все подряд поля справочникаВнешний код записи индексируют всегда: по нему идёт сверка при обмене. Составной индекс собирают в том порядке, в котором поля стоят в фильтре, иначе он работать не будет.
Заливаем данные пачками:
$helper = $conn->getSqlHelper();$values = [];foreach ($rows as $row) { // $rows - порция строк источника $values[] = "('" . $helper->forSql($row['xml']) . "','" . $helper->forSql($row['name']) . "')";}$conn->queryExecute("INSERT INTO {$table} (UF_XML_ID, UF_NAME) VALUES " . implode(',', $values) . " ON DUPLICATE KEY UPDATE UF_NAME = VALUES(UF_NAME)"); // обновление по внешнему кодуПачка в тысячу строк - разумный размер для одного запроса. Больше упирается в ограничение на длину запроса, меньше теряет весь выигрыш в скорости заливки.
Обновление по совпадению работает только при уникальном индексе. Без него та же вставка молча наплодит дубли, и справочник придётся чистить руками.
Читаем большой справочник порциями:
$lastId = 0;while (true) { $rows = $dataClass::getList(['filter' => ['>ID' => $lastId], 'order' => ['ID' => 'ASC'], 'limit' => 500])->fetchAll(); if (!$rows) { break; } $lastId = (int)end($rows)['ID']; // порция по ключу, а не по смещению}Порции по идентификатору не замедляются к концу выборки. Обход по смещению работает ровно наоборот: каждая следующая тысяча читается дольше предыдущей.
Переносим структуру блока кодом:
$hlId = \Bitrix\Highloadblock\HighloadBlockTable::add( ['NAME' => 'Brands', 'TABLE_NAME' => 'vendor_brands'])->getId();
(new \CUserTypeEntity())->Add(['ENTITY_ID' => 'HLBLOCK_' . $hlId, 'FIELD_NAME' => 'UF_XML_ID', 'USER_TYPE_ID' => 'string', 'MANDATORY' => 'Y', 'EDIT_FORM_LABEL' => ['ru' => 'Внешний код']]);Структуру справочника переносят кодом миграции, а не выгрузкой базы. Тогда боевой стенд получает те же поля, а данные заливаются отдельным шагом и в своём темпе.
Перезаливаем справочник целиком:
$conn->queryExecute("CREATE TABLE {$table}_new LIKE {$table}");// заливка пачками во временную таблицу, потом одна быстрая замена$conn->queryExecute("RENAME TABLE {$table} TO {$table}_old, {$table}_new TO {$table}");$conn->queryExecute("DROP TABLE {$table}_old");Замена таблицы целиком не оставляет посетителю пустого справочника. Очистка с последующей заливкой оставляет окно в несколько минут, и в это окно витрина показывает пустые значения.
Проверяем результат выборкой с планом:
$sql = $dataClass::getList(['filter' => ['=UF_XML_ID' => 'primer']])->getQuery();print_r($conn->query('EXPLAIN ' . $sql)->fetchAll()); // видно, взят ли индексПлан запроса показывает, попал ли фильтр в индекс. Полный перебор таблицы в плане на справочнике из сотен тысяч записей означает, что индекс не тот или его нет вовсе.
Ограничения
Права на отдельные записи справочника не поддерживаются. Доступ ограничивают своим кодом в местах вывода, а не настройками самого highload-блока.
Разделов у справочника нет, и вложенность строят полем-ссылкой. Обход дерева при этом придётся писать самому, готового механизма для этого нет.
Имя сущности и имя таблицы после создания менять нельзя. Переименование на живом проекте ломает весь код, который собирал сущность по этому имени.
Данные справочника не переносятся вместе со структурой. Перенос записей - это отдельный шаг выкладки, и о нём вспоминают уже на боевом стенде.
Записи справочника не попадают в поиск по сайту. Если справочник должен искаться, его содержимое отдают в поиск своим кодом или держат в инфоблоке.
Типичные проблемы
Выборка из справочника занимает секунды.
В таблице только первичный ключ, а фильтр идёт по внешнему коду или по признаку активности. Индексы на поля фильтра ставят руками, платформа их не создаёт.
Заливка справочника из обмена длится часами.
Записи добавляются по одной через сущность, и на строку приходится отдельный запрос. Данные заливают пачками по тысяче строк одним запросом.
После повторной заливки в справочнике появились дубли.
Обновление по совпадению работает только при уникальном индексе на внешнем коде. Без индекса каждая заливка добавляет новые строки к старым.
Скрипт обхода справочника падает по памяти.
Выборка читает все записи разом в один массив без ограничения. Большие справочники обходят порциями по идентификатору, а не целиком.
На боевом стенде у справочника нет полей.
Перенесена только запись о блоке, а описания полей остались на разработке. Структуру переносят целиком: и блок, и его пользовательские поля.
Во время обновления справочника витрина показывает пустые значения.
Справочник очищают и заливают заново на живом сайте. Данные готовят во временной таблице и подменяют её одной быстрой операцией.
Частые вопросы
Нужны ли highload-блоку индексы?
Да, и ставить их приходится самому: платформа создаёт только первичный ключ. Первый же фильтр по внешнему коду без индекса читает таблицу целиком.
Как быстро залить много записей?
Пачками примерно по тысяче строк одним запросом вставки. Добавление по одной записи через сущность на сотне тысяч строк растягивается на часы.
Как перенести справочник на боевой сайт?
Структуру - кодом миграции, данные - отдельным шагом заливки. Выгрузка таблицы из базы разработки переносит и лишние записи, и чужие идентификаторы.
Сколько записей выдержит highload-блок?
Миллионы, если есть индексы под фильтры и выборки идут порциями. Ограничение здесь не платформы, а обычной таблицы базы данных.
Можно ли обойтись своей таблицей на ORM?
Можно, и на чисто служебных данных это даже проще. Highload-блок берут ради готового списка и формы правки в административной части.
Смежное
- Highload-блоки на практике - оглавление подтемы
- Highload-блок на практике: создание, поля, привязка к свойству - создание блока и его полей
- Справочник в выборках: вывод значений, фильтр, кэш - вывод значений на витрине
- Своя таблица на ORM: сущность, запросы, изменение структуры - когда интерфейс правки не нужен
- Миграции структуры: перенос настроек между стендами - перенос структуры кодом
- Медленный запрос к базе: поиск, план, индекс - разбор плана запроса
- Highload-блоки - устройство хранилища целиком