Исходные данные:
- лицензия 1С ПРОФ
- платформа 8.3.22
- оперативной памяти на сервере 1С 32 Гб
Описание проблемы:
Отраслевая отчёт-обработка, в которой с помощью сложных запросов с использованием множества временных таблиц строится результирующая (архитектуру реализации решения не обсуждаем, принимаем "как есть"). Результат нужно выгрузить в csv и отправить в надзорный орган.
Результат - таблица с более 800 тыс. строк и 30-тью колонками. Запрос на стороне СУБД исполняется достаточно быстро за период 2017-2023 гг, без нагрузки на сервер, план запроса - огромнейшая простыня на 5-ть экранов, визуально "по-диагонали" ничего криминального нет.
Выполнение отчёта генерирует исключение, в ТЖ “Недостаточно памяти для получения результата запроса к базе данных“, MemoryPeak > 4,5Гб для CALL.
Настройки параметров рабочего сервера "по умолчанию".
Согласно документации, настройка "Временно допустимый объем памяти процессов" при значении 0 в нашем случае равна 25 Гб (80% от ОЗУ). Настройка "Безопасный расход памяти за один вызов" при значении 0 в нашем случае равна 2.5 Гб (10% от "Временно допустимый объем памяти процессов").
Лицензия ПРОФ не позволяет изменять значение "Безопасный расход памяти за один вызов" (как раз "наш случай"). Исключительно ради эксперимента установил значение "Временно допустимый объем памяти процессов" (лицензия ПРОФ позволяет изменять данный параметр начиная с версии 8.3.19) в размер, 5-ти кратно превышающий размер ОЗУ 160 Гб - отчёт стал без проблем формироваться (расчётный "Безопасный расход памяти за один вызов" при этом получается 12,8 Гб). Но на продуктиве так делать, вероятно, нельзя.
Подходящий способ решения для нашей ситуации:
Очевидно, что нужно получать данные порциями (на ИТС даже есть статья, на это намекающая "Оптимизация использования оперативной памяти"). Но добавить ограничивающие выборку условия в нашем случае не представляется возможным (разумными трудозатратами) из-за чрезвычайно сложной логики работы.
И тут пришла идея - поместить результат выполнения запросов во временную таблицу и в цикле запрашивать данные порционно (с помощью менеджера временных таблиц). Но как их получать порционно? А для этого пронумеруем результирующие строки функцией АВТОНОМЕРЗАПИСИ(). Сказано = реализовано и протестировано!
Для примера я продемонстрирую простой виртуальной пример (не имеющий практической ценности). Исходный запрос:
МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = МенеджерВременныхТаблиц;
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 500000
| скДСП.Представление КАК Представление,
| скДСП.ДоговорКонтрагента.Представление КАК ДоговорКонтрагентаПредставление,
| скДСП.Контрагент.Представление КАК КонтрагентПредставление,
| скДСП.Организация.Представление КАК ОрганизацияПредставление,
| АВТОНОМЕРЗАПИСИ() КАК Индекс
|ПОМЕСТИТЬ врем
|ИЗ
| Документ.скДСП КАК скДСП
|
|ИНДЕКСИРОВАТЬ ПО
| Индекс";
РезультатЗапроса = Запрос.Выполнить();
Получение и обработка результата порциями:
Запрос.Текст =
"ВЫБРАТЬ
| врем.Индекс КАК Индекс,
| врем.Представление КАК Представление,
| врем.ДоговорКонтрагентаПредставление КАК ДоговорКонтрагентаПредставление,
| врем.КонтрагентПредставление КАК КонтрагентПредставление,
| врем.ОрганизацияПредставление КАК ОрганизацияПредставление
|ИЗ
| врем КАК врем
|ГДЕ
| врем.Индекс МЕЖДУ &Индекс1 И &Индекс2";
Для аа = 1 По 5 Цикл
Запрос.УстановитьПараметр("Индекс1", 1 + 100000 * (аа-1));
Запрос.УстановитьПараметр("Индекс2", 100000 * аа);
РезультатЗапроса = Запрос.Выполнить();
ВыборкаДетальныеЗаписи = РезультатЗапроса.Выбрать();
инд = 0;
Пока ВыборкаДетальныеЗаписи.Следующий() Цикл
Если инд = 0 Тогда
Сообщить(ВыборкаДетальныеЗаписи.Индекс);
КонецЕсли;
а1 = ВыборкаДетальныеЗаписи.Представление;
а2 = ВыборкаДетальныеЗаписи.ДоговорКонтрагентаПредставление;
а3 = ВыборкаДетальныеЗаписи.КонтрагентПредставление;
а4 = ВыборкаДетальныеЗаписи.ОрганизацияПредставление;
инд = инд + 1;
КонецЦикла;
Сообщить("Получено: " + инд);
КонецЦикла;
В цикле я изначально знаю, что записей фиксированное количество, для правильной обработки нужно получить количество строк в результате:
Запрос.Текст =
"ВЫБРАТЬ
| КОЛИЧЕСТВО(*) КАК КолВо
|ИЗ
| врем КАК врем";
ВыборкаДетальныеЗаписи = Запрос.Выполнить().Выгрузить();
ВсегоЗаписей = ВыборкаДетальныеЗаписи[0].КолВо;
Сообщить("Всего записей: " + ВсегоЗаписей);
И обязательно обратить внимание на документацию для функции АВТОНОМЕРЗАПИСИ(): "Начальное значение счетчика зависит от используемой СУБД и, в общем случае, может быть любым. Не гарантируется, что начальное значение счетчика будет равно 1 для любой временной таблицы.". Но гарантируется "последовательно возрастающее значение". Дополним наш код получением начального значения счётчика:
ИндексПервый = 0;
Если ВсегоЗаписей > 0 Тогда
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1
| врем.Индекс КАК Индекс
|ИЗ
| врем КАК врем
|
|УПОРЯДОЧИТЬ ПО
| Индекс";
ВыборкаДетальныеЗаписи = Запрос.Выполнить().Выгрузить();
ИндексПервый = ВыборкаДетальныеЗаписи[0].Индекс;
Сообщить("Первый индекс: " + ИндексПервый);
КонецЕсли;
Тестируем:
Всего записей: 500 000
Первый индекс: 1
1
Получено: 100 000
100 001
Получено: 100 000
200 001
Получено: 100 000
300 001
Получено: 100 000
400 001
Получено: 100 000
Дорабатываем наш отчёт по данной методике, тестируем. MemoryPeak при этом не поднимается > 1.5 Гб. Цель достигнута.