Вопрос разбиения массивов или коллекций на более мелкие порции часто возникает при работе с большими объемами данных в 1С:Предприятии. Рассмотрим типичную ситуацию: нам необходимо обработать полмиллиона позиций номенклатуры, например, для поиска ссылок на них, но выполнять такую операцию для всего массива сразу система не позволяет, выдавая ошибку или зависая. В таком случае оптимальным решением становится обработка данных пачками, скажем, по 2000 позиций. Как же эффективно реализовать такую логику в 1С? Мы рассмотрим несколько подходов, начиная от прямого программного разбиения и заканчивая более эффективными методами, ориентированными на работу с большими данными.
Начнем с самого прямого способа – написания функции, которая программно разбивает исходный массив на массив массивов (пачек) заданного размера. Этот подход вполне подойдет для массивов среднего размера, когда объем данных не критичен для оперативной памяти.
Суть метода: Мы будем итерировать по элементам исходной коллекции и по мере заполнения текущей пачки добавлять ее в результирующий массив, затем создавая новую пустую пачку.
Давайте посмотрим на несколько реализаций.
Это одна из наиболее интуитивно понятных реализаций, предложенная в обсуждении. Мы создаем новую пачку каждый раз, когда предыдущая достигает заданного размера, или в самом начале цикла. Проанализируем ее:
Функция РазбитьМассивПоПачкам(М, РазмерПачки)
ММ = Новый Массив();
Р = Неопределено;
Для Каждого Эл из М Цикл
Если Р = Неопределено Тогда
Р = Новый Массив(); ММ.Добавить(Р);
КонецЕсли;
Р.Добавить(Эл);
Если Р.Количество() = РазмерПачки Тогда
Р = Неопределено;
КонецЕсли;
КонецЦикла;
Возврат ММ;
КонецФункции
Как это работает:
ММ, в который будем добавлять наши пачки.Р (текущая пачка) инициализируется как Неопределено.Для Каждого Эл из М Цикл:
Р еще не определена (что происходит при первом элементе или после того, как предыдущая пачка была "сброшена"), мы создаем новый массив для Р и добавляем его в ММ.Эл в Р.Р достигло РазмерПачки, мы сбрасываем Р в Неопределено. Это сигнализирует о том, что для следующего элемента нужно будет создать новую пачку.ММ – массив, содержащий все пачки.Этот подход использует математический оператор деления по модулю (%) для определения момента создания новой пачки. Он также достаточно компактен и эффективен.
Функция ПорционироватьКоллекцию(Коллекция, РазмерПорции) Экспорт
Результат = Новый Массив;
Счетчик = 0;
Для каждого Эл Из Коллекция Цикл
Если Счетчик%РазмерПорции = 0 Тогда
ТекПорция = Новый Массив;
Результат.Добавить(ТекПорция);
КонецЕсли;
ТекПорция.Добавить(Эл);
Счетчик = Счетчик+1;
КонецЦикла;
Возврат Результат;
КонецФункции
Как это работает:
Результат – массив для хранения пачек, и Счетчик элементов.Счетчик делится на РазмерПорции без остатка, это означает, что нам нужна новая пачка.
ТекПорция как новый массив.ТекПорция в Результат.Эл в ТекПорция.Счетчик.Важный момент здесь – переменная ТекПорция указывает на последний добавленный массив, что позволяет нам постоянно добавлять в него элементы, пока не придет время создать новую пачку.
Еще один вариант программного разбиения, который держит ссылку на последнюю пачку и проверяет ее заполненность.
Функция РазбитьМассивПоПачкам(М, РазмерПачки)
Р = Новый Массив();
Если М.Количество() <> 0 Тогда Р.Добавить(Новый Массив()); КонецЕсли;
Для Каждого Эл из М Цикл
Если Р[Р.Количество() - 1].Количество() = РазмерПачки Тогда
Р.Добавить(Новый Массив());
КонецЕсли;
Р[Р.Количество() - 1].Добавить(Эл);
КонецЦикла;
Возврат Р;
КонецФункции
Как это работает:
Р – массив для хранения пачек.М не пустой, сразу добавляем в Р первую пустую пачку.Для Каждого Эл из М Цикл:
Р[Р.Количество() - 1]) равно РазмерПачки, то создаем и добавляем новую пачку в Р.Эл в последнюю пачку.Этот метод отличается от предыдущих тем, что не использует условные операторы для создания новой пачки, а вычисляет индекс пачки для каждого элемента с помощью деления нацело.
Функция ПорционироватьКоллекцию(Коллекция, РазмерПорции) Экспорт
Результат = Новый Массив;
Для Сч = 0 По Коллекция.ВГраница() Цикл
Если Сч%РазмерПорции = 0 Тогда
Результат.Добавить(Новый Массив);
КонецЕсли;
Результат[Цел(Сч/РазмерПорции)].Добавить(Коллекция[Сч]);
КонецЦикла;
Возврат Результат;
КонецФункции
Как это работает:
Результат – массив для хранения пачек.Для Сч = 0 По Коллекция.ВГраница() Цикл:
Сч делится на РазмерПорции без остатка, это означает, что мы начинаем новую пачку, и мы добавляем новый массив в Результат.Коллекция[Сч] добавляется в пачку, индекс которой вычисляется как Цел(Сч/РазмерПорции). Это гарантирует, что каждый элемент попадет в свою пачку, а пачки будут создаваться по мере необходимости.Недостатки программного разбиения для больших данных:
Хотя эти программные решения функциональны, при работе с 500 000 позициями или более они могут привести к значительному потреблению оперативной памяти. В памяти одновременно будут находиться исходный массив на 500 000 элементов, а также массив, состоящий из 250 (500000 / 2000) массивов по 2 000 элементов каждый. Такая ситуация может вызвать перегрузку сервера 1С и привести к снижению производительности или аварийному завершению работы. Поэтому для действительно больших объемов данных мы рекомендуем рассмотреть другие, более эффективные подходы.
Для оптимизации работы с сотнями тысяч и миллионами записей в 1С:Предприятии крайне важно избегать загрузки всего объема данных в оперативную память приложения сразу. Вместо этого, мы будем использовать возможности СУБД и платформы 1С для обработки данных частями, что значительно снижает потребление памяти и повышает производительность.
Наиболее эффективным способом работы с данными, когда они изначально находятся в базе, является не выгрузка всего результата запроса в массив или таблицу значений, а пошаговая обработка Выборки результата запроса. Это позволяет избежать чрезмерного потребления оперативной памяти на сервере 1С и клиенте.
Как это работает:
Выборки: После выполнения запроса метод Выбрать() возвращает объект ВыборкаИзРезультатаЗапроса, который позволяет последовательно перебирать записи, не загружая их все в память.Пока Выборка.Следующий() собирать нужное количество элементов для "пачки" во временный массив.Посмотрим на пример логики с выборкой (без выгрузки всего в массив):
// Предположим, у нас есть процедура для обработки пачки данных
Процедура ОбработатьПачку(ПачкаДанных)
// Здесь размещаем логику обработки, например, поиск ссылок
Сообщить("Обработка пачки из " + ПачкаДанных.Количество() + " элементов.");
КонецПроцедуры;
// Основная функция для итерационной обработки
Функция ОбработатьНоменклатуруПачками()
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ Ссылка ИЗ Справочник.Номенклатура КАК Номенклатура";
Выборка = Запрос.Выполнить().Выбрать();
РазмерПачки = 2000;
ТекущаяПачка = Новый Массив();
Пока Выборка.Следующий() Цикл
ТекущаяПачка.Добавить(Выборка.Ссылка);
Если ТекущаяПачка.Количество() = РазмерПачки Тогда
// Здесь вызываем функцию обработки пачки
ОбработатьПачку(ТекущаяПачка);
ТекущаяПачка = Новый Массив(); // Очищаем пачку для следующих элементов
КонецЕсли;
КонецЦикла;
// Обработать остатки, если последняя пачка неполная
Если ТекущаяПачка.Количество() > 0 Тогда
ОбработатьПачку(ТекущаяПачка);
КонецЕсли;
КонецФункции
Этот подход минимизирует пиковые нагрузки на память, поскольку в памяти одновременно находится только одна "пачка" данных, а не весь исходный массив.
Также важно знать, как работать с количеством записей в выборке. Например, для проверки, есть ли 0, 1 или более записей, нам не нужно выгружать все в таблицу значений:
// Пример проверки количества записей в выборке
Функция ПроверитьКоличествоЗаписейВВыборке(Запрос)
РезультатЗ = Запрос.Выполнить();
Если РезультатЗ.Пустой() Тогда
Возврат "Пусто, записей нет.";
Иначе
ВыборкаЗ = РезультатЗ.Выбрать();
Если ВыборкаЗ.Количество() = 1 Тогда
// Переходим на первую (и единственную) запись для работы с ней
ВыборкаЗ.Следующий();
Возврат "Тут 1 строка: " + ВыборкаЗ.Ссылка; // Пример получения значения
Иначе
Сообщить("Тут много строк, первая из них: " + ВыборкаЗ.Следующий().Ссылка); // Пример получения первой записи, если > 1
Возврат "Тут много строк.";
КонецЕсли;
КонецЕсли;
КонецФункции
В этом примере мы используем методы Пустой() и Количество() объекта ВыборкаИзРезультатаЗапроса для эффективной проверки без избыточного расхода памяти.
Подход с использованием временных таблиц и итеративного выполнения запроса с параметром для получения "страниц" данных очень эффективен. Он позволяет полностью переложить нагрузку по разбиению на части на СУБД, что, как правило, гораздо быстрее и эффективнее, чем программное манипулирование массивами в 1С. Этот метод особенно ценен, когда нужно обеспечить высокую производительность при работе с очень большими объемами данных.
Как это работает:
АВТОНОМЕРЗАПИСИ(), которая присваивает уникальный последовательный номер каждой записи в результате запроса.ВЫБРАТЬ ПЕРВЫЕ N (где N - размер нашей пачки) и условие ГДЕ НомерЗаписи > &ПоследнийНомер для получения следующей порции данных.&ПоследнийНомер, чтобы следующий запрос начинался с того места, где закончился предыдущий.Рассмотрим пример реализации этого подхода:
// Предположим, у нас есть процедура для обработки пачки данных,
// которая принимает массив ссылок
Процедура ЧотаДелаюСМассивом(МассивСсылок)
// Здесь размещаем логику обработки, например, поиск ссылок
Сообщить("Обработка пачки из " + МассивСсылок.Количество() + " элементов.");
КонецПроцедуры;
// Основная функция для обработки данных через пагинацию запросами
Функция ОбработатьНоменклатуруПачкамиЧерезЗапрос()
М = Новый МенеджерВременныхТаблиц; // Создаем менеджер временных таблиц
РазмерПачки = 2000;
ЗапросПервичный = Новый Запрос;
ЗапросПервичный.Текст =
"ВЫБРАТЬ
| АВТОНОМЕРЗАПИСИ() КАК Номер,
| Ссылка КАК Ссылка
|ПОМЕСТИТЬ Т
|ИЗ Справочник.Номенклатура
|ИНДЕКСИРОВАТЬ По Номер
|УПОРЯДОЧИТЬ ПО Номер"; // Важно: добавляем УПОРЯДОЧИТЬ ПО
ЗапросПервичный.МенеджерВременныхТаблиц = М;
ЗапросПервичный.Выполнить(); // Заполняем временную таблицу Т
ЗапросПагинации = Новый Запрос;
ЗапросПагинации.Текст = "ВЫБРАТЬ ПЕРВЫЕ &РазмерПачки Ссылка ИЗ Т ГДЕ Номер > &ПоследнийНомер УПОРЯДОЧИТЬ ПО Номер";
ЗапросПагинации.МенеджерВременныхТаблиц = М; // Используем тот же менеджер ВТ
ПоследнийНомер = 0; // Инициализируем параметр для первого запроса
Пока Истина Цикл
ЗапросПагинации.УстановитьПараметр("РазмерПачки", РазмерПачки);
ЗапросПагинации.УстановитьПараметр("ПоследнийНомер", ПоследнийНомер);
РезультатЗапроса = ЗапросПагинации.Выполнить();
Если РезультатЗапроса.Пустой() Тогда
Прервать; // Нет больше данных
КонецЕсли;
// Получаем ссылки из текущей пачки
ТекущаяПачкаСсылок = РезультатЗапроса.Выгрузить().ВыгрузитьКолонку("Ссылка");
// Обрабатываем полученную пачку
ЧотаДелаюСМассивом(ТекущаяПачкаСсылок);
// Обновляем ПоследнийНомер для следующего запроса
// Берем Номер последней записи в текущей пачке, чтобы следующая пачка начиналась после нее
// Если РезультатЗапроса.Количество() < РазмерПачки, то это была последняя пачка
ПоследнийНомер = ПоследнийНомер + ТекущаяПачкаСсылок.Количество();
КонецЦикла;
КонецФункции
Важное замечание по `УПОРЯДОЧИТЬ ПО`: Для корректной работы последовательной выборки с ВЫБРАТЬ ПЕРВЫЕ и условием по номеру записи, критически важно убедиться, что данные во временной таблице или в основном запросе упорядочены (например, по НомерЗаписи или по ключевому полю). Иначе порядок выборки может быть неопределенным, и некоторые элементы могут быть пропущены или продублированы в разных пачках. Мы добавили УПОРЯДОЧИТЬ ПО Номер в оба запроса для обеспечения стабильности.
Альтернативный запрос для получения индекса пачки:
Существует также вариант запроса, который сразу вычисляет индекс пачки для каждой записи, позволяя сгруппировать данные по этому индексу. Это может быть полезно, если вы хотите получить весь набор данных, уже размеченный по пачкам, и затем обработать их, например, в ДеревоЗначений или ТаблицуЗначений.
// Пример запроса с вычислением индекса пачки
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Номенклатура.Ссылка,
| АВТОНОМЕРЗАПИСИ() КАК НомерЗаписи
|ПОМЕСТИТЬ Таблица
|ИЗ
| Справочник.Номенклатура КАК Номенклатура
|ИНДЕКСИРОВАТЬ ПО НомерЗаписи
|УПОРЯДОЧИТЬ ПО НомерЗаписи
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| Таблица.Ссылка,
| ЦЕЛ(Таблица.НомерЗаписи / &РазмерПорции) КАК ИндексПорции
|ИЗ
| Таблица КАК Таблица
|УПОРЯДОЧИТЬ ПО ИндексПорции, Ссылка"; // Упорядочиваем для стабильности
В этом запросе мы сначала помещаем все ссылки номенклатуры с их порядковыми номерами во временную таблицу Таблица. Затем во втором запросе для каждой ссылки вычисляем ИндексПорции, деля НомерЗаписи на РазмерПорции нацело. Этот индекс позволит нам программно группировать записи в пачки.
Иногда, несмотря на потенциальные проблемы с памятью при больших объемах, может быть удобно сначала выгрузить результат запроса в ТаблицуЗначений (ТЗ), а затем уже в ней производить разбиение на пачки. Этот подход может быть оправдан, если:
ТаблицаЗначений необходима для последующей интерактивной работы, фильтрации с помощью НайтиСтрок() или сложных манипуляций, которые проще реализовать в ТЗ.Как это работает:
ТаблицуЗначений.Скопировать() с отбором по "НомерПачки", чтобы получить каждую отдельную пачку как новую ТЗ.Пример кода для заполнения колонки "НомерПачки" в ТЗ:
// Предположим, ТЗ уже содержит все данные из запроса
// ТЗ = Запрос.Выполнить().Выгрузить();
РазмерПачки = 2000;
// Добавляем колонку для номера пачки
ТЗ.Колонки.Добавить("НомерПачки", Новый ОписаниеТипов("Число"));
Для Каждого ТекСтр Из ТЗ Цикл
// Используем Цел от деления индекса строки на размер пачки
// Добавляем 1, чтобы нумерация пачек начиналась с 1
ТекСтр.НомерПачки = Цел(ТЗ.Индекс(ТекСтр) / РазмерПачки) + 1;
КонецЦикла;
// Теперь ТЗ готова к разбиению на пачки с помощью метода Скопировать()
// Например, для получения первой пачки (номер 1):
// Пачка1 = ТЗ.Скопировать(Новый Структура("НомерПачки", 1));
После заполнения колонки "НомерПачки" мы можем в цикле перебирать номера пачек и использовать метод Скопировать() для получения отдельных ТаблицЗначений, каждая из которых будет представлять собой одну пачку данных.
// Продолжение примера: итерация по пачкам
КоличествоПачек = Цел(ТЗ.Количество() / РазмерПачки) + ?(ТЗ.Количество() % РазмерПачки > 0, 1, 0);
Для ИндексПачки = 1 По КоличествоПачек Цикл
ПачкаДанныхТЗ = ТЗ.Скопировать(Новый Структура("НомерПачки", ИндексПачки));
// Теперь ПачкаДанныхТЗ содержит текущую пачку, которую можно обработать
// Например:
Сообщить("Обработана пачка №" + ИндексПачки + " с " + ПачкаДанныхТЗ.Количество() + " элементами.");
// Дополнительная логика обработки ПачкаДанныхТЗ
КонецЦикла;
Этот подход позволяет использовать все удобства работы с ТаблицейЗначений, но требует внимательности к объему загружаемых данных.
Выбор метода для разбиения массива или коллекции на пачки в 1С зависит от нескольких ключевых факторов:
Выборки результата запроса, либо разбиение данных на части средствами СУБД с использованием временных таблиц и параметризованных запросов. Эти методы существенно повышают производительность и стабильность работы системы.Выборки или запросов с пагинацией является предпочтительным, так как позволяет работать с данными напрямую из СУБД.Выборки без предварительного полного разбиения на массив массивов будет наиболее эффективным.В заключение, всегда стремитесь к максимальной производительности и минимальному потреблению памяти при работе с большими данными. Хотя "лаконичный код" желателен, в контексте работы с платформой 1С иногда приходится жертвовать кажущейся простотой ради эффективной работы с памятью и процессором СУБД. Выбирайте подход, который наилучшим образом соответствует вашим задачам и ограничениям системы.