При работе с крупными базами данных на платформе 1С программисты часто сталкиваются с ситуацией, когда запрос, который «вчера работал нормально», внезапно начинает выдавать ошибку нехватки памяти (Out of Memory), даже если серверная часть запущена в 64-битном режиме. Проблема в том, что 64 бита дают доступ к большему объему памяти, но не защищают от экспоненциального роста объема данных внутри самого SQL-сервера или в оперативной памяти сервера приложений 1С. Разберем на конкретном примере, предоставленном автором темы, какие именно архитектурные ошибки в запросе приводят к такому результату.
Проанализируем первый пакет запроса, где формируется таблица ВтЦеныНоменклатуры. Мы видим следующее соединение:
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.КурсыВалют.СрезПоследних(, Валюта = &ВалютаУПР) КАК КурсВалютыУправлУчета
ПО (ИСТИНА)
Использование условия ПО (ИСТИНА) — это явный признак создания декартова произведения (Cross Join). В данном конкретном случае автор рассчитывает, что СрезПоследних вернет одну строку (курс управленческой валюты). Однако, если в параметрах виртуальной таблицы не указана дата или отбор сработал некорректно, SQL-сервер может попытаться перемножить каждую строку из основной таблицы цен на каждую строку таблицы курсов. Рассмотрим ситуацию подробнее: если в ценах 10 000 позиций, а в курсах по какой-то причине оказалось 10 строк, на выходе мы получим 100 000 строк. На больших объемах данных это мгновенно раздувает временные таблицы (TempDB).
Разберем самый опасный участок запроса, который находится в пакете ТаблицаОстатков. Здесь мы видим самосоединение временной таблицы:
ИЗ
ТабТоварыВРознице КАК ТабТоварыВРознице
ЛЕВОЕ СОЕДИНЕНИЕ ТабТоварыВРознице КАК ТабТоварыВРознице1
ПО ТабТоварыВРознице.Склад = ТабТоварыВРознице1.Склад
И ТабТоварыВРознице.Номенклатура = ТабТоварыВРознице1.Номенклатура
И ТабТоварыВРознице.Период < ТабТоварыВРознице1.Период
Проанализируем условие ТабТоварыВРознице.Период < ТабТоварыВРознице1.Период. В программировании это называется неравенством в условии соединения (Inequality Join). Вычислительная сложность такой операции составляет O(N²). Выясним причину: для каждой строки из первой таблицы SQL-сервер должен найти все строки из второй таблицы, где дата больше. Если по конкретному товару на складе есть движения за 100 дней, то для первого дня найдется 99 строк, для второго — 98 и так далее. Количество итоговых строк будет равно сумме арифметической прогрессии.
Когда во временной таблице ТабТоварыВРознице находятся тысячи записей, результат такого соединения может исчисляться миллионами или миллиардами строк. Именно на этом этапе чаще всего происходит «вылет» по памяти. SQL-сервер пытается построить огромную промежуточную таблицу в памяти или в TempDB, что приводит к критической ошибке.
В тексте запроса часто встречается обращение к виртуальным таблицам (СрезПоследних, ОстаткиИОбороты) непосредственно в секции соединения. Важно помнить: виртуальная таблица в 1С — это сложный подзапрос на уровне SQL. Проанализируем последствия:
ПОМЕСТИТЬ) перед тем, как соединять их между собой. Это позволяет СУБД построить статистику и использовать индексы временных таблиц.Рассмотрим еще один проблемный фрагмент в соединении ТабТоварыВРознице и ВтЦеныНоменклатуры:
ПО ТоварыВРозницеОстаткиИОбороты.Номенклатура = ВтЦеныНоменклатуры.Номенклатура
И (ВЫБОР
КОГДА ТоварыВРозницеОстаткиИОбороты.Номенклатура.ВестиУчетПоХарактеристикам
ТОГДА ТоварыВРозницеОстаткиИОбороты.ХарактеристикаНоменклатуры
ИНАЧЕ ЗНАЧЕНИЕ(Справочник.ХарактеристикиНоменклатуры.ПустаяСсылка)
КОНЕЦ = ВтЦеныНоменклатуры.Характеристика)
Здесь мы видим сразу две серьезные ошибки оптимизации:
Номенклатура.ВестиУчетПоХарактеристикам заставляет 1С неявно присоединять таблицу справочника Номенклатура. Это усложняет план запроса.ВЫБОР внутри ПО делает невозможным использование Index Seek (поиск по индексу). SQL-сервер будет вынужден выполнять Full Scan (полное сканирование) временных таблиц, что крайне медленно и ресурсозатратно.Рациональнее заранее подготовить данные во временной таблице ВтЦеныНоменклатуры, чтобы поле Характеристика уже содержало нужные значения для сопоставления, и само соединение шло по простому равенству полей.
Посмотрим на предпоследний пакет запроса (Продажи):
ИЗ
РегистрНакопления.Продажи.Обороты(...) КАК ПродажиОбороты,
ВтЦеныНоменклатуры КАК ВтЦеныНоменклатуры
Обратите внимание на отсутствие секции ГДЕ или ВНУТРЕННЕЕ СОЕДИНЕНИЕ ... ПО .... Это классическое перечисление таблиц через запятую. В мире SQL это означает полное декартово произведение. Каждая продажа будет умножена на каждую цену из таблицы ВтЦеныНоменклатуры. Если у вас 500 продаж и 2000 цен, вы получите 1 000 000 строк. Это грубейшая ошибка, которая гарантированно «подвешивает» систему при росте объема данных.
Чтобы исправить этот запрос и избавиться от ошибок памяти, проделаем следующие шаги:
Номенклатура, Склад, Характеристика). Это критически важно для работы оптимизатора.Период < Период для расчета разности дат, лучше использовать оконные функции (если версия платформы 8.3.20+) или предварительно подготовить таблицу периодов.ГДЕ. Это значительно уменьшает объем данных, которые SQL-сервер считывает с диска.Соблюдение этих правил позволит запросу выполняться за секунды даже на базах объемом в сотни гигабайт, потребляя при этом минимальное количество оперативной памяти.