При работе с внешними источниками данных разработчики 1С часто сталкиваются с необходимостью импорта или сверки информации из файлов формата DBF. Если размер файла составляет несколько мегабайт, стандартные методы платформы справляются с задачей за секунды. Однако, когда речь заходит о действительно больших объемах — например, файлах размером более 1.4 ГБ, содержащих свыше 120 миллионов строк, — простая попытка прочитать данные «в лоб» и загрузить их в массив или таблицу значений неизбежно приводит к аварийному завершению работы системы с ошибкой «Недостаточно памяти».
В этой статье мы подробно разберем, почему возникает эта проблема, проанализируем ошибки в архитектуре кода и рассмотрим эффективные практические решения, которые позволят вам быстро и стабильно обрабатывать гигантские массивы данных, сохраняя стабильность сервера 1С.
Перед тем как перейти к написанию кода, выясним причину, по которой сервер приложений 1С (процесс rphost) или клиентский сеанс начинают лавинообразно потреблять оперативную память. В исходном коде автора темы прослеживаются три критические архитектурные ошибки:
&НаКлиенте), а процедура сравнения — на сервере (&НаСервере). При каждом вызове серверного метода в цикле платформа 1С вынуждена сериализовать и передавать по сети весь контекст управляемой формы, включая тяжелые таблицы значений (такие как Реквизит1, Реквизит2 и другие). При миллионных объемах данных это мгновенно забивает ОЗУ.tempdb. Если делать это циклически для каждой порции данных, СУБД и менеджер временных таблиц 1С не будут успевать освобождать выделенные ресурсы.Рассмотрим подробнее три основных способа решения этой проблемы, начиная с оптимизации стандартного механизма XBase и заканчивая альтернативными системными архитектурами.
Самый простой способ избежать переполнения памяти — читать файл небольшими порциями (пакетами), обрабатывать каждую порцию независимо и сразу же очищать память перед чтением следующей. Объект 1С XBase не загружает весь DBF-файл в оперативную память целиком, а предоставляет последовательный доступ к физическим записям на диске. Нам нужно лишь научиться вовремя останавливать чтение и запоминать позицию курсора.
Для этого мы будем использовать два ключевых свойства объекта XBase:
НомерЗаписи (или RecNo) — возвращает физический порядковый номер текущей записи в таблице базы данных (начиная с 1).Перейти() (или GoTo()) — позиционирует курсор на запись по ее физическому порядковому номеру.Разберем по шагам правильную реализацию этого алгоритма. Весь процесс чтения и сравнения мы перенесем на сервер без передачи контекста формы (используя директиву &НаСервереБезКонтекста), чтобы исключить накладные расходы на сериализацию.
Посмотрим на пример кода, реализующий этот подход:
&НаСервереБезКонтекста
Процедура ОбработатьОгромныйФайлНаСервере(ПутьКФайлу, РазмерПартии)
// Инициализируем объект для работы с DBF
ФайлDBF = Новый XBase;
Попытка
ФайлDBF.ОткрытьФайл(ПутьКФайлу, , Истина); // Открываем только для чтения
Исключение
ЗаписатьЛогСобытий("Не удалось открыть файл: " + ОписаниеОшибки());
Возврат;
КонецПопытки;
// Получаем общее количество записей для контроля процесса
ВсегоЗаписей = ФайлDBF.КоличествоЗаписей();
ТекущийНомерЗаписи = 1;
// Подготавливаем структуру для временного хранения пакета данных
ТаблицаПакета = СоздатьТаблицуДляПакета();
Пока ТекущийНомерЗаписи <= ВсегоЗаписей Цикл
// Позиционируем курсор на начало очередной партии
ФайлDBF.Перейти(ТекущийНомерЗаписи);
СчетчикПартии = 0;
ТаблицаПакета.Очистить();
// Считываем порцию данных в таблицу значений
Пока НЕ ФайлDBF.ВКонце() И СчетчикПартии < РазмерПартии Цикл
НоваяСтрока = ТаблицаПакета.Добавить();
// Формируем уникальный ключ для сопоставления
СерияНомер = СокрЛП(ФайлDBF.Series) + СокрЛП(ФайлDBF.Number);
НоваяСтрока.СерияНомер = СтрЗаменить(СерияНомер, " ", "");
ФайлDBF.Следующая();
СчетчикПартии = СчетчикПартии + 1;
КонецЦикла;
// Запоминаем позицию, на которой остановились
ТекущийНомерЗаписи = ФайлDBF.НомерЗаписи;
// Передаем накопленный пакет на сравнение с базой данных
СверитьПакетДанныхСБазой(ТаблицаПакета);
// Принудительно очищаем память
ТаблицаПакета.Очистить();
// Позволяем операционной системе и сборщику мусора 1С высвободить ресурсы
Если ТекущийНомерЗаписи % (РазмерПартии * 10) = 0 Тогда
#Если Клиент Тогда
ОбработкаПрерыванияПользователя();
#КонецЕсли
КонецЕсли;
КонецЦикла;
ФайлDBF.ЗакрытьФайл();
КонецПроцедуры
В этом примере параметр РазмерПартии рекомендуется устанавливать в диапазоне от 50 000 до 100 000 записей. Это оптимальный баланс между скоростью работы СУБД и потреблением оперативной памяти.
Если для каждого прочитанного пакета из 100 000 строк вы будете выполнять классический запрос СУБД с передачей таблицы значений в качестве параметра, это все равно может сильно нагрузить систему. Проанализируем ситуацию: нам нужно сравнить данные файла с существующими записями в базе данных 1С.
Вместо постоянных запросов к СУБД гораздо эффективнее один раз выгрузить ключевые данные из базы 1С (например, действующие паспорта абонентов) в оперативную память сервера в виде объекта Соответствие. Поиск по ключу в объекте Соответствие (который реализован в платформе как хеш-таблица) происходит практически мгновенно и не требует обращений к СУБД в цикле.
Рассмотрим, как это реализовать на практике:
&НаСервереБезКонтекста
Функция ПолучитьКартуПаспортовИзБазы(КодПодразделения)
// Создаем хеш-таблицу для быстрого поиска
КартаПаспортов = Новый Соответствие;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ПаспортныеДанные.ДокументСерия КАК Серия,
| ПаспортныеДанные.ДокументНомер КАК Номер,
| Абоненты.Ссылка КАК Абонент
|ИЗ
| Справочник.Абоненты КАК Абоненты
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ПаспортныеДанныеФизЛиц.СрезПоследних КАК ПаспортныеДанные
| ПО Абоненты.ВладелецЛС = ПаспортныеДанные.ФизЛицо
|ГДЕ
| Абоненты.Подразделения = &Подразделения";
Запрос.УстановитьПараметр("Подразделения", КодПодразделения);
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
// Формируем уникальный ключ поиска
КлючСвязи = СокрЛП(Выборка.Серия) + СокрЛП(Выборка.Номер);
КлючСвязи = СтрЗаменить(КлючСвязи, " ", "");
// Записываем в соответствие: Ключ -> Ссылка на абонента
КартаПаспортов.Вставить(КлючСвязи, Выборка.Абонент);
КонецЦикла;
Возврат КартаПаспортов;
КонецФункции
Теперь при чтении очередной порции из DBF-файла нам не нужно делать запросы. Мы просто проверяем наличие ключа в нашей карте паспортов прямо в памяти сервера:
// Внутри цикла чтения файла DBF:
Пока НЕ ФайлDBF.ВКонце() И СчетчикПартии < РазмерПартии Цикл
СерияНомер = СокрЛП(ФайлDBF.Series) + СокрЛП(ФайлDBF.Number);
КлючПоиска = СтрЗаменить(СерияНомер, " ", "");
// Ищем совпадение в подготовленном соответствии
НайденныйАбонент = КартаПаспортов.Получить(КлючПоиска);
Если НайденныйАбонент <> Неопределено Тогда
// Совпадение найдено! Выполняем бизнес-логику (например, записываем в лог или ТЗ результатов)
РегистрироватьСовпадение(НайденныйАбонент, КлючПоиска);
КонецЕсли;
ФайлDBF.Следующая();
СчетчикПартии = СчетчикПартии + 1;
КонецЦикла;
Этот подход позволяет обрабатывать миллионы записей в минуту, так как вся работа по сопоставлению происходит в оперативной памяти без накладных расходов на обращения к диску и СУБД.
Если вы хотите полностью отказаться от написания сложного кода обхода файлов и доверить всю работу механизмам платформы и СУБД, идеальным решением станет использование механизма Внешних источников данных в 1С 8.3.
Этот способ позволяет подключить каталог с вашими DBF-файлами непосредственно к схеме базы данных 1С через ODBC-драйвер. Платформа будет видеть файлы DBF как обычные таблицы базы данных, к которым можно обращаться с помощью стандартных запросов 1С.
Разберем этот процесс по шагам:
КаталогDBF).После этого вам больше не нужно писать циклы чтения. Сравнение 120 миллионов строк с данными вашей базы сводится к одному простому и элегантному запросу:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ПаспортаИзФайла.Series КАК Серия,
| ПаспортаИзФайла.Number КАК Номер,
| Абоненты.Ссылка КАК Абонент
|ИЗ
| ВнешнийИсточникДанных.КаталогDBF.ТаблицаПаспортов КАК ПаспортаИзФайла
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.Абоненты КАК Абоненты
| ПО (Абоненты.ДокументСерия + Абоненты.ДокументНомер) = (ПаспортаИзФайла.Series + ПаспортаИзФайла.Number)";
Результат = Запрос.Выполнить().Выгрузить();
Важное преимущество: СУБД (например, MS SQL Server или PostgreSQL) выполнит это соединение на уровне своих внутренних алгоритмов оптимизации. Платформа 1С вообще не будет считывать 120 миллионов строк в память приложения — СУБД выполнит сопоставление и вернет в 1С только те строки, по которым были найдены реальные совпадения. Это самый быстрый и профессиональный метод работы с большими файлами.
Если вы все же решили использовать построчное чтение файлов, придерживайтесь следующих правил для предотвращения утечек памяти:
Неопределено (например, ТаблицаПакета = Неопределено;), чтобы сборщик мусора платформы 1С мог вовремя освободить оперативную память сервера.Применяя данные методики на практике, вы сможете легко справляться с обработкой любых объемов данных, а интеграция через Внешние источники данных позволит перевести скорость работы ваших решений на принципиально новый уровень.