При разработке учетных механизмов и проведении документов в 1С часто возникает задача: необходимо взять строки табличной части документа и рассчитать суммы на основании актуальных цен из периодического регистра сведений. Сложность заключается в том, что цены по разным позициям номенклатуры устанавливаются в разное время (например, по одному товару цена действует с апреля, по другому — с июня), и для каждой строки нужно получить строго последнее актуальное значение на момент документа.
Разберем типичные ошибки при решении этой задачи, проанализируем ограничения платформы и рассмотрим правильные варианты построения запросов.
Рассмотрим частую ошибку, возникающую при попытке соединить табличную часть напрямую с виртуальной таблицей СрезПоследних:
ВЫБРАТЬ
Номенклатура.КатегорияТовара,
СУММА(ПрайсЛистСрезПоследних.Цена * (ПолнаяТаблица.Скидка / 100) * ПолнаяТаблица.Количество) КАК Сумма
ИЗ
Документ.Заявка.ПолнаяТаблица КАК ПолнаяТаблица
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ПрайсЛист.СрезПоследних(&ПериодРегистрации, ) КАК ПрайсЛистСрезПоследних
ПО ПолнаяТаблица.Номенклатура.Артикул = ПрайсЛистСрезПоследних.Артикул
ГДЕ
ПолнаяТаблица.Ссылка = &Ссылка
Выясним, какие скрытые проблемы содержит такой подход:
NULL: При левом соединении для позиций, по которым цена не найдена, поле Цена принимает значение NULL. В языке запросов 1С любая арифметическая операция со значением NULL (например, NULL * Количество) возвращает NULL, в результате чего строка полностью выпадает из расчета. Поле цены обязательно нужно оборачивать в функцию ЕСТЬNULL(ПрайсЛистСрезПоследних.Цена, 0).ПолнаяТаблица.Номенклатура.Артикул в секции соединения порождает неявное соединение со справочником номенклатуры на уровне СУБД, что ухудшает производительность. Связывать таблицы следует напрямую по ссылочным полям либо выгружать артикулы во временную таблицу.СрезПоследних, СУБД сначала рассчитывает срез по всей таблице регистра, и лишь затем соединяет результат с документом.Если дата расчета цены одинакова для всех строк документа (например, дата проведения или реквизит &ПериодРегистрации), наиболее эффективным решением будет предварительная выгрузка списка номенклатуры во временную таблицу и передача ее в параметры виртуальной таблицы.
Разберем по шагам построение такого запроса:
ВЫБРАТЬ РАЗЛИЧНЫЕ
ТаблицаДокумента.Номенклатура КАК Номенклатура
ПОМЕСТИТЬ ВТ_ТоварыДокумента
ИЗ
Документ.Заявка.ПолнаяТаблица КАК ТаблицаДокумента
ГДЕ
ТаблицаДокумента.Ссылка = &Ссылка
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ТаблицаДокумента.Номенклатура КАК Номенклатура,
ТаблицаДокумента.Количество КАК Количество,
ЕСТЬNULL(Цены.Цена, 0) КАК Цена,
ЕСТЬNULL(Цены.Цена, 0) * (ТаблицаДокумента.Скидка / 100) * ТаблицаДокумента.Количество КАК СуммаСоСкидкой
ИЗ
Документ.Заявка.ПолнаяТаблица КАК ТаблицаДокумента
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ПрайсЛист.СрезПоследних(
&ПериодРегистрации,
Номенклатура В (ВЫБРАТЬ ВТ_ТоварыДокумента.Номенклатура ИЗ ВТ_ТоварыДокумента)
) КАК Цены
ПО ТаблицаДокумента.Номенклатура = Цены.Номенклатура
ГДЕ
ТаблицаДокумента.Ссылка = &Ссылка
В этом случае платформа 1С на уровне СУБД сформирует оптимальный срез только для тех товаров, которые присутствуют в документе, с использованием системных индексов.
Виртуальная таблица СрезПоследних принимает только один фиксированный скалярный параметр периода. Если в таблице содержатся строки с разными датами (например, при анализе периодических движений или расчете цен на дату каждой отдельной операции), виртуальная таблица не подойдет.
В такой ситуации проанализируем надежный и производительный алгоритм на базе реальной таблицы регистра:
Период <= ДатаСреза и вычисляем МАКСИМУМ(Период) для каждой номенклатуры.Посмотрим на пример реализации запроса:
ВЫБРАТЬ
ДокументТаблица.Номенклатура КАК Номенклатура,
ДокументТаблица.Количество КАК Количество,
ДокументТаблица.Скидка КАК Скидка,
&ПериодРегистрации КАК ДатаСреза
ПОМЕСТИТЬ ВТ_ИсходныеДанные
ИЗ
Документ.Заявка.ПолнаяТаблица КАК ДокументТаблица
ГДЕ
ДокументТаблица.Ссылка = &Ссылка
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Исходные.Номенклатура КАК Номенклатура,
МАКСИМУМ(ПрайсЛист.Период) КАК МаксПериод
ПОМЕСТИТЬ ВТ_МаксимальныеПериоды
ИЗ
ВТ_ИсходныеДанные КАК Исходные
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ПрайсЛист КАК ПрайсЛист
ПО Исходные.Номенклатура = ПрайсЛист.Номенклатура
И ПрайсЛист.Период <= Исходные.ДатаСреза
СГРУППИРОВАТЬ ПО
Исходные.Номенклатура
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Исходные.Номенклатура КАК Номенклатура,
Исходные.Количество КАК Количество,
ЕСТЬNULL(ПрайсЛист.Цена, 0) КАК Цена,
ЕСТЬNULL(ПрайсЛист.Цена, 0) * (Исходные.Скидка / 100) * Исходные.Количество КАК Сумма
ИЗ
ВТ_ИсходныеДанные КАК Исходные
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_МаксимальныеПериоды КАК МаксПериоды
ПО Исходные.Номенклатура = МаксПериоды.Номенклатура
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ПрайсЛист КАК ПрайсЛист
ПО МаксПериоды.Номенклатура = ПрайсЛист.Номенклатура
И МаксПериоды.МаксПериод = ПрайсЛист.Период
На практике часто встречается способ соединения через подзапрос с конструкцией ВЫБРАТЬ ПЕРВЫЕ 1. Рассмотрим этот пример:
ВЫБРАТЬ
Продажи.Номенклатура КАК Номенклатура,
Продажи.КоличествоОборот КАК Количество,
ЦеныНоменклатуры.Цена КАК Цена
ИЗ
РегистрНакопления.Продажи.Обороты(&НачалоПериода, &КонецПериода, День, ) КАК Продажи
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры КАК ЦеныНоменклатуры
ПО Продажи.Номенклатура = ЦеныНоменклатуры.Номенклатура
И (ЦеныНоменклатуры.ТипЦен = &ТипЦен)
И (ЦеныНоменклатуры.Период В
(ВЫБРАТЬ ПЕРВЫЕ 1
Цены.Период
ИЗ
РегистрСведений.ЦеныНоменклатуры КАК Цены
ГДЕ
Цены.Период <= Продажи.Период
И Цены.Номенклатура = Продажи.Номенклатура
И Цены.ТипЦен = &ТипЦен
УПОРЯДОЧИТЬ ПО
Цены.Период УБЫВ))
Обратите внимание: данный вариант прост в написании, однако конструкция Период В (ВЫБРАТЬ ПЕРВЫЕ 1...) в условии соединения заставляет СУБД выполнять вложенные циклы (Nested Loops) для каждой строки выборки. На больших объемах данных такой запрос может работать значительно медленнее, чем схема с временными таблицами и группировкой по максимальному периоду.
Подведем итог и выделим ключевые правила работы с периодическими регистрами:
РегистрСведений.ИмяРегистра.СрезПоследних с обязательной передачей массива или временной таблицы номенклатуры во второй параметр виртуальной таблицы.МАКСИМУМ(Период) через временные таблицы.ЕСТЬNULL во избежание потери данных при арифметических операциях.