В процессе разработки в системе «1С:Предприятие» программисты регулярно сталкиваются с необходимостью наложить фильтр в запросе на конкретный элемент справочника. Ситуация выглядит простой, если элемент является предопределенным — в таком случае достаточно применить стандартную конструкцию ЗНАЧЕНИЕ(). Однако, если элемент был создан пользователем непосредственно в режиме «1С:Предприятие» (в пользовательском интерфейсе) и не имеет предопределенного аналога в конфигураторе, привычный подход вызывает ошибку компиляции запроса.
В этой статье мы подробно разберем, почему возникает эта ошибка, проанализируем популярные решения с форумов (включая их скрытые угрозы для производительности СУБД) и рассмотрим лучшие архитектурные практики для надежного решения этой задачи.
Для начала выясним причину возникновения ошибки компиляции. Конструкция ЗНАЧЕНИЕ() — это специальная функция языка запросов 1С, которая вычисляется на этапе компиляции запроса платформой еще до его отправки в СУБД (MS SQL Server, PostgreSQL и др.).
Платформа может преобразовать текстовое представление объекта в его реальную ссылку в базе данных только в том случае, если информация об этом объекте жестко зафиксирована в метаданных конфигурации. К таким объектам относятся:
ЗНАЧЕНИЕ(Справочник.Номенклатура.ПустаяСсылка) или конкретный предопределенный элемент, созданный разработчиком).ЗНАЧЕНИЕ(Перечисление.ЮрФизЛицо.ФизическоеЛицо)).Если элемент справочника (например, элемент «Супермаркет» в справочнике СегментыРынка) был создан пользователем вручную, он существует только как физическая строка в таблице базы данных на сервере СУБД. Платформа 1С на уровне метаданных конфигуратора ничего не знает о существовании этой записи. Попытка написать ЗНАЧЕНИЕ(Справочник.СегментыРынка.Супермаркет) приведет к синтаксической ошибке, так как компилятор не обнаружит объект с таким именем в структуре метаданных.
На форумах часто предлагают быстрые "обходные" пути, которые при невнимательном подходе могут серьезно замедлить работу базы данных или сделать логику программы крайне хрупкой. Проанализируем эти варианты.
Антипаттерн №1: Использование конструкции ВЫБОР КОГДА в секции ГДЕ
В сообщениях темы часто проскальзывает предложение использовать конструкцию вида:
ГДЕ ВЫБОР КОГДА МаршрутноеЗаданиеДокументыРеализации.Реализация.ТорговаяТочка.ТипТоргТочки = ЗНАЧЕНИЕ(Перечисление.ТипТоргТочки.Супермаркет) ТОГДА ИСТИНА ИНАЧЕ ЛОЖЬ КОНЕЦ
Использование условных операторов ВЫБОР в секциях ГДЕ или в условиях соединения таблиц — это один из самых серьезных антипаттернов оптимизации запросов в 1С. Когда СУБД строит план выполнения такого запроса, она не может эффективно использовать индексы для поиска по полям, обернутым в логические конструкции. Вместо быстрого поиска по индексу (Index Seek) СУБД будет вынуждена выполнять полное сканирование таблицы (Table Scan / Clustered Index Scan). На базах данных большого объема это гарантированно приведет к длительным транзакционным блокировкам и зависанию системы.
Важное правило оптимизации: условия в секции ГДЕ должны быть максимально плоскими и простыми, например: ГДЕ Поле = &Параметр.
Антипаттерн №2: Поиск по жестко зашитому наименованию прямо в запросе
Другой популярный совет — искать элемент в запросе по его строковому представлению с помощью вложенных конструкций:
ГДЕ МаршрутноеЗаданиеДокументыРеализации.Реализация.Контрагент.КаналСбыта В (ВЫБРАТЬ Ссылка ИЗ Справочник.СегментыРынка ГДЕ Наименование = "Супермаркет")
Этот подход делает систему крайне хрупкой и уязвимой. Любой пользователь (например, контент-менеджер, оператор или бухгалтер) может случайно или намеренно изменить наименование элемента в режиме «1С:Предприятие» (например, переименовать в «Супермаркеты», добавить лишний пробел в конце или изменить регистр букв). После этого логика программы мгновенно сломается, а запрос перестанет находить нужные данные без выдачи каких-либо предупреждений или ошибок в конфигураторе. Найти причину такого сбоя в крупной системе бывает очень непросто.
Рассмотрим по шагам надежные методы фильтрации данных по непредопределенному элементу справочника.
Этот метод основан на том, что мы находим нужную ссылку во встроенном коде 1С и передаем ее в запрос в качестве параметра. Посмотрим на пример кода:
// 1. Находим нужный элемент справочника во встроенном коде
СсылкаСупермаркет = Справочники.СегментыРынка.НайтиПоНаименованию("Супермаркет");
Если СсылкаСупермаркет.Пустая() Тогда
ВызватьИсключение "В справочнике 'Сегменты рынка' не найден элемент 'Супермаркет'!";
КонецЕсли;
// 2. Создаем и настраиваем запрос
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| МаршрутноеЗаданиеДокументыРеализации.Ссылка КАК МаршрутныйЛист
|ИЗ
| Документ.МаршрутноеЗадание.ДокументыРеализации КАК МаршрутноеЗаданиеДокументыРеализации
|ГДЕ
| МаршрутноеЗаданиеДокументыРеализации.Реализация.Контрагент.КаналСбыта = &КаналСбыта";
// 3. Устанавливаем параметр
Запрос.УстановитьПараметр("КаналСбыта", СсылкаСупермаркет);
РезультатЗапроса = Запрос.Выполнить();
Этот способ решает главную проблему производительности СУБД: в физический SQL-запрос передается готовая ссылка (уникальный идентификатор), благодаря чему СУБД строит оптимальный план выполнения и использует индекс по полю КаналСбыта.
Чтобы избежать уязвимости перед случайным изменением наименования пользователями (как в Способе 1), безопаснее привязываться не к строковому наименованию, а к уникальному идентификатору элемента в базе данных (UUID). Этот идентификатор генерируется СУБД один раз при создании элемента и никогда не меняется.
Выясним уникальный идентификатор нужного нам элемента с помощью специальной обработки (или программно) и зафиксируем его в коде:
// Получаем ссылку по известному уникальному идентификатору (GUID)
Идентификатор = Новый УникальныйИдентификатор("7d2bc51e-9a1b-11ed-a8fc-00155d013e02");
СсылкаСупермаркет = Справочники.СегментыРынка.ПолучитьСсылку(Идентификатор);
// Передаем ссылку в запрос в качестве параметра
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| МаршрутноеЗаданиеДокументыРеализации.Ссылка КАК МаршрутныйЛист
|ИЗ
| Документ.МаршрутноеЗадание.ДокументыРеализации КАК МаршрутноеЗаданиеДокументыРеализации
|ГДЕ
| МаршрутноеЗаданиеДокументыРеализации.Реализация.Контрагент.КаналСбыта = &КаналСбыта";
Запрос.УстановитьПараметр("КаналСбыта", СсылкаСупермаркет);
РезультатЗапроса = Запрос.Выполнить();
Этот подход гарантирует стабильность работы программы при любых изменениях наименования или кода элемента пользователями. Однако стоит помнить, что при переносе базы данных «с нуля» через xml-выгрузку без сохранения идентификаторов GUID может измениться, поэтому данный способ требует контроля за уникальными идентификаторами при миграции данных между базами.
Для крупных корпоративных решений «зашивание» каких-либо строковых имен или жестких GUID-ов непосредственно в код программы считается плохим тоном разработки. Наиболее гибким решением является вынесение выбора сегмента на уровень административных настроек системы.
Разберем по шагам реализацию этого подхода:
ОсновнойСегментСупермаркет и типом данных СправочникСсылка.СегментыРынка.
// Считываем ссылку из константы
СегментИзНастроек = Константы.ОсновнойСегментСупермаркет.Получить();
Если Не ЗначениеЗаполнено(СегментИзНастроек) Тогда
ВызватьИсключение "В настройках системы не заполнен основной сегмент рынка для супермаркетов!";
КонецЕсли;
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| МаршрутноеЗаданиеДокументыРеализации.Ссылка КАК МаршрутныйЛист
|ИЗ
| Документ.МаршрутноеЗадание.ДокументыРеализации КАК МаршрутноеЗаданиеДокументыРеализации
|ГДЕ
| МаршрутноеЗаданиеДокументыРеализации.Реализация.Контрагент.КаналСбыта = &КаналСбыта";
Запрос.УстановитьПараметр("КаналСбыта", СегментИзНастроек);
РезультатЗапроса = Запрос.Выполнить();
Этот подход полностью защищает конфигурацию от любых изменений структуры данных пользователями и позволяет гибко менять логику работы системы без изменения программного кода и необходимости повторного обновления конфигурации.
Если у вас есть доступ к изменению структуры метаданных конфигурации, самым очевидным и простым решением будет создание предопределенного элемента прямо в конфигураторе.
СегментыРынка.Супермаркет.После обновления конфигурации базы данных вы сможете использовать конструкцию ЗНАЧЕНИЕ() прямо в тексте запроса без передачи параметров:
ВЫБРАТЬ
МаршрутноеЗаданиеДокументыРеализации.Ссылка КАК МаршрутныйЛист
ИЗ
Документ.МаршрутноеЗадание.ДокументыРеализации КАК МаршрутноеЗаданиеДокументыРеализации
ГДЕ
МаршрутноеЗаданиеДокументыРеализации.Реализация.Контрагент.КаналСбыта = ЗНАЧЕНИЕ(Справочник.СегментыРынка.Супермаркет)
Если в вашей базе данных уже существуют тысячи элементов, созданных пользователями, и вы решили внедрить предопределенный элемент конфигурации, платформа может автоматически не сопоставить их, из-за чего в справочнике появится дубль. В этом случае на помощь приходит встроенный язык 1С. Мы можем программно связать предопределенное значение с существующей записью в базе данных с помощью метода УстановитьСвязьПредопределенного() менеджера справочника.
Рассмотрим пример кода:
// Получаем ссылку на существующий элемент, созданный пользователем
СсылкаСупермаркет = Справочники.СегментыРынка.НайтиПоНаименованию("Супермаркет");
Если Не СсылкаСупермаркет.Пустая() Тогда
// Устанавливаем связь существующего элемента с предопределенным именем "Супермаркет"
Справочники.СегментыРынка.УстановитьСвязьПредопределенного("Супермаркет", СсылкаСупермаркет);
Сообщить("Связь предопределенного элемента успешно установлена!");
Иначе
Сообщить("Элемент 'Супермаркет' не найден в базе данных для сопоставления.");
КонецЕсли;
После выполнения данного кода система свяжет UUID существующего элемента с именем предопределенного элемента метаданных. Конструкция ЗНАЧЕНИЕ(Справочник.СегментыРынка.Супермаркет) начнет корректно ссылаться на данный элемент без заведения новых записей в информационной базе.
Обратим внимание на конструкцию в тексте исходного запроса:
МаршрутноеЗаданиеДокументыРеализации.Реализация.Контрагент.КаналСбыта
Использование обращения к реквизитам через точку (разыменование полей) приводит к тому, что платформа 1С на уровне СУБД генерирует неявные левые соединения (LEFT JOIN). В данном случае для проверки одного условия СУБД выполнит три левых соединения:
РеализацияТоваровУслуг.Контрагенты.СегментыРынка.Если таблицы содержат миллионы записей, производительность такого запроса резко упадет. Для оптимизации рекомендуется переписывать такие участки с использованием явных соединений (ВНУТРЕННЕЕ СОЕДИНЕНИЕ или ЛЕВОЕ СОЕДИНЕНИЕ) только с необходимыми таблицами. Посмотрим на оптимизированный пример:
ВЫБРАТЬ
МаршрутноеЗадание.Ссылка КАК МаршрутныйЛист
ИЗ
Документ.МаршрутноеЗадание.ДокументыРеализации КАК МаршрутноеЗадание
// Явно соединяем с таблицей реализации
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг КАК Реализация
ПО МаршрутноеЗадание.Реализация = Реализация.Ссылка
// Явно соединяем с таблицей контрагентов
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.Контрагенты КАК Контрагенты
ПО Реализация.Контрагент = Контрагенты.Ссылка
ГДЕ
Контрагенты.КаналСбыта = &КаналСбыта
Такой запрос отработает значительно быстрее, так как СУБД сможет построить более эффективный план выполнения, используя индексы по внешним ключам связываемых таблиц.
Подведем итог нашему анализу:
ЗНАЧЕНИЕ() для элементов, созданных только в пользовательском режиме. Это приведет к ошибке компиляции запроса.ВЫБОР КОГДА в секции ГДЕ, чтобы не ломать индексы СУБД.