Как в запросе 1С убрать повторяющиеся записи о состоянии процесса?

Программист 1С v8.3 (Управляемые формы) Управленческий учет Промышленность, строительство и АПК
← На главную

В процессе работы с данными, особенно с логами оборудования или систем, часто возникает задача очистки данных от "шума". Классический пример — это таблица состояний процесса, где оборудование постоянно отправляет сигнал "Идет", и только изредка — "Закрыто". Отображение всех этих сигналов в отчете или динамическом списке неудобно и неинформативно. Требуется оставить только значимые события: начало нового этапа (первая запись "Идет" в серии) и его завершение (запись "Закрыто").

Рассмотрим на примере таблицы состояний:


Период      Этап        Состояние
----------  ----------  ----------
05.03.24    Процесс 1   Идет
06.03.24    Процесс 1   Идет      // Лишняя запись
07.03.24    Процесс 1   Закрыто
08.03.24    Процесс 1   Идет
09.03.24    Процесс 1   Идет      // Лишняя запись
10.03.24    Процесс 1   Закрыто
11.03.24    Процесс 2   Идет
12.03.24    Процесс 1   Идет
13.03.24    Процесс 2   Идет      // Лишняя запись

Желаемый результат должен выглядеть так:


Период      Этап        Состояние
----------  ----------  ----------
05.03.24    Процесс 1   Идет
07.03.24    Процесс 1   Закрыто
08.03.24    Процесс 1   Идет
10.03.24    Процесс 1   Закрыто
11.03.24    Процесс 2   Идет
12.03.24    Процесс 1   Идет

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

Подход 1: Программная обработка данных в коде 1С

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

Вариант А: Вложенные циклы и поиск

Можно перебрать все уникальные процессы, а для каждого процесса — найти и обработать его состояния.


// Предполагаем, что "Таблица" - это ТаблицаЗначений с исходными данными
ТабЭтапы = Таблица.Скопировать();
ТабЭтапы.Свернуть("Этап");

тКопия = Таблица.Скопировать();
тКопия.Очистить();

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

тКопия.Сортировать("Период");
// В тКопия будет содержаться результат

Этот код работает, но он неэффективен из-за использования метода НайтиСтроки внутри цикла, что на больших объемах данных приведет к серьезному падению производительности.

Вариант Б: Оптимизированный обход с предварительной сортировкой

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

  1. Сортируем исходную таблицу по ключевым полям: Этап, затем Период. Это гарантирует, что все события одного процесса будут идти подряд в хронологическом порядке.
  2. Инициализируем переменные для хранения состояния предыдущей строки (например, ПредыдущийЭтап и ПредыдущееСостояние).
  3. Проходим по отсортированной таблице в цикле. Для каждой строки сравниваем ее данные с данными предыдущей.
  4. Добавляем строку в результирующую таблицу только в том случае, если она не является "лишней". Строка является лишней, если ее этап и состояние совпадают с этапом и состоянием предыдущей строки.

Такой алгоритм имеет линейную сложность и будет работать значительно быстрее на больших таблицах.

Подход 2: Решение с помощью многоступенчатого запроса

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

Рассмотрим пример такого запроса. Он довольно громоздкий, но демонстрирует возможности языка запросов 1С.


// Шаг 1: Помещаем исходные данные во временную таблицу
ВЫБРАТЬ
    ТЗ.Период КАК Период,
    ТЗ.Этап КАК Этап,
    ТЗ.Состояние КАК Состояние
ПОМЕСТИТЬ ВременнаяТаблица
ИЗ
    &ТЗ КАК ТЗ
;

////////////////////////////////////////////////////////////////////////////////
// Шаг 2: Для каждой записи "Идет" находим следующую за ней запись
// с другим состоянием в рамках того же этапа.
ВЫБРАТЬ
    ВТ.Период КАК Период,
    ВТ.Этап КАК Этап,
    ВТ.Состояние КАК Состояние,
    МИНИМУМ(СледующиеЗаписи.Период) КАК ПериодСледующегоСостояния,
    СледующиеЗаписи.Этап КАК ЭтапСледующегоСостояния,
    СледующиеЗаписи.Состояние КАК СостояниеСледующегоСостояния
ПОМЕСТИТЬ ВТ_Связи
ИЗ
    ВременнаяТаблица КАК ВТ
        ЛЕВОЕ СОЕДИНЕНИЕ ВременнаяТаблица КАК СледующиеЗаписи
        ПО ВТ.Этап = СледующиеЗаписи.Этап
            И ВТ.Период < СледующиеЗаписи.Период
            И ВТ.Состояние <> СледующиеЗаписи.Состояние
