В процессе разработки в среде 1С часто возникает классическая задача: у нас есть определенный документ-основание (например, заказ клиента, интерес или сделка), на базе которого пользователи могут создавать множество подчиненных документов различных типов (например, взаимодействия, коммерческие предложения, встречи или задачи). Перед разработчиком ставится задача — получить для каждого документа-основания ровно одну строку, содержащую самый первый (наиболее ранний по времени) подчиненный документ, независимо от его типа.
На форумах часто предлагают быстрые решения с объединением и группировкой, однако при их реализации легко совершить критические ошибки, которые приведут либо к неверной выборке данных, либо к падению производительности, либо к скрытым багам, когда документы будут периодически «пропадать» из отчетов. В этой статье мы подробно рассмотрим три различных подхода к решению этой задачи, проанализируем подводные камни каждого из них и найдем идеальный вариант.
Проанализируем обсуждение на форуме. В финальных сообщениях участники предлагают использовать агрегатную функцию МАКСИМУМ(МоментВремени) или МАКСИМУМ(Дата) для группировки. Но давайте вспомним исходную задачу: автору темы необходимо получить первый по дате введенный документ.
Использование функции МАКСИМУМ вернет нам самый последний (самый свежий) документ, созданный на основании. Для того чтобы получить именно первый, самый ранний документ, мы обязательно должны использовать агрегатную функцию МИНИМУМ. Это критически важный нюанс, невнимательность к которому приведет к искажению всей бизнес-логики отчета.
Если нам нужно решить задачу универсальным способом, который будет работать в любом месте платформы (будь то динамический список, программная выгрузка в таблицу значений или обычный запрос), мы должны использовать пакет запросов с временными таблицами.
Разберем это решение по шагам:
ОБЪЕДИНИТЬ ВСЕ вместо простой ОБЪЕДИНИТЬ, так как первая работает значительно быстрее, не выполняя избыточную операцию поиска дублей на уровне СУБД.ДокументОснование и находим МИНИМУМ(МоментВремени).ДокументОснование и МоментВремени, чтобы вытащить ссылку на конкретный документ.Обратите внимание: для связи таблиц мы используем именно МоментВремени, а не Дата. В реальной базе данных несколько документов могут быть записаны в одну и ту же секунду. Если мы будем соединять таблицы только по дате, СУБД вернет дублирующиеся строки (задвоение результатов). Поле МоментВремени содержит в себе как дату со временем, так и уникальную ссылку на документ, что гарантирует получение ровно одной записи.
Посмотрим на готовый шаблон запроса:
// Шаг 1: Объединяем все подчиненные документы разных типов
ВЫБРАТЬ
ДокТип1.Ссылка КАК Ссылка,
ДокТип1.ДокументОснование КАК ДокументОснование,
ДокТип1.МоментВремени КАК МоментВремени
ПОМЕСТИТЬ ВТ_ВсеДокументы
ИЗ
Документ.CRM_Взаимодействие КАК ДокТип1
ГДЕ
НЕ ДокТип1.ПометкаУдаления
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
ДокТип2.Ссылка,
ДокТип2.ДокументОснование,
ДокТип2.МоментВремени
ИЗ
Документ.Встреча КАК ДокТип2
ГДЕ
НЕ ДокТип2.ПометкаУдаления
;
// Шаг 2: Находим МИНИМАЛЬНЫЙ момент времени для каждого основания
ВЫБРАТЬ
ВТ.ДокументОснование КАК ДокументОснование,
МИНИМУМ(ВТ.МоментВремени) КАК МоментВремени
ПОМЕСТИТЬ ВТ_ПервыеМоменты
ИЗ
ВТ_ВсеДокументы КАК ВТ
СГРУППИРОВАТЬ ПО
ВТ.ДокументОснование
;
// Шаг 3: Соединяем исходные данные с минимальными моментами времени
ВЫБРАТЬ
ВТ_ВсеДокументы.ДокументОснование КАК ДокументОснование,
ВТ_ВсеДокументы.Ссылка КАК ПервыйДокумент,
ВТ_ВсеДокументы.МоментВремени КАК МоментВремени
ИЗ
ВТ_ПервыеМоменты КАК ВТ_Первые
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ВсеДокументы КАК ВТ_ВсеДокументы
ПО ВТ_Первые.ДокументОснование = ВТ_ВсеДокументы.ДокументОснование
И ВТ_Первые.МоментВремени = ВТ_ВсеДокументы.МоментВремени
При работе с большими объемами данных в реальных базах (например, когда количество документов-оснований и подчиненных документов исчисляется сотнями тысяч), производительность нашего классического запроса может снизиться. Чтобы этого избежать, мы рекомендуем проиндексировать временную таблицу ВТ_ВсеДокументы по полям ДокументОснование и МоментВремени перед выполнением группировки и финального соединения. Это позволит СУБД построить оптимальный план запроса и мгновенно выполнить сопоставление строк.
Посмотрим на доработанный вариант первого шага с индексацией:
ВЫБРАТЬ
ДокТип1.Ссылка КАК Ссылка,
ДокТип1.ДокументОснование КАК ДокументОснование,
ДокТип1.МоментВремени КАК МоментВремени
ПОМЕСТИТЬ ВТ_ВсеДокументы
ИЗ
Документ.CRM_Взаимодействие КАК ДокТип1
ГДЕ
НЕ ДокТип1.ПометкаУдаления
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
ДокТип2.Ссылка,
ДокТип2.ДокументОснование,
ДокТип2.МоментВремени
ИЗ
Документ.Встреча КАК ДокТип2
ГДЕ
НЕ ДокТип2.ПометкаУдаления
ИНДЕКСИРОВАТЬ ПО
ДокументОснование,
МоментВремени
;
В процессе обсуждения автор упомянул, что его отчет разрабатывается в Системе компоновки данных (СКД). Это в корне меняет дело! Разработчики часто забывают, что СКД обладает собственным мощным языком выражений, способным избавить нас от написания сложных SQL-запросов со множеством соединений.
Выясним, как решить задачу в СКД за несколько простых шагов:
ОБЪЕДИНИТЬ ВСЕ без каких-либо группировок (аналогично первому шагу из предыдущего способа).Ссылка) в колонке «Выражение» прописываем специальную системную функцию:
ВычислитьВыражение("Ссылка", , "Дата Возр", "Первая")
Разберем подробнее, как работают параметры этой функции в СКД:
Теперь в настройках структуры отчета достаточно сделать группировку по полю ДокументОснование. СКД автоматически выполнит все расчеты на лету. Этот метод работает невероятно быстро, так как перекладывает логику выбора первой записи на оптимизированные внутренние алгоритмы СКД.
Один из участников форума предложил использовать функцию АВТОНОМЕРЗАПИСИ(). Разберем этот инструмент подробнее, так как он имеет критические ограничения, о которых редко пишут в документации.
Теоретически, использование функции выглядит следующим образом:
ВЫБРАТЬ
АВТОНОМЕРЗАПИСИ() КАК НомерСтроки,
Док.Ссылка КАК Ссылка,
Док.ДокументОснование КАК ДокументОснование
ПОМЕСТИТЬ ВТ_Временная
ИЗ
Документ.CRM_Взаимодействие КАК Док
УПОРЯДОЧИТЬ ПО
Док.Дата ИСХ
Казалось бы, решение идеальное: пронумеровали строки в порядке возрастания даты, а затем во втором пакете просто выбрали строки, где НомерСтроки = 1. Однако здесь нас подстерегает опасная ловушка!
Согласно официальной документации «1С», поведение функции АВТОНОМЕРЗАПИСИ() зависит от конкретной используемой СУБД (MS SQL, PostgreSQL, Oracle) и настроек сервера. Платформа не гарантирует, что нумерация записей всегда будет начинаться ровно с единицы (1) при каждом новом обращении к временной таблице в рамках одного сеанса. В некоторых случаях счетчик СУБД не сбрасывается, и при повторном формировании отчета номера строк могут начаться с 11, 101 и так далее. Если вы напишете жесткое условие ГДЕ НомерСтроки = 1, ваш отчет внезапно станет пустым, и вы столкнетесь со сложным плавающим багом.
Кроме того, у АВТОНОМЕРЗАПИСИ() есть жесткие синтаксические ограничения:
ПОМЕСТИТЬ).АВТОНОМЕРЗАПИСИ() + 1 вызовут ошибку компиляции).Именно поэтому использовать автонумерацию для решения подобных задач в реальных промышленных базах крайне не рекомендуется.
Мы детально рассмотрели проблему выбора первого подчиненного документа среди разных типов. Подведем итоги нашей совместной работы:
ВычислитьВыражение(). Это самый быстрый, надежный и простой в поддержке способ.МИНИМУМУ и обязательным соединением по уникальному полю МоментВремени. Также не забывайте индексировать временные таблицы для повышения быстродействия.АВТОНОМЕРЗАПИСИ() для фильтрации первых строк из-за нестабильного поведения счетчика на различных СУБД.