Как эффективно добавлять новые записи, обновлять существующие и оставлять без изменений остальные записи в независимом регистре сведений 1С?

Программист 1С v8.3 (Управляемые формы)
← На главную

При работе с большими объемами данных в системах 1С:Предприятие часто возникает задача массового обновления информации. В частности, речь идет о независимых регистрах сведений, которые могут содержать миллионы записей. Стандартный подход с использованием менеджера записи в цикле оказывается крайне неэффективным и медленным. Рассмотрим, как мы можем оптимизировать этот процесс, чтобы обеспечить быстрое добавление, обновление и сохранение данных, которые отсутствуют во входной таблице.

Введение в проблему неэффективного обновления

Представьте, что у нас есть независимый регистр сведений, например, ДополнительныеСведения, со структурой Объект (измерение), Свойство (измерение), Значение (ресурс). В этом регистре хранится, допустим, 10 миллионов записей. Мы получаем извне или формируем в процессе работы таблицу значений (ТЗ) аналогичной структуры, содержащую 100 тысяч строк. Наша цель — синхронизировать данные регистра с этой ТЗ:

  1. Новые записи: Добавить те записи из ТЗ, которых нет в регистре.
  2. Существующие записи: Обновить значение ресурса (Значение) для тех записей, которые есть в ТЗ и уже существуют в регистре (совпадают по Объект и Свойство).
  3. Неизмененные записи: Записи, которые есть в регистре, но отсутствуют в нашей ТЗ, должны остаться без изменений.

Тривиальный, но медленный подход выглядит следующим образом:


Для каждого Об из ТЗ Цикл
    Менеджер = РегистрыСведений.ДополнительныеСведения.СоздатьМенеджерЗаписи();
    Менеджер.Объект = Об.Объект;
    Менеджер.Свойство = Об.Свойство;
    Менеджер.Значение = Об.Значение;
    Менеджер.Записать(Истина); // Параметр Истина - обновить, если есть, добавить, если нет.
КонецЦикла;

Этот код выполняет каждую запись отдельно, что генерирует большое количество операций ввода-вывода и транзакций, делая его крайне неэффективным для тысяч и сотен тысяч записей. Для решения этой задачи существуют гораздо более быстрые методы, которые мы сейчас подробно разберем.

1. Использование объекта "НаборЗаписей" с режимом замещения "Слияние" (платформа 8.3.26+)

Для современных версий платформы 1С:Предприятие (начиная с 8.3.26) был введен новый режим записи для объекта НаборЗаписей – РежимЗамещения.Слияние (Merge). Этот режим идеально подходит для нашей задачи, поскольку он позволяет добавлять новые записи и обновлять существующие, не затрагивая записи, которых нет в передаваемом наборе.

Разберем по шагам, как мы можем использовать этот режим:

  1. Подготовка таблицы значений (ТЗ): Мы формируем таблицу значений, которая содержит все данные, которые необходимо добавить или обновить. Структура этой ТЗ должна точно соответствовать структуре регистра сведений (измерения и ресурсы).
  2. Создание НабораЗаписей: Мы создаем объект НаборЗаписей для нашего регистра сведений.
  3. Загрузка данных в НаборЗаписей: Мы используем метод Загрузить() для быстрого переноса всех строк из нашей ТЗ в объект НаборЗаписей. Это значительно быстрее, чем добавлять записи по одной.
  4. Вызов метода Записать() с режимом Слияние: Самый важный шаг – это вызов метода Записать(), указав в качестве параметра РежимЗамещения.Слияние. Этот режим укажет платформе, что необходимо провести операцию слияния данных.

Посмотрим на пример кода:


// 1. Формируем таблицу значений с данными для обновления/добавления
// Предположим, что ВашаТаблицаЗначений уже сформирована и содержит 100 000 строк.
// Например:
// ВашаТаблицаЗначений = Новый ТаблицаЗначений;
// ВашаТаблицаЗначений.Колонки.Добавить("Объект");
// ВашаТаблицаЗначений.Колонки.Добавить("Свойство");
// ВашаТаблицаЗначений.Колонки.Добавить("Значение");
// Для каждого СтрокаТЗ из ИсточникДанных Цикл
//     НоваяСтрока = ВашаТаблицаЗначений.Добавить();
//     НоваяСтрока.Объект = СтрокаТЗ.Объект;
//     НоваяСтрока.Свойство = СтрокаТЗ.Свойство;
//     НоваяСтрока.Значение = СтрокаТЗ.Значение;
// КонецЦикла;

