При разработке и оптимизации отчетов в среде 1С:Предприятие разработчики нередко сталкиваются с ситуацией, когда один и тот же текст запроса с идентичными параметрами при запуске в консоли запросов и при выполнении в коде модуля конфигурации выдает разные данные. Типичный пример такой проблемы — получение разных значений реквизитов (например, даты ввода или изменения статуса), когда в одном случае мы видим начальное значение, а в другом — конечное.
Разберем детально, почему возникает подобное поведение, какие скрытые архитектурные ошибки в тексте запроса и механизмах постобработки приводят к непредсказуемым результатам, и как правильно переписать запрос для гарантированного получения нужных данных.
Проанализируем исходную логику запроса. Если перед нами стоит задача получить строго последнюю дату для каждого объекта, ключевой ошибкой является попытка решить эту задачу с помощью секций ВЫБРАТЬ РАЗЛИЧНЫЕ и УПОРЯДОЧИТЬ ПО.
Конструкция ВЫБРАТЬ РАЗЛИЧНЫЕ не выполняет поиск максимального или минимального значения. Она лишь исключает из результирующей таблицы полные строки-дубликаты. Если у одного компьютера в табличной части свойств заполнено несколько разных дат, запрос вернет все эти строки, каждую со своей датой.
Разница в отображении «в консоли последняя, а в коде первая» чаще всего возникает из-за того, как именно результат запроса обрабатывается далее:
Пока Выборка.Следующий() Цикл Структура.Дата = Выборка.ДатаВвода; КонецЦикла;, переменная будет последовательно перезаписываться, и в итоге в ней останется значение из последней строки выборки.ТаблицаЗначений через метод Выгрузить() и для поиска строки применяется метод Найти() или НайтиСтроки(), платформа всегда возвращает первое совпадение сверху таблицы.Чтобы гарантированно получить последнюю дату на уровне базы данных без влияния клиентского кода, применим группировку и агрегатную функцию МАКСИМУМ.
ВЫБРАТЬ
КомпьютерыСвойства.Ссылка КАК Компьютер,
МАКСИМУМ(ВЫРАЗИТЬ(КомпьютерыСвойства.ЗначениеСвойства КАК ДАТА)) КАК ПоследняяДатаВвода
ПОМЕСТИТЬ ВТ_ПоследниеДаты
ИЗ
Справочник.Компьютеры.Свойства КАК КомпьютерыСвойства
ГДЕ
КомпьютерыСвойства.ВидСвойства В(&ВидСвойства)
И НЕ КомпьютерыСвойства.ВидСвойства = &ВидСвойства2
СГРУППИРОВАТЬ ПО
КомпьютерыСвойства.Ссылка
;
Рассмотрим структуру соединений в исходном тексте запроса:
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.Компьютеры КАК Компьютеры
ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Компьютеры.Свойства КАК КомпьютерыСвойства
ПО Компьютеры.Ссылка = КомпьютерыСвойства.Ссылка.Ссылка
ПО КомпьютерыРабочаяСтанция.Ссылка.Ссылка = Компьютеры.Ссылка
Обратим внимание на конструкцию КомпьютерыСвойства.Ссылка.Ссылка. Поле Ссылка табличной части уже является ссылкой на родительский элемент справочника. Обращение через точку к реквизиту Ссылка.Ссылка заставляет платформу 1С формировать дополнительное скрытое соединение с таблицей шапки справочника в итоговом SQL-запросе к СУБД.
Такие неявные соединения не только снижают производительность, но и запутывают синтаксический анализатор и оптимизатор запросов, нарушая порядок связывания таблиц при каскадных соединениях. Исправим связи на явные и корректные:
ИЗ
Справочник.Компьютеры.РабочаяСтанция КАК КомпьютерыРабочаяСтанция
ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.Компьютеры КАК Компьютеры
ПО КомпьютерыРабочаяСтанция.Ссылка = Компьютеры.Ссылка
ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Компьютеры.Свойства КАК КомпьютерыСвойства
ПО Компьютеры.Ссылка = КомпьютерыСвойства.Ссылка
Выясним причину, почему порядок строк в консоли и в коде может физически различаться при одинаковом тексте запроса. В секции сортировки указано:
УПОРЯДОЧИТЬ ПО
ДатаВвода,
Контрагент
Если у нескольких строк значения полей ДатаВвода и Контрагент полностью совпадают, СУБД (Microsoft SQL Server или PostgreSQL) возвращает такие строки в произвольном порядке. Порядок записей с одинаковыми ключами сортировки не гарантируется стандартом SQL, если в упорядочивании не участвует уникальный идентификатор строки.
План выполнения запроса (Execution Plan) в консоли запросов и при вызове из кода модуля может отличаться из-за разных параметров кэширования, уровня параллелизма (MAXDOP) или контекста выполнения. В результате один вызов возвращает строки в одном физическом порядке, а другой — в обратном.
Для обеспечения стабильности результата всегда добавляем уникальное ключевое поле (например, Ссылка или НомерСтроки) в конец списка сортировки:
УПОРЯДОЧИТЬ ПО
ДатаВвода УБЫВ,
Контрагент,
НомерКомпьютера
Реквизит ЗначениеСвойства в табличных частях дополнительных реквизитов и сведений, как правило, имеет составной тип данных (Строка, Число, Дата, СправочникСсылка и др.).
При использовании конструкции ВЫРАЗИТЬ(КомпьютерыСвойства.ЗначениеСвойства КАК ДАТА) для всех записей, где тип значения не является датой, результат выражения будет равен NULL.
Сравнение значений NULL в условиях ГДЕ и их сортировка на уровне СУБД имеют свои особенности. Чтобы избежать неоднозначности при вычислениях и соединениях, явно ограничим типы данных перед приведением:
ГДЕ
ТИПЗНАЧЕНИЯ(КомпьютерыСвойства.ЗначениеСвойства) = ТИП(ДАТА)
И ВЫРАЗИТЬ(КомпьютерыСвойства.ЗначениеСвойства КАК ДАТА) >= &НачалоПериода13
И ВЫРАЗИТЬ(КомпьютерыСвойства.ЗначениеСвойства КАК ДАТА) <= &КонецПериода13
Проанализируем контекст выполнения запроса. Консоль запросов запускается разработчиком с полными административными правами или в привилегированном режиме. В этот момент ограничения на уровне записей (RLS) отключены.
Если код в конфигурации выполняется в сеансе обычного пользователя, платформа динамически внедряет в исходный SQL-текст подзапросы проверки прав доступа к таблицам и табличным частям. Это может приводить к отсечению части строк (например, по конкретным контрагентам или местам хранения) и изменению общего результата выборки.
Для исключения влияния прав при отладке проверим работу метода с использованием привилегированного режима:
УстановитьПривилегированныйРежим(Истина);
Результат = Запрос.Выполнить();
УстановитьПривилегированныйРежим(Ложь);
Соберем все рекомендации воедино: избавимся от неявных соединений, выделим получение последней даты во временную таблицу через агрегацию МАКСИМУМ и свяжем результат с основными данными объекта.
ВЫБРАТЬ
КомпьютерыСвойства.Ссылка КАК Компьютер,
МАКСИМУМ(ВЫРАЗИТЬ(КомпьютерыСвойства.ЗначениеСвойства КАК ДАТА)) КАК ДатаВвода
ПОМЕСТИТЬ ВТ_МаксимальныеДаты
ИЗ
Справочник.Компьютеры.Свойства КАК КомпьютерыСвойства
ГДЕ
КомпьютерыСвойства.ВидСвойства В(&ВидСвойства)
И НЕ КомпьютерыСвойства.ВидСвойства = &ВидСвойства2
И ТИПЗНАЧЕНИЯ(КомпьютерыСвойства.ЗначениеСвойства) = ТИП(ДАТА)
И ВЫРАЗИТЬ(КомпьютерыСвойства.ЗначениеСвойства КАК ДАТА) МЕЖДУ &НачалоПериода13 И &КонецПериода13
СГРУППИРОВАТЬ ПО
КомпьютерыСвойства.Ссылка
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ РАЗЛИЧНЫЕ
Компьютеры.Ссылка КАК НомерКомпьютера,
Компьютеры.Контрагент КАК Контрагент,
КомпьютерыРабочаяСтанция.Номенклатура КАК Оборудование,
ВТ_МаксимальныеДаты.ДатаВвода КАК ДатаВвода,
ДОБАВИТЬКДАТЕ(ВТ_МаксимальныеДаты.ДатаВвода, МЕСЯЦ, 13) КАК ДатаОкончания,
Компьютеры.ВидРабочейСтанции КАК ВидРабочейСтанции,
Компьютеры.МестоНахождения КАК МестоНахождения
ИЗ
Справочник.Компьютеры КАК Компьютеры
ВНУТРЕННЕЕ СОЕДИНЕНИЕ ВТ_МаксимальныеДаты КАК ВТ_МаксимальныеДаты
ПО Компьютеры.Ссылка = ВТ_МаксимальныеДаты.Компьютер
ЛЕВОЕ СОЕДИНЕНИЕ Справочник.Компьютеры.РабочаяСтанция КАК КомпьютерыРабочаяСтанция
ПО Компьютеры.Ссылка = КомпьютерыРабочаяСтанция.Ссылка
ГДЕ
Компьютеры.ВидРабочейСтанции = &ВидРабочейСтанции1
И Компьютеры.Ссылка В(&СписокКомпьютеров)
И НЕ Компьютеры.Родитель.Наименование ПОДОБНО &Исключение
И Компьютеры.Контрагент В(&Контрагент)
УПОРЯДОЧИТЬ ПО
ДатаВвода УБЫВ,
Контрагент,
НомерКомпьютера
Такой подход гарантирует, что запрос вернет строго однозначные, детерминированные данные независимо от того, где он выполняется — в консоли отчетов, внешней обработке или серверном модуле конфигурации.