Как в запросе 1С получить предшествующий документ поступления для реализации?

Программист 1С v8.3 (Управляемые формы) 1С:Управление торговлей Управленческий учет Торговля и дистрибуция
← На главную

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

На первый взгляд задача кажется тривиальной, однако попытки решить ее «в лоб» часто приводят к неверным результатам в отчетах или к критическому падению производительности информационной базы. Мы детально проанализируем ситуацию, выявим скрытые ловушки СУБД и построим надежный, быстрый и оптимизированный пакетный запрос.

Главная ловушка: почему нельзя просто выбрать МАКС(ДокументПоступления)

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


// НЕПРАВИЛЬНЫЙ ЗАПРОС! ПРИМЕР ОШИБКИ!
ВЫБРАТЬ
    т1.Номенклатура КАК Номенклатура,
    т1.ДокументРеализации КАК ДокументРеализации,
    МАКС(т2.Дата) КАК ДатаПоступления,
    МАКС(т2.ДокументПоступления) КАК ДокументПоступления
ИЗ
    ТаблицаРеализаций КАК т1
    ЛЕВОЕ СОЕДИНЕНИЕ ТаблицаПоступлений КАК т2
        ПО т1.Номенклатура = т2.Номенклатура
        И т2.Дата < т1.Дата
СГРУППИРОВАТЬ ПО
    т1.Номенклатура,
    т1.ДокументРеализации

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

В базах данных SQL и на платформе 1С агрегатная функция МАКС для ссылочных типов данных (таких как документы, справочники и т.д.) работает не по хронологическому порядку и не по дате создания документа. Сравнение ссылок происходит по их внутреннему уникальному идентификатору — GUID (UUID). GUID генерируется операционной системой или СУБД случайным образом. В результате функция МАКС(ДокументПоступления) вернет случайный документ, чей уникальный идентификатор оказался «больше» при побитовом сравнении. Вы получите правильную максимальную дату поступления, но ссылка на документ будет принадлежать совершенно другому поступлению!

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

Чтобы гарантированно получить верную ссылку на документ, нам необходимо разделить решение задачи на два последовательных шага. Для этого мы будем использовать механизм временных таблиц в пакете запросов. Это не только обеспечит стопроцентную точность данных, но и позволит оптимизировать производительность СУБД.

Рассмотрим детально наш план действий:

  1. Шаг 1. Получение максимальной даты предшествующего поступления. Мы соединяем таблицу реализаций с таблицей поступлений по номенклатуре и условию т2.Дата < т1.Дата. Сгруппировав результат, мы получаем для каждой уникальной пары «Номенклатура + Реализация» точную дату последнего предшествующего поступления. Этот результат мы сохраняем во временную таблицу.
  2. Шаг 2. Получение ссылки на документ по найденной дате. Мы берем нашу временную таблицу и соединяем ее обратно с исходной таблицей поступлений. Соединение выполняется уже по двум жестким равенствам: совпасть должна и Номенклатура, и найденная МаксимальнаяДатаПоступления. Так мы однозначно вытаскиваем ссылку на документ, соответствующий этой дате.

Реализация решения в конструкторе запросов

Давайте посмотрим на практический пример реализации этого алгоритма в виде готового пакетного запроса 1С.


ВЫБРАТЬ
    Реализации.Номенклатура КАК Номенклатура,
    Реализации.ДокументРеализации КАК ДокументРеализации,
    Реализации.Дата КАК ДатаРеализации,
    МАКС(Поступления.Дата) КАК МаксДатаПоступления
ПОМЕСТИТЬ ВТ_МаксимальныеДаты
ИЗ
    ВТ_Реализации КАК Реализации
    ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Поступления КАК Поступления
        ПО Реализации.Номенклатура = Поступления.Номенклатура
        И Поступления.Дата < Реализации.Дата

СГРУППИРОВАТЬ ПО
    Реализации.Номенклатура,
    Реализации.ДокументРеализации,
    Реализации.Дата

ИНДЕКСИРОВАТЬ ПО
    Номенклатура,
    МаксДатаПоступления
;