// 2. Создаем НаборЗаписей для целевого регистра
НаборЗаписейРС = РегистрыСведений.ДополнительныеСведения.СоздатьНаборЗаписей();

// 3. Загружаем данные из ТЗ в НаборЗаписей
// Важно! Отбор набора записей в этом случае лучше не устанавливать,
// так как обрабатываемые записи определяются содержимым самого набора.
НаборЗаписейРС.Загрузить(ВашаТаблицаЗначений);

// 4. Вызываем Записать() с режимом Слияние
// Это добавит новые записи, обновит существующие и оставит без изменений те,
// которых нет в ВашейТаблицеЗначений, но есть в регистре.
Попытка
    НаборЗаписейРС.Записать(РежимЗамещения.Слияние);
    Сообщить("Данные успешно синхронизированы в регистре сведений.");
Исключение Как Исключение
    Сообщить("Ошибка при записи в регистр сведений: " + Исключение.Описание);
КонецПопытки;

Важно: При использовании режимов Слияние и Удаление отбор набора записей не устанавливайте, поскольку множество обрабатываемых записей определяется содержимым самого набора. Если установить отбор, то все записи в наборе должны будут соответствовать этому отбору, что может привести к ошибке или нежелательному поведению.

Помимо Слияние, существуют и другие режимы замещения, появившиеся в 8.3.26 и 8.3.27:

  1. Добавление (Append): Добавляет записи, если таких нет. Выбрасывает исключение, если записи с совпадающими ключевыми полями уже существуют. Функционально аналогичен Записать(Ложь).
  2. Замещение (Replace): Удаляет все записи, соответствующие отбору, а затем записывает новые из набора. Функционально аналогичен Записать(Истина). Может быть неэффективным для частичного обновления.
  3. Удаление (Delete): Удаляет из регистра записи, совпадающие по значениям ключевых полей с записями, присутствующими в наборе записей.
  4. Обновление (Update): (Добавлен в 8.3.27) Позволяет обновить только те ресурсы записей, которые есть в наборе, не затрагивая другие поля и не удаляя записи. Полезен, если нужно заполнить новый ресурс без перезаписи всех остальных данных.

2. Подход для более ранних версий платформы (до 8.3.26) с полным соединением

Если вы работаете на более старой версии платформы 1С:Предприятие, где режим РежимЗамещения.Слияние еще не доступен, нам придется использовать другой, более сложный алгоритм, чтобы достичь той же логики "добавить/обновить/не трогать". Этот подход требует предварительного сбора всех необходимых данных в одну временную структуру, которая затем будет записана.

Рассмотрим пошаговый алгоритм:

  1. Установка блокировки (по необходимости): Если в системе возможны одновременные изменения тех же данных, имеет смысл установить программную блокировку на регистр сведений по соответствующим измерениям (например, по Свойство, если мы работаем с записями для одного конкретного свойства). Это поможет избежать конфликтов при параллельной работе. Однако для независимых регистров и пакетной записи без отбора это менее критично.
  2. Формирование объединенной таблицы данных: Самый сложный шаг. Мы должны запросом получить все записи из регистра сведений, которые потенциально могут быть затронуты, и выполнить полное соединение этих данных с нашей входной таблицей значений (ТЗ). Это соединение будет выполняться по ключевым полям регистра (Объект и Свойство). В результате мы получим таблицу, которая будет содержать:
    • Записи из ТЗ, которых нет в регистре (для добавления).
    • Записи из регистра, которых нет в ТЗ (их мы должны сохранить).
    • Записи, которые есть и там, и там (для обновления).
    При этом мы будем использовать функцию ЕСТЬNULL() для выбора правильного значения ресурса Значение: если запись есть в ТЗ, берем ее Значение, иначе – Значение из регистра.
  3. Выгрузка результата в новую таблицу значений: Результат запроса выгружается в новую таблицу значений. Эта таблица будет содержать окончательный набор данных, который мы хотим видеть в регистре.
  4. Создание НабораЗаписей с отбором (если применимо) и загрузка: Мы создаем объект НаборЗаписей. Если мы работали с отбором по какому-либо измерению (например, Свойство), мы устанавливаем этот отбор в НаборЗаписей. Если нет, то оставляем без отбора. Затем мы загружаем данные из нашей объединенной таблицы значений в этот НаборЗаписей.
  5. Запись НабораЗаписей: Вызываем метод Записать(Истина). Поскольку НаборЗаписей содержит уже объединенные и подготовленные данные, параметр Истина приведет к удалению всех записей, соответствующих отбору (если он установлен), и записи новых данных из набора. Если отбора нет, то будет удален весь регистр и записаны данные из набора (чего мы хотим избежать). Поэтому важно, чтобы запрос в шаге 2 формировал только те записи, которые *действительно должны* быть в регистре после операции. Этот подход более рискован, если неверно сформировать запрос, так как Записать(Истина) удаляет всё по отбору.

