При работе с большими массивами данных в 1С программисты часто сталкиваются с проблемой нехватки оперативной памяти или ограничениями внешних систем при выгрузке. Если попытаться выгрузить 100 000 или 1 000 000 записей в одну таблицу значений, сервер приложений может аварийно завершить работу. В таких случаях логично "нарезать" данные на ровные ломтики, например, по 1000 записей. Разберем, какими способами это можно реализовать максимально эффективно. Для решения задачи порционной выборки и пакетной фильтрации можно использовать инструмент для редактирования запросов с порционной выборкой данных.
Это классический метод, который работает на любой версии платформы 1С:Предприятие 8. Суть заключается в том, чтобы в цикле выполнять запрос, который выбирает только первую тысячу записей, значение ключевого поля которых больше, чем у последней записи из предыдущей порции. Проанализируем пример кода, реализующий этот алгоритм:
// Подготовка запроса
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1000
| Номенклатура.Ссылка КАК Ссылка,
| Номенклатура.Код КАК Код
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ГДЕ
| Номенклатура.Код > &ПоследнийКод
|УПОРЯДОЧИТЬ ПО
| Код";
ПоследнийКод = ""; // Начальное значение для первой итерации
Пока Истина Цикл
Запрос.УстановитьПараметр("ПоследнийКод", ПоследнийКод);
РезультатЗапроса = Запрос.Выполнить();
Если РезультатЗапроса.Пустой() Тогда
Прервать;
КонецЕсли;
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
// Обработка данных (например, запись в файл)
ПоследнийКод = Выборка.Код;
КонецЦикла;
// Здесь можно закрыть текущий файл и начать новый
КонецЦикла;
Важный нюанс: Чтобы этот способ работал быстро, поле, по которому происходит фильтрация (в нашем примере Код), должно быть проиндексировано. В противном случае на каждой итерации СУБД будет выполнять полное сканирование таблицы, что сведет на нет всю оптимизацию. Если вы используете поле Ссылка, индексация гарантирована платформой, но порядок ссылок (UUID) может быть хаотичным.
Начиная с версии платформы 8.3.13, разработчикам доступна функция АВТОНОМЕРЗАПИСИ(). Она позволяет пронумеровать строки внутри временной таблицы. Это открывает возможность для "математической" нарезки данных на страницы. Рассмотрим, как это работает через создание временной таблицы и последующую группировку по "порциям".
Сначала мы помещаем все нужные нам данные во временную таблицу и добавляем номер строки. Затем, используя целочисленное деление, мы можем вычислить номер "пакета" для каждой записи.
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка КАК Ссылка,
| АВТОНОМЕРЗАПИСИ() КАК НомерЗаписи
|ПОМЕСТИТЬ ВТ_Данные
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ_Данные.Ссылка КАК Ссылка,
| (ВТ_Данные.НомерЗаписи - 1) / 1000 КАК НомерПорции
|ИЗ
| ВТ_Данные КАК ВТ_Данные
|ИТОГИ ПО
| НомерПорции";
Результат = Запрос.Выполнить();
ВыборкаПорций = Результат.Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам);
Пока ВыборкаПорций.Следующий() Цикл
// Мы получили новую порцию (страницу) данных
ВыборкаЗаписей = ВыборкаПорций.Выбрать();
Пока ВыборкаЗаписей.Следующий() Цикл
// Обрабатываем конкретную запись внутри порции
КонецЦикла;
КонецЦикла;
Этот метод удобен тем, что мы обращаемся к основной таблице БД только один раз. Однако стоит помнить, что создание гигантской временной таблицы в tempdb также потребляет ресурсы сервера.
Многие начинающие разработчики совершают ошибку, используя связку Результат.Выполнить().Выгрузить(). Метод Выгрузить() принудительно копирует все данные из результата запроса в оперативную память сервера в виде объекта ТаблицаЗначений. Если записей очень много, это приведет к перерасходу памяти.
Вместо того чтобы "резать" сам текст запроса, мы можем выполнить один общий запрос, но обходить его результат "ленивым" способом с помощью объекта ВыборкаИзРезультатаЗапроса. Этот объект читает данные из БД порциями по мере вызова метода Следующий().
Рассмотрим алгоритм записи в разные файлы по 1000 строк внутри одного цикла:
Запрос = Новый Запрос("ВЫБРАТЬ Ссылка ИЗ Справочник.Номенклатура");
Выборка = Запрос.Выполнить().Выбрать();
Счетчик = 0;
НомерФайла = 1;
ТекстФайла = Новый ЗаписьТекста("C:\Export\data_" + НомерФайла + ".csv");
Пока Выборка.Следующий() Цикл
Счетчик = Счетчик + 1;
// Записываем данные в текущий файл
ТекстФайла.ЗаписатьСтроку(Строка(Выборка.Ссылка));
Если Счетчик >= 1000 Тогда
ТекстФайла.Закрыть(); // Закрываем текущую порцию
НомерФайла = НомерФайла + 1;
Счетчик = 0;
// Открываем новый файл для следующей тысячи
ТекстФайла = Новый ЗаписьТекста("C:\Export\data_" + НомерФайла + ".csv");
КонецЕсли;
КонецЦикла;
ТекстФайла.Закрыть();
Преимущество: Это самый производительный способ, так как мы не нагружаем СУБД повторными запросами и не создаем тяжелых временных таблиц. Память сервера при этом используется минимально.
Если ваша задача — нарезать данные регистра (например, РегистрСведений.ЦеныНоменклатуры), то фильтрации по одному полю будет недостаточно. В регистрах уникальность записи часто определяется комбинацией полей. Чтобы корректно получить "следующую" порцию, вам придется запоминать и передавать в параметры запроса весь ключ: Период, Регистратор, НомерСтроки и все измерения. Условие в ГДЕ при этом станет многоуровневым (сложная логика "больше или равно"), что делает первый способ довольно громоздким для регистров.
Проанализировав все варианты, можно сделать следующие выводы:
ТаблицуЗначений, используйте Способ 1 или Способ 2.АВТОНОМЕРЗАПИСИ), помните, что он работает только внутри временных таблиц.УПОРЯДОЧИТЬ ПО СУБД не гарантирует одинаковую последовательность строк в разных запросах, и вы рискуете получить дубли или пропуски в своих "ломтиках".