Как разбить массив или коллекцию на пачки заданного размера в 1С?

Программист 1С v8.3 (Управляемые формы)
← На главную

Вопрос разбиения массивов или коллекций на более мелкие порции часто возникает при работе с большими объемами данных в 1С:Предприятии. Рассмотрим типичную ситуацию: нам необходимо обработать полмиллиона позиций номенклатуры, например, для поиска ссылок на них, но выполнять такую операцию для всего массива сразу система не позволяет, выдавая ошибку или зависая. В таком случае оптимальным решением становится обработка данных пачками, скажем, по 2000 позиций. Как же эффективно реализовать такую логику в 1С? Мы рассмотрим несколько подходов, начиная от прямого программного разбиения и заканчивая более эффективными методами, ориентированными на работу с большими данными.

Программное разбиение массива на пачки

Начнем с самого прямого способа – написания функции, которая программно разбивает исходный массив на массив массивов (пачек) заданного размера. Этот подход вполне подойдет для массивов среднего размера, когда объем данных не критичен для оперативной памяти.

Суть метода: Мы будем итерировать по элементам исходной коллекции и по мере заполнения текущей пачки добавлять ее в результирующий массив, затем создавая новую пустую пачку.

Давайте посмотрим на несколько реализаций.

Реализация 1: Итеративный подход с явным сбросом пачки

Это одна из наиболее интуитивно понятных реализаций, предложенная в обсуждении. Мы создаем новую пачку каждый раз, когда предыдущая достигает заданного размера, или в самом начале цикла. Проанализируем ее:


Функция РазбитьМассивПоПачкам(М, РазмерПачки)
  ММ = Новый Массив();
  Р = Неопределено;
  Для Каждого Эл из М Цикл
    Если Р = Неопределено Тогда
      Р = Новый Массив(); ММ.Добавить(Р);
    КонецЕсли;
    Р.Добавить(Эл);
    Если Р.Количество() = РазмерПачки Тогда
      Р = Неопределено;
    КонецЕсли;
  КонецЦикла;
  Возврат ММ;
КонецФункции

Как это работает:

  1. Создаем основной массив ММ, в который будем добавлять наши пачки.
  2. Переменная Р (текущая пачка) инициализируется как Неопределено.
  3. В цикле Для Каждого Эл из М Цикл:
    • Если Р еще не определена (что происходит при первом элементе или после того, как предыдущая пачка была "сброшена"), мы создаем новый массив для Р и добавляем его в ММ.
    • Добавляем текущий элемент Эл в Р.
    • Если количество элементов в Р достигло РазмерПачки, мы сбрасываем Р в Неопределено. Это сигнализирует о том, что для следующего элемента нужно будет создать новую пачку.
  4. В конце функция возвращает ММ – массив, содержащий все пачки.

Реализация 2: Использование оператора деления по модулю

Этот подход использует математический оператор деления по модулю (%) для определения момента создания новой пачки. Он также достаточно компактен и эффективен.


Функция ПорционироватьКоллекцию(Коллекция, РазмерПорции) Экспорт
    Результат = Новый Массив;
    Счетчик = 0;
    Для каждого Эл Из Коллекция Цикл
        Если Счетчик%РазмерПорции = 0 Тогда
            ТекПорция = Новый Массив;
            Результат.Добавить(ТекПорция);
        КонецЕсли;                                  
        ТекПорция.Добавить(Эл);
        Счетчик = Счетчик+1;
    КонецЦикла;                                 
    Возврат Результат;
КонецФункции

Как это работает:

  1. Инициализируем Результат – массив для хранения пачек, и Счетчик элементов.
  2. При каждом проходе цикла проверяем: если Счетчик делится на РазмерПорции без остатка, это означает, что нам нужна новая пачка.
    • Создаем ТекПорция как новый массив.
    • Добавляем ТекПорция в Результат.
  3. Добавляем текущий элемент Эл в ТекПорция.
  4. Увеличиваем Счетчик.

Важный момент здесь – переменная ТекПорция указывает на последний добавленный массив, что позволяет нам постоянно добавлять в него элементы, пока не придет время создать новую пачку.

Реализация 3: Использование индекса последней пачки

Еще один вариант программного разбиения, который держит ссылку на последнюю пачку и проверяет ее заполненность.


Функция РазбитьМассивПоПачкам(М, РазмерПачки)
  Р = Новый Массив();
  Если М.Количество() <> 0 Тогда Р.Добавить(Новый Массив()); КонецЕсли;
  Для Каждого Эл из М Цикл
    Если Р[Р.Количество() - 1].Количество() = РазмерПачки Тогда
      Р.Добавить(Новый Массив());
    КонецЕсли;
    Р[Р.Количество() - 1].Добавить(Эл);
  КонецЦикла;
  Возврат Р;