Посмотрим на концептуальный пример кода для шага 2 и 3:


// Предположим, ВашаТаблицаЗначений (ТЗ) уже сформирована
// (100 тыс. строк) и РегистрСведений.ДополнительныеСведения (РС)
// содержит 10 млн. записей.

// Создаем временную таблицу для нашей ТЗ, чтобы использовать ее в запросе
МенеджерВТ = Новый МенеджерВременныхТаблиц;
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = МенеджерВТ;

Запрос.Текст = 
    "ВЫБРАТЬ
    |    ТЗ.Объект КАК Объект,
    |    ТЗ.Свойство КАК Свойство,
    |    ТЗ.Значение КАК Значение
    |ПОМЕСТИТЬ ВТ_ТЗ
    |ИЗ
    |    &ВашаТаблицаЗначений КАК ТЗ";
Запрос.УстановитьПараметр("ВашаТаблицаЗначений", ВашаТаблицаЗначений);
Запрос.Выполнить();

// Формируем окончательный набор данных
Запрос.Текст = 
    "ВЫБРАТЬ
    |    ЕСТЬNULL(ВТ_ТЗ.Объект, РС.Объект) КАК Объект,
    |    ЕСТЬNULL(ВТ_ТЗ.Свойство, РС.Свойство) КАК Свойство,
    |    ЕСТЬNULL(ВТ_ТЗ.Значение, РС.Значение) КАК Значение
    |ИЗ
    |    РегистрСведений.ДополнительныеСведения КАК РС
    |        ПОЛНОЕ СОЕДИНЕНИЕ ВТ_ТЗ КАК ВТ_ТЗ
    |        ПО (РС.Объект = ВТ_ТЗ.Объект
    |            И РС.Свойство = ВТ_ТЗ.Свойство)";
            
ТаблицаДляЗаписи = Запрос.Выполнить().Выгрузить();

// Теперь ТаблицаДляЗаписи содержит объединенные данные.
// Далее мы можем использовать НаборЗаписей для записи:
НаборЗаписейРС = РегистрыСведений.ДополнительныеСведения.СоздатьНаборЗаписей();
// Если отбор не нужен (т.е. мы работаем со всем регистром или его большой частью),
// то отбор не устанавливаем. В этом случае Записать(Истина) удалит весь регистр,
// что может быть нежелательно для больших объемов.
// Если есть отбор, например, по Свойству:
// НаборЗаписейРС.Отбор.Свойство.Установить("МоеСвойство");
// Но в нашем случае (полное соединение) это не сработает напрямую для сохранения 'нетронутых'.
// Этот метод требует формирования полного набора данных, который должен быть в регистре.

// Загрузка и запись
НаборЗаписейРС.Загрузить(ТаблицаДляЗаписи);
Попытка
    // Важно: если отбор не установлен, этот метод перезапишет ВЕСЬ регистр.
    // Если отбор установлен, перезапишет только по отбору.
    НаборЗаписейРС.Записать(Истина); // Замещает все записи, попадающие под отбор.
    Сообщить("Данные успешно синхронизированы в регистре сведений (старый метод).");
Исключение Как Исключение
    Сообщить("Ошибка при записи в регистр сведений: " + Исключение.Описание);
КонецПопытки;

Важный нюанс: Подход с полным соединением и последующей записью НаборЗаписей.Записать(Истина) без отбора приведет к перезаписи всего регистра. Если же мы устанавливаем отбор (например, по Свойство), то Записать(Истина) удалит все записи регистра, соответствующие этому отбору, и вставит те, что есть в НаборЗаписей. Это означает, что наш запрос для объединения должен собрать именно те записи, которые должны остаться в регистре после операции, включая те, что не менялись.

Этот подход более сложен в реализации и требует тщательного контроля за формированием объединенной таблицы, чтобы случайно не удалить данные, которые должны были остаться.

