При разработке и оптимизации систем на платформе 1С:Предприятие одной из частых задач является массовое добавление или обновление записей в независимых регистрах сведений. Классический пример — регистрация штрихкодов номенклатуры, цен товаров, аналогов или ключевых спецификаций, поступающих из внешних источников или документов. Основная сложность заключается в том, что часть записываемых данных уже может существовать в базе данных, а часть является абсолютно новой.
В этой статье мы подробно разберем, почему стандартные подходы могут приводить к критическому падению производительности, проанализируем классический паттерн оптимизации через запросы и транзакции, а также рассмотрим современный революционный инструмент — встроенный режим слияния, появившийся в платформе 1С начиная с версии 8.3.25.
Рассмотрим классическую ситуацию: к нам поступает документ или внешний пакет данных (например, JSON-файл или таблица значений), содержащий список номенклатуры и штрихкодов. Нам необходимо отразить эти данные в независимом регистре сведений ШтрихкодыНоменклатуры с измерениями Номенклатура и Штрихкод.
У начинающих разработчиков часто возникает соблазн использовать один из двух очевидных, но неоптимальных путей:
РегистрСведенийМенеджерЗаписи, заполняются ключевые поля, вызывается метод Прочитать(), а затем метод Записать().
В чем проблема: Этот подход порождает так называемую проблему "N+1 запроса". Если в документе 100 строк, платформа выполнит 100 отдельных обращений к СУБД для чтения данных и еще 100 обращений для записи. На больших объемах данных это приводит к катастрофическим задержкам из-за накладных расходов на сетевой трафик и транзакционную логику СУБД.
РегистрСведенийНаборЗаписей, заполняется новыми данными и записывается с параметром Замещать = Истина.
В чем проблема: Набор записей в независимом регистре требует жесткой установки отборов по всем измерениям, для которых мы хотим перезаписать данные. Однако мы не можем установить отбор вида "В Списке" для измерений набора записей. Если мы не установим отбор вовсе, метод Записать(Истина) просто очистит весь регистр сведений и запишет туда только наши 20-100 новых строк, уничтожив всю остальную базу данных штрихкодов.
Выяснив ограничения стандартных механизмов, проанализируем правильные архитектурные решения для разных версий платформы.
Если ваша информационная база работает на версии платформы 8.3.24 или ниже, то наилучшим решением будет предварительное разделение входящего потока данных непосредственно на стороне СУБД. Нам нужно отсечь те записи, которые уже присутствуют в регистре, оставив только уникальные новые строки. После этого мы сможем записать их одной транзакцией без риска получить исключение о дублировании ключевых записей.
Разберем этот алгоритм по шагам:
ГДЕ Регистр.Измерение ЕСТЬ NULL. Таким образом, мы получаем только те строки, которых физически еще нет в нашей базе данных.Записать() параметр Ложь (не замещать).Рассмотрим практический пример реализации этого метода на встроенном языке 1С:
// Предположим, что ИсточникДанных - это ТаблицаЗначений с колонками Номенклатура и Штрихкод
Процедура ЗаписатьНовыеШтрихкодыОптимально(ИсточникДанных)
МенеджерВТ = Новый МенеджерВременныхТаблиц;
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = МенеджерВТ;
// Помещаем входящие данные во временную таблицу для быстрой обработки на сервере СУБД
Запрос.Текст =
"ВЫБРАТЬ
| Исходник.Номенклатура КАК Номенклатура,
| Исходник.Штрихкод КАК Штрихкод
|ПОМЕСТИТЬ ВТ_ВходящиеДанные
|ИЗ
| &ТаблицаИсточника КАК Исходник";
Запрос.УстановитьПараметр("ТаблицаИсточника", ИсточникДанных);
Запрос.Выполнить();
// Выбираем только те записи, которых еще нет в регистре сведений
Запрос.Текст =
"ВЫБРАТЬ
| Входящие.Номенклатура КАК Номенклатура,
| Входящие.Штрихкод КАК Штрихкод
|ИЗ
| ВТ_ВходящиеДанные КАК Входящие
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ШтрихкодыНоменклатуры КАК РегШтрихкоды
| ПО Входящие.Номенклатура = РегШтрихкоды.Номенклатура
| И Входящие.Штрихкод = РегШтрихкоды.Штрихкод
|ГДЕ
| РегШтрихкоды.Номенклатура ЕСТЬ NULL";
РезультатЗапроса = Запрос.Выполнить();
Если Не РезультатЗапроса.Пустой() Тогда
ТаблицаДляЗаписи = РезультатЗапроса.Выгрузить();
// Создаем набор записей без установки отборов
НаборЗаписей = РегистрыСведений.ШтрихкодыНоменклатуры.СоздатьНаборЗаписей();
НаборЗаписей.Загрузить(ТаблицаДляЗаписи);
// Записываем данные в режиме добавления (Замещать = Ложь)
// Это предотвратит очистку существующих данных в регистре
НаборЗаписей.Записать(Ложь);
КонецЕсли;
КонецПроцедуры
Этот метод работает чрезвычайно быстро, так как сравнение сотен тысяч записей перекладывается на плечи СУБД, которая использует индексы таблицы регистра. В базу данных уходит всего один запрос на чтение и один пакетный запрос на запись.
Несмотря на высокую скорость работы первого решения, при его использовании в высоконагруженных многопользовательских системах (HighLoad) кроется серьезная опасность. Когда мы вызываем метод НаборЗаписей.Записать(Ложь) без установленных отборов, СУБД не знает заранее, какие именно диапазоны записей будут изменены.
В зависимости от настроек СУБД (MS SQL Server, PostgreSQL) и объема записываемых данных, это может привести к эскалации блокировок — вместо блокировки конкретных строк (Row Locks) СУБД может заблокировать целые страницы данных (Page Locks) или даже всю таблицу регистра сведений (Table Lock). В этот момент другие пользователи, пытающиеся записать или прочитать штрихкод совершенно другого товара, столкнутся с ожиданиями на блокировках или ошибкой Deadlock (взаимная блокировка).
Как снизить риски блокировок на старых платформах:
МенеджерЗаписи, но обязательно обернуть весь цикл обработки в единую транзакцию. В таком случае СУБД наложит точечные строчные блокировки, а скорость останется высокой благодаря отсутствию накладных расходов на открытие отдельных транзакций под каждую строку.Разработчики технологической платформы 1С детально проанализировали эту проблему и в версии **8.3.25** представили кардинальное обновление механизмов работы с независимыми регистрами. Теперь нам больше не нужно писать сложные запросы с левыми соединениями и заботиться об исключениях уникальности записей.
В метод Записать() объекта РегистрСведенийНаборЗаписей был добавлен новый параметр, принимающий значения системного перечисления РежимЗамещения (или ReplacementMode в англоязычном варианте):
РежимЗамещения.Замещение (аналог старого Замещать = Истина) — заменяет записи, соответствующие установленному отбору.РежимЗамещения.Добавление (аналог старого Замещать = Ложь) — добавляет записи в регистр, вызывая ошибку при совпадении ключевых полей.РежимЗамещения.Слияние (Merge) — ключевой инструмент оптимизации. Платформа формирует специальный SQL-запрос трансляции операции MERGE (для MS SQL) или ON CONFLICT DO UPDATE (для PostgreSQL). СУБД за одно обращение проверяет существование записей: если запись с такими измерениями уже есть — она обновляет ее ресурсы и реквизиты, если записи нет — создает новую.РежимЗамещения.Удаление — удаляет из БД записи, которые совпадают по ключевым полям с записями в наборе.Проанализируем пример кода с использованием нового режима слияния:
Процедура ЗаписатьШтрихкодыСлиянием(ИсточникДанных)
// Создаем набор записей без установки отборов
НаборЗаписей = РегистрыСведений.ШтрихкодыНоменклатуры.СоздатьНаборЗаписей();
// Напрямую загружаем всю таблицу входящих данных в набор записей
НаборЗаписей.Загрузить(ИсточникДанных);
// Выполняем запись в режиме слияния.
// Платформа сама определит, какие строки добавить, а какие обновить.
// При этом будут наложены минимальные и максимально точечные блокировки на уровне СУБД!
НаборЗаписей.Записать(РежимЗамещения.Слияние);
КонецПроцедуры
Использование режима РежимЗамещения.Слияние дает разработчику сразу несколько весомых преимуществ:
Выбор оптимального метода записи данных в независимый регистр сведений напрямую зависит от версии технологической платформы, на которой развернуто ваше прикладное решение:
НаборЗаписей.Записать(РежимЗамещения.Слияние). Это наиболее быстрый, безопасный и лаконичный способ массовой вставки и обновления данных.НаборЗаписей.Записать(Ложь). При высоких рисках параллельных блокировок отдавайте предпочтение точечной записи через МенеджерЗаписи, обязательно обернутой в единую транзакцию.