В процессе работы с данными, особенно с логами оборудования или систем, часто возникает задача очистки данных от "шума". Классический пример — это таблица состояний процесса, где оборудование постоянно отправляет сигнал "Идет", и только изредка — "Закрыто". Отображение всех этих сигналов в отчете или динамическом списке неудобно и неинформативно. Требуется оставить только значимые события: начало нового этапа (первая запись "Идет" в серии) и его завершение (запись "Закрыто").
Рассмотрим на примере таблицы состояний:
Период Этап Состояние
---------- ---------- ----------
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: Помещаем исходные данные во временную таблицу
ВЫБРАТЬ
ТЗ.Период КАК Период,
ТЗ.Этап КАК Этап,
ТЗ.Состояние КАК Состояние
ПОМЕСТИТЬ ВременнаяТаблица
ИЗ
&ТЗ КАК ТЗ
;
////////////////////////////////////////////////////////////////////////////////
// Шаг 2: Для каждой записи "Идет" находим следующую за ней запись
// с другим состоянием в рамках того же этапа.
ВЫБРАТЬ
ВТ.Период КАК Период,
ВТ.Этап КАК Этап,
ВТ.Состояние КАК Состояние,
МИНИМУМ(СледующиеЗаписи.Период) КАК ПериодСледующегоСостояния,
СледующиеЗаписи.Этап КАК ЭтапСледующегоСостояния,
СледующиеЗаписи.Состояние КАК СостояниеСледующегоСостояния
ПОМЕСТИТЬ ВТ_Связи
ИЗ
ВременнаяТаблица КАК ВТ
ЛЕВОЕ СОЕДИНЕНИЕ ВременнаяТаблица КАК СледующиеЗаписи
ПО ВТ.Этап = СледующиеЗаписи.Этап
И ВТ.Период < СледующиеЗаписи.Период
И ВТ.Состояние <> СледующиеЗаписи.Состояние
ГДЕ
ВТ.Состояние = "Идет"
СГРУППИРОВАТЬ ПО
ВТ.Период,
ВТ.Этап,
ВТ.Состояние,
СледующиеЗаписи.Этап,
СледующиеЗаписи.Состояние
;
////////////////////////////////////////////////////////////////////////////////
// Шаг 3: Группируем записи "Идет" по следующему событию.
// Таким образом, все последовательные "Идет", за которыми следует одно и то же "Закрыто",
// попадут в одну группу. В каждой такой группе мы выбираем МИНИМУМ(Период).
ВЫБРАТЬ
МИНИМУМ(ВТ_Связи.Период) КАК Период,
ВТ_Связи.Этап КАК Этап,
ВТ_Связи.Состояние КАК Состояние,
ВТ_Связи.ПериодСледующегоСостояния,
ВТ_Связи.ЭтапСледующегоСостояния,
ВТ_Связи.СостояниеСледующегоСостояния
ПОМЕСТИТЬ ВТ_НачалаПроцессов
ИЗ
ВТ_Связи КАК ВТ_Связи
СГРУППИРОВАТЬ ПО
ВТ_Связи.Этап,
ВТ_Связи.Состояние,
ВТ_Связи.ПериодСледующегоСостояния,
ВТ_Связи.ЭтапСледующегоСостояния,
ВТ_Связи.СостояниеСледующегоСостояния
;
////////////////////////////////////////////////////////////////////////////////
// Шаг 4: Собираем итоговый результат.
// Объединяем найденные начала процессов (первые "Идет" в каждой серии)
// и соответствующие им события "Закрыто".
ВЫБРАТЬ
ВТ_НачалаПроцессов.Период,
ВТ_НачалаПроцессов.Этап,
ВТ_НачалаПроцессов.Состояние
ИЗ
ВТ_НачалаПроцессов КАК ВТ_НачалаПроцессов
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
ВТ_НачалаПроцессов.ПериодСледующегоСостояния,
ВТ_НачалаПроцессов.ЭтапСледующегоСостояния,
ВТ_НачалаПроцессов.СостояниеСледующегоСостояния
ИЗ
ВТ_НачалаПроцессов КАК ВТ_НачалаПроцессов
ГДЕ
ВТ_НачалаПроцессов.ПериодСледующегоСостояния ЕСТЬ НЕ NULL
УПОРЯДОЧИТЬ ПО
Период
Этот запрос решает поставленную задачу, но его сложность может стать проблемой при отладке и поддержке. Кроме того, на очень больших объемах данных производительность соединений может быть недостаточной.
Наиболее элегантный и потенциально производительный подход на уровне запроса основан на идее, схожей с программной обработкой: чтобы понять, является ли текущая строка лишней, нужно знать состояние предыдущей строки. В стандартном SQL для этого существуют оконные функции (например, LAG()), но в языке запросов 1С их нет. Однако этот механизм можно эмулировать.
Основной принцип:
ЛЕВОГО СОЕДИНЕНИЯ таблицы с самой собой. Условие соединения будет связывать строки одного и того же процесса. Чтобы найти именно предыдущую, мы ищем максимальную дату из `Т2`, которая меньше даты `Т1`.Хотя полный запрос может выглядеть сложным из-за необходимости находить именно предыдущую запись (через вложенный запрос с МАКСИМУМ), сама логика фильтрации очень мощная. Этот метод "убивает" записи, которые не несут новой информации: старт уже начатого процесса или закрытие уже закрытого.
Важно понимать, что выполнение сложных, многоступенчатых запросов непосредственно в динамическом списке — это плохая практика. Динамический список должен работать максимально быстро, а такие запросы могут вызывать значительные задержки при открытии формы или прокрутке списка.
Наиболее правильное архитектурное решение для подобных задач, особенно когда "сырые" данные поступают в большом объеме от оборудования, — это предварительная обработка данных.
Такой подход разделяет сложную обработку данных и их отображение, что обеспечивает высокую производительность и комфортную работу пользователей с интерфейсом.