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

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

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

В этой статье мы подробно разберем, почему простой запрос с функцией МАКСИМУМ не решает задачу напрямую, как правильно составить пакетный запрос с временными таблицами, исправим синтаксические ошибки популярных решений с форумов, а также рассмотрим важные архитектурные нюансы оптимизации СУБД.

Почему исходный запрос автора не работает?

Проанализируем сначала некорректную попытку решения задачи «в лоб». Автор темы попытался использовать следующий запрос:


ВЫБРАТЬ
    СобытияПоВоспроизводству.Животное,
    СобытияПоВоспроизводству.РезультатОсмотра,
    СобытияПоВоспроизводству.ЭтапЦиклаВоспроизводства,
    МАКСИМУМ(СобытияПоВоспроизводству.Период) КАК Дата
ИЗ
    РегистрНакопления.СобытияПоВоспроизводству КАК СобытияПоВоспроизводству

Этот запрос содержит сразу несколько критических ошибок:

  1. Отсутствие секции СГРУППИРОВАТЬ ПО: В запросе используется агрегатная функция МАКСИМУМ(СобытияПоВоспроизводству.Период). По правилам языка запросов 1С (и стандартов SQL), если в выборке присутствует хотя бы одно агрегатное поле, все остальные неагрегатные поля должны быть перечислены в секции СГРУППИРОВАТЬ ПО. Без этого запрос попросту выдаст синтаксическую ошибку и не запустится.
  2. Логическая ошибка группировки: Даже если мы добавим группировку по полям Животное, РезультатОсмотра и ЭтапЦиклаВоспроизводства, мы не избавимся от дублирования. Платформа сгруппирует данные по уникальным комбинациям всех этих трех полей. Если в одном месяце у животного было два осмотра с разными результатами, мы все равно получим две строки в отчете, просто для каждой строки будет показана ее максимальная дата.

Выясним причину: чтобы получить срез последних данных по нечисловым реквизитам (состояниям), нельзя выполнять агрегацию в одной таблице вместе с этими реквизитами. Нам необходимо сначала вычислить точные даты последних событий, а затем «вытащить» соответствующие им строки.

Решение проблемы: Пакетный запрос с временной таблицей

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

  1. Шаг 1: Создаем временную таблицу, в которой сгруппируем данные по животным и месяцам, чтобы определить точную дату (момент времени) последнего события для каждого животного в конкретном месяце.
  2. Шаг 2: Соединяем исходную таблицу регистра с нашей временной таблицей по полям связи Животное и Период. Таким образом, мы отсечем все промежуточные события и оставим только финальные.

Ниже представлен исправленный и оптимизированный вариант запроса:


ВЫБРАТЬ
    НАЧАЛОПЕРИОДА(СобытияПоВоспроизводству.Период, МЕСЯЦ) КАК ПериодМесяц,
    СобытияПоВоспроизводству.Животное КАК Животное,
    МАКСИМУМ(СобытияПоВоспроизводству.Период) КАК ПоследняяДата
ПОМЕСТИТЬ ВТ_ПоследниеДаты
ИЗ
    РегистрНакопления.СобытияПоВоспроизводству КАК СобытияПоВоспроизводству

СГРУППИРОВАТЬ ПО
    НАЧАЛОПЕРИОДА(СобытияПоВоспроизводству.Период, МЕСЯЦ),
    СобытияПоВоспроизводству.Животное

ИНДЕКСИРОВАТЬ ПО
    Животное,
    ПоследняяДата
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    ВТ_ПоследниеДаты.ПериодМесяц КАК ПериодМесяц,
    СобытияПоВоспроизводству.Животное КАК Животное,
    СобытияПоВоспроизводству.РезультатОсмотра КАК РезультатОсмотра,
    СобытияПоВоспроизводству.ЭтапЦиклаВоспроизводства КАК ЭтапЦиклаВоспроизводства,
    СобытияПоВоспроизводству.Период КАК ТочнаяДатаСобытия
