При работе с большими объемами данных в системах 1С:Предприятие часто возникает задача массового обновления информации. В частности, речь идет о независимых регистрах сведений, которые могут содержать миллионы записей. Стандартный подход с использованием менеджера записи в цикле оказывается крайне неэффективным и медленным. Рассмотрим, как мы можем оптимизировать этот процесс, чтобы обеспечить быстрое добавление, обновление и сохранение данных, которые отсутствуют во входной таблице.
Представьте, что у нас есть независимый регистр сведений, например, ДополнительныеСведения, со структурой Объект (измерение), Свойство (измерение), Значение (ресурс). В этом регистре хранится, допустим, 10 миллионов записей. Мы получаем извне или формируем в процессе работы таблицу значений (ТЗ) аналогичной структуры, содержащую 100 тысяч строк. Наша цель — синхронизировать данные регистра с этой ТЗ:
Значение) для тех записей, которые есть в ТЗ и уже существуют в регистре (совпадают по Объект и Свойство).Тривиальный, но медленный подход выглядит следующим образом:
Для каждого Об из ТЗ Цикл
Менеджер = РегистрыСведений.ДополнительныеСведения.СоздатьМенеджерЗаписи();
Менеджер.Объект = Об.Объект;
Менеджер.Свойство = Об.Свойство;
Менеджер.Значение = Об.Значение;
Менеджер.Записать(Истина); // Параметр Истина - обновить, если есть, добавить, если нет.
КонецЦикла;
Этот код выполняет каждую запись отдельно, что генерирует большое количество операций ввода-вывода и транзакций, делая его крайне неэффективным для тысяч и сотен тысяч записей. Для решения этой задачи существуют гораздо более быстрые методы, которые мы сейчас подробно разберем.
Для современных версий платформы 1С:Предприятие (начиная с 8.3.26) был введен новый режим записи для объекта НаборЗаписей – РежимЗамещения.Слияние (Merge). Этот режим идеально подходит для нашей задачи, поскольку он позволяет добавлять новые записи и обновлять существующие, не затрагивая записи, которых нет в передаваемом наборе.
Разберем по шагам, как мы можем использовать этот режим:
НаборЗаписей для нашего регистра сведений.Загрузить() для быстрого переноса всех строк из нашей ТЗ в объект НаборЗаписей. Это значительно быстрее, чем добавлять записи по одной.Записать(), указав в качестве параметра РежимЗамещения.Слияние. Этот режим укажет платформе, что необходимо провести операцию слияния данных.Посмотрим на пример кода:
// 1. Формируем таблицу значений с данными для обновления/добавления
// Предположим, что ВашаТаблицаЗначений уже сформирована и содержит 100 000 строк.
// Например:
// ВашаТаблицаЗначений = Новый ТаблицаЗначений;
// ВашаТаблицаЗначений.Колонки.Добавить("Объект");
// ВашаТаблицаЗначений.Колонки.Добавить("Свойство");
// ВашаТаблицаЗначений.Колонки.Добавить("Значение");
// Для каждого СтрокаТЗ из ИсточникДанных Цикл
// НоваяСтрока = ВашаТаблицаЗначений.Добавить();
// НоваяСтрока.Объект = СтрокаТЗ.Объект;
// НоваяСтрока.Свойство = СтрокаТЗ.Свойство;
// НоваяСтрока.Значение = СтрокаТЗ.Значение;
// КонецЦикла;
// 2. Создаем НаборЗаписей для целевого регистра
НаборЗаписейРС = РегистрыСведений.ДополнительныеСведения.СоздатьНаборЗаписей();
// 3. Загружаем данные из ТЗ в НаборЗаписей
// Важно! Отбор набора записей в этом случае лучше не устанавливать,
// так как обрабатываемые записи определяются содержимым самого набора.
НаборЗаписейРС.Загрузить(ВашаТаблицаЗначений);
// 4. Вызываем Записать() с режимом Слияние
// Это добавит новые записи, обновит существующие и оставит без изменений те,
// которых нет в ВашейТаблицеЗначений, но есть в регистре.
Попытка
НаборЗаписейРС.Записать(РежимЗамещения.Слияние);
Сообщить("Данные успешно синхронизированы в регистре сведений.");
Исключение Как Исключение
Сообщить("Ошибка при записи в регистр сведений: " + Исключение.Описание);
КонецПопытки;
Важно: При использовании режимов Слияние и Удаление отбор набора записей не устанавливайте, поскольку множество обрабатываемых записей определяется содержимым самого набора. Если установить отбор, то все записи в наборе должны будут соответствовать этому отбору, что может привести к ошибке или нежелательному поведению.
Помимо Слияние, существуют и другие режимы замещения, появившиеся в 8.3.26 и 8.3.27:
Записать(Ложь).Записать(Истина). Может быть неэффективным для частичного обновления.Если вы работаете на более старой версии платформы 1С:Предприятие, где режим РежимЗамещения.Слияние еще не доступен, нам придется использовать другой, более сложный алгоритм, чтобы достичь той же логики "добавить/обновить/не трогать". Этот подход требует предварительного сбора всех необходимых данных в одну временную структуру, которая затем будет записана.
Рассмотрим пошаговый алгоритм:
Свойство, если мы работаем с записями для одного конкретного свойства). Это поможет избежать конфликтов при параллельной работе. Однако для независимых регистров и пакетной записи без отбора это менее критично.Объект и Свойство). В результате мы получим таблицу, которая будет содержать:
ЕСТЬNULL() для выбора правильного значения ресурса Значение: если запись есть в ТЗ, берем ее Значение, иначе – Значение из регистра.НаборЗаписей. Если мы работали с отбором по какому-либо измерению (например, Свойство), мы устанавливаем этот отбор в НаборЗаписей. Если нет, то оставляем без отбора. Затем мы загружаем данные из нашей объединенной таблицы значений в этот НаборЗаписей.Записать(Истина). Поскольку НаборЗаписей содержит уже объединенные и подготовленные данные, параметр Истина приведет к удалению всех записей, соответствующих отбору (если он установлен), и записи новых данных из набора. Если отбора нет, то будет удален весь регистр и записаны данные из набора (чего мы хотим избежать). Поэтому важно, чтобы запрос в шаге 2 формировал только те записи, которые *действительно должны* быть в регистре после операции. Этот подход более рискован, если неверно сформировать запрос, так как Записать(Истина) удаляет всё по отбору.Посмотрим на концептуальный пример кода для шага 2 и 3:
// Предположим, ВашаТаблицаЗначений (ТЗ) уже сформирована
// (100 тыс. строк) и РегистрСведений.ДополнительныеСведения (РС)
// содержит 10 млн. записей.
// Создаем временную таблицу для нашей ТЗ, чтобы использовать ее в запросе
МенеджерВТ = Новый МенеджерВременныхТаблиц;
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = МенеджерВТ;
Запрос.Текст =
"ВЫБРАТЬ
| ТЗ.Объект КАК Объект,
| ТЗ.Свойство КАК Свойство,
| ТЗ.Значение КАК Значение
|ПОМЕСТИТЬ ВТ_ТЗ
|ИЗ
| &ВашаТаблицаЗначений КАК ТЗ";
Запрос.УстановитьПараметр("ВашаТаблицаЗначений", ВашаТаблицаЗначений);
Запрос.Выполнить();
// Формируем окончательный набор данных
Запрос.Текст =
"ВЫБРАТЬ
| ЕСТЬNULL(ВТ_ТЗ.Объект, РС.Объект) КАК Объект,
| ЕСТЬNULL(ВТ_ТЗ.Свойство, РС.Свойство) КАК Свойство,
| ЕСТЬNULL(ВТ_ТЗ.Значение, РС.Значение) КАК Значение
|ИЗ
| РегистрСведений.ДополнительныеСведения КАК РС
| ПОЛНОЕ СОЕДИНЕНИЕ ВТ_ТЗ КАК ВТ_ТЗ
| ПО (РС.Объект = ВТ_ТЗ.Объект
| И РС.Свойство = ВТ_ТЗ.Свойство)";
ТаблицаДляЗаписи = Запрос.Выполнить().Выгрузить();
// Теперь ТаблицаДляЗаписи содержит объединенные данные.
// Далее мы можем использовать НаборЗаписей для записи:
НаборЗаписейРС = РегистрыСведений.ДополнительныеСведения.СоздатьНаборЗаписей();
// Если отбор не нужен (т.е. мы работаем со всем регистром или его большой частью),
// то отбор не устанавливаем. В этом случае Записать(Истина) удалит весь регистр,
// что может быть нежелательно для больших объемов.
// Если есть отбор, например, по Свойству:
// НаборЗаписейРС.Отбор.Свойство.Установить("МоеСвойство");
// Но в нашем случае (полное соединение) это не сработает напрямую для сохранения 'нетронутых'.
// Этот метод требует формирования полного набора данных, который должен быть в регистре.
// Загрузка и запись
НаборЗаписейРС.Загрузить(ТаблицаДляЗаписи);
Попытка
// Важно: если отбор не установлен, этот метод перезапишет ВЕСЬ регистр.
// Если отбор установлен, перезапишет только по отбору.
НаборЗаписейРС.Записать(Истина); // Замещает все записи, попадающие под отбор.
Сообщить("Данные успешно синхронизированы в регистре сведений (старый метод).");
Исключение Как Исключение
Сообщить("Ошибка при записи в регистр сведений: " + Исключение.Описание);
КонецПопытки;
Важный нюанс: Подход с полным соединением и последующей записью НаборЗаписей.Записать(Истина) без отбора приведет к перезаписи всего регистра. Если же мы устанавливаем отбор (например, по Свойство), то Записать(Истина) удалит все записи регистра, соответствующие этому отбору, и вставит те, что есть в НаборЗаписей. Это означает, что наш запрос для объединения должен собрать именно те записи, которые должны остаться в регистре после операции, включая те, что не менялись.
Этот подход более сложен в реализации и требует тщательного контроля за формированием объединенной таблицы, чтобы случайно не удалить данные, которые должны были остаться.
Независимо от выбранного метода записи (НаборЗаписей.Записать(РежимЗамещения.Слияние) или более старые подходы), при работе с большими объемами данных крайне рекомендуется разбивать процесс записи на более мелкие порции, или "пакеты" (батчи), и оборачивать каждую порцию в отдельную транзакцию.
Почему это важно?
В большинстве случаев рекомендуется порционная запись наборов из 1000 до 5000 записей. Мы можем разбить нашу исходную ТЗ на части и обрабатывать каждую часть последовательно.
Посмотрим на пример с использованием пакетной обработки и РежимЗамещения.Слияние:
// ВашаТаблицаЗначений - исходная ТЗ (100 000 строк)
РазмерПакета = 2000; // Оптимальный размер пакета может варьироваться
// Создаем временную таблицу для итерации по пакетам
ВТ_ИсходныеДанные = Новый ТаблицаЗначений;
ВТ_ИсходныеДанные.Колонки.Добавить("Объект");
ВТ_ИсходныеДанные.Колонки.Добавить("Свойство");
ВТ_ИсходныеДанные.Колонки.Добавить("Значение");
Для каждого СтрокаТЗ из ВашаТаблицаЗначений Цикл
СтрокаВТ = ВТ_ИсходныеДанные.Добавить();
СтрокаВТ.Объект = СтрокаТЗ.Объект;
СтрокаВТ.Свойство = СтрокаТЗ.Свойство;
СтрокаВТ.Значение = СтрокаТЗ.Значение;
КонецЦикла;
// Итерация по пакетам
НачалоИндекса = 0;
КонецИндекса = 0;
ВсегоСтрок = ВТ_ИсходныеДанные.Количество();
Пока НачалоИндекса < ВсегоСтрок Цикл
КонецИндекса = Мин(НачалоИндекса + РазмерПакета, ВсегоСтрок);
ПакетСтрок = Новый ТаблицаЗначений;
ПакетСтрок.Колонки.Добавить("Объект");
ПакетСтрок.Колонки.Добавить("Свойство");
ПакетСтрок.Колонки.Добавить("Значение");
Для Индекс = НачалоИндекса По КонецИндекса - 1 Цикл
СтрокаПакета = ПакетСтрок.Добавить();
СтрокаПакета.Объект = ВТ_ИсходныеДанные[Индекс].Объект;
СтрокаПакета.Свойство = ВТ_ИсходныеДанные[Индекс].Свойство;
СтрокаПакета.Значение = ВТ_ИсходныеДанные[Индекс].Значение;
КонецЦикла;
НачатьТранзакцию();
Попытка
НаборЗаписейРС = РегистрыСведений.ДополнительныеСведения.СоздатьНаборЗаписей();
НаборЗаписейРС.Загрузить(ПакетСтрок);
НаборЗаписейРС.Записать(РежимЗамещения.Слияние);
ЗафиксироватьТранзакцию();
Сообщить("Записан пакет с " + НачалоИндекса + " по " + (КонецИндекса - 1));
Исключение Как Исключение
ОтменитьТранзакцию();
Сообщить("Ошибка при записи пакета с " + НачалоИндекса + " по " + (КонецИндекса - 1) + ": " + Исключение.Описание);
// Можно добавить логику для повторной попытки или логирования ошибки
КонецПопытки;
НачалоИндекса = КонецИндекса;
КонецЦикла;
Для исключительно больших объемов данных (сотни миллионов записей) и в случаях, когда требуется максимальная производительность, самым быстрым способом может быть прямое выполнение SQL-запросов к СУБД. Это позволяет использовать оптимизации СУБД на полную мощность. Для контроля нагрузки и исследования выполняемых СУБД команд пригодится инструмент для анализа SQL-сервера и запросов 1С.
Мы можем использовать команды INSERT ... ON CONFLICT UPDATE (для PostgreSQL) или MERGE (для MS SQL Server), или комбинацию INSERT и UPDATE.
Однако, этот подход имеет серьезные недостатки и риски:
По этой причине, прямой SQL обычно рассматривается как крайняя мера, когда все другие методы оптимизации исчерпаны, и только при наличии высокой квалификации и тщательно проведенного тестирования. Мы не будем приводить конкретные примеры SQL-кода, так как они сильно зависят от используемой СУБД и требуют специфических знаний о внутренней структуре таблиц 1С.
Помимо выбора правильного метода записи, существуют общие рекомендации, которые помогут нам добиться наилучшей производительности при работе с большими объемами данных:
Давайте еще раз проанализируем, почему МенеджерЗаписи не подходит для массовых операций, и почему НаборЗаписей является предпочтительным инструментом:
Записать() через менеджер записи инициирует отдельную транзакцию в СУБД (если нет внешней транзакции), что крайне неэффективно из-за накладных расходов.Прочитать()) → обработка → массовая фиксация.Загрузить() и Записать() выполняют операции с данными в СУБД гораздо более эффективно, часто одной транзакцией (или несколькими, если записи много и платформа разбивает их сама), минимизируя накладные расходы.Таким образом, мы видим, что для нашей задачи массового обновления данных в независимом регистре сведений, НаборЗаписей является ключевым инструментом, а новый режим РежимЗамещения.Слияние делает процесс максимально простым и эффективным на современных платформах 1С.