Приветствуем вас! Сегодня мы разберем одну из классических и наиболее часто встречающихся задач в разработке на платформе 1С:Предприятие — нахождение среза последних (или предыдущих) данных на каждую произвольную дату. В нашем конкретном случае мы подробно рассмотрим, как для каждого документа реализации найти непосредственно предшествующий ему документ поступления по определенной номенклатуре.
На первый взгляд задача кажется тривиальной, однако попытки решить ее «в лоб» часто приводят к неверным результатам в отчетах или к критическому падению производительности информационной базы. Мы детально проанализируем ситуацию, выявим скрытые ловушки СУБД и построим надежный, быстрый и оптимизированный пакетный запрос.
Давайте проанализируем типичную ошибку, которую совершают многие начинающие разработчики. Пытаясь решить задачу одним простым запросом, они пишут конструкцию следующего вида:
// НЕПРАВИЛЬНЫЙ ЗАПРОС! ПРИМЕР ОШИБКИ!
ВЫБРАТЬ
т1.Номенклатура КАК Номенклатура,
т1.ДокументРеализации КАК ДокументРеализации,
МАКС(т2.Дата) КАК ДатаПоступления,
МАКС(т2.ДокументПоступления) КАК ДокументПоступления
ИЗ
ТаблицаРеализаций КАК т1
ЛЕВОЕ СОЕДИНЕНИЕ ТаблицаПоступлений КАК т2
ПО т1.Номенклатура = т2.Номенклатура
И т2.Дата < т1.Дата
СГРУППИРОВАТЬ ПО
т1.Номенклатура,
т1.ДокументРеализации
На первый взгляд кажется, что этот запрос вернет то, что нужно. Мы соединили реализации с поступлениями, ограничили дату поступления (она должна быть строго меньше даты реализации) и сгруппировали результат. Но давайте разберемся, что произойдет на уровне СУБД с полем ДокументПоступления при использовании агрегатной функции МАКС.
В базах данных SQL и на платформе 1С агрегатная функция МАКС для ссылочных типов данных (таких как документы, справочники и т.д.) работает не по хронологическому порядку и не по дате создания документа. Сравнение ссылок происходит по их внутреннему уникальному идентификатору — GUID (UUID). GUID генерируется операционной системой или СУБД случайным образом. В результате функция МАКС(ДокументПоступления) вернет случайный документ, чей уникальный идентификатор оказался «больше» при побитовом сравнении. Вы получите правильную максимальную дату поступления, но ссылка на документ будет принадлежать совершенно другому поступлению!
Чтобы гарантированно получить верную ссылку на документ, нам необходимо разделить решение задачи на два последовательных шага. Для этого мы будем использовать механизм временных таблиц в пакете запросов. Это не только обеспечит стопроцентную точность данных, но и позволит оптимизировать производительность СУБД.
Рассмотрим детально наш план действий:
т2.Дата < т1.Дата. Сгруппировав результат, мы получаем для каждой уникальной пары «Номенклатура + Реализация» точную дату последнего предшествующего поступления. Этот результат мы сохраняем во временную таблицу.Номенклатура, и найденная МаксимальнаяДатаПоступления. Так мы однозначно вытаскиваем ссылку на документ, соответствующий этой дате.Давайте посмотрим на практический пример реализации этого алгоритма в виде готового пакетного запроса 1С.
ВЫБРАТЬ
Реализации.Номенклатура КАК Номенклатура,
Реализации.ДокументРеализации КАК ДокументРеализации,
Реализации.Дата КАК ДатаРеализации,
МАКС(Поступления.Дата) КАК МаксДатаПоступления
ПОМЕСТИТЬ ВТ_МаксимальныеДаты
ИЗ
ВТ_Реализации КАК Реализации
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Поступления КАК Поступления
ПО Реализации.Номенклатура = Поступления.Номенклатура
И Поступления.Дата < Реализации.Дата
СГРУППИРОВАТЬ ПО
Реализации.Номенклатура,
Реализации.ДокументРеализации,
Реализации.Дата
ИНДЕКСИРОВАТЬ ПО
Номенклатура,
МаксДатаПоступления
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ_Даты.Номенклатура КАК Номенклатура,
ВТ_Даты.ДокументРеализации КАК ДокументРеализации,
ВТ_Даты.ДатаРеализации КАК ДатаРеализации,
ВТ_Даты.МаксДатаПоступления КАК ДатаПоступления,
МАКС(Поступления.ДокументПоступления) КАК ДокументПоступления
ИЗ
ВТ_МаксимальныеДаты КАК ВТ_Даты
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Поступления КАК Поступления
ПО ВТ_Даты.Номенклатура = Поступления.Номенклатура
И ВТ_Даты.МаксДатаПоступления = Поступления.Дата
СГРУППИРОВАТЬ ПО
ВТ_Даты.Номенклатура,
ВТ_Даты.ДокументРеализации,
ВТ_Даты.ДатаРеализации,
ВТ_Даты.МаксДатаПоступления
Давайте проанализируем ключевые моменты написанного запроса, которые обеспечивают его стабильность и быстродействие:
ИНДЕКСИРОВАТЬ ПО Номенклатура, МаксДатаПоступления в первом запросе пакета. Индексирование полей соединения во временных таблицах — это критически важный шаг для СУБД (MS SQL Server, PostgreSQL и др.). Построение индекса позволяет второму левому соединению выполняться мгновенно, без полного сканирования таблиц (Table Scan).МАКС(Ссылка) работает некорректно! Почему же во втором запросе мы снова ее используем?». В данном узком контексте это абсолютно безопасно и оправданно. На втором шаге мы соединяем таблицы по точному совпадению даты вплоть до секунды. Если в одну и ту же секунду было проведено два разных документа поступления по одной и той же номенклатуре, то СУБД выдаст две дублирующие строки для одной реализации. Чтобы свернуть эти дубли и оставить только один документ, мы используем МАКС(ДокументПоступления). Внутри одной и той же секунды нам, как правило, не важен точный порядок документов, а выбор по GUID гарантирует, что запрос вернет ровно одну строку без дублирования реализации в результирующей выборке.Если вы запустите данное решение на рабочей базе данных, где накоплены сотни тысяч строк документов реализации и поступлений, вы можете столкнуться с сильным замедлением работы системы. Выясним причину этой проблемы и способы ее оптимизации.
Основная нагрузка ложится на первое соединение по условию «меньше» (Поступления.Дата < Реализации.Дата). Это так называемое неэквивалентное соединение (Non-equi join). СУБД гораздо сложнее оптимизировать такие операции, чем простые соединения по равенству. Чтобы минимизировать нагрузку, придерживайтесь следующих рекомендаций:
Документ.РеализацияТоваровУслуг или Документ.ПоступлениеТоваровУслуг) — это антипаттерн в 1С. Таблицы документов содержат множество избыточных полей и не имеют индексов, оптимизированных под партионный учет. Гораздо быстрее и правильнее использовать таблицы регистров накопления (например, ТоварыНаСкладах, Закупки или Продажи), так как они оптимизированы СУБД и содержат готовые индексы по измерениям и периоду.Давайте посмотрим, как можно переписать эту задачу более профессионально, используя таблицы регистров. Регистры накопления изначально спроектированы под хранение остатков и оборотов во времени. Если нам нужно понять состояние учета на момент реализации, мы можем использовать виртуальную таблицу Обороты или обратиться к движениям регистра.
Проанализируем пример получения последнего поступления (или последней закупки) через регистр накопления Закупки:
ВЫБРАТЬ
Реализации.Номенклатура КАК Номенклатура,
Реализации.ДокументРеализации КАК ДокументРеализации,
Реализации.Дата КАК ДатаРеализации,
МАКС(Закупки.Период) КАК МаксДатаПоступления
ПОМЕСТИТЬ ВТ_ДатыИзРегистра
ИЗ
ВТ_Реализации КАК Реализации
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.Закупки КАК Закупки
ПО Реализации.Номенклатура = Закупки.Номенклатура
И Закупки.Период < Реализации.Дата
СГРУППИРОВАТЬ ПО
Реализации.Номенклатура,
Реализации.ДокументРеализации,
Реализации.Дата
ИНДЕКСИРОВАТЬ ПО
Номенклатура,
МаксДатаПоступления
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ_Даты.Номенклатура КАК Номенклатура,
ВТ_Даты.ДокументРеализации КАК ДокументРеализации,
ВТ_Даты.ДатаРеализации КАК ДатаРеализации,
ВТ_Даты.МаксДатаПоступления КАК ДатаПоступления,
МАКС(Закупки.Регистратор) КАК ДокументПоступления
ИЗ
ВТ_ДатыИзРегистра КАК ВТ_Даты
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.Закупки КАК Закупки
ПО ВТ_Даты.Номенклатура = Закупки.Номенклатура
И ВТ_Даты.МаксДатаПоступления = Закупки.Период
СГРУППИРОВАТЬ ПО
ВТ_Даты.Номенклатура,
ВТ_Даты.ДокументРеализации,
ВТ_Даты.ДатаРеализации,
ВТ_Даты.МаксДатаПоступления
В данном запросе поле Регистратор выступает в роли ссылки на документ поступления. Поскольку виртуальные и физические таблицы регистров накопления всегда проиндексированы по измерениям (включая Номенклатура) и по полю Период, скорость выполнения такого запроса будет в разы выше, чем при работе с реальными таблицами документов.
В платформе 1С хронологическая последовательность документов определяется не просто датой и временем, а специальным объектом — МоментВремени. Он представляет собой комбинацию даты, времени и ссылки на сам документ. Это позволяет однозначно выстроить последовательность документов, даже если они записаны в одну и ту же секунду.
Если в вашем бизнес-процессе критически важно определять порядок документов, проведенных в пределах одной секунды, то простое сравнение дат по условию Поступления.Дата < Реализации.Дата не сработает. Нам придется усложнить условие соединения в первом запросе пакета. Рассмотрим, как это сделать в запросе:
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_Поступления КАК Поступления
ПО Реализации.Номенклатура = Поступления.Номенклатура
И (Поступления.Дата < Реализации.Дата
ИЛИ (Поступления.Дата = Реализации.Дата И Поступления.Ссылка < Реализации.Ссылка))
В данном условии мы сначала сравниваем даты. Если даты документов полностью совпадают (включая секунды), СУБД переходит к сравнению ссылок. Сравнение ссылок гарантирует, что мы сможем выстроить четкую виртуальную хронологию даже внутри одной секунды. Однако помните, что условие с оператором ИЛИ в секции соединения (ПО) очень тяжело переносится оптимизатором СУБД, поэтому использовать данный подход стоит только при крайней необходимости и на относительно небольших объемах данных.
При реализации сложных отчетов с использованием описанного двухэтапного алгоритма разработчики иногда допускают ошибки, приводящие к неявным багам. Давайте предостережем себя от них:
ИНДЕКСИРОВАТЬ ПО... во временных таблицах при объемах данных более 10 000 строк гарантированно приведет к деградации производительности СУБД при повторном соединении.ИЗ (вместо использования пакетного запроса с временными таблицами) — частая ошибка. Оптимизатор СУБД не всегда может построить эффективный план выполнения для сложных вложенных конструкций, в то время как временные таблицы гарантируют предсказуемый и стабильный план выполнения.Мы подробно разобрали, как решить задачу нахождения предшествующего документа поступления в 1С с помощью правильного построения пакета запросов, избежали ловушки с группировкой по ссылке, а также рассмотрели методы оптимизации быстродействия. Применяйте эти паттерны в своей ежедневной работе, и ваши отчеты всегда будут работать быстро и выдавать исключительно корректные данные!