В практике разработки на платформе 1С часто встречается парадоксальная ситуация: отбор в динамическом списке по полю с индексом работает значительно медленнее, чем по неиндексированному полю или полю с составным типом данных. Особенно остро эта проблема проявляется в крупных базах данных объемом в несколько терабайт. Проанализируем причины этого явления и рассмотрим методы оптимизации.
Разберем понятие кардинальности (cardinality). Это количество уникальных значений в конкретном столбце таблицы. Если мы рассматриваем поле СостояниеОплаты, которое является перечислением и имеет всего 3–4 значения, то его кардинальность крайне низка для таблицы в несколько миллионов строк.
Проанализируем ситуацию с точки зрения SQL-сервера. Когда мы накладываем отбор по значению с низкой селективностью (например, «Оплачено»), оптимизатор SQL может ошибочно выбрать план запроса Index Seek в сочетании с Key Lookup. Если под критерий «Оплачено» попадает 70–80% записей огромной таблицы, то СУБД будет вынуждена для каждой строки из индекса обращаться к основной таблице за остальными данными. Это порождает колоссальное количество операций чтения и приводит к «зависанию» списка.
В то же время, при работе с неиндексированным полем или сложным составным типом, SQL-сервер чаще выбирает Table Scan (полное сканирование). На больших объемах последовательное чтение таблицы часто оказывается быстрее, чем миллионы разрозненных обращений через неселективный индекс.
В динамических списках часто используется ЛЕВОЕ СОЕДИНЕНИЕ с виртуальными таблицами, например, РегистрСведений.ИмяРегистра.СрезПоследних. Это «некошерный» подход для высоконагруженных систем по нескольким причинам:
ГДЕ внутрь подзапроса виртуальной таблицы мог работать нестабильно. Вместо того чтобы получить срез последних только по нужным заказам, система сначала строит срез по всему регистру, а затем соединяет его с заказами.Рассмотрим способ оптимизации через обращение к основной таблице регистра сведений вместо использования СрезПоследних. Это позволит оптимизатору SQL лучше понять структуру данных. Попробуем переписать запрос динамического списка, используя коррелированный подзапрос или соединение с физической таблицей:
ВЫБРАТЬ
Заказы.Ссылка,
Статусы.СостояниеОплаты
ИЗ
Документ.ЗаказКлиенту КАК Заказы
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.СостоянияЗаказов КАК Статусы
ПО Заказы.Ссылка = Статусы.Заказ
ГДЕ
Статусы.Период = (
ВЫБРАТЬ МАКСИМУМ(Т.Период)
ИЗ РегистрСведений.СостоянияЗаказов КАК Т
ГДЕ Т.Заказ = Заказы.Ссылка
)
Важно: Такой подход требует наличия качественных индексов по измерениям регистра, но он работает более предсказуемо в динамических списках.
Для баз объемом 2 ТБ и выше самым эффективным решением является денормализация. Если вам нужно постоянно отображать «Состояние оплаты» в списке заказов и выполнять по нему быстрые отборы, лучше всего добавить соответствующий реквизит напрямую в объект Документ.ЗаказКлиенту.
Проанализируем шаги для реализации этого метода:
СостояниеОплаты в документ ЗаказКлиенту и индексируем его.ПриЗаписи регистра сведений, который хранит эти состояния, прописываем обновление реквизита в документе.Это полностью исключает JOIN в запросе динамического списка, что дает максимально возможную производительность при любых отборах.
Выясним, какие настройки динамического списка могут влиять на скорость:
Документ.ЗаказКлиенту). Это критически важно для работы механизма динамического считывания данных порциями.СостояниеОплаты не всегда нужно для отображения, но по нему часто идет отбор, флаг Использовать всегда может как помочь, так и навредить, заставляя систему выполнять лишние соединения.UPDATE STATISTICS на уровне SQL-сервера для таблиц, участвующих в запросе динамического списка.Если вы столкнулись с тормозами при отборе по индексированному полю, не спешите винить платформу. Проанализируйте кардинальность данных: если уникальных значений мало, индекс может только мешать. В таких случаях либо доверьтесь полному сканированию таблицы, либо переходите к денормализации данных, вынося статусы в реквизиты основных документов.