При разработке и модификации информационных систем на платформе "1С:Предприятие 8" часто возникает задача добавления нового измерения в уже существующий регистр сведений. Если регистр содержит небольшое количество записей, заполнить новое измерение не составляет труда. Однако, когда речь идет о десятках и сотнях тысяч, а порой и миллионах записей, традиционные методы, такие как перебор в цикле с использованием МенеджераЗаписи, становятся крайне неэффективными и могут привести к длительным блокировкам базы данных.
В этой статье мы подробно рассмотрим, как решить задачу массового заполнения нового измерения в независимом регистре сведений, используя наиболее производительные и безопасные подходы, а также разберем распространенные ошибки и ограничения.
Независимый регистр сведений — это объект метаданных, который хранит произвольные данные, не привязанные напрямую к другим объектам конфигурации, таким как документы или справочники. Он может быть периодическим или непериодическим, а также иметь несколько измерений и ресурсов. Когда мы добавляем новое измерение в такой регистр, оно по умолчанию остается пустым для всех существующих записей.
Основная проблема при массовом заполнении большого количества записей заключается в том, что каждая операция записи в 1С (через МенеджерЗаписи или НаборЗаписей) инициирует взаимодействие с базой данных (СУБД). Каждая такая операция выполняется в рамках транзакции, которая может блокировать изменяемые данные. Если таких операций очень много и они выполняются последовательно в одной большой транзакции, это приводит к следующим проблемам:
Именно поэтому нам необходимо использовать подходы, которые минимизируют время блокировки и позволяют эффективно обрабатывать данные порциями.
НаборЗаписей с порционной обработкойМетод, который считается одним из самых производительных и безопасных для массовых операций с регистрами сведений, основан на использовании объекта НаборЗаписей в сочетании с порционной обработкой и управлением транзакциями. Давайте разберем его подробнее.
Объект НаборЗаписей позволяет оперировать сразу целым множеством записей регистра. В отличие от МенеджераЗаписи, который работает с одной записью, НаборЗаписей может быть выгружен в ТаблицуЗначений, модифицирован и затем загружен обратно и записан одной пакетной операцией. Это значительно сокращает количество обращений к базе данных и оптимизирует процесс записи.
Для работы с очень большими объемами данных, чтобы избежать упомянутых выше проблем с блокировками, необходимо использовать порционную обработку. Суть метода заключается в следующем:
ТаблицуЗначений, в ней заполняется новое измерение, и затем эта ТаблицаЗначений загружается обратно в НаборЗаписей.НаборЗаписей записывается. Ключевой момент: запись каждой порции производится в отдельной, короткой транзакции. Это предотвращает длительные блокировки.Оптимальный размер порции обычно находится в диапазоне от 1000 до 10 000 записей, но может варьироваться в зависимости от производительности сервера, количества полей в регистре и общей нагрузки на базу данных.
Начнем с определения имени нашего регистра сведений, имени нового измерения и значения, которым мы хотим его заполнить. Также установим размер порции.
ИмяРегистра = "ВашРегистрСведений"; // Замените на имя вашего регистра
ИмяНовогоИзмерения = "НовоеИзмерение"; // Замените на имя нового измерения
ЗначениеДляИзмерения = Перечисления.ЗначенияНовогоИзмерения.Значение1; // Например, ссылка на элемент справочника, перечисление и т.д.
РазмерПорции = 5000; // Оптимальный размер порции
Мы будем использовать цикл Пока, который будет продолжаться до тех пор, пока мы не обработаем все записи регистра. В каждой итерации цикла мы будем считывать следующую порцию данных.
КоличествоОбработанных = 0;
Продолжать = Истина;
КлючиПоследнейЗаписиПредыдущейПорции = Неопределено; // Для пагинации
Пока Продолжать Цикл
// ... (дальнейший код для выборки и обработки порции)
КонецЦикла;
Для выборки порции данных используем объект Запрос. Важно выбрать все ключевые измерения регистра, так как они необходимы для однозначной идентификации записей в НабореЗаписей. Мы также используем конструкцию ВЫБРАТЬ ПЕРВЫЕ <РазмерПорции> для ограничения количества выбираемых записей и условие УПОРЯДОЧИТЬ ПО для обеспечения стабильного порядка выборки, что критично для правильной пагинации.
Условие для пагинации (ГДЕ) будет зависеть от ключевых измерений вашего регистра. Если у вас несколько ключевых измерений, условие может быть сложным. Для простоты мы будем использовать пример с несколькими измерениями Измерение1, Измерение2, Измерение3 и предположим, что Измерение1 является основным для сортировки.
Запрос = Новый Запрос;
ТекстЗапроса = "ВЫБРАТЬ ПЕРВЫЕ " + РазмерПорции + "
| РС.Измерение1,
| РС.Измерение2,
| РС.Измерение3, // Все ключевые измерения регистра
| РС." + ИмяНовогоИзмерения + " // Выбираем и новое измерение
|ИЗ
| РегистрСведений." + ИмяРегистра + " КАК РС
|ГДЕ ИСТИНА"; // Базовое условие
Если КлючиПоследнейЗаписиПредыдущейПорции <> Неопределено Тогда
// Условие для пагинации. Это пример, который необходимо адаптировать
// под конкретную структуру ваших ключевых измерений и их порядок сортировки.
// Для сложных составных ключей условие может быть значительно сложнее.
// Например, для ключей (Измерение1, Измерение2):
// (РС.Измерение1 > &Измерение1) ИЛИ
// (РС.Измерение1 = &Измерение1 И РС.Измерение2 > &Измерение2)
// И так далее для всех ключевых измерений.
// Здесь мы используем упрощенный подход, полагаясь на то, что Измерение1
// является основным для сортировки и обеспечивает уникальность в рамках порции.
ТекстЗапроса = ТекстЗапроса + " И РС.Измерение1 > &ПоследнееИзмерение1";
Запрос.УстановитьПараметр("ПоследнееИзмерение1", КлючиПоследнейЗаписиПредыдущейПорции.Измерение1);
// Если Измерение1 может быть неуникальным в последней записи порции,
// и чтобы избежать пропусков или дублирований, нужно добавлять условия по другим ключевым измерениям.
// Например: Запрос.УстановитьПараметр("ПоследнееИзмерение2", КлючиПоследнейЗаписиПредыдущейПорции.Измерение2);
КонецЕсли;
ТекстЗапроса = ТекстЗапроса + "
|УПОРЯДОЧИТЬ ПО РС.Измерение1, РС.Измерение2, РС.Измерение3"; // Важно для стабильной пагинации
Запрос.Текст = ТекстЗапроса;
РезультатЗапроса = Запрос.Выполнить();
ТаблицаИзмерений = РезультатЗапроса.Выгрузить();
Если в порции есть записи, мы выгружаем их в ТаблицуЗначений, затем используем метод ЗаполнитьЗначения() для быстрого заполнения нужной колонки (нового измерения) одним и тем же значением. После этого создаем НаборЗаписей для нашего регистра и загружаем в него модифицированную ТаблицуЗначений.
Если ТаблицаИзмерений.Количество() = 0 Тогда
Продолжать = Ложь; // Больше нет записей для обработки
Прервать;
КонецЕсли;
// Заполняем новое измерение нужным значением
ТаблицаИзмерений.ЗаполнитьЗначения(ЗначениеДляИзмерения, ИмяНовогоИзмерения);
// Создаем набор записей и загружаем в него данные
НаборЗаписей = РегистрыСведений[ИмяРегистра].СоздатьНаборЗаписей();
НаборЗаписей.Загрузить(ТаблицаИзмерений);
Перед записью каждой порции мы начинаем транзакцию с помощью НачатьТранзакцию(), а после успешной записи фиксируем ее с помощью ЗафиксироватьТранзакцию(). Это гарантирует, что каждая порция будет записана атомарно, и длительных блокировок не произойдет. В случае ошибки транзакцию можно отменить.
При записи НабораЗаписей мы используем метод Записать(). Если в регистре уже есть записи с такими же ключевыми измерениями, но пустым новым измерением, то по умолчанию 1С будет обновлять их. Для явного указания режима обновления или замещения, особенно в более новых версиях платформы (8.3.10+), можно использовать свойство РежимЗаписи объекта НаборЗаписей.
НаборЗаписей.Записать(Истина): Устаревший, но все еще рабочий метод, который означает "заместить записи". Он может быть менее эффективным, чем новые режимы.НаборЗаписей.РежимЗаписи = РежимЗамещения.Обновление;: Этот режим позволяет только обновить уже существующие записи в регистре, не добавляя новые. Это значительно быстрее, так как выполняется только операция UPDATE на уровне СУБД, без предварительного DELETE.НаборЗаписей.РежимЗаписи = РежимЗамещения.Слияние;: Данный режим позволяет одновременно обновить существующие записи и добавить новые.
// Начинаем транзакцию для каждой порции
НачатьТранзакцию();
Попытка
// Записываем набор записей.
// Для версий 8.3.10+ предпочтительнее: НаборЗаписей.РежимЗаписи = РежимЗамещения.Обновление;
// Тогда просто: НаборЗаписей.Записать();
НаборЗаписей.Записать(Истина);
ЗафиксироватьТранзакцию();
КоличествоОбработанных = КоличествоОбработанных + ТаблицаИзмерений.Количество();
Сообщить("Обработано записей: " + КоличествоОбработанных);
// Обновляем ключи для следующей порции, берем последние значения из текущей порции
Если ТаблицаИзмерений.Количество() > 0 Тогда
КлючиПоследнейЗаписиПредыдущейПорции = ТаблицаИзмерений.Получить(ТаблицаИзмерений.Количество() - 1);
// Если ключевых измерений несколько, нужно сохранить значения всех ключевых измерений
// из последней записи, чтобы правильно построить условие для следующей порции.
// Например, если ключи: Измерение1, Измерение2, Измерение3
// КлючиПоследнейЗаписиПредыдущейПорции = Новый Структура("Измерение1, Измерение2, Измерение3",
// ПоследняяЗапись.Измерение1, ПоследняяЗапись.Измерение2, ПоследняяЗапись.Измерение3);
КонецЕсли;
Если ТаблицаИзмерений.Количество() < РазмерПорции Тогда
Продолжать = Ложь; // Обработаны все записи
КонецЕсли;
Исключение
ОтменитьТранзакцию();
Сообщить("Ошибка при обработке порции записей: " + ОписаниеОшибки());
Продолжать = Ложь; // Прекращаем обработку при ошибке
КонецПопытки;
Процедура ЗаполнитьНовоеИзмерениеВРегистреСведений(ЗначениеДляИзмерения)
ИмяРегистра = "ВашРегистрСведений"; // Замените на имя вашего регистра
ИмяНовогоИзмерения = "НовоеИзмерение"; // Замените на имя нового измерения
РазмерПорции = 5000; // Оптимальный размер порции для вашей системы
Сообщить("Начало заполнения измерения '" + ИмяНовогоИзмерения + "' значением '" + ЗначениеДляИзмерения + "'...");
КоличествоОбработанных = 0;
Продолжать = Истина;
КлючиПоследнейЗаписиПредыдущейПорции = Неопределено;
Пока Продолжать Цикл
НачатьТранзакцию();
Попытка
Запрос = Новый Запрос;
ТекстЗапроса = "ВЫБРАТЬ ПЕРВЫЕ " + РазмерПорции + "
| РС.Измерение1,
| РС.Измерение2,
| РС.Измерение3, // Укажите ВСЕ ключевые измерения вашего регистра
| РС." + ИмяНовогоИзмерения + " // Выбираем и само новое измерение
|ИЗ
| РегистрСведений." + ИмяРегистра + " КАК РС
|ГДЕ ИСТИНА";
// Условие для пагинации. Это - упрощенный пример, который нужно адаптировать
// под конкретную структуру ключевых измерений вашего регистра.
// Если ключевых измерений несколько, и они могут быть неуникальными по отдельности,
// потребуется более сложное условие для гарантированной выборки "следующей" порции.
// Например, для ключей (Измерение1, Измерение2):
// (РС.Измерение1 > &МаксИзмерение1) ИЛИ
// (РС.Измерение1 = &МаксИзмерение1 И РС.Измерение2 > &МаксИзмерение2)
// и т.д.
Если КлючиПоследнейЗаписиПредыдущейПорции <> Неопределено Тогда
ТекстУсловияПагинации = " (РС.Измерение1 > &ПредыдущееИзмерение1) ";
Запрос.УстановитьПараметр("ПредыдущееИзмерение1", КлючиПоследнейЗаписиПредыдущейПорции.Измерение1);
// Если Измерение1 может быть неуникальным, добавьте дополнительные условия:
// ТекстУсловияПагинации = ТекстУсловияПагинации + " ИЛИ (РС.Измерение1 = &ПредыдущееИзмерение1 И РС.Измерение2 > &ПредыдущееИзмерение2) ";
// Запрос.УстановитьПараметр("ПредыдущееИзмерение2", КлючиПоследнейЗаписиПредыдущейПорции.Измерение2);
// (И так далее для всех ключевых измерений)
ТекстЗапроса = ТекстЗапроса + " И " + ТекстУсловияПагинации;
КонецЕсли;
ТекстЗапроса = ТекстЗапроса + "
|УПОРЯДОЧИТЬ ПО РС.Измерение1, РС.Измерение2, РС.Измерение3"; // Укажите ВСЕ ключевые измерения для сортировки
Запрос.Текст = ТекстЗапроса;
РезультатЗапроса = Запрос.Выполнить();
ТаблицаИзмерений = РезультатЗапроса.Выгрузить();
Если ТаблицаИзмерений.Количество() = 0 Тогда
Продолжать = Ложь; // Больше нет записей
Прервать;
КонецЕсли;
// Заполняем новое измерение нужным значением
ТаблицаИзмерений.ЗаполнитьЗначения(ЗначениеДляИзмерения, ИмяНовогоИзмерения);
// Создаем набор записей и загружаем в него данные
НаборЗаписей = РегистрыСведений[ИмяРегистра].СоздатьНаборЗаписей();
НаборЗаписей.Загрузить(ТаблицаИзмерений);
// Записываем набор записей.
// Для платформы 8.3.10+ и выше рекомендуется использовать РежимЗаписи:
// НаборЗаписей.РежимЗаписи = РежимЗамещения.Обновление; // Обновляет только существующие
// НаборЗаписей.Записать();
// Для более старых версий или если нужно замещать записи:
НаборЗаписей.Записать(Истина); // Истина означает "заместить записи"
ЗафиксироватьТранзакцию(); // Фиксируем транзакцию для текущей порции
КоличествоОбработанных = КоличествоОбработанных + ТаблицаИзмерений.Количество();
Сообщить("Обработано записей: " + КоличествоОбработанных + ". Последняя порция: " + ТаблицаИзмерений.Количество());
// Обновляем ключи для следующей порции
Если ТаблицаИзмерений.Количество() > 0 Тогда
// Получаем значения всех ключевых измерений последней записи порции
ПоследняяЗаписьПорции = ТаблицаИзмерений.Получить(ТаблицаИзмерений.Количество() - 1);
КлючиПоследнейЗаписиПредыдущейПорции = Новый Структура;
КлючиПоследнейЗаписиПредыдущейПорции.Вставить("Измерение1", ПоследняяЗаписьПорции.Измерение1);
// Добавьте сюда другие ключевые измерения вашего регистра
// КлючиПоследнейЗаписиПредыдущейПорции.Вставить("Измерение2", ПоследняяЗаписьПорции.Измерение2);
// КлючиПоследнейЗаписиПредыдущейПорции.Вставить("Измерение3", ПоследняяЗаписьПорции.Измерение3);
КонецЕсли;
Если ТаблицаИзмерений.Количество() < РазмерПорции Тогда
Продолжать = Ложь; // Текущая порция меньше размера, значит, это была последняя
КонецЕсли;
Исключение
ОтменитьТранзакцию(); // Отменяем транзакцию в случае ошибки
Сообщить("Ошибка при заполнении измерения: " + ОписаниеОшибки());
Продолжать = Ложь; // Останавливаем обработку
КонецПопытки;
КонецЦикла;
Если Продолжать = Ложь И КоличествоОбработанных > 0 Тогда
Сообщить("Заполнение измерения завершено успешно. Всего обработано записей: " + КоличествоОбработанных);
ИначеЕсли КоличествоОбработанных = 0 Тогда
Сообщить("Не найдено записей для обработки или произошла ошибка в самом начале.");
КонецЕсли;
КонецПроцедуры
МенеджераЗаписи в циклеВ рамках обсуждения на форуме был предложен пример кода, использующий МенеджерЗаписи для каждой записи. Давайте рассмотрим этот подход и объясним, почему он не подходит для больших объемов данных.
Процедура УстановитьНовоеИзмерениеНеэффективно(Значение)
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ
| РС.Измерение1,
| РС.Измерение2 // Предполагаемые ключевые измерения
|ИЗ
| РегистрСведений.РС КАК РС";
Результат = Запрос.Выполнить();
Выборка = Результат.Выбрать();
Пока Выборка.Следующий Цикл
МенеджерЗаписи = РегистрыСведений.РС.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Измерение1 = Выборка.Измерение1;
МенеджерЗаписи.Измерение2 = Выборка.Измерение2;
МенеджерЗаписи.Прочитать(); // Чтение записи по ключу
Если МенеджерЗаписи.Выбран Тогда
МенеджерЗаписи.НовоеИзмерение = Значение;
МенеджерЗаписи.Записать(); // Запись каждой записи по отдельности
Иначе
Сообщить("Менеджер записи не выбран: " + МенеджерЗаписи.Измерение1 + ", " + МенеджерЗаписи.Измерение2);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Как было справедливо отмечено в сообщении 7, "записей очень много и простым перебором в цикле будет долго". Причины следующие:
НачатьТранзакцию() и ЗафиксироватьТранзакцию()), то это приведет к длительной блокировке таблицы, что еще хуже для производительности и доступности базы.МенеджерЗаписи.Прочитать() и МенеджерЗаписи.Записать() — это отдельный запрос к базе данных. Для миллиона записей это означает два миллиона обращений к СУБД (миллион чтений и миллион записей), что создает колоссальную нагрузку и замедляет выполнение до неприемлемых значений.МенеджерЗаписи, потребляет значительные ресурсы памяти и процессора.Поэтому этот метод категорически не рекомендуется для работы с большими объемами данных.
Для клиент-серверных баз данных (например, на MS SQL Server, PostgreSQL) существует наиболее быстрый, но и самый рискованный метод — прямое выполнение SQL-запроса на обновление таблицы регистра сведений. Такой запрос может обновить миллионы записей за считанные секунды.
Пример SQL-запроса (концептуально):
UPDATE _Reference23456 SET _Fld345 = 'ЗНАЧЕНИЕ' WHERE _Fld345 IS NULL;
Где _Reference23456 — это таблица регистра сведений в СУБД, а _Fld345 — колонка, соответствующая новому измерению.
Однако, у этого подхода есть существенные недостатки:
Рекомендация: Этот метод следует применять только в крайних случаях, при наличии очень большого опыта работы с СУБД 1С, и обязательно на предварительно созданной резервной копии базы данных. В большинстве случаев использование объектной модели 1С с порционной обработкой является предпочтительным.
Как было упомянуто в сообщениях 2 и 5, для решения подобных задач можно использовать готовые универсальные обработки для группового изменения данных. Такие обработки часто уже содержат оптимизированные алгоритмы для работы с большими объемами информации, включая порционную обработку. Они позволяют в пользовательском режиме настроить отборы, указать, какие реквизиты и как изменять, и выполнить операцию без необходимости писать код вручную.
Создание своей внешней обработки (как предложено в сообщении 5) также является хорошим решением, так как позволяет инкапсулировать всю логику в один файл, который легко переносить и использовать в разных базах.
Выбрав правильный подход и следуя этим рекомендациям, вы сможете эффективно и безопасно заполнить новое измерение в регистре сведений, даже при работе с очень большим объемом данных.