КонецФункции

Как это работает:

  1. Инициализируем Р – массив для хранения пачек.
  2. Если исходный массив М не пустой, сразу добавляем в Р первую пустую пачку.
  3. В цикле Для Каждого Эл из М Цикл:
    • Проверяем, если количество элементов в последней добавленной пачке (Р[Р.Количество() - 1]) равно РазмерПачки, то создаем и добавляем новую пачку в Р.
    • Добавляем текущий элемент Эл в последнюю пачку.

Реализация 4: Математический подход с прямым расчетом индекса пачки

Этот метод отличается от предыдущих тем, что не использует условные операторы для создания новой пачки, а вычисляет индекс пачки для каждого элемента с помощью деления нацело.


Функция ПорционироватьКоллекцию(Коллекция, РазмерПорции) Экспорт
    Результат = Новый Массив;
    Для Сч = 0 По Коллекция.ВГраница() Цикл
        Если Сч%РазмерПорции = 0 Тогда
            Результат.Добавить(Новый Массив);
        КонецЕсли;          
        Результат[Цел(Сч/РазмерПорции)].Добавить(Коллекция[Сч]);
    КонецЦикла;                                 
    Возврат Результат;
КонецФункции

Как это работает:

  1. Инициализируем Результат – массив для хранения пачек.
  2. В цикле Для Сч = 0 По Коллекция.ВГраница() Цикл:
    • Если текущий счетчик Сч делится на РазмерПорции без остатка, это означает, что мы начинаем новую пачку, и мы добавляем новый массив в Результат.
    • Затем элемент Коллекция[Сч] добавляется в пачку, индекс которой вычисляется как Цел(Сч/РазмерПорции). Это гарантирует, что каждый элемент попадет в свою пачку, а пачки будут создаваться по мере необходимости.

Недостатки программного разбиения для больших данных:

Хотя эти программные решения функциональны, при работе с 500 000 позициями или более они могут привести к значительному потреблению оперативной памяти. В памяти одновременно будут находиться исходный массив на 500 000 элементов, а также массив, состоящий из 250 (500000 / 2000) массивов по 2 000 элементов каждый. Такая ситуация может вызвать перегрузку сервера 1С и привести к снижению производительности или аварийному завершению работы. Поэтому для действительно больших объемов данных мы рекомендуем рассмотреть другие, более эффективные подходы.

Эффективная обработка больших объемов данных: Работа с выборками и запросами

Для оптимизации работы с сотнями тысяч и миллионами записей в 1С:Предприятии крайне важно избегать загрузки всего объема данных в оперативную память приложения сразу. Вместо этого, мы будем использовать возможности СУБД и платформы 1С для обработки данных частями, что значительно снижает потребление памяти и повышает производительность.

Подход 1: Итерационная обработка Выборки результата запроса

Наиболее эффективным способом работы с данными, когда они изначально находятся в базе, является не выгрузка всего результата запроса в массив или таблицу значений, а пошаговая обработка Выборки результата запроса. Это позволяет избежать чрезмерного потребления оперативной памяти на сервере 1С и клиенте.

Как это работает:

  1. Выполнение запроса и получение Выборки: После выполнения запроса метод Выбрать() возвращает объект ВыборкаИзРезультатаЗапроса, который позволяет последовательно перебирать записи, не загружая их все в память.
  2. Формирование пачки в цикле: Вместо того чтобы сразу выгружать все данные, мы можем в цикле Пока Выборка.Следующий() собирать нужное количество элементов для "пачки" во временный массив.
  3. Обработка и очистка: После формирования пачки мы вызываем функцию обработки, а затем очищаем временный массив для продолжения сбора следующей пачки.
  4. Обработка остатков: Важно не забыть обработать последнюю, возможно, неполную пачку после завершения основного цикла.

Посмотрим на пример логики с выборкой (без выгрузки всего в массив):


// Предположим, у нас есть процедура для обработки пачки данных
Процедура ОбработатьПачку(ПачкаДанных)
    // Здесь размещаем логику обработки, например, поиск ссылок
    Сообщить("Обработка пачки из " + ПачкаДанных.Количество() + " элементов.");
КонецПроцедуры;