ГДЕ
    ВТ.Состояние = "Идет"
СГРУППИРОВАТЬ ПО
    ВТ.Период,
    ВТ.Этап,
    ВТ.Состояние,
    СледующиеЗаписи.Этап,
    СледующиеЗаписи.Состояние
;

////////////////////////////////////////////////////////////////////////////////
// Шаг 3: Группируем записи "Идет" по следующему событию.
// Таким образом, все последовательные "Идет", за которыми следует одно и то же "Закрыто",
// попадут в одну группу. В каждой такой группе мы выбираем МИНИМУМ(Период).
ВЫБРАТЬ
    МИНИМУМ(ВТ_Связи.Период) КАК Период,
    ВТ_Связи.Этап КАК Этап,
    ВТ_Связи.Состояние КАК Состояние,
    ВТ_Связи.ПериодСледующегоСостояния,
    ВТ_Связи.ЭтапСледующегоСостояния,
    ВТ_Связи.СостояниеСледующегоСостояния
ПОМЕСТИТЬ ВТ_НачалаПроцессов
ИЗ
    ВТ_Связи КАК ВТ_Связи
СГРУППИРОВАТЬ ПО
    ВТ_Связи.Этап,
    ВТ_Связи.Состояние,
    ВТ_Связи.ПериодСледующегоСостояния,
    ВТ_Связи.ЭтапСледующегоСостояния,
    ВТ_Связи.СостояниеСледующегоСостояния
;

////////////////////////////////////////////////////////////////////////////////
// Шаг 4: Собираем итоговый результат.
// Объединяем найденные начала процессов (первые "Идет" в каждой серии)
// и соответствующие им события "Закрыто".
ВЫБРАТЬ
    ВТ_НачалаПроцессов.Период,
    ВТ_НачалаПроцессов.Этап,
    ВТ_НачалаПроцессов.Состояние
ИЗ
    ВТ_НачалаПроцессов КАК ВТ_НачалаПроцессов

ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
    ВТ_НачалаПроцессов.ПериодСледующегоСостояния,
    ВТ_НачалаПроцессов.ЭтапСледующегоСостояния,
    ВТ_НачалаПроцессов.СостояниеСледующегоСостояния
ИЗ
    ВТ_НачалаПроцессов КАК ВТ_НачалаПроцессов
ГДЕ
    ВТ_НачалаПроцессов.ПериодСледующегоСостояния ЕСТЬ НЕ NULL

УПОРЯДОЧИТЬ ПО
    Период

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

Подход 3: Эмуляция оконных функций для фильтрации "лишних" записей

Наиболее элегантный и потенциально производительный подход на уровне запроса основан на идее, схожей с программной обработкой: чтобы понять, является ли текущая строка лишней, нужно знать состояние предыдущей строки. В стандартном SQL для этого существуют оконные функции (например, LAG()), но в языке запросов 1С их нет. Однако этот механизм можно эмулировать.

Основной принцип:

  1. Вся информация уже есть в исходной таблице. Нам нужно для каждой строки (назовем ее `Т1`) найти предшествующую ей строку (`Т2`) для того же процесса.
  2. Это делается с помощью ЛЕВОГО СОЕДИНЕНИЯ таблицы с самой собой. Условие соединения будет связывать строки одного и того же процесса. Чтобы найти именно предыдущую, мы ищем максимальную дату из `Т2`, которая меньше даты `Т1`.
  3. Получив таблицу, где каждая строка дополнена данными из предыдущей, мы можем легко отфильтровать ненужные записи. Условие фильтрации будет простым: `Т1.Состояние <> Т2.Состояние ИЛИ Т2.Состояние ЕСТЬ NULL` (вторая часть условия нужна для самых первых записей по каждому процессу, у которых нет предыдущих).

Хотя полный запрос может выглядеть сложным из-за необходимости находить именно предыдущую запись (через вложенный запрос с МАКСИМУМ), сама логика фильтрации очень мощная. Этот метод "убивает" записи, которые не несут новой информации: старт уже начатого процесса или закрытие уже закрытого.

Рекомендации для динамических списков и больших объемов данных

Важно понимать, что выполнение сложных, многоступенчатых запросов непосредственно в динамическом списке — это плохая практика. Динамический список должен работать максимально быстро, а такие запросы могут вызывать значительные задержки при открытии формы или прокрутке списка.

Наиболее правильное архитектурное решение для подобных задач, особенно когда "сырые" данные поступают в большом объеме от оборудования, — это предварительная обработка данных.

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

Такой подход разделяет сложную обработку данных и их отображение, что обеспечивает высокую производительность и комфортную работу пользователей с интерфейсом.

← На главную