При разработке и рефакторинге систем на платформе 1С:Предприятие часто возникает задача переноса исторических данных. Типичный сценарий: в справочнике (например, Контрагенты) со временем накопились данные в нескольких табличных частях, которые архитектурно правильнее хранить в независимых регистрах сведений. Когда объем базы данных исчисляется десятками тысяч элементов, стандартные "лобовые" методы переноса могут вызывать критические ошибки СУБД, зависание сервера приложений или банальную нехватку оперативной памяти (out of memory).
В этой статье мы подробно разберем, как решить эту задачу красиво, быстро и профессионально. Мы проанализируем различные подходы — от простых циклов до оптимизированных пакетных запросов и попорционной обработки данных, выделив наиболее эффективные практики программирования в среде 1С.
Представим, что у нас есть справочник Контрагенты, содержащий три табличные части:
ТЗКоммуникация (реквизиты: Дата, Сотрудник, Тип, Описание, ВходИсход)ПрофессиональныеНавыки (реквизиты: ПрофНавык, Примечание)Автомобили (реквизиты: МаркаМодель, Тип, Мест, Примечание)Нам необходимо перенести все эти данные в три соответствующих независимых регистра сведений. В каждом регистре набор измерений и ресурсов полностью совпадает с реквизитами табличных частей, плюс добавлено ключевое измерение Контрагент (тип СправочникСсылка.Контрагенты).
Общее количество контрагентов в базе данных составляет около 20 000 элементов. Задача требует переноса всей информации за один раз.
---
Для начала разберем типичные ошибки, которые совершают начинающие разработчики при решении подобных задач.
Ошибка 1: Запрос к родительскому объекту и выборка вложенных таблиц
На первый взгляд кажется логичным написать один запрос к справочнику и выбрать все его табличные части вложенными таблицами:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Контрагенты.Ссылка КАК Контрагент,
| Контрагенты.ТЗКоммуникация.(
| Дата, Сотрудник, Тип, Описание, ВходИсход
| ) КАК ТЗКоммуникация,
| Контрагенты.ПрофессиональныеНавыки.(
| ПрофНавык, Примечание
| ) КАК ПрофессиональныеНавыки,
| Контрагенты.Автомобили.(
| МаркаМодель, Тип, Мест, Примечание
| ) КАК Автомобили
|ИЗ
| Справочник.Контрагенты КАК Контрагенты";
Выясним причину, почему это плохо. Обращение к табличным частям через точку от родительского объекта заставляет СУБД выполнять сложные внутренние соединения (JOIN) и формировать тяжелые структуры для передачи на сервер приложений 1С. Если выгрузить этот результат в память, платформа попытается создать вложенные таблицы значений внутри строк основной таблицы. На объеме в 20 000 записей это гарантированно приведет к колоссальному расходу оперативной памяти и, скорее всего, аварийному завершению рабочего процесса сервера.
Ошибка 2: Обработка выборки в цикле по каждому контрагенту
Другой интуитивный подход — обходить полученную выборку контрагентов в цикле, получать их табличные части и записывать их в регистр:
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
// Выгрузка ТЧ конкретного контрагента
ТаблицаКоммуникаций = Выборка.ТЗКоммуникация.Выгрузить();
// Запись в регистр по одному контрагенту
Набор = РегистрыСведений.Коммуникации.СоздатьНаборЗаписей();
Набор.Отбор.Контрагент.Установить(Выборка.Контрагент);
Для Каждого СтрокаТЧ Из ТаблицаКоммуникаций Цикл
НоваяЗапись = Набор.Добавить();
ЗаполнитьЗначенияСвойств(НоваяЗапись, СтрокаТЧ);
НоваяЗапись.Контрагент = Выборка.Contragent;
КонецЦикла;
Набор.Записать();
КонецЦикла;
Проанализируем эту ситуацию. Запись набора записей в цикле 20 000 раз создает колоссальную нагрузку на СУБД. Каждый вызов метода Записать() инициирует отдельную транзакцию, осуществляет запись на диск и обновляет индексы таблицы регистра. На практике такая обработка может выполняться часами, парализуя работу пользователей.
---
Самым простым в реализации и при этом чрезвычайно быстрым решением является выполнение трех независимых запросов напрямую к физическим таблицам табличных частей справочника. Такой подход позволяет получить абсолютно "плоские" таблицы данных без использования вложенных объектов.
Рассмотрим этот алгоритм по шагам:
Справочник.Контрагенты.ТЗКоммуникация. В качестве контрагента используем поле Ссылка (оно автоматически является ссылкой на владельца строки табличной части).Посмотрим на пример кода, реализующий этот алгоритм:
// Шаг 1: Перенос коммуникаций
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ТЧ.Ссылка КАК Контрагент,
| ТЧ.Дата КАК Дата,
| ТЧ.Сотрудник КАК Сотрудник,
| ТЧ.Тип КАК Тип,
| ТЧ.Описание КАК Описание,
| ТЧ.ВходИсход КАК ВходИсход
|ИЗ
| Справочник.Контрагенты.ТЗКоммуникация КАК ТЧ";
РезультатЗапроса = Запрос.Выполнить();
Если Не РезультатЗапроса.Пустой() Тогда
ТаблицаДанных = РезультатЗапроса.Выгрузить();
НаборЗаписей = РегистрыСведений.Коммуникации.СоздатьНаборЗаписей();
НаборЗаписей.Загрузить(ТаблицаДанных);
НаборЗаписей.Записать(Истина); // Замещение = Истина очистит старые данные при первоначальном заполнении
КонецЕсли;
// Шаг 2: Перенос профессиональных навыков
Запрос.Текст =
"ВЫБРАТЬ
| ТЧ.Ссылка КАК Контрагент,
| ТЧ.ПрофНавык КАК ПрофНавык,
| ТЧ.Примечание КАК Примечание
|ИЗ
| Справочник.Контрагенты.ПрофессиональныеНавыки КАК ТЧ";
РезультатЗапроса = Запрос.Выполнить();
Если Не РезультатЗапроса.Пустой() Тогда
ТаблицаДанных = РезультатЗапроса.Выгрузить();
НаборЗаписей = РегистрыСведений.ПрофессиональныеНавыки.СоздатьНаборЗаписей();
НаборЗаписей.Загрузить(ТаблицаДанных);
НаборЗаписей.Записать(Истина);
КонецЕсли;
// Шаг 3: Перенос автомобилей
Запрос.Текст =
"ВЫБРАТЬ
| ТЧ.Ссылка КАК Контрагент,
| ТЧ.МаркаМодель КАК МаркаМодель,
| ТЧ.Тип КАК Тип,
| ТЧ.Мест КАК Мест,
| ТЧ.Примечание КАК Примечание
|ИЗ
| Справочник.Контрагенты.Автомобили КАК ТЧ";
РезультатЗапроса = Запрос.Выполнить();
Если Не РезультатЗапроса.Пустой() Тогда
ТаблицаДанных = РезультатЗапроса.Выгрузить();
НаборЗаписей = РегистрыСведений.АвтомобилиКонтрагентов.СоздатьНаборЗаписей();
НаборЗаписей.Загрузить(ТаблицаДанных);
НаборЗаписей.Записать(Истина);
КонецЕсли;
Преимущества данного решения:
НаборЗаписей.Загрузить() в сочетании с Записать() выполняет пакетную вставку (Bulk Insert) на уровне базы данных. Весь процесс для 20 000 контрагентов занимает не более 15-30 секунд.---
Если мы хотим написать более лаконичный и изящный код без дублирования вызовов создания объекта Запрос, мы можем использовать механизм пакетных запросов платформы 1С. Метод ВыполнитьПакет() позволяет СУБД обработать все три запроса за одно обращение к серверу баз данных.
Рассмотрим, как реализовать этот подход:
ПакетныйЗапрос = Новый Запрос;
ПакетныйЗапрос.Текст =
"ВЫБРАТЬ
| ТЧ.Ссылка КАК Контрагент,
| ТЧ.Дата КАК Дата,
| ТЧ.Сотрудник КАК Сотрудник,
| ТЧ.Тип КАК Тип,
| ТЧ.Описание КАК Описание,
| ТЧ.ВходИсход КАК ВходИсход
|ИЗ
| Справочник.Контрагенты.ТЗКоммуникация КАК ТЧ
|;
|ВЫБРАТЬ
| ТЧ.Ссылка КАК Контрагент,
| ТЧ.ПрофНавык КАК ПрофНавык,
| ТЧ.Примечание КАК Примечание
|ИЗ
| Справочник.Контрагенты.ПрофессиональныеНавыки КАК ТЧ
|;
|ВЫБРАТЬ
| ТЧ.Ссылка КАК Контрагент,
| ТЧ.МаркаМодель КАК МаркаМодель,
| ТЧ.Тип КАК Тип,
| ТЧ.Мест КАК Мест,
| ТЧ.Примечание КАК Примечание
|ИЗ
| Справочник.Контрагенты.Автомобили КАК ТЧ";
МассивРезультатов = ПакетныйЗапрос.ВыполнитьПакет();
// Обрабатываем результат 1 (Коммуникации)
ТаблицаКоммуникаций = МассивРезультатов[0].Выгрузить();
Набор1 = РегистрыСведений.Коммуникации.СоздатьНаборЗаписей();
Набор1.Загрузить(ТаблицаКоммуникаций);
Набор1.Записать(Истина);
// Обрабатываем результат 2 (Навыки)
ТаблицаНавыков = МассивРезультатов[1].Выгрузить();
Набор2 = РегистрыСведений.ПрофессиональныеНавыки.СоздатьНаборЗаписей();
Набор2.Загрузить(ТаблицаНавыков);
Набор2.Записать(Истина);
// Обрабатываем результат 3 (Автомобили)
ТаблицаАвтомобилей = МассивРезультатов[2].Выгрузить();
Набор3 = РегистрыСведений.АвтомобилиКонтрагентов.СоздатьНаборЗаписей();
Набор3.Загрузить(ТаблицаАвтомобилей);
Набор3.Записать(Истина);
Этот способ объединяет скорость плоских запросов и изящество кода. Однако стоит учитывать, что при таком подходе все три таблицы результатов одновременно загружаются в оперативную память сервера приложений 1С перед началом записи. Если ваши таблицы содержат миллионы строк, лучше вернуться к первому варианту с последовательным выполнением.
---
Что делать, если база данных со временем вырастет с 20 000 до 500 000 контрагентов? В этом случае даже прямая выгрузка всей таблицы в память может вызвать сбой. Профессиональный стандарт разработки на платформе 1С требует использования порционной (попакетной) обработки данных.
Суть метода заключается в том, что мы выбираем данные небольшими порциями (например, по 5000 контрагентов за одну транзакцию), записываем их в регистр и переходим к следующему пакету. Таким образом, потребление памяти сервером приложений остается стабильным и минимальным на протяжении всей работы алгоритма.
Давайте разберем пример реализации порционной выгрузки для одной из табличных частей:
// Определим размер одной порции данных
РазмерПорции = 5000;
ПоследняяСсылка = Справочники.Контрагенты.ПустаяСсылка();
ЗапросПорций = Новый Запрос;
ЗапросПорций.Текст =
"ВЫБРАТЬ ПЕРВЫЕ " + Формат(РазмерПорции, "ЧГ=0") + "
| Контрагенты.Ссылка КАК Ссылка
|ИЗ
| Справочник.Контрагенты КАК Контрагенты
|ГДЕ
| Контрагенты.Ссылка > &ПоследняяСсылка
|УПОРЯДОЧИТЬ ПО
| Ссылка";
ЗапросДанных = Новый Запрос;
ЗапросДанных.Текст =
"ВЫБРАТЬ
| ТЧ.Ссылка КАК Контрагент,
| ТЧ.Дата КАК Дата,
| ТЧ.Сотрудник КАК Сотрудник,
| ТЧ.Тип КАК Тип,
| ТЧ.Описание КАК Описание,
| ТЧ.ВходИсход КАК ВходИсход
|ИЗ
| Справочник.Контрагенты.ТЗКоммуникация КАК ТЧ
|ГДЕ
| ТЧ.Ссылка В (&МассивСсылок)";
Пока Истина Цикл
// Шаг 1: Получаем порцию ссылок контрагентов
ЗапросПорций.УстановитьПараметр("ПоследняяСсылка", ПоследняяСсылка);
ТаблицаСсылок = ЗапросПорций.Perform().Выгрузить();
Если ТаблицаСсылок.Количество() = 0 Тогда
Прервать; // Все данные успешно обработаны
КонецЕсли;
МассивСсылок = ТаблицаСсылок.ВыгрузитьКолонку("Ссылка");
// Запоминаем последнюю ссылку для следующей итерации
ПоследняяСсылка = МассивСсылок[МассивСсылок.Количество() - 1];
// Шаг 2: Выбираем данные табличной части только для этой порции
ЗапросДанных.УстановитьПараметр("МассивСсылок", МассивСсылок);
ТаблицаЗаписей = ЗапросДанных.Выполнить().Выгрузить();
Если ТаблицаЗаписей.Количество() > 0 Тогда
// Записываем данные в регистр порциями
// Внимание: для независимого регистра при записи порциями замещение должно быть ЛОЖЬ,
// чтобы не затереть ранее записанные порции других контрагентов!
НаборЗаписей = РегистрыСведений.Коммуникации.СоздатьНаборЗаписей();
НаборЗаписей.Загрузить(ТаблицаЗаписей);
НаборЗаписей.Записать(Ложь);
КонецЕсли;
КонецЦикла;
Использование конструкции ГДЕ Контрагенты.Ссылка > &ПоследняяСсылка УПОРЯДОЧИТЬ ПО Ссылка — это классический паттерн быстрого перебора таблиц в СУБД. Он работает значительно быстрее, чем использование смещения (OFFSET/СКИДКА), так как задействует первичный индекс таблицы СУБД.
---
При массовом переносе данных важно соблюдать стандарты разработки 1С, касающиеся блокировок и транзакционности:
НачатьТранзакцию() и ЗафиксироватьТранзакцию(). Если в процессе записи произойдет непредвиденный сбой, транзакция откатится, сохранив целостность базы данных.Для качественного и быстрого переноса нескольких табличных частей в регистры сведений откажитесь от вложенных таблиц в запросах и обхода элементов в цикле. Используйте плоские запросы непосредственно к таблицам табличных частей (например, Справочник.Контрагенты.ТЗКоммуникация). Для средних объемов отлично подойдут отдельные плоские запросы или ВыполнитьПакет(), а для масштабируемых систем с миллионами записей всегда применяйте попорционную выборку данных по первичному ключу.