При разработке алгоритмов выборки данных из регистров сведений разработчики нередко сталкиваются с ситуацией, когда запрос, сформированный без явных синтаксических ошибок, возвращает пустую выборку. Проанализируем типичный пример: в запрос передается перечень номенклатуры из табличной части документа, задается отбор по типу цен и периоду, однако на выходе итератор строк пуст. Разберем по шагам, почему так происходит, какие скрытые нюансы несет в себе передача коллекций в параметры и как правильно получать актуальные цены номенклатуры в платформе 1С:Предприятие.
Рассмотрим фрагмент кода, вызвавший затруднения:
СписокНоменклатуры = Новый СписокЗначений;
СписокНоменклатуры = Товары.ВыгрузитьКолонку("Номенклатура");
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЦеныНоменклатуры.Номенклатура,
| ЦеныНоменклатуры.Цена КАК Цена
|ИЗ
| РегистрСведений.ЦеныНоменклатуры КАК ЦеныНоменклатуры
|ГДЕ
| ЦеныНоменклатуры.Период = &Период
| И ЦеныНоменклатуры.ТипЦен = &ТипЦен
| И ЦеныНоменклатуры.Номенклатура В(&СписокНоменклатуры)";
Запрос.УстановитьПараметр("СписокНоменклатуры", СписокНоменклатуры);
Запрос.УстановитьПараметр("Период", ДокументОбъект.Дата);
Запрос.УстановитьПараметр("ТипЦен", УправлениеПользователями.ПолучитьЗначениеПоУмолчанию(глЗначениеПеременной("глТекущийПользователь"), "ОсновнаяЦенаЗакупки"));
В приведенном фрагменте допущено сразу несколько логических и архитектурных ошибок. Разберем каждую из них подробнее:
СписокЗначений, однако следующая же строка присваивает переменной результат работы метода Товары.ВыгрузитьКолонку("Номенклатура"). Метод табличной части возвращает тип Массив. В итоге ссылка на созданный список значений мгновенно теряется, а переменная становится обычным массивом. Ошибки выполнения здесь нет, так как запрос одинаково корректно принимает как СписокЗначений, так и Массив, но создание первого объекта абсолютно избыточно.РегистрСведений.ЦеныНоменклатуры производит выборку из физической таблицы записей. Регистр цен является периодическим, и актуальная цена на момент проведения документа определяется не точной датой одной конкретной записи, а последней установленной ценой на шкале времени вплоть до даты документа.ЦеныНоменклатуры.Период = &Период отбирает только те записи, у которых дата и время совпадают с ДокументОбъект.Дата с точностью до секунды. Если документ создан в 14:35:22, а цены устанавливались документом от 00:00:00 или в предыдущие дни, условие вернет ложь для всех строк без исключения. Это и является главной причиной возврата пустого результата.Выясним, как работает условие В (&Параметр) в запросах 1С. Для платформы нет принципиальной разницы, передается ли в параметр объект Массив или СписокЗначений. Оба варианта транслируются во внутреннее условие SQL вида IN (...).
Если для решения прикладной задачи действительно необходим именно СписокЗначений, правильный синтаксис его наполнения из колонки выглядит следующим образом:
СписокНоменклатуры = Новый СписокЗначений;
СписокНоменклатуры.ЗагрузитьЗначения(Товары.ВыгрузитьКолонку("Номенклатура"));
Однако передавать массив напрямую значительно проще и быстрее, так как не тратится память на создание промежуточного объекта списка значений:
МассивНоменклатуры = Товары.ВыгрузитьКолонку("Номенклатура");
Запрос.УстановитьПараметр("СписокНоменклатуры", МассивНоменклатуры);
Обратите внимание на важные ограничения:
ВыгрузитьКолонку() возвратит пустую коллекцию. При передаче пустого массива в параметр условия с оператором В платформа подставляет заведомо ложное выражение (аналог 1 = 0), и запрос вернет пустой результат. Всегда проверяйте количество строк перед выполнением запроса.В (&Массив) разворачивается СУБД в перечисление значений. При передаче массива размером более 1000–2000 элементов СУБД (например, Microsoft SQL Server или PostgreSQL) может сгенерировать ошибку превышения лимита параметров. Кроме того, метод ВыгрузитьКолонку() сохраняет все дубли позиций, что перегружает план запроса.Для периодических регистров сведений разработчики платформы предусмотрели виртуальную таблицу СрезПоследних. Она берет на себя вычисление актуальных значений измерений и ресурсов на заданную дату с учетом периодичности регистра.
Разберем рабочий и лаконичный вариант запроса с получением последних действующих цен:
МассивНоменклатуры = Товары.ВыгрузитьКолонку("Номенклатура");
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЦеныНоменклатурыСрезПоследних.Номенклатура КАК Номенклатура,
| ЦеныНоменклатурыСрезПоследних.Цена КАК Цена
|ИЗ
| РегистрСведений.ЦеныНоменклатуры.СрезПоследних(
| &Период,
| ТипЦен = &ТипЦен
| И Номенклатура В (&СписокНоменклатуры)) КАК ЦеныНоменклатурыСрезПоследних";
Запрос.УстановитьПараметр("СписокНоменклатуры", МассивНоменклатуры);
Запрос.УстановитьПараметр("Период", Новый Граница(ДокументОбъект.Дата, ВидГраницы.Включая));
Запрос.УстановитьПараметр("ТипЦен", УправлениеПользователями.ПолучитьЗначениеПоУмолчанию(глЗначениеПеременной("глТекущийПользователь"), "ОсновнаяЦенаЗакупки"));
Выборка = Запрос.Выполнить().Выбрать();
Пока Выборка.Следующий() Цикл
Сообщить(Строка(Выборка.Номенклатура) + ": " + Выборка.Цена);
КонецЦикла;
Обратите внимание: отборы по номенклатуре и типу цен помещены строго в параметры виртуальной таблицы, а не в секцию ГДЕ. Это критично для производительности, так как платформа рассчитывает срез только для нужных записей, а не по всей таблице регистра.
Также для параметра &Период рекомендуется использовать объект Граница с видом ВидГраницы.Включая. Если цена была зарегистрирована в ту же секунду, которой датирован документ, обычная передача даты может исключить эту запись из-за строгого сравнения границ времени.
Если в табличной части может быть много строк или позиции дублируются, наилучшей практикой согласно стандартам разработки 1С является передача таблицы во временную таблицу с последующим соединением. Это исключает дублирование, снимает ограничения на размер условия IN и ускоряет работу СУБД.
Рассмотрим реализацию этого подхода:
ТаблицаТоваров = Товары.Выгрузить(, "Номенклатура");
ТаблицаТоваров.Свернуть("Номенклатура"); // Удаляем дублирующиеся строки
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = Новый МенеджерВременныхТаблиц;
Запрос.УстановитьПараметр("ТаблицаТоваров", ТаблицаТоваров);
Запрос.УстановитьПараметр("Период", Новый Граница(ДокументОбъект.Дата, ВидГраницы.Включая));
Запрос.УстановитьПараметр("ТипЦен", УправлениеПользователями.ПолучитьЗначениеПоУмолчанию(глЗначениеПеременной("глТекущийПользователь"), "ОсновнаяЦенаЗакупки"));
Запрос.Текст =
"ВЫБРАТЬ
| ТаблицаТоваров.Номенклатура КАК Номенклатура
|ПОМЕСТИТЬ ВТ_Товары
|ИЗ
| &ТаблицаТоваров КАК ТаблицаТоваров
|ИНДЕКСИРОВАТЬ ПО
| Номенклатура;
|
|////////////////////////////////////////////////////////////////////////////////
|ВЫБРАТЬ
| ВТ_Товары.Номенклатура КАК Номенклатура,
| ЕСТЬNULL(Цены.Цена, 0) КАК Цена
|ИЗ
| ВТ_Товары КАК ВТ_Товары
| ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(
| &Период,
| ТипЦен = &ТипЦен
| И Номенклатура В (ВЫБРАТЬ Номенклатура ИЗ ВТ_Товары)) КАК Цены
| ПО ВТ_Товары.Номенклатура = Цены.Номенклатура";
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
Сообщить(Строка(Выборка.Номенклатура) + ": " + Выборка.Цена);
КонецЦикла;
Использование конструкции ЛЕВОЕ СОЕДИНЕНИЕ и функции ЕСТЬNULL гарантирует, что в результирующую выборку попадут абсолютно все товары из документа, даже если для некоторых из них цена в регистре никогда не устанавливалась.