При разработке на платформе 1С:Предприятие программисты часто сталкиваются с задачей пакетной обработки данных. Типичный пример: у нас есть таблица номенклатуры и плоская таблица с именами файлов фотографий. Нам необходимо распределить фотографии по товарам так, чтобы каждому товару досталось ровно по N штук (например, по 3 фотографии на каждый товар), объединив их имена в строку через запятую.
На первый взгляд задача кажется простой, однако при работе с большими объемами данных (более 100 000 строк) стандартные и очевидные алгоритмы могут приводить к фатальным ошибкам переполнения индексов или критическому снижению производительности системы. В этой статье мы подробно разберем, как правильно реализовать выборку строк с заданным шагом, проанализируем частые ошибки разработчиков и рассмотрим наиболее производительные методы решения этой задачи.
Рассмотрим стандартный подход, который часто пытаются применить начинающие разработчики. Он заключается в запуске цикла по основной таблице товаров и вызове функции получения списка файлов по расчетным индексам:
// Пример проблемного кода
Шаг = 3;
НомерСтроки = 1;
Для Каждого Стр Из ТЗ Цикл
Стр.СписокФото = ПолучитьСписокИмен(НомерСтроки, ТЗимен);
НомерСтроки = НомерСтроки + 1;
КонецЦикла;
Внутри функции получения имен обычно пишется цикл, который обращается к строкам таблицы по индексу:
Функция ПолучитьСписокИмен(НомерСтроки, ТЗимен)
ИтСтрокаСФото = "";
Для п = 1 По ВыбКоличествоФотона1товар Цикл
// Здесь происходит обращение к ТЗимен по индексу
ИтСтрокаСФото = ИтСтрокаСФото + "," + ТЗимен[Индекс].Имя;
КонецЦикла;
Возврат ИтСтрокаСФото;
КонецФункции
Проанализируем две критические проблемы этого кода:
ТЗимен не кратно шагу N, то на последней итерации цикл попытается обратиться к несуществующей строке таблицы значений. Это приведет к аварийному завершению работы всей обработки.ИтСтрокаСФото = ИтСтрокаСФото + "," + ... внутри циклов при больших объемах данных (те самые «100 500 строк») — это катастрофа для производительности. При каждом сложении строк платформа 1С выделяет новый блок памяти и копирует туда данные. На больших объемах это приводит к сильному замедлению работы и перегрузке сборщика мусора.Выяснив причины неэффективности стандартного подхода, рассмотрим правильные и оптимизированные способы решения этой задачи.
Вместо того чтобы для каждой строки основной таблицы запускать тяжелый вложенный цикл поиска в подчиненной таблице, мы можем реализовать один общий проход по массиву данных. Мы будем накапливать элементы во временный массив порциями по N штук, а затем переносить их в очередную строку таблицы товаров.
Для ускорения работы предварительно выгрузим колонку имен в плоский массив с помощью метода ВыгрузитьКолонку(). Это позволит избежать лишних обращений к объекту строки таблицы значений.
Рассмотрим этот алгоритм детально:
// Выгружаем колонку имен в плоский массив для максимального ускорения работы
МассивИмен = ТЗимен.ВыгрузитьКолонку("Имя");
ИндексТовара = 0;
КоличествоТоваров = ТЗ.Количество();
ВременныйМассивПорции = Новый Массив;
ШагN = ВыбКоличествоФотона1товар; // Заданный пользователем шаг N
Для Каждого ИмяФайла Из МассивИмен Цикл
ВременныйМассивПорции.Добавить(ИмяФайла);
// Если набрали нужный шаг N
Если ВременныйМассивПорции.Количество() = ШагN Тогда
Если ИндексТовара < КоличествоТоваров Тогда
// Платформенный метод СтрСоединить работает значительно быстрее конкатенации "+"
ТЗ[ИндексТовара].СписокФото = СтрСоединить(ВременныйМассивПорции, ", ");
ИндексТовара = ИндексТовара + 1;
КонецЕсли;
ВременныйМассивПорции.Очистить(); // Освобождаем массив для следующей порции
КонецЕсли;
КонецЦикла;
// Важно: Записываем оставшийся "хвост", если файлов не хватило на полный шаг N для последнего товара
Если ВременныйМассивПорции.Количество() > 0 И ИндексТовара < КоличествоТоваров Тогда
ТЗ[ИндексТовара].СписокФото = СтрСоединить(ВременныйМассивПорции, ", ");
КонецЕсли;
Почему этот способ работает быстрее всех? Во-первых, мы осуществляем всего один проход по массиву файлов. Во-вторых, мы используем встроенную функцию платформы СтрСоединить(), которая написана на C++ и осуществляет сборку строки в оперативной памяти практически мгновенно, минуя накладные расходы интерпретатора 1С.
Если вам часто приходится решать подобные задачи, хорошей практикой программирования будет вынесение логики разбиения списка элементов в отдельную универсальную функцию. Такая функция будет принимать на вход плоский массив и размер порции, а возвращать массив массивов (пакетов).
Разберем создание такой универсальной функции:
Функция РазбитьМассивНаПорции(ИсходныйМассив, РазмерПорции) Экспорт
МассивПорций = Новый Массив;
ТекущаяПорция = Новый Массив;
Для Каждого Элемент Из ИсходныйМассив Цикл
ТекущаяПорция.Добавить(Элемент);
Если ТекущаяПорция.Количество() = РазмерПорции Тогда
МассивПорций.Добавить(ТекущаяПорция);
ТекущаяПорция = Новый Массив; // Создаем новый пустой массив под следующую порцию
КонецЕсли;
КонецЦикла;
// Добавляем оставшийся "хвост", если он есть
Если ТекущаяПорция.Количество() > 0 Тогда
МассивПорций.Добавить(ТекущаяПорция);
КонецЕсли;
Возврат МассивПорций;
КонецФункции
Теперь мы можем применить эту функцию к нашей исходной задаче. Посмотрим на лаконичность и понятность итогового кода:
// Получаем плоский массив имен
МассивИмен = ТЗимен.ВыгрузитьКолонку("Имя");
// Разбиваем массив на порции по N элементов
ПорцииФото = РазбитьМассивНаПорции(МассивИмен, ВыбКоличествоФотона1товар);
// Заполняем основную таблицу товаров
КоличествоТоваров = ТЗ.Количество();
ПределЦикла = Мин(ПорцииФото.ВГраница(), КоличествоТоваров - 1);
Для Индекс = 0 По ПределЦикла Цикл
ТЗ[Индекс].СписокФото = СтрСоединить(ПорцииФото[Индекс], ", ");
КонецЦикла;
Этот метод отличается высокой масштабируемостью. Сама функция РазбитьМассивНаПорции полностью универсальна и не привязана к контексту конкретных таблиц значений, что позволяет использовать ее в любых других механизмах конфигурации.
Если по каким-то причинам архитектура вашего решения требует сохранения математического подхода с вычислением начального и конечного индекса строк (например, при реализации постраничной навигации или при жесткой привязке к номерам строк), вам необходимо использовать предохранитель в виде функции Мин().
Посмотрим, как переписать функцию выборки имен по индексам, чтобы она гарантированно не вызывала падение системы:
Функция ПолучитьСписокИменБезопасно(Начало, Конец, ТЗФото)
// Предотвращаем выход за пределы таблицы значений
РеальныйКонец = Мин(Конец, ТЗФото.Количество() - 1);
МассивИмен = Новый Массив;
Для Счетчик = Начало По РеальныйКонец Цикл
МассивИмен.Добавить(ТЗФото[Счетчик].Имя);
КонецЦикла;
Возврат СтрСоединить(МассивИмен, ", ");
КонецФункции
И вызов этой функции в основном цикле:
РазмерШага = ВыбКоличествоФотона1товар;
НомерСтроки = 1;
Для Каждого Стр Из ТЗ Цикл
// Вычисляем начальный и конечный индекс с учетом индексации с 0
ИндексНачала = РазмерШага * (НомерСтроки - 1);
ИндексКонца = (РазмерШага * НомерСтроки) - 1;
// Если начальный индекс уже превысил количество файлов, прерываем обработку
Если ИндексНачала >= ТЗимен.Количество() Тогда
Прервать;
КонецЕсли;
Стр.СписокФото = ПолучитьСписокИменБезопасно(ИндексНачала, ИндексКонца, ТЗимен);
НомерСтроки = НомерСтроки + 1;
КонецЦикла;
Данный вариант полностью безопасен, так как функция Мин() автоматически обрежет верхнюю границу итератора до реального размера таблицы данных, предотвращая падение по ошибке индексации.
Для того чтобы выбрать наиболее подходящий вариант для вашей задачи, сопоставим предложенные методы по ключевым критериям:
«Индекс находится за пределами границы» за счет использования проверок границ массивов.При работе с действительно большими объемами данных (сотни тысяч строк) мы рекомендуем полностью отказаться от классического сложения строк через оператор + и использовать исключительно связку методов ВыгрузитьКолонку() и СтрСоединить(). Это сохранит стабильность работы вашей информационной базы и сбережет ресурсы сервера.