// Основная функция для итерационной обработки
Функция ОбработатьНоменклатуруПачками()

    Запрос = Новый Запрос;
    Запрос.Текст = "ВЫБРАТЬ Ссылка ИЗ Справочник.Номенклатура КАК Номенклатура";
    Выборка = Запрос.Выполнить().Выбрать();

    РазмерПачки = 2000;
    ТекущаяПачка = Новый Массив();

    Пока Выборка.Следующий() Цикл
        ТекущаяПачка.Добавить(Выборка.Ссылка);

        Если ТекущаяПачка.Количество() = РазмерПачки Тогда
            // Здесь вызываем функцию обработки пачки
            ОбработатьПачку(ТекущаяПачка);
            ТекущаяПачка = Новый Массив(); // Очищаем пачку для следующих элементов
        КонецЕсли;
    КонецЦикла;

    // Обработать остатки, если последняя пачка неполная
    Если ТекущаяПачка.Количество() > 0 Тогда
        ОбработатьПачку(ТекущаяПачка);
    КонецЕсли;

КонецФункции

Этот подход минимизирует пиковые нагрузки на память, поскольку в памяти одновременно находится только одна "пачка" данных, а не весь исходный массив.

Также важно знать, как работать с количеством записей в выборке. Например, для проверки, есть ли 0, 1 или более записей, нам не нужно выгружать все в таблицу значений:


// Пример проверки количества записей в выборке
Функция ПроверитьКоличествоЗаписейВВыборке(Запрос)

    РезультатЗ = Запрос.Выполнить();
    Если РезультатЗ.Пустой() Тогда
        Возврат "Пусто, записей нет.";
    Иначе
        ВыборкаЗ = РезультатЗ.Выбрать();
        Если ВыборкаЗ.Количество() = 1 Тогда
            // Переходим на первую (и единственную) запись для работы с ней
            ВыборкаЗ.Следующий(); 
            Возврат "Тут 1 строка: " + ВыборкаЗ.Ссылка; // Пример получения значения
        Иначе
            Сообщить("Тут много строк, первая из них: " + ВыборкаЗ.Следующий().Ссылка); // Пример получения первой записи, если > 1
            Возврат "Тут много строк.";
        КонецЕсли;    
    КонецЕсли;

КонецФункции

В этом примере мы используем методы Пустой() и Количество() объекта ВыборкаИзРезультатаЗапроса для эффективной проверки без избыточного расхода памяти.

Подход 2: Пагинация данных с помощью временных таблиц и запросов

Подход с использованием временных таблиц и итеративного выполнения запроса с параметром для получения "страниц" данных очень эффективен. Он позволяет полностью переложить нагрузку по разбиению на части на СУБД, что, как правило, гораздо быстрее и эффективнее, чем программное манипулирование массивами в 1С. Этот метод особенно ценен, когда нужно обеспечить высокую производительность при работе с очень большими объемами данных.

Как это работает:

  1. Подготовка временной таблицы: Сначала загружаем все необходимые данные во временную таблицу (ВТ), добавляя колонку с порядковым номером записи. Для этого отлично подходит функция АВТОНОМЕРЗАПИСИ(), которая присваивает уникальный последовательный номер каждой записи в результате запроса.
  2. Циклическое выполнение запроса: Затем в цикле выполняем запросы к этой временной таблице. Каждый запрос использует оператор ВЫБРАТЬ ПЕРВЫЕ N (где N - размер нашей пачки) и условие ГДЕ НомерЗаписи > &ПоследнийНомер для получения следующей порции данных.
  3. Обновление параметра: После обработки каждой пачки мы обновляем параметр &ПоследнийНомер, чтобы следующий запрос начинался с того места, где закончился предыдущий.

Рассмотрим пример реализации этого подхода:


// Предположим, у нас есть процедура для обработки пачки данных, 
// которая принимает массив ссылок
Процедура ЧотаДелаюСМассивом(МассивСсылок)
    // Здесь размещаем логику обработки, например, поиск ссылок
    Сообщить("Обработка пачки из " + МассивСсылок.Количество() + " элементов.");
КонецПроцедуры;

