Как в 1С избежать дублирования записей в периодическом регистре сведений при неизменных данных?

Программист 1С v8.3 (Обычные формы) IT и автоматизация бизнеса
← На главную

При разработке механизмов загрузки данных или ежедневной фиксации состояний (например, цен или статусов транзакций) часто возникает ситуация, когда данные приходят каждый день, но реально изменяются лишь в 10–20% случаев. В периодическом регистре сведений 1С запись новой строки с тем же набором измерений, но новой датой (периодом), создает очередную запись в базе данных. Если ресурсов много, а база объемная, это приводит к неоправданному росту таблиц (DBInfoReg). Наша задача — научиться эффективно проверять наличие идентичной записи на предыдущие даты и записывать данные только в том случае, если значения ресурсов действительно изменились.

Рассмотрим несколько подходов к решению этой задачи, проанализируем их производительность и выберем оптимальный вариант в зависимости от архитектуры вашего регистра.

Способ 1: Использование метода ПолучитьПоследнее()

Этот способ является наиболее лаконичным, если мы работаем с одной конкретной записью (например, в форме или при итеративной обработке небольшого массива). Метод ПолучитьПоследнее() позволяет получить значения ресурсов на определенную дату по заданному фильтру измерений.

Разберем пример реализации, который учитывает большое количество измерений. Вместо того чтобы перечислять каждое измерение вручную, мы можем программно обойти метаданные регистра. Это сделает код универсальным и "чистым".

Проанализируем следующий алгоритм:


// Подготовим структуру отбора по всем измерениям регистра
МетаданныеРегистра = Метаданные.РегистрыСведений.TEL_Transactions;
Отбор = Новый Структура();

Для Каждого Измерение Из МетаданныеРегистра.Измерения Цикл
    // Заполняем структуру значениями из нашего текущего объекта (например, МенеджераЗаписи)
    Отбор.Вставить(Измерение.Имя, МенеджерЗаписи[Измерение.Имя]);
КонецЦикла;

// Получаем состояние на текущий момент (период)
АктуальныеДанные = РегистрыСведений.TEL_Transactions.ПолучитьПоследнее(МенеджерЗаписи.Период, Отбор);

// Проверяем, существует ли запись и изменились ли ресурсы
// Важно: проверяем системное поле Период, так как значения ресурсов могут быть пустыми по умолчанию
Если ЗначениеЗаполнено(АктуальныеДанные.Период) Тогда
    Если АктуальныеДанные.TransferPrice = МенеджерЗаписи.TransferPrice И
         АктуальныеДанные.TransactionStatus = МенеджерЗаписи.TransactionStatus Тогда
        // Данные не изменились, записывать не нужно
        Возврат; 
    КонецЕсли;
КонецЕсли;

МенеджерЗаписи.Записать();

В данном примере мы используем МенеджерЗаписи, но логика будет аналогичной для любой структуры данных. Обратите внимание: проверка ЗначениеЗаполнено(АктуальныеДанные.КакойТоРесурс) может быть ошибочной, если ресурс по умолчанию имеет значение 0 или пустую ссылку. Надежнее проверять именно АктуальныеДанные.Период.

Способ 2: Использование объекта НаборЗаписей

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

Выясним, как это работает на практике:


Набор = РегистрыСведений.TEL_Transactions.СоздатьНаборЗаписей();

// Устанавливаем фильтры по измерениям
Набор.Отбор.Customer.Установить(ТекущийКлиент);
Набор.Отбор.Supplier.Установить(ТекущийПоставщик);
// ... остальные измерения ...

// Читаем данные из БД
Набор.Прочитать();

НужноЗаписывать = Истина;
Если Набор.Количество() > 0 Тогда
    // Ищем запись с максимальной датой, которая меньше или равна нашей текущей
    // Либо просто сравниваем с последней записью в наборе (если они отсортированы)
    Для Каждого Запись Из Набор Цикл
        Если Запись.Период <= НоваяДата И Запись.TransferPrice = НоваяЦена Тогда
             НужноЗаписывать = Ложь;
             Прервать;
        КонецЕсли;
    КонецЦикла;
КонецЕсли;

Если НужноЗаписывать Тогда
    НоваяЗапись = Набор.Добавить();
    ЗаполнитьЗначенияСвойств(НоваяЗапись, ДанныеДляЗагрузки);
    Набор.Записать(Ложь); // Ложь - значит не замещать записи, не входящие в отбор
КонецЕсли;

Этот метод хорош тем, что за одно обращение к серверу мы получаем историю по конкретному ключу измерений. Однако, если история изменений по этому ключу огромна (тысячи записей за годы), чтение всего набора в память может стать узким местом.

Способ 3: Оптимизированный запрос (Массовая обработка)

Если нам нужно обработать массив из тысяч строк, вызов ПолучитьПоследнее() в цикле создаст катастрофическую нагрузку на сервер БД (проблема N+1 запросов). В этом случае правильным решением будет использование временных таблиц и соединения со срезом последних.

Рассмотрим шаги реализации:

  1. Выгрузим все новые данные (которые планируем записать) во временную таблицу (ВТ).
  2. Соединим ВТ с виртуальной таблицей РегистрСведений.СрезПоследних.
  3. В условии соединения укажем все измерения.
  4. В блоке ГДЕ отфильтруем только те строки, где ресурсы ВТ НЕ РАВНЫ ресурсам из среза последних или где данных в регистре еще вообще нет (ЕСТЬ NULL).

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


ВЫБРАТЬ
    НовыеДанные.Период,
    НовыеДанные.Customer,
    НовыеДанные.TransferPrice
ПОМЕСТИТЬ ВТ_НовыеЗаписи
ИЗ
    &ТаблицаДанных КАК НовыеДанные
;

ВЫБРАТЬ
    Т1.Период,
    Т1.Customer,
    Т1.TransferPrice
ИЗ
    ВТ_НовыеЗаписи КАК Т1
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.TEL_Transactions.СрезПоследних(&ДатаСреза, ) КАК Т2
    ПО Т1.Customer = Т2.Customer
ГДЕ
    Т1.TransferPrice <> Т2.TransferPrice
    ИЛИ Т2.Период ЕСТЬ NULL

Такой подход позволяет за один запрос к базе данных отсеять 90% лишней информации и оставить только те строки, которые действительно несут изменения. Это наиболее производительный метод для систем с высокой нагрузкой.

Продвинутый метод: Хеширование для "широких" регистров

Проанализируем ситуацию, упомянутую в обсуждении: что делать, если измерений 20 или 30? Написание условий соединения или структур отбора по 30 полям превращает код в трудночитаемое полотно. Кроме того, индексы на 30 полях работают крайне медленно.

Решение: Введение технического измерения ХешКлюч (Строка или Число).

Перед записью мы собираем значения всех измерений в одну строку и вычисляем MD5 или SHA1 хеш. При проверке мы делаем отбор только по одному полю — этому хешу. Это гарантирует уникальность комбинации измерений и на порядок ускоряет поиск.

Важные нюансы при работе с периодичностью

Разберем несколько критических моментов, которые часто упускают программисты:

Подводя итог, можно сказать: выбор метода зависит от объема данных. Для разовой записи идеально подходит ПолучитьПоследнее() с программным обходом метаданных, а для массового импорта — пакетный запрос с использованием временных таблиц.

← На главную