При решении учетных и аналитических задач разработчикам 1С часто требуется получить данные о последней закупочной цене номенклатуры. Типичный сценарий — обращение к регистру накопления Закупки (или аналогичному регистру движений поступлений) с поиском максимальной даты документа. Однако при составлении таких запросов легко допустить ошибку в группировках или столкнуться с неочевидными проблемами производительности и задвоения строк. Разберем подробно причины возникновения таких ошибок, пошаговые способы исправления и архитектурно правильные подходы к получению последних цен.
Рассмотрим типовую ситуацию, когда разработчик пытается связать таблицу регистра с вложенным запросом, вычисляющим максимальную дату поступления:
ВЫБРАТЬ
Закупки.Регистратор,
Закупки.Стоимость,
Закупки.Номенклатура,
Закупки.Количество
ИЗ
РегистрНакопления.Закупки КАК Закупки
ВНУТРЕННЕЕ СОЕДИНЕНИЕ (
ВЫБРАТЬ
ПоследниеДатыЗакупки.Номенклатура КАК Номенклатура,
МАКСИМУМ(ПоследниеДатыЗакупки.Период) КАК Период,
ПоследниеДатыЗакупки.Регистратор КАК Регистратор
ИЗ
РегистрНакопления.Закупки КАК ПоследниеДатыЗакупки
СГРУППИРОВАТЬ ПО
ПоследниеДатыЗакупки.Номенклатура,
ПоследниеДатыЗакупки.Регистратор
) КАК ДатыПоследнихДокументов
ПО Закупки.Период = ДатыПоследнихДокументов.Период
И Закупки.Номенклатура = ДатыПоследнихДокументов.Номенклатура
Проанализируем, почему этот запрос выводит все документы и все цены вместо одной последней по каждой позиции. Причина кроется во вложенном запросе ДатыПоследнихДокументов.
В секции СГРУППИРОВАТЬ ПО указаны два поля: Номенклатура и Регистратор. СУБД выполняет группировку по уникальной комбинации этих полей. Поскольку поле Регистратор указывает на конкретный документ, агрегатная функция МАКСИМУМ(Период) отрабатывает в разрезе каждого отдельного документа. В итоге вложенный запрос возвращает список абсолютно всех документов поступления для каждой позиции номенклатуры. После внутреннего соединения основной таблицы с таким подзапросом результирующая выборка содержит все движения без какой-либо фильтрации по последней дате.
Выясним, как быстро исправить исходный запрос. Для этого необходимо убрать поле Регистратор из выборки и секции СГРУППИРОВАТЬ ПО вложенного запроса, оставив группировку исключительно по полю Номенклатура.
Разберем исправленный вариант запроса:
ВЫБРАТЬ
Закупки.Регистратор КАК Регистратор,
Закупки.Номенклатура КАК Номенклатура,
Закупки.Количество КАК Количество,
Закупки.Стоимость КАК Стоимость,
ВЫБОР
КОГДА Закупки.Количество <> 0
ТОГДА Закупки.Стоимость / Закупки.Количество
ИНАЧЕ 0
КОНЕЦ КАК Цена
ИЗ
РегистрНакопления.Закупки КАК Закупки
ВНУТРЕННЕЕ СОЕДИНЕНИЕ (
ВЫБРАТЬ
ПоследниеДаты.Номенклатура КАК Номенклатура,
МАКСИМУМ(ПоследниеДаты.Период) КАК Период
ИЗ
РегистрНакопления.Закупки КАК ПоследниеДаты
СГРУППИРОВАТЬ ПО
ПоследниеДаты.Номенклатура
) КАК ДатыПоследнихДокументов
ПО Закупки.Номенклатура = ДатыПоследнихДокументов.Номенклатура
И Закупки.Период = ДатыПоследнихДокументов.Период
В данном варианте подзапрос возвращает строго одну запись на каждую позицию номенклатуры — с самой поздней датой движения. Также мы добавили безопасный расчет цены за единицу с проверкой деления на ноль через конструкцию ВЫБОР КОГДА ... ТОГДА.
Рассмотрим скрытую проблему, с которой можно столкнуться при использовании поля Период. Поле Период в регистре накопления хранит значение даты и времени с точностью до секунды. Если в базе данных в одну и ту же секунду будут проведены два документа поступления по одной номенклатуре (например, в результате автоматического обмена данными, группового перепроведения или пакетной загрузки), соединение по полям Период и Номенклатура вернет обе строки, что снова приведет к задвоению данных в отчете.
Разберем, почему нельзя просто написать МАКСИМУМ(Регистратор.МоментВремени):
МоментВремени. Это свойство присуще ссылочным объектам информационной базы (документам).Закупки.Регистратор.МоментВремени приводит к неявному левому соединению с таблицами всех типов документов, которые могут быть регистраторами данного регистра. Это существенно замедляет выполнение запроса.Чтобы исключить коллизию одинаковых дат и гарантировать выборку строго одного документа, применяют двухэтапный поиск: сначала находят максимальную дату, а затем среди записей с этой датой выбирают максимальный Регистратор (ссылку на документ):
ВЫБРАТЬ
Закупки.Номенклатура КАК Номенклатура,
МАКСИМУМ(Закупки.Период) КАК Период
ПОМЕСТИТЬ ВТ_МаксимальныеДаты
ИЗ
РегистрНакопления.Закупки КАК Закупки
СГРУППИРОВАТЬ ПО
Закупки.Номенклатура
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Закупки.Номенклатура КАК Номенклатура,
МАКСИМУМ(Закупки.Регистратор) КАК Регистратор
ПОМЕСТИТЬ ВТ_ПоследниеДокументы
ИЗ
РегистрНакопления.Закупки КАК Закупки
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_МаксимальныеДаты КАК МаксДаты
ПО Закупки.Номенклатура = МаксДаты.Номенклатура
И Закупки.Период = МаксДаты.Период
СГРУППИРОВАТЬ ПО
Закупки.Номенклатура
ИНДЕКСИРОВАТЬ ПО
Номенклатура,
Регистратор
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Закупки.Регистратор КАК Регистратор,
Закупки.Номенклатура КАК Номенклатура,
Закупки.Количество КАК Количество,
Закупки.Стоимость КАК Стоимость,
ВЫБОР
КОГДА Закупки.Количество <> 0
ТОГДА Закупки.Стоимость / Закупки.Количество
ИНАЧЕ 0
КОНЕЦ КАК Цена
ИЗ
РегистрНакопления.Закупки КАК Закупки
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ПоследниеДокументы КАК ПослДокументы
ПО Закупки.Номенклатура = ПослДокументы.Номенклатура
И Закупки.Регистратор = ПослДокументы.Регистратор
Проанализируем производительность запросов к СУБД. Соединение физических таблиц с вложенными подзапросами считается антипаттерном в разработке 1С. Оптимизатор СУБД (например, в MS SQL Server или PostgreSQL) часто не может построить эффективный план выполнения для сложных вложенных выборок, что приводит к полному сканированию таблиц (Table Scan) и блокировкам данных.
Стандарты разработки 1С предписывают использовать пакетные запросы с временными таблицами и их обязательным индексированием. Разберем оптимизированный вариант:
ВТ_ПоследниеДаты с созданием индекса по полю Номенклатура.
ВЫБРАТЬ
Закупки.Номенклатура КАК Номенклатура,
МАКСИМУМ(Закупки.Период) КАК Период
ПОМЕСТИТЬ ВТ_ПоследниеДаты
ИЗ
РегистрНакопления.Закупки КАК Закупки
СГРУППИРОВАТЬ ПО
Закупки.Номенклатура
ИНДЕКСИРОВАТЬ ПО
Номенклатура
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
Закупки.Регистратор КАК Регистратор,
Закупки.Номенклатура КАК Номенклатура,
Закупки.Количество КАК Количество,
Закупки.Стоимость КАК Стоимость,
ВЫБОР
КОГДА Закупки.Количество <> 0
ТОГДА Закупки.Стоимость / Закупки.Количество
ИНАЧЕ 0
КОНЕЦ КАК Цена
ИЗ
РегистрНакопления.Закупки КАК Закупки
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_ПоследниеДаты КАК ПоследниеДаты
ПО Закупки.Номенклатура = ПоследниеДаты.Номенклатура
И Закупки.Период = ПоследниеДаты.Период
Посмотрим на задачу с точки зрения архитектуры типовых конфигураций (1С:Управление торговлей, 1С:ERP, 1С:Комплексная автоматизация, 1С:УНФ). Регистры накопления предназначены для учета оборотов и остатков (количественных и суммовых), но не для оперативного получения актуальных цен.
Для хранения цен предназначены периодические регистры сведений (например, ЦеныНоменклатуры, ЦеныНоменклатурыПоставщиков или ЦеныНоменклатурыДокументов). Главное преимущество регистра сведений — наличие готовой виртуальной таблицы СрезПоследних.
Пример запроса к виртуальной таблице СрезПоследних:
ВЫБРАТЬ
Цены.Номенклатура КАК Номенклатура,
Цены.ВидЦены КАК ВидЦены,
Цены.Цена КАК Цена,
Цены.Период КАК Период,
Цены.Регистратор КАК Регистратор
ИЗ
РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&Период, ) КАК Цены
Преимущества использования виртуальной таблицы СрезПоследних:
МАКСИМУМ(Период) и делить стоимость на количество.Если конфигурация предусматривает фиксацию цен поступления при проведении документов (например, регистрацию цен поставщиков), наиболее правильным решением будет получение актуальной цены именно из регистра сведений, а не из регистра накопления.