// Основная функция для обработки данных через пагинацию запросами
Функция ОбработатьНоменклатуруПачкамиЧерезЗапрос()

    М = Новый МенеджерВременныхТаблиц; // Создаем менеджер временных таблиц
    РазмерПачки = 2000;

    ЗапросПервичный = Новый Запрос;
    ЗапросПервичный.Текст = 
    "ВЫБРАТЬ 
    |    АВТОНОМЕРЗАПИСИ() КАК Номер, 
    |    Ссылка КАК Ссылка 
    |ПОМЕСТИТЬ Т 
    |ИЗ Справочник.Номенклатура 
    |ИНДЕКСИРОВАТЬ По Номер
    |УПОРЯДОЧИТЬ ПО Номер"; // Важно: добавляем УПОРЯДОЧИТЬ ПО

    ЗапросПервичный.МенеджерВременныхТаблиц = М;
    ЗапросПервичный.Выполнить(); // Заполняем временную таблицу Т

    ЗапросПагинации = Новый Запрос;
    ЗапросПагинации.Текст = "ВЫБРАТЬ ПЕРВЫЕ &РазмерПачки Ссылка ИЗ Т ГДЕ Номер > &ПоследнийНомер УПОРЯДОЧИТЬ ПО Номер"; 
    ЗапросПагинации.МенеджерВременныхТаблиц = М; // Используем тот же менеджер ВТ

    ПоследнийНомер = 0; // Инициализируем параметр для первого запроса

    Пока Истина Цикл         
        ЗапросПагинации.УстановитьПараметр("РазмерПачки", РазмерПачки);
        ЗапросПагинации.УстановитьПараметр("ПоследнийНомер", ПоследнийНомер);
        
        РезультатЗапроса = ЗапросПагинации.Выполнить();
        
        Если РезультатЗапроса.Пустой() Тогда
            Прервать; // Нет больше данных
        КонецЕсли;
        
        // Получаем ссылки из текущей пачки
        ТекущаяПачкаСсылок = РезультатЗапроса.Выгрузить().ВыгрузитьКолонку("Ссылка"); 
        
        // Обрабатываем полученную пачку
        ЧотаДелаюСМассивом(ТекущаяПачкаСсылок); 
        
        // Обновляем ПоследнийНомер для следующего запроса
        // Берем Номер последней записи в текущей пачке, чтобы следующая пачка начиналась после нее
        // Если РезультатЗапроса.Количество() < РазмерПачки, то это была последняя пачка
        ПоследнийНомер = ПоследнийНомер + ТекущаяПачкаСсылок.Количество();
        
    КонецЦикла;

КонецФункции

Важное замечание по `УПОРЯДОЧИТЬ ПО`: Для корректной работы последовательной выборки с ВЫБРАТЬ ПЕРВЫЕ и условием по номеру записи, критически важно убедиться, что данные во временной таблице или в основном запросе упорядочены (например, по НомерЗаписи или по ключевому полю). Иначе порядок выборки может быть неопределенным, и некоторые элементы могут быть пропущены или продублированы в разных пачках. Мы добавили УПОРЯДОЧИТЬ ПО Номер в оба запроса для обеспечения стабильности.

Альтернативный запрос для получения индекса пачки:

Существует также вариант запроса, который сразу вычисляет индекс пачки для каждой записи, позволяя сгруппировать данные по этому индексу. Это может быть полезно, если вы хотите получить весь набор данных, уже размеченный по пачкам, и затем обработать их, например, в ДеревоЗначений или ТаблицуЗначений.


// Пример запроса с вычислением индекса пачки
Запрос = Новый Запрос;
Запрос.Текст = 
"ВЫБРАТЬ
|    Номенклатура.Ссылка,
|    АВТОНОМЕРЗАПИСИ() КАК НомерЗаписи
|ПОМЕСТИТЬ Таблица
|ИЗ
|    Справочник.Номенклатура КАК Номенклатура
|ИНДЕКСИРОВАТЬ ПО НомерЗаписи
|УПОРЯДОЧИТЬ ПО НомерЗаписи
|;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
|    Таблица.Ссылка,
|    ЦЕЛ(Таблица.НомерЗаписи / &РазмерПорции) КАК ИндексПорции
|ИЗ
|    Таблица КАК Таблица
|УПОРЯДОЧИТЬ ПО ИндексПорции, Ссылка"; // Упорядочиваем для стабильности

В этом запросе мы сначала помещаем все ссылки номенклатуры с их порядковыми номерами во временную таблицу Таблица. Затем во втором запросе для каждой ссылки вычисляем ИндексПорции, деля НомерЗаписи на РазмерПорции нацело. Этот индекс позволит нам программно группировать записи в пачки.

Подход 3: Разбиение в таблице значений после выгрузки

Иногда, несмотря на потенциальные проблемы с памятью при больших объемах, может быть удобно сначала выгрузить результат запроса в ТаблицуЗначений (ТЗ), а затем уже в ней производить разбиение на пачки. Этот подход может быть оправдан, если:

  1. Объем данных является "средним" (например, десятки тысяч, а не сотни тысяч или миллионы), и вы уверены, что это не вызовет проблем с памятью.
  2. ТаблицаЗначений необходима для последующей интерактивной работы, фильтрации с помощью НайтиСтрок() или сложных манипуляций, которые проще реализовать в ТЗ.
  3. Для удобства отладки, так как с ТЗ зачастую работать в отладчике нагляднее.