3. Разбиение на транзакции (пакеты)

Независимо от выбранного метода записи (НаборЗаписей.Записать(РежимЗамещения.Слияние) или более старые подходы), при работе с большими объемами данных крайне рекомендуется разбивать процесс записи на более мелкие порции, или "пакеты" (батчи), и оборачивать каждую порцию в отдельную транзакцию.

Почему это важно?

  1. Снижение нагрузки на СУБД: Одна огромная транзакция может заблокировать значительные части базы данных и привести к высокому потреблению ресурсов СУБД (памяти, диска). Небольшие транзакции быстрее завершаются и освобождают ресурсы.
  2. Уменьшение объема журнала транзакций: Большие транзакции генерируют огромные записи в журнале транзакций СУБД, что может привести к его переполнению и замедлению работы.
  3. Повышение стабильности: При возникновении ошибки в середине очень большой транзакции, весь процесс придется откатывать, что может занять много времени и потребовать значительных ресурсов. Небольшие транзакции позволяют зафиксировать часть работы и продолжить с места последней успешной фиксации.
  4. Снижение времени блокировок: Длительные транзакции увеличивают вероятность возникновения взаимоблокировок (deadlocks) и конкуренции за ресурсы.

В большинстве случаев рекомендуется порционная запись наборов из 1000 до 5000 записей. Мы можем разбить нашу исходную ТЗ на части и обрабатывать каждую часть последовательно.

Посмотрим на пример с использованием пакетной обработки и РежимЗамещения.Слияние:


// ВашаТаблицаЗначений - исходная ТЗ (100 000 строк)
РазмерПакета = 2000; // Оптимальный размер пакета может варьироваться

// Создаем временную таблицу для итерации по пакетам
ВТ_ИсходныеДанные = Новый ТаблицаЗначений;
ВТ_ИсходныеДанные.Колонки.Добавить("Объект");
ВТ_ИсходныеДанные.Колонки.Добавить("Свойство");
ВТ_ИсходныеДанные.Колонки.Добавить("Значение");

Для каждого СтрокаТЗ из ВашаТаблицаЗначений Цикл
    СтрокаВТ = ВТ_ИсходныеДанные.Добавить();
    СтрокаВТ.Объект = СтрокаТЗ.Объект;
    СтрокаВТ.Свойство = СтрокаТЗ.Свойство;
    СтрокаВТ.Значение = СтрокаТЗ.Значение;
КонецЦикла;

// Итерация по пакетам
НачалоИндекса = 0;
КонецИндекса = 0;
ВсегоСтрок = ВТ_ИсходныеДанные.Количество();

Пока НачалоИндекса < ВсегоСтрок Цикл
    КонецИндекса = Мин(НачалоИндекса + РазмерПакета, ВсегоСтрок);
    
    ПакетСтрок = Новый ТаблицаЗначений;
    ПакетСтрок.Колонки.Добавить("Объект");
    ПакетСтрок.Колонки.Добавить("Свойство");
    ПакетСтрок.Колонки.Добавить("Значение");
    
    Для Индекс = НачалоИндекса По КонецИндекса - 1 Цикл
        СтрокаПакета = ПакетСтрок.Добавить();
        СтрокаПакета.Объект = ВТ_ИсходныеДанные[Индекс].Объект;
        СтрокаПакета.Свойство = ВТ_ИсходныеДанные[Индекс].Свойство;
        СтрокаПакета.Значение = ВТ_ИсходныеДанные[Индекс].Значение;
    КонецЦикла;

    НачатьТранзакцию();
    Попытка
        НаборЗаписейРС = РегистрыСведений.ДополнительныеСведения.СоздатьНаборЗаписей();
        НаборЗаписейРС.Загрузить(ПакетСтрок);
        НаборЗаписейРС.Записать(РежимЗамещения.Слияние);
        ЗафиксироватьТранзакцию();
        Сообщить("Записан пакет с " + НачалоИндекса + " по " + (КонецИндекса - 1));
    Исключение Как Исключение
        ОтменитьТранзакцию();
        Сообщить("Ошибка при записи пакета с " + НачалоИндекса + " по " + (КонецИндекса - 1) + ": " + Исключение.Описание);
        // Можно добавить логику для повторной попытки или логирования ошибки
    КонецПопытки;
    
    НачалоИндекса = КонецИндекса;
