Как эффективно выгрузить характеристики номенклатуры в текстовый файл, избегая ошибок нехватки памяти и медленной работы?

Программист 1С v8.3 (Управляемые формы) 1С:Управление торговлей Управленческий учет Торговля и дистрибуция
← На главную

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

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

1. Объединение запросов в один большой

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

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

Рассмотрим пример структуры такого запроса. Предположим, нам нужны Артикул, Наименование, текущий остаток, ширина, высота, вес и штрихкод для каждой номенклатуры, которая есть в наличии.


Запрос = Новый Запрос;
Запрос.Текст = 
    "ВЫБРАТЬ
    |    Номенклатура.Ссылка КАК Ссылка,
    |    Номенклатура.Артикул КАК Артикул,
    |    Номенклатура.Наименование КАК Наименование,
    |    Номенклатура.Вес КАК Вес,
    |    Номенклатура.Ширина КАК Ширина,
    |    Номенклатура.Высота КАК Высота,
    |    СвободныеОстатки.ВНаличииОстаток КАК Остаток,
    |    // Добавляем поля для картинок, штрихкодов и т.п.
    |    ЕСТЬNULL(НоменклатураПрисоединенныеФайлы.Файл, НЕОПРЕДЕЛЕНО) КАК Картинка,
    |    Штрихкоды.Штрихкод КАК Штрихкод
    |ИЗ
    |    Справочник.Номенклатура КАК Номенклатура
    |        ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.СвободныеОстатки КАК СвободныеОстатки
    |        ПО Номенклатура.Ссылка = СвободныеОстатки.Номенклатура
    |        ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.НоменклатураПрисоединенныеФайлы КАК НоменклатураПрисоединенныеФайлы
    |        ПО Номенклатура.Ссылка = НоменклатураПрисоединенныеФайлы.Объект
    |        ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.Штрихкоды КАК Штрихкоды
    |        ПО Номенклатура.Ссылка = Штрихкоды.Владелец
    |ГДЕ
    |    СвободныеОстатки.ВНаличииОстаток > 0"; // Отбираем только те, что есть на остатках

Выборка = Запрос.Выполнить().Выбрать();

Пока Выборка.Следующий() Цикл
    // Здесь мы обрабатываем данные и записываем их в файл
    // Например:
    // СтрокаДляФайла = Выборка.Артикул + ";" + Выборка.Наименование + ";" + Выборка.Остаток;
    // ...
КонецЦикла;

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

2. Оптимизация работы с большими текстовыми файлами

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

Вместо ТекстовыйДокумент мы должны использовать объект ЗаписьТекста.

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

Рассмотрим пример использования ЗаписьТекста в комбинации с выборкой данных:


ПутьКФайлу = "C:\Temp\ХарактеристикиНоменклатуры.txt";
Кодировка = КодировкиТекста.UTF8; // Или любая другая необходимая кодировка
Разделитель = Символы.Таб; // Или ";" или ","

Попытка
    Запись = Новый ЗаписьТекста(ПутьКФайлу, Кодировка);

    Запрос = Новый Запрос;
    Запрос.Текст = 
        "ВЫБРАТЬ
        |    Номенклатура.Артикул,
        |    Номенклатура.Наименование,
        |    СвободныеОстатки.ВНаличииОстаток
        |ИЗ
        |    Справочник.Номенклатура КАК Номенклатура
        |        ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.СвободныеОстатки КАК СвободныеОстатки
        |        ПО Номенклатура.Ссылка = СвободныеОстатки.Номенклатура
        |ГДЕ
        |    СвободныеОстатки.ВНаличииОстаток > 0";
    
    Выборка = Запрос.Выполнить().Выбрать();
    
    // Записываем заголовок файла
    Запись.ЗаписатьСтроку("Артикул" + Разделитель + "Наименование" + Разделитель + "Остаток");

    Пока Выборка.Следующий() Цикл
        СтрокаДляЗаписи = Выборка.Артикул + Разделитель 
                       + Выборка.Наименование + Разделитель 
                       + Строка(Выборка.ВНаличииОстаток);
        Запись.ЗаписатьСтроку(СтрокаДляЗаписи);
    КонецЦикла;

Исключение
    Сообщить("Ошибка при записи файла: " + ОписаниеОшибки());
КонецПопытки;

Если Запись <> Неопределено Тогда
    Запись.Закрыть();
КонецЕсли;

3. Порционная обработка данных (для очень больших объемов)

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

Использование СправочникВыборка

