Каждый разработчик на платформе 1С:Предприятие рано или поздно сталкивается с задачами импорта и обработки огромных массивов данных. Это могут быть ежедневные отчеты о продажах крупных федеральных сетей, транзакции платежных систем или логистические реестры. При попытке загрузить такой объем данных «в лоб» через стандартную табличную часть документа мы неизбежно упираемся в классическое и, казалось бы, непреодолимое ограничение платформы — не более 99 999 строк в одной табличной части.
В этой статье мы подробно разберем, почему возникает это ограничение, как его можно обойти архитектурными методами в старых версиях платформы, а также рассмотрим революционное решение этой проблемы, которое фирма «1С» представила в последних обновлениях платформы.
---
Выясним причину этого лимита на уровне СУБД. Исторически платформа 1С проектировалась с жесткой структурой таблиц базы данных. Для каждой табличной части любого документа в СУБД (будь то MS SQL, PostgreSQL или IBM DB2) создается отдельная физическая таблица. В этой таблице за позиционирование строк отвечает служебное поле номера строки — _LineNo.
Для оптимизации размера базы данных и ускорения индексации разработчики платформы изначально зафиксировали точность этого поля как numeric(5, 0). Это означает, что СУБД физически не может записать в данную колонку число, состоящее более чем из 5 цифр. Максимальное число, которое помещается в этот диапазон — 99 999. При попытке записать 100 000-ю строку СУБД возвращает ошибку переполнения, и транзакция записи документа полностью откатывается. При этом в оперативной памяти (на стороне сервера приложений или тонкого клиента) таблица значений может содержать миллионы строк, но при попытке вызова метода Записать() происходит аварийное завершение транзакции.
---
Рассмотрим долгожданное штатное решение проблемы, которое появилось в платформе начиная с версии 8.3.27. Фирма «1С» переработала внутреннюю структуру СУБД и предоставила разработчикам прямой инструмент для управления этим лимитом.
В свойствах объекта метаданных «Табличная часть» появилось новое стандартное свойство — «Длина номера строки» (в кодовой базе определяемое как LineNumberLength).
Разберем по шагам, как настроить это свойство в конфигураторе:
Если мы выберем максимальное значение 9, СУБД переопределит тип поля номера строки, что позволит хранить в одной табличной части до 999 999 999 строк (почти один миллиард записей!). Начиная с версии 8.3.28, это свойство также можно будет переопределять непосредственно в расширениях конфигурации, что позволит увеличивать лимиты в типовых базах без снятия их с поддержки.
---
Если ваша информационная база работает на версии платформы ниже 8.3.27 или вы хотите избежать чрезмерного раздувания транзакционных таблиц документов, нам необходимо использовать проверенные архитектурные обходы. Проанализируем самые эффективные из них.
Это один из самых надежных и производительных способов, который успешно применил автор исходного обсуждения. Суть метода заключается в том, что «сырые» строки документа хранятся не в самом документе, а в независимом или подчиненном регистре сведений, где ключевым измерением выступает ссылка на наш документ-регистратор.
Посмотрим на пример реализации данного механизма:
Документ.ЗагрузкаПродаж) вообще без табличной части. Он будет выступать только в роли регистратора и контейнера для общих реквизитов (дата, склад, контрагент).РегистрСведений.ДанныеТабличныхЧастейДокументов.ДокументСсылка (тип: ДокументСсылка.ЗагрузкаПродаж, свойство «Ведущее» — Истина. Это важно, так как при удалении документа его строки в регистре очистятся автоматически).ИндексСтроки (тип: Число(10, 0). Заменяет стандартный номер строки табличной части).При таком подходе мы можем программно считывать данные файла и записывать их напрямую в регистр сведений порциями. На форме документа мы можем отображать эти данные с помощью динамического списка с отбором по ссылке текущего документа, что решает проблему зависания интерфейса на больших объемах данных.
Ниже представлен пример кода для сохранения данных формы в регистр сведений:
Процедура СохранитьДанныеТабличнойЧастиВРегистр(СсылкаНаДокумент, ТаблицаДанных) Экспорт
// Очищаем старые записи по документу
НаборЗаписей = РегистрыСведений.ДанныеТабличныхЧастейДокументов.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.ДокументСсылка.Установить(СсылкаНаДокумент);
// Заполняем набор новыми данными порциями для оптимизации памяти
Индекс = 0;
Для Каждого СтрокаДанных Из ТаблицаДанных Цикл
НоваяЗапись = НаборЗаписей.Добавить();
НоваяЗапись.ДокументСсылка = СсылкаНаДокумент;
НоваяЗапись.ИндексСтроки = Индекс;
ЗаполнитьЗначенияСвойств(НоваяЗапись, СтрокаДанных);
Индекс = Индекс + 1;
КонецЦикла;
// Записываем данные в СУБД в рамках одной транзакции
НаборЗаписей.Записать(Истина);
КонецПроцедуры
---
Если цель создания документа — исключительно отражение оборотов в учете (например, фиксация продаж для последующего анализа в СКД), то хранить эти миллионы строк в табличной части самого документа совершенно нерационально. Документ должен выступать исключительно как регистратор («крючок» в системе), а все данные должны сразу записываться в движения (наборы записей) соответствующих регистров.
Проанализируем этот процесс шаг за шагом:
Посмотрим на пример кода прямой записи в движения регистра накопления с оптимизацией транзакций:
Процедура ЗагрузитьПродажиНапрямуюВРегистр(СсылкаНаДокумент, ИмяФайлаИсточника) Экспорт
// Создаем набор записей регистра накопления "Продажи"
НаборДвижений = РегистрыНакопления.Продажи.СоздатьНаборЗаписей();
НаборДвижений.Отбор.Регистратор.Установить(СсылкаНаДокумент);
// Открываем файл для потокового чтения (избегаем загрузки всего файла в ОЗУ)
ЧтениеТекста = Новый ТекстовыйДокумент;
ЧтениеТекста.Прочитать(ИмяФайлаИсточника);
КоличествоСтрок = ЧтениеТекста.КоличествоСтрок();
// Начинаем транзакцию для обеспечения целостности данных
НачатьТранзакцию();
Попытка
Для НомерСтроки = 1 По КоличествоСтрок Цикл
СтрокаФайла = ЧтениеТекста.ПолучитьСтроку(НомерСтроки);
// Разбираем строку (например, формата CSV)
МассивДанных = СтроковыеФункцииКлиентСервер.РазложитьСтрокуВМассивПодстрок(СтрокаФайла, ";");
НовоеДвижение = НаборДвижений.Добавить();
НовоеДвижение.Регистратор = СсылкаНаДокумент;
НовоеДвижение.Период = ТекущаяДата();
// Сопоставляем данные
НовоеДвижение.Номенклатура = Справочники.Номенклатура.НайтиПоРеквизиту("Артикул", МассивДанных[0]);
НовоеДвижение.Количество = Число(МассивДанных[1]);
НовоеДвижение.Сумма = Число(МассивДанных[2]);
// Записываем порциями по 50 000 строк во избежание переполнения памяти транзакции
Если НаборДвижений.Количество() >= 50000 Тогда
НаборДвижений.Записать(Ложь); // Записываем без очистки предыдущих в этой транзакции
НаборДвижений.Очистить();
КонецЕсли;
КонецЦикла;
// Записываем оставшийся хвост данных
Если НаборДвижений.Количество() > 0 Тогда
НаборДвижений.Записать(Ложь);
КонецЕсли;
ЗафиксироватьТранзакцию();
Сообщить("Загрузка успешно завершена!");
Исключение
ОтменитьТранзакцию();
ЗаписьЖурналаРегистрации("ЗагрузкаДанных", УровеньЖурналаРегистрации.Ошибка,,, ПодробноеПредставлениеОшибки(ИнформацияОбОшибке()));
ВызватьИсключение "Ошибка при загрузке данных в регистр накопления. Запись отменена.";
КонецПопытки;
КонецПроцедуры
---
Иногда бизнес-логика требует хранить именно «сырые» исходные данные файла внутри документа для возможности их повторного анализа или перезаполнения справочников задним числом, но при этом регистры сведений использовать не хочется. В таком случае отличным выходом будет сериализация таблицы данных в двоичные данные. Для оптимизации хранения текстовых данных можно использовать методы сжатия строк встроенными средствами 1С.
Рассмотрим, как устроен этот механизм:
ДанныеФайлаХранилище с типом ХранилищеЗначений.ТаблицуЗначений в памяти сервера, наполняем её строками (хоть 1 000 000 строк).ХранилищеЗначений сжатым архивом.Пример кода упаковки и распаковки данных:
// Код упаковки таблицы на сервере перед записью документа
ТаблицаДляСохранения = ПолучитьТаблицуСМиллиономСтрок();
ОбъектДокумент = СсылкаНаДокумент.ПолучитьОбъект();
ОбъектДокумент.ДанныеФайлаХранилище = Новый ХранилищеЗначений(ТаблицаДляСохранения, Новый СжатиеДанных(9));
ОбъектДокумент.Записать();
// Код распаковки таблицы при открытии формы или анализе данных
ТаблицаДанных = СсылкаНаДокумент.ДанныеФайлаХранилище.Получить();
Если ТипЗнч(ТаблицаДанных) = Тип("ТаблицаЗначений") Тогда
// Работаем с таблицей значений в памяти
КонецЕсли;
---
Проанализируем технические нюансы, с которыми столкнулся автор темы при проведении и хранении 100 миллионов записей. На таких масштабах стандартные методы работы 1С могут привести к критическим зависаниям оборудования. Для контроля занимаемого объема дискового пространства в СУБД можно использовать обработку для анализа размера таблиц MS SQL и PostgreSQL в разрезе метаданных 1С.
При записи миллионов строк в оборотные или остаточные регистры СУБД тратит до 80% времени транзакции на параллельный пересчет таблиц итогов регистра накопления. Чтобы ускорить загрузку данных в несколько раз, обязательно используйте временное отключение итогов перед массовой записью движений с последующим их пересчетом.
Посмотрим, как правильно реализовать это в коде:
// Отключаем использование итогов для ускорения записи
РегистрыНакопления.Продажи.УстановитьИспользованиеИтогов(Ложь);
Попытка
// Выполняем массовую загрузку данных в регистр
ВыполнитьМассовуюЗагрузкуДанных();
// Включаем итоги обратно и запускаем их пересчет
РегистрыНакопления.Продажи.УстановитьИспользованиеИтогов(Истина);
РегистрыНакопления.Продажи.ПересчитатьИтоги();
Исключение
// В случае сбоя обязательно включаем итоги назад
РегистрыНакопления.Продажи.УстановитьИспользованиеИтогов(Истина);
ВызватьИсключение КраткоеПредставлениеОшибки(ИнформацияОбОшибке());
КонецПопытки;
Как справедливо заметил автор темы, при чтении Excel файлов размером более 500 000 строк стандартные методы COM-соединения могут потреблять до 8 ГБ оперативной памяти и зависать. Для быстрой и экономной загрузки данных из внешних файлов рекомендуется:
ТабДок.Прочитать(ИмяФайла, СпособЧтения), который работает на порядок быстрее устаревшего COM-соединения.ЧтениеТекста.---
Подведем итоги нашего исследования проблемы переполнения табличной части в 1С:
LineNumberLength в свойствах табличной части до 8 или 9.