Как это работает:

  1. Выгружаем весь результат запроса в ТаблицуЗначений.
  2. Добавляем в ТЗ новую колонку, например, "НомерПачки".
  3. Итерируем по строкам ТЗ и заполняем "НомерПачки" для каждой строки, основываясь на ее индексе и заданном размере пачки.
  4. Затем используем метод Скопировать() с отбором по "НомерПачки", чтобы получить каждую отдельную пачку как новую ТЗ.

Пример кода для заполнения колонки "НомерПачки" в ТЗ:


// Предположим, ТЗ уже содержит все данные из запроса
// ТЗ = Запрос.Выполнить().Выгрузить();

РазмерПачки = 2000;

// Добавляем колонку для номера пачки
ТЗ.Колонки.Добавить("НомерПачки", Новый ОписаниеТипов("Число"));

Для Каждого ТекСтр Из ТЗ Цикл
    // Используем Цел от деления индекса строки на размер пачки
    // Добавляем 1, чтобы нумерация пачек начиналась с 1
    ТекСтр.НомерПачки = Цел(ТЗ.Индекс(ТекСтр) / РазмерПачки) + 1;
КонецЦикла;

// Теперь ТЗ готова к разбиению на пачки с помощью метода Скопировать()
// Например, для получения первой пачки (номер 1):
// Пачка1 = ТЗ.Скопировать(Новый Структура("НомерПачки", 1));

После заполнения колонки "НомерПачки" мы можем в цикле перебирать номера пачек и использовать метод Скопировать() для получения отдельных ТаблицЗначений, каждая из которых будет представлять собой одну пачку данных.


// Продолжение примера: итерация по пачкам
КоличествоПачек = Цел(ТЗ.Количество() / РазмерПачки) + ?(ТЗ.Количество() % РазмерПачки > 0, 1, 0);

Для ИндексПачки = 1 По КоличествоПачек Цикл
    ПачкаДанныхТЗ = ТЗ.Скопировать(Новый Структура("НомерПачки", ИндексПачки));
    // Теперь ПачкаДанныхТЗ содержит текущую пачку, которую можно обработать
    // Например:
    Сообщить("Обработана пачка №" + ИндексПачки + " с " + ПачкаДанныхТЗ.Количество() + " элементами.");
    // Дополнительная логика обработки ПачкаДанныхТЗ
КонецЦикла;

Этот подход позволяет использовать все удобства работы с ТаблицейЗначений, но требует внимательности к объему загружаемых данных.

Обобщение и рекомендации

Выбор метода для разбиения массива или коллекции на пачки в 1С зависит от нескольких ключевых факторов:

  1. Объем данных:
    • Для небольших или средних массивов (до нескольких десятков тысяч элементов), программная реализация разбиения массива на части (как в первых четырех примерах) вполне приемлема и проста в написании. Она позволяет быстро получить желаемую структуру "массив массивов".
    • Для больших массивов (сотни тысяч и более элементов), категорически рекомендуется использовать подходы, которые не загружают весь объем данных в память 1С сразу. Это либо последовательная обработка Выборки результата запроса, либо разбиение данных на части средствами СУБД с использованием временных таблиц и параметризованных запросов. Эти методы существенно повышают производительность и стабильность работы системы.
  2. Источник данных:
    • Если данные уже находятся в памяти (например, получены из другого источника или являются результатом небольшой выборки), программное разбиение массива уместно.
    • Если данные извлекаются из базы данных, то использование Выборки или запросов с пагинацией является предпочтительным, так как позволяет работать с данными напрямую из СУБД.
  3. Цель обработки:
    • Если требуется передать каждую пачку в отдельную функцию или обработку, принимающую массив, то создание массива массивов необходимо.
    • Если же цель – просто последовательная обработка элементов в рамках одной операции, то использование Выборки без предварительного полного разбиения на массив массивов будет наиболее эффективным.

В заключение, всегда стремитесь к максимальной производительности и минимальному потреблению памяти при работе с большими данными. Хотя "лаконичный код" желателен, в контексте работы с платформой 1С иногда приходится жертвовать кажущейся простотой ради эффективной работы с памятью и процессором СУБД. Выбирайте подход, который наилучшим образом соответствует вашим задачам и ограничениям системы.

← На главную