При разработке отчетов по продажам программисты часто сталкиваются с необходимостью вывести цену номенклатуры, которая действовала именно в момент совершения сделки (на дату расходной накладной). Стандартная виртуальная таблица СрезПоследних регистра сведений хорошо работает, когда нужно получить цены на одну конкретную дату (например, на сегодня). Однако, если в одном запросе нужно получить разные цены для множества документов с разными датами, обычное соединение со СрезПоследних не поможет, так как параметры виртуальной таблицы нельзя динамически менять для каждой строки.
В этой статье мы подробно разберем, почему возникает ошибка NULL при попытке прямого соединения, и изучим правильный алгоритм получения данных с помощью временных таблиц и группировок.
Рассмотрим типичную ошибку начинающих разработчиков. Попытка соединить таблицу оборотов продаж с виртуальной таблицей цен выглядит примерно так:
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&КонецПериода, ) КАК Цены
ПО Продажи.Номенклатура = Цены.Номенклатура
Проблема заключается в том, что 1С рассчитывает виртуальную таблицу СрезПоследних один раз на дату, указанную в параметре (в данном случае &КонецПериода). Если цена на товар менялась в течение месяца, то для всех документов продажи за этот месяц запрос подтянет одну и ту же последнюю цену, что фактически искажает данные отчета. Если же на дату &КонецПериода цены нет, запрос вернет NULL, даже если цена была установлена ранее.
Проанализируем ситуацию: нам нужно для каждой строки продажи найти в регистре сведений ЦеныНоменклатуры запись, у которой период меньше или равен дате документа, и при этом эта запись должна быть самой «свежей» из всех предшествующих. Рассмотрим, как реализовать это правильно.
Для решения задачи мы будем использовать механизм временных таблиц. Это позволит нам разделить сложную задачу на логические этапы, что значительно упростит отладку и ускорит выполнение запроса сервером СУБД.
ЦеныНоменклатуры. Условие соединения: период записи регистра должен быть меньше или равен дате документа продажи. С помощью группировки по каждой строке продажи мы находим МАКСИМУМ(Период) из регистра цен.Разберем этот алгоритм на примере кода.
Посмотрим на структуру эффективного запроса. В данном примере мы также учтем характеристики номенклатуры, так как это критически важно для точности данных.
// ЭТАП 1: Получаем данные о продажах
ВЫБРАТЬ
Продажи.Номенклатура КАК Номенклатура,
Продажи.ХарактеристикаНоменклатуры КАК ХарактеристикаНоменклатуры,
Продажи.Регистратор КАК ДокументПродажи,
Продажи.Период КАК ДатаДокумента,
Продажи.КоличествоОборот КАК Количество,
Продажи.СтоимостьОборот КАК Стоимость
ПОМЕСТИТЬ ВТ_Продажи
ИЗ
РегистрНакопления.Продажи.Обороты(&НачДата, &КонДата, Регистратор, ) КАК Продажи;
////////////////////////////////////////////////////////////////////////////////
// ЭТАП 2: Находим дату последней установки цены перед продажей
ВЫБРАТЬ
Продажи.Номенклатура КАК Номенклатура,
Продажи.ХарактеристикаНоменклатуры КАК ХарактеристикаНоменклатуры,
Продажи.ДокументПродажи КАК ДокументПродажи,
МАКСИМУМ(Цены.Период) КАК ПериодЦены
ПОМЕСТИТЬ ВТ_ДатыЦен
ИЗ
ВТ_Продажи КАК Продажи
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры КАК Цены
ПО Продажи.Номенклатура = Цены.Номенклатура
И Продажи.ХарактеристикаНоменклатуры = Цены.ХарактеристикаНоменклатуры
И Продажи.ДатаДокумента >= Цены.Период
ГДЕ
Цены.ТипЦен = &ВыбранныйТипЦен
СГРУППИРОВАТЬ ПО
Продажи.Номенклатура,
Продажи.ХарактеристикаНоменклатуры,
Продажи.ДокументПродажи;
////////////////////////////////////////////////////////////////////////////////
// ЭТАП 3: Соединяем все данные и получаем итоговую цену
ВЫБРАТЬ
Продажи.Номенклатура,
Продажи.ДокументПродажи,
Продажи.Количество,
Продажи.Стоимость,
ЕСТЬNULL(ЦеныЗначения.Цена, 0) КАК ЦенаНаДату
ИЗ
ВТ_Продажи КАК Продажи
ЛЕВОЕ СОЕДИНЕНИЕ ВТ_ДатыЦен КАК Даты
ПО Продажи.Номенклатура = Даты.Номенклатура
И Продажи.ХарактеристикаНоменклатуры = Даты.ХарактеристикаНоменклатуры
И Продажи.ДокументПродажи = Даты.ДокументПродажи
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры КАК ЦеныЗначения
ПО Даты.Номенклатура = ЦеныЗначения.Номенклатура
И Даты.ХарактеристикаНоменклатуры = ЦеныЗначения.ХарактеристикаНоменклатуры
И Даты.ПериодЦены = ЦеныЗначения.Период
И (ЦеныЗначения.ТипЦен = &ВыбранныйТипЦен)
Проанализируем несколько важных моментов, которые помогут вашему запросу работать быстрее и стабильнее:
ЕСТЬNULL(Цена, 0). Если на какой-то товар цена никогда не устанавливалась, результат соединения будет NULL, что приведет к ошибкам в арифметических вычислениях (например, при расчете налога или прибыли).Продажи.Организация.Наименование ПОДОБНО "%ИП%". В языке SQL 1С каждое обращение через точку к реквизиту ссылочного типа порождает неявное соединение (LEFT JOIN) с соответствующей таблицей. Если в отчете тысячи строк, это создает колоссальную нагрузку. Правильнее передавать ссылку на конкретную организацию в параметрах запроса: ГДЕ Организация = &ОрганизацияСсылка.ПОДОБНО для сравнения ссылочных объектов по их строковому представлению — крайне плохая практика. Сравнение строк в базе данных происходит значительно медленнее, чем сравнение уникальных идентификаторов (ссылок).ВидуЦен или ХарактеристикеНоменклатуры, из-за чего на одну дату документа система находит несколько цен и создает дублирующиеся строки.Если вы разрабатываете отчет на базе СКД, то вышеописанный сложный запрос можно заменить более простым механизмом Связи наборов данных. Рассмотрим этот вариант подробнее:
Вы можете создать два отдельных набора данных — запроса. Первый выбирает только продажи. Второй — виртуальную таблицу ЦеныНоменклатуры.СрезПоследних. В настройках связей наборов данных вы указываете, что поле Период для второго набора должно заполняться значением ДатаДокумента из первого набора. В этом случае СКД сама сформирует оптимизированные запросы к базе данных для каждой строки, обеспечив получение актуальной цены на нужный момент времени без использования громоздких временных таблиц вручную.
Выбор метода зависит от ваших задач: для простых отчетов в СКД используйте связи наборов данных, для сложных вычислений внутри кода (например, в обработках проведения или заполнения) лучше подойдет метод с временными таблицами.