КонецЦикла;

4. Использование прямых SQL-запросов

Для исключительно больших объемов данных (сотни миллионов записей) и в случаях, когда требуется максимальная производительность, самым быстрым способом может быть прямое выполнение SQL-запросов к СУБД. Это позволяет использовать оптимизации СУБД на полную мощность. Для контроля нагрузки и исследования выполняемых СУБД команд пригодится инструмент для анализа SQL-сервера и запросов 1С.

Мы можем использовать команды INSERT ... ON CONFLICT UPDATE (для PostgreSQL) или MERGE (для MS SQL Server), или комбинацию INSERT и UPDATE.

Однако, этот подход имеет серьезные недостатки и риски:

  1. Обход логики платформы: Прямые SQL-запросы обходят всю бизнес-логику платформы 1С, включая проверку прав, подписки на события, контроль целостности данных, механизмы регистрации изменений для распределенных баз данных и т.д.
  2. Высокие требования к квалификации: Требуется глубокое понимание структуры базы данных 1С (как таблицы 1С мапятся на таблицы СУБД), знание SQL, а также принципов работы СУБД.
  3. Риск нарушения целостности данных: Неправильно составленный SQL-запрос может привести к порче данных, которую будет крайне сложно исправить.
  4. Сложность реализации: Для выполнения прямых SQL-запросов из 1С требуются специальные механизмы, например, внешние компоненты, которые могут взаимодействовать с СУБД напрямую.

По этой причине, прямой SQL обычно рассматривается как крайняя мера, когда все другие методы оптимизации исчерпаны, и только при наличии высокой квалификации и тщательно проведенного тестирования. Мы не будем приводить конкретные примеры SQL-кода, так как они сильно зависят от используемой СУБД и требуют специфических знаний о внутренней структуре таблиц 1С.

Рекомендации по производительности

Помимо выбора правильного метода записи, существуют общие рекомендации, которые помогут нам добиться наилучшей производительности при работе с большими объемами данных:

  1. Оптимизация запросов и индексы: Убедитесь, что запросы, используемые для подготовки исходной ТЗ или для выборки данных из регистра, максимально оптимизированы. Это включает использование индексов для ускорения поиска и выборки данных. Если измерения регистра не индексированы, процесс поиска и обновления будет очень медленным.
  2. Многопоточность: Для систем с очень большим количеством записей (от 10-15 миллионов) мы можем рассмотреть использование многопоточности. Это позволяет распараллелить процесс обработки и записи данных, используя несколько фоновых заданий или потоков, что может значительно сократить общее время выполнения операции. Каждое фоновое задание будет обрабатывать свой пакет данных.
  3. Тестирование производительности: Всегда тестируйте выбранный подход на реальных или близких к реальным объемах данных в среде, максимально приближенной к рабочей. Например, как было продемонстрировано на форуме, 90 тыс. записей обновились за 9 секунд в файловом режиме при отборе по свойству, что является хорошим результатом.

Сравнение МенеджераЗаписи и НабораЗаписей

Давайте еще раз проанализируем, почему МенеджерЗаписи не подходит для массовых операций, и почему НаборЗаписей является предпочтительным инструментом:

  1. МенеджерЗаписи:
    • Предназначен для работы с одной записью регистра.
    • Используется, когда требуется установить отбор одновременно по всем измерениям для точечного изменения.
    • Каждая операция Записать() через менеджер записи инициирует отдельную транзакцию в СУБД (если нет внешней транзакции), что крайне неэффективно из-за накладных расходов.
    • Хорошо подходит для пользовательского интерфейса или небольших, единичных изменений.
  2. НаборЗаписей:
    • Предназначен для пакетной выборки и записи множества записей.
    • Концентрирует весь цикл работы: фильтрация (если установлен отбор) → чтение (если вызывается Прочитать()) → обработка → массовая фиксация.
    • Методы Загрузить() и Записать() выполняют операции с данными в СУБД гораздо более эффективно, часто одной транзакцией (или несколькими, если записи много и платформа разбивает их сама), минимизируя накладные расходы.
    • Это рекомендуемый инструмент для массовых изменений и синхронизации данных в регистрах сведений.

Таким образом, мы видим, что для нашей задачи массового обновления данных в независимом регистре сведений, НаборЗаписей является ключевым инструментом, а новый режим РежимЗамещения.Слияние делает процесс максимально простым и эффективным на современных платформах 1С.

← На главную