////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
    ВТ_Даты.Номенклатура КАК Номенклатура,
    ВТ_Даты.ДокументРеализации КАК ДокументРеализации,
    ВТ_Даты.ДатаРеализации КАК ДатаРеализации,
    ВТ_Даты.МаксДатаПоступления КАК ДатаПоступления,
    МАКС(Поступления.ДокументПоступления) КАК ДокументПоступления
ИЗ
    ВТ_МаксимальныеДаты КАК ВТ_Даты
    ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Поступления КАК Поступления
        ПО ВТ_Даты.Номенклатура = Поступления.Номенклатура
        И ВТ_Даты.МаксДатаПоступления = Поступления.Дата

СГРУППИРОВАТЬ ПО
    ВТ_Даты.Номенклатура,
    ВТ_Даты.ДокументРеализации,
    ВТ_Даты.ДатаРеализации,
    ВТ_Даты.МаксДатаПоступления

Разбор важных деталей запроса

Давайте проанализируем ключевые моменты написанного запроса, которые обеспечивают его стабильность и быстродействие:

Проблемы производительности на больших объемах данных

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

Основная нагрузка ложится на первое соединение по условию «меньше» (Поступления.Дата < Реализации.Дата). Это так называемое неэквивалентное соединение (Non-equi join). СУБД гораздо сложнее оптимизировать такие операции, чем простые соединения по равенству. Чтобы минимизировать нагрузку, придерживайтесь следующих рекомендаций:

  1. Ограничивайте период выборок: Никогда не выбирайте документы за всю историю существования базы данных. Перед началом пакета запросов обязательно накладывайте жесткий фильтр по периоду (например, текущий месяц, квартал или год) как на таблицу реализаций, так и на таблицу поступлений.
  2. Откажитесь от таблиц документов в пользу регистров: Прямое обращение к физическим таблицам документов (например, Документ.РеализацияТоваровУслуг или Документ.ПоступлениеТоваровУслуг) — это антипаттерн в 1С. Таблицы документов содержат множество избыточных полей и не имеют индексов, оптимизированных под партионный учет. Гораздо быстрее и правильнее использовать таблицы регистров накопления (например, ТоварыНаСкладах, Закупки или Продажи), так как они оптимизированы СУБД и содержат готовые индексы по измерениям и периоду.

Альтернативный вариант: Использование виртуальных таблиц регистров накопления

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

Проанализируем пример получения последнего поступления (или последней закупки) через регистр накопления Закупки:


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

ИНДЕКСИРОВАТЬ ПО
    Номенклатура,
    МаксДатаПоступления
;

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

В данном запросе поле Регистратор выступает в роли ссылки на документ поступления. Поскольку виртуальные и физические таблицы регистров накопления всегда проиндексированы по измерениям (включая Номенклатура) и по полю Период, скорость выполнения такого запроса будет в разы выше, чем при работе с реальными таблицами документов.

Нюанс с Моментом Времени

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

Если в вашем бизнес-процессе критически важно определять порядок документов, проведенных в пределах одной секунды, то простое сравнение дат по условию Поступления.Дата < Реализации.Дата не сработает. Нам придется усложнить условие соединения в первом запросе пакета. Рассмотрим, как это сделать в запросе:


ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Поступления КАК Поступления
    ПО Реализации.Номенклатура = Поступления.Номенклатура
    И (Поступления.Дата < Реализации.Дата 
        ИЛИ (Поступления.Дата = Реализации.Дата И Поступления.Ссылка < Реализации.Ссылка))

В данном условии мы сначала сравниваем даты. Если даты документов полностью совпадают (включая секунды), СУБД переходит к сравнению ссылок. Сравнение ссылок гарантирует, что мы сможем выстроить четкую виртуальную хронологию даже внутри одной секунды. Однако помните, что условие с оператором ИЛИ в секции соединения (ПО) очень тяжело переносится оптимизатором СУБД, поэтому использовать данный подход стоит только при крайней необходимости и на относительно небольших объемах данных.

Типичные ошибки при работе с временными таблицами в подобных задачах

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

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

← На главную