При разработке отчетов и обработок в среде 1С перед программистом регулярно возникает задача: вывести список товаров (номенклатуры) так, чтобы разные виды цен (например, оптовая, розничная, закупочная) отображались в отдельных колонках для каждой позиции. Начинающие разработчики часто сталкиваются с трудностями при составлении таких запросов, получая либо пустой результат, либо некорректно сдублированные строки.
В этой статье мы подробно разберем, почему возникает популярная ошибка с логическим условием И, как правильно связать несколько видов цен через механизм соединений, рассмотрим альтернативный высокопроизводительный метод группировки (пивотный запрос) и разберем методологические нюансы работы с параметрами виртуальных таблиц.
Рассмотрим типичную ошибку, с которой разработчик обратился на форум. В его исходном коде использовался следующий запрос:
ВЫБРАТЬ
ЦеныНоменклатуры.Номенклатура.Код КАК Код,
ЦеныНоменклатуры.Номенклатура.Наименование КАК Наименование,
ЦеныНоменклатуры.Цена КАК Цена,
ЦеныНоменклатуры.ВидЦен КАК ВидЦен
ИЗ
РегистрСведений.ЦеныНоменклатуры КАК ЦеныНоменклатуры
ГДЕ
ЦеныНоменклатуры.ВидЦен.Код = &УчетнаяЦенаШГ
И ЦеныНоменклатуры.ВидЦен.Код = &Розничная
Проанализируем логику этого условия. Регистр сведений ЦеныНоменклатуры хранит каждую запись (цену для конкретного товара и конкретного типа цен) как отдельную строку физической таблицы базы данных. Фильтр ГДЕ применяется построчно к каждой физической записи.
Когда СУБД начинает проверять строки, она пытается найти такую запись, у которой поле ВидЦен.Код одновременно равно значению &УчетнаяЦенаШГ И значению &Розничная. Очевидно, что в одной физической строке значение поля ВидЦен не может быть одновременно двумя разными элементами. Логическое условие И приводит к тому, что ни одна строка в таблице не удовлетворяет данному критерию, и результатом выполнения запроса становится пустая выборка.
Если изменить условие на ИЛИ, мы получим список, в котором один и тот же товар будет выводиться несколько раз — отдельной строкой для каждого вида цены. Но наша цель — получить одну строку для одного товара, где цены будут распределены по разным колонкам.
Для решения этой задачи в 1С существует два основных подхода: соединение виртуальных таблиц и группировка со сжатием (условный выбор). Разберем каждый из них подробно.
---
Чтобы получить две разные цены в одной строке, нам необходимо обратиться к виртуальной таблице СрезПоследних регистра цен дважды, присвоить им разные псевдонимы (алиасы) и соединить их по ключевому полю — ссылке на номенклатуру.
Выясним причину, почему важно накладывать отборы на виды цен именно в параметрах виртуальной таблицы, а не во внешнем условии ГДЕ. Если наложить отбор через ГДЕ, система сначала выберет последние цены вообще для всей номенклатуры, и только потом отфильтрует их. Если же передать отбор в параметры СрезПоследних(&НаДату, ВидЦен = &НужныйВид), платформа 1С на уровне СУБД выберет срез последних цен строго для указанного вида, что значительно ускорит работу запроса и снизит нагрузку на сервер.
Рассмотрим правильный код запроса с использованием ВНУТРЕННЕГО СОЕДИНЕНИЯ:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЦеныУчетные.Номенклатура.Код КАК Код,
| ЦеныУчетные.Номенклатура.Наименование КАК Наименование,
| ЦеныУчетные.Цена КАК ЦенаУчетная,
| ЦеныРозничные.Цена КАК ЦенаРозничная
|ИЗ
| РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&НаДату, ВидЦен = &УчетнаяЦена) КАК ЦеныУчетные
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&НаДату, ВидЦен = &РозничнаяЦена) КАК ЦеныРозничные
| ПО ЦеныУчетные.Номенклатура = ЦеныРозничные.Номенклатура";
Запрос.УстановитьПараметр("НаДату", ТекущаяДата());
Запрос.УстановитьПараметр("УчетнаяЦена", Справочники.ВидыЦен.НайтиПоКоду("УФ-000003"));
Запрос.УстановитьПараметр("РозничнаяЦена", Справочники.ВидыЦен.НайтиПоКоду("UN-000001"));
РезультатЗапроса = Запрос.Выполнить();
Важная архитектурная деталь: В данном примере используется ВНУТРЕННЕЕ СОЕДИНЕНИЕ. Это означает, что если по какому-то товару установлена учетная цена, но нет розничной цены (или наоборот), то этот товар вообще не попадет в итоговую выборку.
Если перед нами стоит задача вывести абсолютно всю номенклатуру (или все товары, у которых есть хотя бы одна из цен), то необходимо использовать ЛЕВОЕ СОЕДИНЕНИЕ, взяв за основу справочник Номенклатура:
ВЫБРАТЬ
СпрНоменклатура.Ссылка КАК Номенклатура,
СпрНоменклатура.Код КАК Код,
СпрНоменклатура.Наименование КАК Наименование,
ЕСТЬNULL(ЦеныУчетные.Цена, 0) КАК ЦенаУчетная,
ЕСТЬNULL(ЦеныРозничные.Цена, 0) КАК ЦенаРозничная
ИЗ
Справочник.Номенклатура КАК СпрНоменклатура
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&НаДату, ВидЦен = &УчетнаяЦена) КАК ЦеныУчетные
ПО СпрНоменклатура.Ссылка = ЦеныУчетные.Номенклатура
ЛЕВОЕ СОЕДИНЕНИЕ РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&НаДату, ВидЦен = &РозничнаяЦена) КАК ЦеныРозничные
ПО СпрНоменклатура.Ссылка = ЦеныРозничные.Номенклатура
ГДЕ
НЕ СпрНоменклатура.ЭтоГруппа
Использование функции ЕСТЬNULL позволяет нам заменить пустое значение (если цена не задана и соединение вернуло NULL) на числовой ноль для корректного отображения в отчете.
---
Соединение нескольких виртуальных таблиц СрезПоследних — крайне ресурсоемкая операция для СУБД. Платформа 1С для получения среза последних на лету строит тяжелые вложенные подзапросы с поиском максимальных дат. Если вам понадобится вывести не 2 вида цен, а 5 или 10, количество левых соединений увеличится, и запрос начнет критически «тормозить» на реальной базе данных.
Проанализируем альтернативный, гораздо более производительный и масштабируемый способ решения задачи через группировку и условную выборку. Этот метод обращается к виртуальной таблице цен всего один раз.
Разберем этот метод по шагам:
СрезПоследних мы передаем массив, содержащий все интересующие нас виды цен. Нам не нужно делать подзапрос под каждый вид цены отдельно.ВЫБРАТЬ мы используем условную конструкцию ВЫБОР КОГДА ... ТОГДА ... ИНАЧЕ ... КОНЕЦ. Если строка относится к учетной цене, мы выводим цену в первую колонку, если к розничной — во вторую.Номенклатура и используем агрегатную функцию МАКСИМУМ для колонок цен. Поскольку для одного товара и конкретной колонки будет только одно ненулевое значение цены, функция МАКСИМУМ корректно «схлопнет» строки, убрав лишние пустые значения.Посмотрим на пример реализации такого запроса:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ЦеныНоменклатуры.Номенклатура КАК Номенклатура,
| ЦеныНоменклатуры.Номенклатура.Код КАК Код,
| ЦеныНоменклатуры.Номенклатура.Наименование КАК Наименование,
| МАКСИМУМ(ВЫБОР
| КОГДА ЦеныНоменклатуры.ВидЦен = &УчетнаяЦена
| ТОГДА ЦеныНоменклатуры.Цена
| ИНАЧЕ 0
| КОНЕЦ) КАК ЦенаУчетная,
| МАКСИМУМ(ВЫБОР
| КОГДА ЦеныНоменклатуры.ВидЦен = &РозничнаяЦена
| ТОГДА ЦеныНоменклатуры.Цена
| ИНАЧЕ 0
| КОНЕЦ) КАК ЦенаРозничная
|ИЗ
| РегистрСведений.ЦеныНоменклатуры.СрезПоследних(&НаДату, ВидЦен В (&МассивВидовЦен)) КАК ЦеныНоменклатуры
|ГРУППИРОВАТЬ ПО
| ЦеныНоменклатуры.Номенклатура,
| ЦеныНоменклатуры.Номенклатура.Код,
| ЦеныНоменклатуры.Номенклатура.Наименование";
// Подготовим массив видов цен для передачи в параметры виртуальной таблицы
МассивВидов = Новый Массив;
ВидУчетная = Справочники.ВидыЦен.НайтиПоКоду("УФ-000003");
ВидРозничная = Справочники.ВидыЦен.НайтиПоКоду("UN-000001");
МассивВидов.Добавить(ВидУчетная);
МассивВидов.Добавить(ВидРозничная);
Запрос.УстановитьПараметр("НаДату", ТекущаяДата());
Запрос.УстановитьПараметр("УчетнаяЦена", ВидУчетная);
Запрос.УстановитьПараметр("РозничнаяЦена", ВидРозничная);
Запрос.УстановитьПараметр("МассивВидовЦен", МассивВидов);
РезультатЗапроса = Запрос.Выполнить();
Данный подход обладает огромным преимуществом: СУБД делает лишь одно сканирование индекса регистра сведений, независимо от количества колонок с ценами, которые вам необходимо вывести. Это золотой стандарт написания подобных запросов с точки зрения производительности.
---
Обратим внимание на важную деталь в коде автора темы. Он совершает двойную методологическую ошибку при заполнении параметров:
Справочники.ВидыЦен.НайтиПоКоду("...").Код. Во-первых, если код элемента справочника в пользовательском режиме изменится, логика программы сломается. Во-вторых, получать текстовый код элемента, чтобы затем передавать его в запрос и сравнивать текстовые поля — крайне неоптимально. СУБД гораздо быстрее сравнивает ссылки (уникальные идентификаторы GUID), которые индексируются автоматически.Никогда не используйте «жесткое кодирование» текстовых кодов внутри модулей без крайней необходимости. Если предопределенные элементы отсутствуют, организуйте выбор нужных видов цен на форме отчета или обработки, чтобы пользователь сам мог указать, какие именно цены он хочет видеть в колонках «Учетная» и «Розничная».