В практике разработки на платформе 1С:Предприятие часто возникает классическая задача: из множества подчиненных объектов (например, заказов) выбрать только один — самый первый по дате для каждого головного объекта (например, контракта или договора). На первый взгляд задача кажется простой, но она таит в себе подводные камни, такие как наличие нескольких документов с одинаковым временем или вопросы производительности при больших объемах данных. В этой статье мы подробно разберем, как правильно составить запрос для решения этой задачи, проанализируем нюансы оптимизации и выясним, почему обычного использования функции МИНИМУМ() не всегда достаточно.
Представим ситуацию: у нас есть справочник Контракты и документ ЗаказПокупателя. К одному контракту может быть привязано множество заказов. Нам необходимо получить список всех контрактов и для каждого из них вывести только один заказ — тот, который был оформлен раньше всех остальных. Рассмотрим, почему нельзя решить это одним простым объединением.
Если мы просто сгруппируем таблицу заказов по контрагенту и выберем МИНИМУМ(Дата), мы получим дату первого заказа, но не сможем в этой же строке получить саму ссылку на ЗаказПокупателя. Если же мы добавим ссылку на заказ в группировку, система выдаст нам все заказы, так как ссылка у каждого документа уникальна. Поэтому решение задачи всегда требует использования пакетных запросов и временных таблиц. При написании сложных алгоритмов можно использовать инструмент для автоматической расстановки уничтожения временных таблиц в пакетных запросах.
Разберем наиболее эффективный и «чистый» с точки зрения стандартов разработки метод. Мы разделим логику на три этапа: подготовка данных, поиск минимальной даты и получение итоговой ссылки.
Шаг 1. Подготовка исходных данных
Сначала мы выберем все необходимые нам данные во временную таблицу. Это позволит нам один раз обратиться к физической таблице базы данных и далее работать уже с подготовленным набором в оперативной памяти (или в tempdb SQL-сервера). Проанализируем пример кода:
ВЫБРАТЬ
Заказы.Ссылка КАК Заказ,
Заказы.Дата КАК Дата,
Заказы.Контрагент КАК Контрагент
ПОМЕСТИТЬ ВсеЗаказы
ИЗ
Документ.ЗаказПокупателя КАК Заказы
ГДЕ
Заказы.Проведен = ИСТИНА
И Заказы.Дата МЕЖДУ &НачалоПериода И &КонецПериода;
Шаг 2. Поиск минимальной даты для каждого владельца
Теперь, когда у нас есть список всех заказов, нам нужно выяснить, какая дата является минимальной для каждого контрагента (или контракта). Создадим вторую временную таблицу в этом же пакете запросов:
ВЫБРАТЬ
МИНИМУМ(ВсеЗаказы.Дата) КАК МинимальнаяДата,
ВсеЗаказы.Контрагент КАК Контрагент
ПОМЕСТИТЬ МинимальныеДаты
ИЗ
ВсеЗаказы КАК ВсеЗаказы
СГРУППИРОВАТЬ ПО
ВсеЗаказы.Контрагент;
Шаг 3. Соединение таблиц и получение итогового документа
На финальном этапе мы соединяем наши МинимальныеДаты обратно с таблицей ВсеЗаказы. Но здесь кроется важный нюанс: что если в одну и ту же секунду по одному контрагенту было создано два заказа? В этом случае простое соединение по дате и контрагенту вернет две строки вместо одной. Чтобы избежать дублирования, мы применим функцию МИНИМУМ() к самой ссылке на документ. При необходимости можно воспользоваться материалом с подробным разбором принципов работы соединений в запросах для начинающих.
ВЫБРАТЬ
МИНИМУМ(ВсеЗаказы.Заказ) КАК ПервыйЗаказ,
ВсеЗаказы.Контрагент,
МинимальныеДаты.МинимальнаяДата
ИЗ
ВсеЗаказы КАК ВсеЗаказы
ВНУТРЕННЕЕ СОЕДИНЕНИЕ МинимальныеДаты КАК МинимальныеДаты
ПО
ВсеЗаказы.Контрагент = МинимальныеДаты.Контрагент
И ВсеЗаказы.Дата = МинимальныеДаты.МинимальнаяДата
СГРУППИРОВАТЬ ПО
ВсеЗаказы.Контрагент,
МинимальныеДаты.МинимальнаяДата;
Рассмотрим ситуацию более глубоко. В системе 1С документы часто могут иметь идентичное время (вплоть до секунды). С точки зрения бизнес-логики, один из них все равно считается «первым». Для однозначного определения последовательности в 1С существует объект МоментВремени, который включает в себя и дату, и уникальный идентификатор (GUID).
Хотя в языке запросов 1С нельзя напрямую использовать МИНИМУМ(МоментВремени), мы имитируем это поведение. Когда мы используем МИНИМУМ(Ссылка) для документов с одинаковой датой, мы гарантируем, что результат запроса будет детерминированным — то есть всегда будет выбираться один и тот же документ при каждом запуске. Это критически важно для корректной работы отчетов и алгоритмов проведения.
Проанализируем вопрос производительности. Если в таблице заказов сотни тысяч записей, использование временных таблиц без индексов может замедлить работу. В 1С мы можем явно указать СУБД проиндексировать временную таблицу. Это делается с помощью инструкции ИНДЕКСИРОВАТЬ ПО. Посмотрим, как изменить наш второй шаг:
ВЫБРАТЬ
МИНИМУМ(ВсеЗаказы.Дата) КАК МинимальнаяДата,
ВсеЗаказы.Контрагент КАК Контрагент
ПОМЕСТИТЬ МинимальныеДаты
ИЗ
ВсеЗаказы КАК ВсеЗаказы
СГРУППИРОВАТЬ ПО
ВсеЗаказы.Контрагент
ИНДЕКСИРОВАТЬ ПО
Контрагент,
МинимальнаяДата;
Почему это важно? Без индекса SQL-серверу придется выполнять полный просмотр (Full Scan) временной таблицы при каждом соединении. Индексация превращает эту операцию в быстрый поиск по ключу.
Существуют и другие способы получения минимального значения в связке, которые могут быть полезны в специфических задачах:
ГДЕ будет проверяться отсутствие заказа с меньшей датой (через НЕ В (ВЫБРАТЬ ПЕРВЫЕ 1...)). Однако этот метод крайне не рекомендуется для 1С, так как он создает колоссальную нагрузку на СУБД и плохо оптимизируется.ВычислитьВыражение() или настроить группировки так, чтобы для каждого контрагента выводилась только первая детальная запись (через настройки ПЕРВЫЕ 1 в группировке).Подводя итог нашему разбору, выделим основные правила нахождения минимума в связке «один-ко-многим»:
МИНИМУМ() или МАКСИМУМ() по ссылке на документ, чтобы гарантировать получение ровно одной записи.ИЗ или СОЕДИНЕНИЕ, так как это затрудняет построение оптимального плана запроса SQL-сервером.Следуя этим рекомендациям, вы сможете эффективно решать задачи по выборке уникальных записей, обеспечивая стабильную и быструю работу вашей информационной системы на базе 1С.