Для обхода элементов справочников без загрузки всех данных в память мы можем использовать объект СправочникВыборка. Этот объект возвращается методами Выбрать и ВыбратьИерархически у объекта типа СправочникМенеджер (например, Справочники.Номенклатура.Выбрать()). Ключевая особенность СправочникВыборка заключается в том, что он обходит элементы динамически, выбирая их порциями из базы данных. Такой подход позволяет эффективно работать с большими списками справочников, не загружая их целиком в память.


ВыборкаНоменклатуры = Справочники.Номенклатура.Выбрать();
Пока ВыборкаНоменклатуры.Следующий() Цикл
    // ВыборкаНоменклатуры.Ссылка - ссылка на текущий элемент
    // ВыборкаНоменклатуры.Наименование - наименование текущего элемента
    // И так далее для других реквизитов, которые можно получить без отдельного запроса
    // Например, если вес, ширина, высота - это реквизиты самого справочника Номенклатура,
    // то их можно получить напрямую из выборки.

    // Для получения остатков и других данных, требующих сложных соединений или регистров,
    // мы можем использовать кэшированный запрос с параметром.
КонецЦикла;

Однако СправочникВыборка не позволяет напрямую получить данные из регистров (например, остатки) или других связанных таблиц без выполнения дополнительных запросов. Поэтому мы часто используем комбинированный подход.

Комбинированный подход: СправочникВыборка + кэшированный запрос для остатков

Если нам нужно получить много данных по каждой позиции (например, остатки, которые хранятся в регистре), но при этом мы хотим минимизировать загрузку самой номенклатуры в память, мы можем объединить СправочникВыборка с параметризованным запросом.

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

ЗапросОстатков = Новый Запрос;
ЗапросОстатков.Текст = 
    "ВЫБРАТЬ
    |    СвободныеОстатки.ВНаличииОстаток КАК Остаток,
    |    Штрихкоды.Штрихкод КАК Штрихкод,
    |    НоменклатураПрисоединенныеФайлы.Файл КАК Картинка
    |ИЗ
    |    РегистрНакопления.СвободныеОстатки КАК СвободныеОстатки
    |        ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.Штрихкоды КАК Штрихкоды
    |        ПО СвободныеОстатки.Номенклатура = Штрихкоды.Владелец
    |        ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.НоменклатураПрисоединенныеФайлы КАК НоменклатураПрисоединенныеФайлы
    |        ПО СвободныеОстатки.Номенклатура = НоменклатураПрисоединенныеФайлы.Объект
    |ГДЕ
    |    СвободныеОстатки.Номенклатура = &Номенклатура
    |    И СвободныеОстатки.ВНаличииОстаток > 0"; // Отбираем только те, что есть на остатках

ВыборкаНоменклатуры = Справочники.Номенклатура.Выбрать();

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

Фоновые задания и параллельная обработка

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

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

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

Общие рекомендации по оптимизации запросов

Помимо рассмотренных подходов, мы всегда должны помнить об общих принципах оптимизации запросов в 1С:

  1. Использование индексов: Мы должны убедиться, что поля, по которым происходит отбор или соединение в запросах, индексированы. 1С автоматически создает индексы для ключевых полей, но для часто используемых реквизитов или измерений регистров, по которым ведется поиск, может потребоваться создание дополнительных индексов.
  2. Оптимизация виртуальных таблиц: При работе с виртуальными таблицами, такими как Остатки, важно правильно формировать условия. Как мы уже упоминали, для получения текущих остатков дату в запрос передавать не нужно, чтобы не задействовать таблицы движений. Всегда стараемся использовать все доступные измерения во внешнем запросе для виртуальной таблицы.
  3. Составные типы данных: Если мы получаем реквизиты объекта составного типа данных, происходит неявное соединение со всеми таблицами типов составного поля. В тяжелых выборках мы можем снизить затраты, приводя такие поля к нужным типам через конструкцию ВЫРАЗИТЬ(Поле КАК Тип).
  4. Избегание "ИЛИ" в отборах: Использование логической связки ИЛИ в условиях отбора запроса может снизить производительность, так как может привести к менее эффективному использованию индексов. В таких случаях мы можем разбить запрос на два отдельных и объединить результаты через ОБЪЕДИНИТЬ ВСЕ или использовать конструкцию Поле В (Значение1, Значение2).
  5. Выборка только необходимых полей: Мы всегда должны выбирать только те поля, которые действительно нужны для дальнейшей обработки, а не использовать ВЫБРАТЬ *. Это уменьшает объем передаваемых данных и нагрузку на сервер 1С и СУБД.
  6. Разыменование ссылочных полей: Обращение к свойствам объектов через точку (например, Номенклатура.Поставщик.Страна) в запросе является эффективным способом получения данных из связанных справочников и регистров.

Управление памятью и производительностью на сервере 1С

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

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

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

← На главную