ИЗ
    РегистрНакопления.СобытияПоВоспроизводству КАК СобытияПоВоспроизводству
    ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ПоследниеДаты КАК ВТ_ПоследниеДаты
        ПО СобытияПоВоспроизводству.Животное = ВТ_ПоследниеДаты.Животное
        И СобытияПоВоспроизводству.Период = ВТ_ПоследниеДаты.ПоследняяДата

Важное исправление: Обратите внимание, что в первом запросе пакета после поля СобытияПоВоспроизводству.Животное КАК Животное обязательно должна стоять запятая перед вызовом функции МАКСИМУМ. В исходных сообщениях на форумах часто допускают эту опечатку, из-за чего код не проходит компиляцию.

Анализ производительности: Индексировать или нет?

В ходе обсуждения на форуме возник спор об использовании конструкции ИНДЕКСИРОВАТЬ ПО для временных таблиц. Давайте разберем этот технологический нюанс подробнее.

Индексирование временных таблиц в 1С создает физический индекс в СУБД (MS SQL Server, PostgreSQL и др.) по указанным полям. Это ускоряет операцию ВНУТРЕННЕЕ СОЕДИНЕНИЕ на втором шаге. Однако создание индекса само по себе требует времени и ресурсов сервера.

Рекомендации по стандартам разработки 1С (ИТС):

Скрытая опасность: Проблема «коллизии секунд»

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

Представим ситуацию: в результате программного импорта данных, проведения группового документа или сбоя обмена данными по одному и тому же животному в одну и ту же секунду (например, 15.10.2023 14:05:00) были записаны два разных события:

  1. Осмотр №1 с результатом "Здорова".
  2. Осмотр №2 с результатом "Требует лечения".

Функция МАКСИМУМ(Период) вернет дату 15.10.2023 14:05:00. Но при внутреннем соединении по условию Период = ПоследняяДата СУБД найдет обе эти записи! В результате в отчете снова появится дублирование данных для этого месяца.

Как гарантировать 100% уникальность?

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

Альтернативное решение №1: Использование Регистра Сведений

Обратим внимание на архитектуру метаданных. В исходном вопросе фигурирует РегистрНакопления.СобытияПоВоспроизводству. Однако сущности вроде «Результат осмотра» или «Этап цикла» представляют собой качественные состояния объекта, а не накапливаемые числовые показатели (обороты или остатки).

Использование регистра накопления для хранения истории состояний — это грубая архитектурная ошибка. Для этих целей в 1С предназначен периодический Регистр сведений.

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


ВЫБРАТЬ
    СрезПоследних.Животное КАК Животное,
    СрезПоследних.РезультатОсмотра КАК РезультатОсмотра,
    СрезПоследних.ЭтапЦиклаВоспроизводства КАК ЭтапЦиклаВоспроизводства,
    СрезПоследних.Период КАК Период
ИЗ
    РегистрСведений.СобытияПоВоспроизводству.СрезПоследних(&КонецПериода, ) КАК СрезПоследних

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

Альтернативное решение №2: Механизм связей в СКД

Если вы разрабатываете отчет на базе Системы компоновки данных (СКД), вам не обязательно писать сложные пакетные запросы вручную. СКД умеет самостоятельно рассчитывать срез последних на каждую дату с помощью связей наборов данных.

Разберем, как это настроить по шагам:

  1. Создайте Набор данных - запрос №1 (основной). В нем выберите только уникальный список животных и интересующие вас периоды (например, группировки по месяцам).
  2. Создайте Набор данных - запрос №2 (источник данных). Напишите простой запрос, выбирающий все поля из вашего регистра без группировок.
  3. Перейдите на вкладку Связи наборов данных.
  4. Свяжите Набор №1 и Набор №2 по полю Животное.
  5. В поле «Параметры связи» укажите соответствие дат. Укажите условие связи, при котором из Набора №2 будет выбираться только запись, соответствующая максимальной дате, меньшей или равной дате из Набора №1. Для этого установите флаг «Использовать как параметры связи среза последних».

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

Выводы и рекомендации

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

← На главную