Приветствуем вас, уважаемые коллеги! При разработке отчетов и учетных механизмов в системе «1С:Предприятие» программисты регулярно сталкиваются с классической задачей: требуется получить актуальные остатки товаров на складах или у контрагентов и одновременно вывести дату последнего движения (например, дату последней поставки или установки цены). На первый взгляд, задача кажется тривиальной, но попытка решить её «в лоб» с помощью конструкции ВЫБРАТЬ ПЕРВЫЕ 1 в левом соединении часто приводит к ошибкам или катастрофическому падению производительности СУБД.
В этой статье мы подробно проанализируем ситуацию, разберем типичные ошибки начинающих разработчиков, рассмотрим решение с использованием коррелированного подзапроса и покажем, как сделать этот запрос максимально быстро и правильно с точки зрения стандартов разработки фирмы «1С».
Рассмотрим ситуацию, с которой автор темы обратился на форум. Ему требовалось получить остатки товаров у контрагентов из регистра накопления ТоварыУКонтрагентов.Остатки и для каждой полученной строки сопоставить дату последнего прихода из этого же регистра.
Первоначальный (ошибочный) запрос автора выглядел следующим образом:
ВЫБРАТЬ
запр1.Номенклатура,
запр1.СубАрендатор,
запр1.Контрагент,
запр1.Количество,
запр2.Период КАК ДатаУстановки
ИЗ
(ВЫБРАТЬ
ТоварыУКонтрагентовОстатки.Номенклатура КАК Номенклатура,
ТоварыУКонтрагентовОстатки.СубАрендатор КАК СубАрендатор,
ТоварыУКонтрагентовОстатки.Контрагент КАК Контрагент,
ТоварыУКонтрагентовОстатки.КоличествоОстаток КАК Количество
ИЗ
РегистрНакопления.ТоварыУКонтрагентов.Остатки(
КОНЕЦПЕРИОДА(&ДатаСреза, ДЕНЬ),
Контрагент = &Контрагент
И СубАрендатор = &СубАрендатор) КАК ТоварыУКонтрагентовОстатки) КАК запр1
ЛЕВОЕ СОЕДИНЕНИЕ (ВЫБРАТЬ ПЕРВЫЕ 1
ТоварыУКонтрагентов.Период КАК Период,
ТоварыУКонтрагентов.Контрагент КАК Контрагент,
ТоварыУКонтрагентов.СубАрендатор КАК СубАрендатор,
ТоварыУКонтрагентов.Номенклатура КАК Номенклатура
ИЗ
РегистрНакопления.ТоварыУКонтрагентов КАК ТоварыУКонтрагентов
ГДЕ
ТоварыУКонтрагентов.ВидДвижения = ЗНАЧЕНИЕ(ВидДвиженияНакопления.Приход)
УПОРЯДОЧИТЬ ПО
ТоварыУКонтрагентов.Период УБЫВ) КАК запр2
ПО запр1.Номенклатура = запр2.Номенклатура
И запр1.СубАрендатор = запр2.СубАрендатор
И запр1.Контрагент = запр2.Контрагент
Выясним причину, почему данная конструкция не работает так, как ожидает разработчик. Проблема заключается в логике выполнения запросов СУБД. Когда мы пишем вложенный запрос в секции соединения и ограничиваем его выборку конструкцией ВЫБРАТЬ ПЕРВЫЕ 1, СУБД сначала выполняет этот вложенный запрос **абсолютно автономно**. Он выбирает ровно **одну** самую последнюю запись движения по всему регистру (без привязки к конкретной номенклатуре из левой таблицы).
Затем полученная единственная строка пытается соединиться с таблицей остатков по ключевым полям Номенклатура, Контрагент и СубАрендатор. В результате дата прихода заполнится только у одной-единственной номенклатуры (которая случайно совпала с записью из вложенного запроса), а у всех остальных позиций поле ДатаУстановки останется пустым (будет содержать значение NULL).
В процессе обсуждения на форуме участники предложили автору использовать коррелированный подзапрос непосредственно в условии связи ПО. В итоге автор пришел к следующему итоговому варианту, который решил его сиюминутную проблему:
ВЫБРАТЬ
ТоварыУКонтрагентовОстатки.Номенклатура КАК Номенклатура,
ТоварыУКонтрагентовОстатки.СубАрендатор КАК СубАрендатор,
ТоварыУКонтрагентовОстатки.Контрагент КАК Контрагент,
ТоварыУКонтрагентовОстатки.КоличествоОстаток КАК Количество,
ТоварыУКонтрагентов.Период КАК ДатаУстановки
ИЗ
РегистрНакопления.ТоварыУКонтрагентов.Остатки КАК ТоварыУКонтрагентовОстатки
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.ТоварыУКонтрагентов КАК ТоварыУКонтрагентов
ПО ТоварыУКонтрагентовОстатки.Номенклатура = ТоварыУКонтрагентов.Номенклатура
И ТоварыУКонтрагентовОстатки.СубАрендатор = ТоварыУКонтрагентов.СубАрендатор
И ТоварыУКонтрагентовОстатки.Контрагент = ТоварыУКонтрагентов.Контрагент
И (ТоварыУКонтрагентов.Период В
(ВЫБРАТЬ ПЕРВЫЕ 1
Таб.Период
ИЗ
РегистрНакопления.ТоварыУКонтрагентов КАК Таб
ГДЕ
Таб.Номенклатура = ТоварыУКонтрагентов.Номенклатура
И Таб.СубАрендатор = ТоварыУКонтрагентов.СубАрендатор
И Таб.Контрагент = ТоварыУКонтрагентов.Контрагент
УПОРЯДОЧИТЬ ПО
Таб.Период УБЫВ))
ГДЕ
ТоварыУКонтрагентовОстатки.Контрагент = &Контрагент
И ТоварыУКонтрагентовОстатки.СубАрендатор = &СубАрендатор
Давайте проанализируем, как работает эта схема. Конструкция ТоварыУКонтрагентов.Период В (ВЫБРАТЬ ПЕРВЫЕ 1 ...) заставляет СУБД для каждой пары соединяемых строк производить выборку максимального периода. Этот подход действительно вернет правильный результат в отчет, однако опытные разработчики сразу заметят серьезный изъян. Один из участников обсуждения справедливо заметил: «что только люди не сделают, чтобы временные таблицы не использовать».
Почему же этот код считается плохой практикой (антипаттерном)?
ПО), так как оптимизаторы СУБД (особенно PostgreSQL) часто не могут построить эффективный план выполнения для таких конструкций, что приводит к непредсказуемому времени выполнения.Для того чтобы запрос работал мгновенно даже на миллионных объемах данных, мы должны спроектировать его с использованием механизма временных таблиц. Идея проста: сначала мы подготовим плоский список уникальных аналитик и их максимальных дат во временную таблицу, проиндексируем её, а затем выполним классическое плоское левое соединение.
Разберем это оптимальное решение по шагам.
Мы выбираем все приходные накладные, группируем их по ключевым полям и получаем самую позднюю дату с помощью агрегатной функции МАКСИМУМ(Период). Результат помещаем во временную таблицу ВТ_МаксимальныеДатыПрихода и обязательно индексируем её по связующим полям.
ВЫБРАТЬ
ТоварыУКонтрагентов.Номенклатура КАК Номенклатура,
ТоварыУКонтрагентов.СубАрендатор КАК СубАрендатор,
ТоварыУКонтрагентов.Контрагент КАК Контрагент,
МАКСИМУМ(ТоварыУКонтрагентов.Период) КАК Период
ПОМЕСТИТЬ ВТ_МаксимальныеДатыПрихода
ИЗ
РегистрНакопления.ТоварыУКонтрагентов КАК ТоварыУКонтрагентов
ГДЕ
ТоварыУКонтрагентов.ВидДвижения = ЗНАЧЕНИЕ(ВидДвиженияНакопления.Приход)
И ТоварыУКонтрагентов.Контрагент = &Контрагент
И ТоварыУКонтрагентов.СубАрендатор = &СубАрендатор
СГРУППИРОВАТЬ ПО
ТоварыУКонтрагентов.Номенклатура,
ТоварыУКонтрагентов.СубАрендатор,
ТоварыУКонтрагентов.Контрагент
ИНДЕКСИРОВАТЬ ПО
Номенклатура,
СубАрендатор,
Контрагент
;
Теперь мы можем выполнить итоговую выборку. Мы берем виртуальную таблицу остатков и соединяем её с нашей проиндексированной временной таблицей ВТ_МаксимальныеДатыПрихода. Поскольку во временной таблице на каждую уникальную связку номенклатуры и контрагента гарантированно приходится ровно одна строка, левое соединение отработает корректно и не приведет к «размножению» записей.
ВЫБРАТЬ
ТоварыУКонтрагентовОстатки.Номенклатура КАК Номенклатура,
ТоварыУКонтрагентовОстатки.СубАрендатор КАК СубАрендатор,
ТоварыУКонтрагентовОстатки.Контрагент КАК Контрагент,
ТоварыУКонтрагентовОстатки.КоличествоОстаток КАК Количество,
ЕСТЬNULL(ВТ_МаксимальныеДатыПрихода.Период, ДАТАВРЕМЯ(1, 1, 1)) КАК ДатаУстановки
ИЗ
РегистрНакопления.ТоварыУКонтрагентов.Остатки(
&ГраницаСреза,
Контрагент = &Контрагент
И СубАрендатор = &СубАрендатор) КАК ТоварыУКонтрагентовОстатки
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_МаксимальныеДатыПрихода КАК ВТ_МаксимальныеДатыПрихода
ПО ТоварыУКонтрагентовОстатки.Номенклатура = ВТ_МаксимальныеДатыПрихода.Номенклатура
И ТоварыУКонтрагентовОстатки.СубАрендатор = ВТ_МаксимальныеДатыПрихода.СубАрендатор
И ТоварыУКонтрагентовОстатки.Контрагент = ВТ_МаксимальныеДатыПрихода.Контрагент
Nested Loops (вложенные циклы) СУБД применит высокоэффективные алгоритмы слияния Hash Match или Merge Join.ВТ_МаксимальныеДатыПрихода в отдельности, чтобы проверить правильность расчета максимальных дат.Обратите внимание на параметр &ГраницаСреза во втором запросе. В первоначальном коде автора использовалась конструкция КОНЕЦПЕРИОДА(&ДатаСреза, ДЕНЬ).
В платформе «1С:Предприятие» при передаче обычной даты (даже времени конца дня 23:59:59) в параметры виртуальной таблицы остатков СУБД может не учесть движения, записанные ровно в эту же секунду (в зависимости от СУБД и режима совместимости). Чтобы остатки гарантированно включали все документы, проведенные в последнюю секунду дня, необходимо формировать параметр на стороне встроенного языка с помощью объекта Граница:
ГраницаПериода = Новый Граница(КонецДня(ДатаСреза), ВидГраницы.Включая);
Запрос.УстановитьПараметр("ГраницаСреза", ГраницаПериода);
Применяйте временные таблицы и правильные типы параметров в своих разработках — это гарантирует стабильность и высокую скорость работы ваших учетных систем при любых объемах данных!