При работе с крупными типовыми конфигурациями, такими как 1С:ERP Управление предприятием 2 или 1С:Комплексная автоматизация 2, разработчики регулярно сталкиваются с гигантскими пакетными запросами. Особенно часто это происходит в механизмах проведения документов, расчета себестоимости и отражения документов в регламентированном учете. Подобные запросы могут содержать десятки временных таблиц, а на выходе выполнять объединение (оператор ОБЪЕДИНИТЬ ВСЕ) множества подзапросов.
Когда результирующая таблица возвращает некорректную строку (или, наоборот, нужная строка загадочным образом исчезает), перед программистом встает сложнейшая задача: как быстро локализовать, какой именно из объединенных подзапросов вернул эту строку?
В этой статье мы подробно разберем четыре профессиональных метода отладки таких запросов, а также проведем детальный анализ неочевидной логической ошибки в условиях ГДЕ при использовании ЛЕВОГО СОЕДИНЕНИЯ, из-за которой один подзапрос работает корректно, а второй возвращает пустой результат.
Самый простой и надежный способ понять, откуда «пришла» конкретная строка в выборке ОБЪЕДИНИТЬ ВСЕ, — это добавить в каждый подзапрос числовую или текстовую константу-идентификатор. Мы внедряем в схему запроса дополнительное поле, которое будет служить маркером источника данных.
Рассмотрим, как это выглядит на практике. Допустим, наш итоговый пакетный запрос объединяет результаты нескольких источников. Мы модифицируем текст запроса следующим образом:
ВЫБРАТЬ
"Поступление аванса от клиента" КАК Содержание,
24 КАК НомерПодзапроса,
Операция.Ссылка КАК Ссылка,
Расчеты.СуммаРегл КАК Сумма
ИЗ
ДокументыКОтражению КАК ДокументыКОтражению
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.ПоступлениеБезналичныхДенежныхСредств КАК Операция
ПО ДокументыКОтражению.Ссылка = Операция.Ссылка
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
"Поступление оплаты от клиента" КАК Содержание,
25 КАК НомерПодзапроса,
Операция.Ссылка КАК Ссылка,
Расчеты.СуммаРегл КАК Сумма
ИЗ
ДокументыКОтражению КАК ДокументыКОтражению
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.ПоступлениеБезналичныхДенежныхСредств КАК Операция
ПО ДокументыКОтражению.Ссылка = Операция.Ссылка
После выполнения такого запроса достаточно проанализировать колонку НомерПодзапроса в результирующей таблице значений. Если проблемная строка содержит значение 24, значит, она была получена из подзапроса под номером 24 («Поступление аванса от клиента»). Если 25 — из подзапроса 25 («Поступление оплаты от клиента»).
В типовых конфигурациях текст запроса практически никогда не задается жестко в коде. Он собирается динамически из множества фрагментов в зависимости от настроек учетной политики, хозяйственной операции и видов субконто. Править код конфигурации ради добавления индексов в запросы крайне нежелательно, особенно если система находится на поддержке или работает в режиме распределенной информационной базы (РИБ).
Чтобы обойти это ограничение, мы можем применить изящный метод с использованием внешней обработки и окна оценки выражений (клавиши Shift+F9 в конфигураторе).
Проанализируем алгоритм реализации этого метода по шагам:
Запрос.Выполнить()).Запрос.Текст. Скопируйте этот текст в буфер обмена.C:\temp\) новую внешнюю обработку с именем temp.epf.ВыполнитьЗапрос(ЗапросИсточник). Задача этой функции — принять оригинальный объект запроса со всеми его заполненными параметрами и менеджером временных таблиц, подменить текст запроса на модифицированный (с нашими индексами подзапросов) и вернуть результат.Пример кода модуля объекта внешней обработки temp.epf:
Функция ВыполнитьЗапрос(ЗапросИсточник) Экспорт
НовыйЗапрос = Новый Запрос;
// Передаем менеджер временных таблиц, чтобы сохранить контекст пакета
НовыйЗапрос.МенеджерВременныхТаблиц = ЗапросИсточник.МенеджерВременныхТаблиц;
// Копируем параметры оригинального запроса
Для Каждого Параметр Из ЗапросИсточник.Параметры Цикл
НовыйЗапрос.УстановитьПараметр(Параметр.Ключ, Параметр.Значение);
КонецЦикла;
// Вставляем скопированный ранее текст запроса,
// в котором мы вручную проставили индексы подзапросов "n как номерПодзапроса"
НовыйЗапрос.Текст =
"ВЫБРАТЬ ... // Сюда вставляем модифицированный текст запроса";
Возврат НовыйЗапрос;
КонецФункции
Теперь вернитесь в конфигуратор к точке останова. В окне оценки выражений (Shift+F9) выполните следующую конструкцию:
ВнешниеОбработки.Создать("C:\temp\temp.epf").ВыполнитьЗапрос(ЗапросДанных).МенеджерВременныхТаблиц.Таблицы["Данные"].ПолучитьДанные().Выгрузить()
В этой строке переменная ЗапросДанных — это оригинальный объект запроса конфигурации. Мы передаем его во внешнюю обработку, которая выполняет модифицированный текст в том же контексте временных таблиц и параметров СУБД. В результате мы моментально получаем готовую таблицу значений с пронумерованными строками прямо в отладчике без изменения конфигурации!
Разработчики фирмы «1С» предусмотрели колоссальную сложность отладки механизмов расчета себестоимости и отражения в регламентированном учете, поэтому внедрили в код конфигурации ERP специализированные вспомогательные инструменты.
Выясним, какие штатные функции мы можем использовать:
УниверсальныеМеханизмыПартийИСебестоимости.ВыполнитьЗапросСПроизвольнымТекстом(Запрос, "C:\Temp\Запрос.txt")
Для регулярной работы со сложными запросами настоятельно рекомендуется использовать специализированный комплекс обработок «Инструменты разработчика» (ИР). Это мощнейший софт для платформы 1С, который позволяет исследовать менеджер временных таблиц (МВТ) в интерактивной консоли запросов.
Преимущество этого подхода заключается в том, что вам не нужно писать код для выгрузки таблиц. Инструменты разработчика умеют перехватывать объект Запрос прямо из памяти отладчика, сохраняя все его временные таблицы (вместе с их реальным содержимым в СУБД) и параметры. Вы сможете открыть весь пакет во внешней консоли, увидеть количество строк в каждой временной таблице, отредактировать любой из подзапросов в конструкторе и сразу увидеть изменения.
Если у вас нет возможности установить полный комплект ИР, вы можете анализировать менеджер временных таблиц стандартными средствами платформы 1С:Предприятие в табло отладчика (начиная с платформы 8.3.8) с помощью конструкции:
Запрос.МенеджерВременныхТаблиц.Таблицы.Найти("ИмяВременнойТаблицы").ПолучитьДанные().Выгрузить()
Проанализируем интересную ситуацию, возникшую у автора темы. При сравнении двух соседних подзапросов в объединении выяснилось, что подзапрос 24 («Поступление аванса от клиента») успешно выбирает и возвращает строку данных, а визуально идентичный ему подзапрос 25 («Поступление оплаты от клиента») на тех же самых исходных данных не возвращает ничего.
Выясним причину этой проблемы, детально сопоставив ключевые фрагменты кода обоих запросов.
Посмотрим на структуру связей и условий в подзапросе 24 (Аванс):
ИЗ
ДокументыКОтражению КАК ДокументыКОтражению
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.ПоступлениеБезналичныхДенежныхСредств КАК Операция
ПО ДокументыКОтражению.Ссылка = Операция.Ссылка
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РасчетыСКлиентами КАК Расчеты
ПО Расчеты.Ссылка = Операция.Ссылка
ЛЕВОЕ СОЕДИНЕНИЕ РасчетыСКлиентамиПоДокументам КАК РасчетыПредоплата
ПО Расчеты.Ссылка = РасчетыПредоплата.Ссылка ...
ГДЕ
Операция.ХозяйственнаяОперация = ЗНАЧЕНИЕ(Перечисление.ХозяйственныеОперации.ПоступлениеОплатыОтКлиента)
И ЕСТЬNULL(РасчетыПредоплата.ПредоплатаРегл, Расчеты.СуммаРегл) <> 0
И Операция.ПроведеноБанком
Теперь проанализируем аналогичную секцию подзапроса 25 (Оплата долга):
ИЗ
ДокументыКОтражению КАК ДокументыКОтражению
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Документ.ПоступлениеБезналичныхДенежныхСредств КАК Операция
ПО ДокументыКОтражению.Ссылка = ...
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РасчетыСКлиентами КАК Расчеты
ПО Расчеты.Ссылка = ...
ЛЕВОЕ СОЕДИНЕНИЕ РасчетыСКлиентамиПоДокументам КАК РасчетыПоДокументам
ПО Расчеты.Ссылка = ...
ГДЕ
Операция.ХозяйственнаяОперация = ЗНАЧЕНИЕ(Перечисление.ХозяйственныеОперации.ПоступлениеОплатыОтКлиента)
И (ЕСТЬNULL(РасчетыПоДокументам.ДолгРегл, 0) - ЕСТЬNULL(РасчетыПоДокументам.ЗалогЗаТаруРегл, 0)) <> 0
И Операция.ПроведеноБанком
Разберем по шагам логику работы СУБД при наложении условий в секции ГДЕ для обоих случаев, когда в левой таблице РасчетыСКлиентамиПоДокументам отсутствуют подходящие записи (то есть результатом левого соединения по всем полям этой таблицы становится значение NULL):
Для подзапроса 24 (Аванс):
В секции ГДЕ вычисляется выражение: ЕСТЬNULL(РасчетыПредоплата.ПредоплатаРегл, Расчеты.СуммаРегл) <> 0. Поскольку записи в таблице предоплаты не найдены, поле РасчетыПредоплата.ПредоплатаРегл принимает значение NULL. Функция ЕСТЬNULL перехватывает это и подставляет альтернативное значение из поля Расчеты.СуммаРегл. Так как таблица РасчетыСКлиентами присоединена жестким ВНУТРЕННЕМ СОЕДИНЕНИЕМ, значение суммы в ней гарантированно существует и не равно нулю. Условие преобразуется в СуммаРегл <> 0 (Истина), и строка успешно попадает в финальную выборку.
Для подзапроса 25 (Оплата долга):
В секции ГДЕ вычисляется выражение: (ЕСТЬNULL(РасчетыПоДокументам.ДолгРегл, 0) - ЕСТЬNULL(РасчетыПоДокументам.ЗалогЗаТаруРегл, 0)) <> 0. Если детальных записей по документам расчетов нет (результат соединения NULL), обе функции ЕСТЬNULL возвращают число 0. Выражение преобразуется в математическое уравнение: (0 - 0) <> 0, что эквивалентно утверждению 0 <> 0. Это выражение возвращает Ложь. Соответственно, строка исключается СУБД из результирующего набора данных!
Важный вывод: Архитектурная разница между запросами заключается в том, что первый подзапрос (авансовый) умеет брать обобщенные данные из первичного регистра расчетов РасчетыСКлиентами в случае отсутствия расшифровки по документам. Второй подзапрос (оплатный) жестко завязан на наличие детальных строк долга в таблице РасчетыСКлиентамиПоДокументам. Если детальный долг не распределен или отсутствует, подзапрос 25 полностью игнорирует эту операцию, даже если общая сумма платежа зафиксирована в системе.
Используя предложенные методы отладки, мы можем быстро находить подобные логические расхождения и оперативно исправлять ошибки в схемах компоновки данных и алгоритмах проведения документов.