В разработке на платформе 1С 7.7 часто возникает задача выбрать документы по условию, которое находится не в самом документе, а в связанных данных. Классический пример — найти все накладные по номеру телефона клиента, если телефоны хранятся в отдельном подчиненном справочнике. Попытка решить это "в лоб" одним запросом обычно проваливается из-за ограничений языка запросов 1С 7.7.
Рассмотрим по шагам, как правильно решить эту задачу, проанализируем неверные подходы и найдем наиболее эффективный способ.
Предположим, у нас есть документ Продажа с реквизитом Клиент и подчиненный справочник КлиентыТелефоны, где Владелец — это ссылка на элемент справочника Клиенты.
Первая интуитивная попытка — написать запрос, который связывает документ и подчиненный справочник напрямую:
ТекстЗапроса="
|ОбрабатыватьДокументы Все;
|_Клиент=Документ.Продажа.Клиент, Справочник.КлиентыТелефоны.Владелец;
|_Телефон=Справочник.КлиентыТелефоны.Наименование;
|Условие(_Телефон=тТелефон);
|Группировка Документ;
|";
Такой запрос, скорее всего, либо ничего не вернет, либо вернет все документы, проигнорировав условие. Причина кроется в ограничениях встроенного языка запросов 1С 7.7. Он не умеет выполнять произвольные соединения таблиц, как это делается в SQL с помощью JOIN. Он может "подтягивать" данные по прямым ссылкам (например, получить наименование клиента из документа), но не может построить сложную связь "Документ -> его Клиент -> подчиненный справочник этого клиента". Система не понимает, что Документ.Продажа.Клиент и Справочник.КлиентыТелефоны.Владелец должны быть связаны между собой.
Один из рабочих, но крайне неэффективных способов — использовать функцию, которая будет для каждого документа проверять наличие нужного телефона. Этот подход был предложен в обсуждении.
Шаг 1. Создаем функцию проверки.
Эта функция принимает на вход клиента и искомый телефон, а затем проверяет, есть ли такой телефон в его подчиненном справочнике.
Функция ЕстьТелефон(аКлиент, аТелефон)
С = СоздатьОбъект("Справочник.КлиентыТелефоны");
С.ИспользоватьВладельца(аКлиент);
Возврат С.НайтиПоНаименованию(аТелефон, 1, 1); // 1-искать точно, 1-искать по наименованию
КонецФункции
Шаг 2. Вызываем функцию в условии запроса.
ТекстЗапроса="
|ОбрабатыватьДокументы Все;
|_Клиент=Документ.Продажа.Клиент;
|_Лицо=Документ.Продажа.Лицо;
|Условие(ЕстьТелефон(_Клиент, тТелефон) = 1);
|Группировка Документ;
|";
Почему это плохо?
Этот метод будет работать, но катастрофически медленно на больших базах данных. Проблема в том, что для каждого документа продажи, который попадает в выборку запроса, система будет вызывать функцию ЕстьТелефон(). А эта функция, в свою очередь, каждый раз создает новый объект и выполняет отдельный запрос к базе данных. Если у вас 10 000 накладных, будет выполнено 10 000 дополнительных маленьких запросов, что приведет к огромной нагрузке и долгому ожиданию.
Вывод: этот способ можно использовать только в крайних случаях на очень маленьких объемах данных. В остальных ситуациях его следует избегать.
Это классический и наиболее правильный способ решения подобных задач стандартными средствами 1С 7.7. Идея заключается в том, чтобы разбить одну сложную задачу на две простые.
Шаг 1. Находим всех клиентов с нужным номером телефона.
Сначала мы делаем простой запрос к подчиненному справочнику и находим всех владельцев (клиентов), у которых есть искомый телефон.
Запрос = СоздатьОбъект("Запрос");
ТекстЗапроса = "
|Период с Дата1 по Дата2;
|_Клиент = Справочник.КлиентыТелефоны.Владелец;
|_ТелефонЭлемент = Справочник.КлиентыТелефоны.ТекущийЭлемент;
|Условие(_ТелефонЭлемент.Наименование = тТелефон);
|Группировка _Клиент Без Групп;
|";
Если Запрос.Выполнить(ТекстЗапроса) = 0 Тогда
Возврат;
КонецЕсли;
Шаг 2. Выгружаем найденных клиентов в СписокЗначений.
Результат первого запроса — это список уникальных клиентов. Мы собираем их в объект СписокЗначений, чтобы использовать его в следующем запросе.
СписокКлиентов = СоздатьОбъект("СписокЗначений");
Пока Запрос.Группировка(1) = 1 Цикл
СписокКлиентов.ДобавитьЗначение(Запрос._Клиент);
КонецЦикла;
Шаг 3. Выбираем документы по списку клиентов.
Теперь, когда у нас есть готовый список клиентов, мы выполняем второй, основной запрос. В его условии мы проверяем, входит ли клиент документа в наш подготовленный список. Для этого используется оператор В.
ТекстЗапроса = "
|ОбрабатыватьДокументы Все;
|_Клиент = Документ.Продажа.Клиент;
|_Лицо = Документ.Продажа.Лицо;
|Условие(_Клиент В СписокКлиентов);
|Группировка Документ;
|";
Если Запрос.Выполнить(ТекстЗапроса) = 0 Тогда
Возврат;
КонецЕсли;
// Далее обрабатываем результат
Пока Запрос.Группировка(1) = 1 Цикл
// ... ваш код для вывода данных
КонецЦикла;
Этот подход значительно эффективнее, так как он выполняет всего два оптимизированных запроса к базе данных вместо тысяч. Это стандартный паттерн для 1С 7.7, который следует применять всегда, когда требуется фильтрация по связанным данным, недоступным для прямого соединения.
Для баз, работающих в режиме SQL-сервера, существует еще более быстрый способ — прямые запросы с помощью внешних компонент (например, 1CPP.dll). Этот метод позволяет писать запросы на языке T-SQL, используя всю его мощь, включая INNER JOIN.
Примерная логика такого запроса (псевдокод):
SELECT
Документы.IDDOC, Документы.DATE_TIME_IDDOC
FROM
_1SJOURN AS Журнал
INNER JOIN
DHXXXX AS Документы ON Журнал.IDDOC = Документы.IDDOC
INNER JOIN
SCYYYY AS Телефоны ON Документы.Клиент_ID = Телефоны.PARENTEXT
WHERE
Телефоны.DESCR = 'Нужный_Телефон'
AND Журнал.IDDOCDEF = ZZZZ -- ID вида документа "Продажа"
Плюсы: максимальная производительность, так как вся обработка происходит на стороне SQL-сервера.
Минусы:
Этот метод рекомендуется для высоконагруженных систем, где производительность является критическим фактором.
Итог: Для большинства повседневных задач в 1С 7.7 двухэтапный запрос является золотым стандартом — он эффективен, надежен и использует только встроенные средства платформы.