В практике разработки на платформе 1С:Предприятие 8.3 часто возникает ситуация, когда необходимо получить данные из основного источника, но если их там нет — обратиться к резервному. Например, мы ищем цену товара в специальном прайс-листе, и если она не задана, берем базовую цену из карточки номенклатуры. В этой статье мы подробно разберем, как реализовать такую логику эффективно и красиво.
Рассмотрим несколько подходов: от простого разделения кода до сложных конструкций внутри одного запроса. Мы проанализируем ситуацию с точки зрения производительности и удобства поддержки кода.
Это самый популярный и зачастую самый «красивый» способ решить задачу в рамках одного запроса. Суть метода заключается в том, чтобы объединить результаты двух запросов и добавить техническое поле, которое будет отвечать за порядок строк. Проанализируем, как это работает на практике.
Предположим, нам нужно найти контактное лицо контрагента. Сначала ищем основного менеджера, а если его нет — любого сотрудника этой компании.
Разберем по шагам создание такого запроса:
Приоритет со значением 1.Приоритет со значением 2.ОБЪЕДИНИТЬ ВСЕ.Приоритет.ВЫБРАТЬ ПЕРВЫЕ 1.Посмотрим на пример кода:
ВЫБРАТЬ ПЕРВЫЕ 1
Т.Ссылка КАК Результат,
1 КАК Приоритет
ИЗ
Справочник.КонтактныеЛица КАК Т
ГДЕ
Т.Владелец = &Контрагент
И Т.Основной = ИСТИНА
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ ПЕРВЫЕ 1
Т.Ссылка,
2
ИЗ
Справочник.КонтактныеЛица КАК Т
ГДЕ
Т.Владелец = &Контрагент
УПОРЯДОЧИТЬ ПО
Приоритет
В данном случае, если первый запрос найдет запись, она получит приоритет 1 и станет первой в списке. Если же первый запрос вернет пустой результат, то в итоговой выборке останется только запись из второго запроса с приоритетом 2, которая и попадет в результат благодаря ВЫБРАТЬ ПЕРВЫЕ 1.
Иногда попытка объединить всё в один запрос может привести к снижению производительности. Разберем ситуацию: первый запрос очень легкий (поиск по индексированному полю), а второй — тяжелый (выборка из огромного регистра с агрегатами). Если мы объединим их, сервер СУБД может начать выполнять оба запроса одновременно для построения плана, что излишне нагрузит систему.
Выясним, как сделать это правильно через встроенный язык:
Запрос = Новый Запрос;
Запрос.Текст = "ВЫБРАТЬ Ссылка ИЗ Справочник.Номенклатура ГДЕ Артикул = &Артикул";
Запрос.УстановитьПараметр("Артикул", ИскомыйАртикул);
Результат = Запрос.Выполнить();
Если Результат.Пустой() Тогда
// Если по артикулу не нашли, ищем по наименованию
Запрос.Текст = "ВЫБРАТЬ Ссылка ИЗ Справочник.Номенклатура ГДЕ Наименование ПОДОБНО &Наименование";
Запрос.УстановитьПараметр("Наименование", "%" + ИскомоеИмя + "%");
Результат = Запрос.Выполнить();
КонецЕсли;
Если Не Результат.Пустой() Тогда
Выборка = Результат.Выбрать();
Выборка.Следующий();
// Работаем с результатом
КонецЕсли;
Этот метод считается наиболее надежным в плане производительности, так как второй запрос (потенциально тяжелый) вообще не будет отправлен на сервер СУБД, если первый вернул данные.
В языке запросов 1С нет прямого оператора EXISTS, как в стандартном SQL, но мы можем имитировать его поведение. Это полезно, если вы работаете в сложном динамическом списке или в СКД, где нужно все упаковать в один текст запроса.
Рассмотрим алгоритм решения:
ГДЕ второго запроса прописываем условие отсутствия записей во временной таблице.Посмотрим на пример реализации:
// Помещаем результат первой проверки во временную таблицу
ВЫБРАТЬ ПЕРВЫЕ 1
Т.Ссылка КАК Ссылка
ПОМЕСТИТЬ ВТ_ПервыйПоиск
ИЗ
Справочник.Склады КАК Т
ГДЕ
Т.Код = "ОСНОВНОЙ";
////////////////////////////////////////////////////////////////////////////////
// Выбираем данные из второго источника, только если первая таблица пуста
ВЫБРАТЬ
Т.Ссылка
ИЗ
Справочник.Склады КАК Т
ГДЕ
Т.ПометкаУдаления = ЛОЖЬ
И НЕ 1 В (ВЫБРАТЬ ПЕРВЫЕ 1 1 ИЗ ВТ_ПервыйПоиск)
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
Т.Ссылка
ИЗ
ВТ_ПервыйПоиск КАК Т
Проанализируем конструкцию ГДЕ НЕ 1 В (ВЫБРАТЬ ПЕРВЫЕ 1 1 ИЗ ВТ_ПервыйПоиск). Это эффективный способ сказать серверу: "Не делай эту выборку, если во временной таблице уже что-то есть". Большинство современных СУБД (MS SQL Server, PostgreSQL) корректно оптимизируют такой запрос и не будут сканировать таблицы второго запроса, если условие не выполняется.
Если нам нужно сопоставить данные из двух таблиц и выбрать значение по приоритету (если есть в первой — берем оттуда, если нет — из второй), идеально подходит ЛЕВОЕ СОЕДИНЕНИЕ и функция ЕСТЬNULL.
Разберем пример получения остатков. Допустим, у нас есть "Основной склад" и "Резервный склад". Нам нужно понять, сколько товара брать в работу.
ВЫБРАТЬ
Номенклатура.Ссылка,
ЕСТЬNULL(ОстаткиОсновной.В НаличииОстаток, ОстаткиРезерв.ВНаличииОстаток) КАК Количество
ИЗ
Справочник.Номенклатура КАК Номенклатура
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.Запасы.Остатки(&Момент, Склад = &Склад1) КАК ОстаткиОсновной
ПО Номенклатура.Ссылка = ОстаткиОсновной.Номенклатура
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.Запасы.Остатки(&Момент, Склад = &Склад2) КАК ОстаткиРезерв
ПО Номенклатура.Ссылка = ОстаткиРезерв.Номенклатура
ГДЕ
Номенклатура.Ссылка = &Товар
Здесь функция ЕСТЬNULL проверяет результат первого соединения. Если данных нет (вернулся NULL), она подставляет значение из второго соединения. Это "плоский" и очень быстрый способ получения данных без использования UNION.
Чтобы понять, какой метод использовать, проанализируем следующие критерии:
1. Сложность запросов
Если оба запроса простые (выборка по ссылке или коду), используйте Метод 1 (UNION ALL). Это позволит выполнить всё за один сеанс обращения к базе данных, что экономит время на установку соединения и передачу текста запроса.
2. Нагрузка на сервер
Если второй запрос ресурсоемкий, лучше использовать Метод 2 (Два запроса в коде). Это гарантирует, что тяжелая часть кода выполнится только тогда, когда она действительно необходима.
3. Использование в СКД или списках
Если вы ограничены одним текстом запроса (например, в настройках схемы компоновки данных), ваш выбор — Метод 3 (Временные таблицы) или Метод 4 (Соединения).
При использовании временных таблиц (Метод 3) не забывайте об индексации. Если в первом запросе может быть много строк, которые вы потом используете для соединения или фильтрации, добавление индекса в секции ИНДЕКСИРОВАТЬ ПО значительно ускорит процесс.
Также стоит помнить про механизм RLS (ограничение доступа на уровне записей). Если у пользователя нет прав на одну из таблиц в объединенном запросе, весь запрос может завершиться ошибкой или вернуть пустой результат. В таких случаях разделение на два отдельных вызова Запрос.Выполнить() с проверкой прав через ПравоДоступа() будет более безопасным решением.
Подводя итог, можно сказать, что "красивое" решение не всегда является самым эффективным. Для простых задач идеально подходит ОБЪЕДИНИТЬ ВСЕ с приоритетом, а для сложных систем — логическое разделение на уровне встроенного языка 1С.