Разработчики 1С часто сталкиваются с загадочным поведением платформы: один и тот же запрос, запущенный в коде формы (например, при получении остатка для строки табличной части) и в консоли запросов, выдает разные результаты. В форме документа остаток товара может быть равен единице, а в консоли запросов — двум. При этом параметры передаются абсолютно одинаковые, дублирующиеся элементы в справочниках отсутствуют, а база данных используется одна и та же.
В этой статье мы подробно разберем, почему возникает этот феномен, проанализируем внутренние механизмы платформы 1С и СУБД, а также научимся писать запросы так, чтобы их поведение всегда было предсказуемым и детерминированным.
Проанализируем запрос, который часто используют начинающие разработчики для получения остатков:
ВЫБРАТЬ
ЕСТЬNULL(ОстаткиТоваровНаСкладахОстатки.КоличествоОстаток, 0) КАК Количество
ИЗ
РегистрНакопления.ОстаткиТоваровНаСкладах.Остатки КАК ОстаткиТоваровНаСкладахОстатки
ГДЕ
ОстаткиТоваровНаСкладахОстатки.Номенклатура = &Номенклатура
И ОстаткиТоваровНаСкладахОстатки.МестоХранения = &МестоХранения
На первый взгляд запрос кажется логичным. Однако именно в такой конструкции кроется целый ряд подводных камней, которые приводят к расхождению данных при выполнении в разном контексте. Рассмотрим подробнее каждую причину этого расхождения.
---
Это самая распространенная и грубая ошибка при работе с виртуальными таблицами в 1С. Когда мы пишем условие фильтрации в секции ГДЕ, платформа 1С и СУБД обрабатывают этот запрос крайне неоптимально.
Рассмотрим, что происходит на уровне СУБД:
Остатки не заданы, система пытается получить итоги по всем записям таблицы итогов регистра (_AccumRegTotals).ВЫБРАТЬ мы запрашиваем только ресурс КоличествоОстаток, полностью игнорируя измерения Номенклатура и МестоХранения.ГДЕ, SQL-сервер может построить некорректный план кэширования. В зависимости от текущей нагрузки на рабочий процесс 1С, версии СУБД и состояния индексов, результат выборки без измерений может стать нестабильным и возвращать неверно агрегированные данные.Решение проблемы: Перенесем условия фильтрации непосредственно в параметры виртуальной таблицы. В этом случае платформа 1С передаст СУБД жесткий отбор, и выборка произойдет только по нужным записям изначально. Также хорошей практикой является явный вывод измерений в запросе.
Правильный вариант запроса:
ВЫБРАТЬ
ЕСТЬNULL(ОстаткиТоваровНаСкладахОстатки.КоличествоОстаток, 0) КАК Количество
ИЗ
РегистрНакопления.ОстаткиТоваровНаСкладах.Остатки(&Период, Номенклатура = &Номенклатура И МестоХранения = &МестоХранения) КАК ОстаткиТоваровНаСкладахОстатки
---
Контекст выполнения кода в форме документа и в консоли запросов кардинально различается на уровне транзакционности:
Если в процессе проведения документа остатки меняются (например, списывается товар), то внутри транзакции остаток уже будет равен 1, в то время как внешняя консоль запросов все еще будет видеть старое значение 2, так как транзакция проведения еще не завершена (не зафиксирована).
---
Еще один невидимый фактор, который часто сбивает с толку разработчиков — это механизм RLS (Row-Level Security).
Выясним, как это влияет на результат:
Решение: Для проверки этой гипотезы запустите консоль запросов под тем же пользователем, у которого воспроизводится проблема в форме, либо временно предоставьте пользователю полные права для теста.
---
Если запрос в форме выполняется не явным вызовом объекта Новый Запрос, а через динамический список или СКД:
Система компоновки данных — это мощный инструмент, который перед отправкой запроса в СУБД полностью его перестраивает. Она автоматически накладывает параметры периода, оптимизирует соединения, убирает неиспользуемые поля и добавляет системные отборы формы. Консоль запросов выполняет исходный «сырой» текст, тогда как СКД на форме выполняет глубоко модифицированную версию этого запроса.
---
Обратите внимание на параметр периода. Если в запросе виртуальной таблицы остатков не указан период, система берет итоги на самую актуальную дату (на «сейчас»).
В коде формы период часто передается неявно, например, как МоментВремени() или Граница() документа. В консоли запросов разработчики часто указывают просто дату (например, конец дня).
Разберем разницу:
ВидГраницы.Включая или ВидГраницы.Исключая).Разница в типах передаваемых параметров периода (просто дата в консоли и МоментВремени в форме) — классическая причина расхождения остатков ровно на величину движений текущего документа.
---
В редких случаях (например, после сбоев СУБД, аварийного завершения транзакций или некорректного динамического обновления конфигурации) может произойти рассинхронизация физической таблицы движений регистра накопления и таблицы его текущих итогов.
Запрос к виртуальной таблице без указания периода обращается к таблице текущих итогов. Если она повреждена, СУБД может возвращать неверные данные, которые будут отличаться в зависимости от кэша конкретного сеанса.
Решение: Выполните операцию «Тестирование и исправление» в конфигураторе с установленным флагом «Пересчет итогов».
---
Чтобы раз и навсегда исключить подобные расхождения и гарантировать стабильную работу ваших запросов, придерживайтесь следующих правил:
ГДЕ для фильтрации виртуальных таблиц остатков и оборотов.МоментВремени() или объект Новый Граница(ДокументСсылка.МоментВремени(), ВидГраницы.Исключая) для получения остатков строго «до» проведения документа.СГРУППИРОВАТЬ ПО.Рассмотрим надежный, профессиональный шаблон получения остатков в форме документа:
// Подготовим границу для получения остатков строго до момента времени документа
ГраницаОстатков = Новый Граница(Объект.Ссылка.МоментВремени(), ВидГраницы.Исключая);
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| ОстаткиТоваров.Номенклатура КАК Номенклатура,
| ОстаткиТоваров.МестоХранения КАК МестоХранения,
| СУММА(ОстаткиТоваров.КоличествоОстаток) КАК Количество
|ИЗ
| РегистрНакопления.ОстаткиТоваровНаСкладах.Остатки(&Период, Номенклатура = &Номенклатура И МестоХранения = &МестоХранения) КАК ОстаткиТоваров
|ГРУППИРОВАТЬ ПО
| ОстаткиТоваров.Номенклатура,
| ОстаткиТоваров.МестоХранения";
Запрос.УстановитьПараметр("Период", ГраницаОстатков);
Запрос.УстановитьПараметр("Номенклатура", ВыбраннаяНоменклатура);
Запрос.УстановитьПараметр("МестоХранения", ВыбранныйСклад);
РезультатЗапроса = Запрос.Выполнить();
Этот шаблон гарантирует абсолютно одинаковый результат работы как в коде формы, так и при тестировании в консоли запросов под аналогичным пользователем.