При разработке сложных отчетов или механизмов проведения документов в 1С программисты часто сталкиваются с ограничением виртуальной таблицы Остатки. Суть проблемы заключается в том, что механизм итогов платформы оптимизирован для быстрого получения текущих остатков, но он не позволяет «на лету» отфильтровать движения конкретного документа-регистратора внутри параметров самой виртуальной таблицы. Если нам нужно получить остатки так, будто одного определенного документа никогда не существовало, стандартный фильтр по регистратору в параметрах &Условие работать не будет, так как это поле недоступно в срезе итогов.
В этой статье мы подробно разберем, как элегантно решить эту задачу, проанализируем производительность различных подходов и выясним, почему математическая корректировка данных эффективнее прямого обращения к физическим таблицам.
Для начала выясним причину, по которой обычный запрос к виртуальной таблице Остатки не подходит. Виртуальная таблица обращается к предварительно рассчитанным итогам (таблицы с суффиксом Totals в СУБД). Эти итоги хранятся в разрезе измерений и периодов (кратных месяцу), но не в разрезе регистраторов. Когда мы просим систему выдать остаток на 15-е число, она берет итоги на начало месяца и прибавляет к ним движения из физической таблицы за прошедшие 15 дней. Если мы попытаемся наложить условие Регистратор <> &ИсключаемыйДокумент, платформа не сможет применить его к итогам, хранящимся в базе, так как там информация о регистраторе уже «свернута».
Рассмотрим самый надежный и быстрый способ, который рекомендуют эксперты. Его суть заключается в том, чтобы получить общие остатки, а затем «вычесть» из них влияние ненужного нам документа. Разберем этот алгоритм по шагам.
Шаг 1: Получение основных остатков. Мы обращаемся к виртуальной таблице Остатки обычным способом, передавая нужную дату и фильтры по измерениям.
Шаг 2: Получение движений исключаемого регистратора. Мы делаем запрос к физической таблице регистра накопления, фильтруя данные по конкретной ссылке на документ.
Шаг 3: Инверсия знаков. Чтобы исключить влияние документа, мы должны выполнить обратную операцию:
Посмотрим на пример реализации такого запроса:
ВЫБРАТЬ
Остатки.Номенклатура КАК Номенклатура,
Остатки.КоличествоОстаток КАК Количество
ПОМЕСТИТЬ ВТ_ИтоговыеДанные
ИЗ
РегистрНакопления.ТоварыНаСкладах.Остатки(&Период, ) КАК Остатки
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
Движения.Номенклатура,
ВЫБОР
КОГДА Движения.ВидДвижения = ЗНАЧЕНИЕ(ВидДвиженияНакопления.Приход)
ТОГДА -Движения.Количество
ИНАЧЕ Движения.Количество
КОНЕЦ
ИЗ
РегистрНакопления.ТоварыНаСкладах КАК Движения
ГДЕ
Движения.Регистратор = &ИсключаемыйРегистратор
И Движения.Период <= &Период
;
////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
ВТ.Номенклатура,
СУММА(ВТ.Количество) КАК КоличествоОстаток
ИЗ
ВТ_ИтоговыеДанные КАК ВТ
СГРУППИРОВАТЬ ПО
ВТ.Номенклатура
Важный нюанс: Обратите внимание на условие Движения.Период <= &Период. Мы должны корректировать остатки только в том случае, если исключаемый документ по времени находится «внутри» или «раньше» точки расчета остатков. Если документ записан будущим числом относительно &Период, он и так не попал в виртуальную таблицу Остатки, и корректировать его не нужно.
Иногда программисты решают задачу «в лоб», полностью отказываясь от виртуальной таблицы и обращаясь к физической таблице движений регистра. Проанализируем эту ситуацию. Мы можем просто выбрать все движения, исключив нужный регистратор, и сгруппировать их. Однако, если регистр содержит миллионы записей за несколько лет, такой запрос будет выполняться крайне медленно, так как СУБД придется прочитать все записи с момента «начала времен».
Рассмотрим пример кода для этого (менее рекомендуемого) способа:
ВЫБРАТЬ
Движения.Номенклатура,
СУММА(ВЫБОР
КОГДА Движения.ВидДвижения = ЗНАЧЕНИЕ(ВидДвиженияНакопления.Приход)
ТОГДА Движения.Количество
ИНАЧЕ -Движения.Количество
КОНЕЦ) КАК КоличествоОстаток
ИЗ
РегистрНакопления.ТоварыНаСкладах КАК Движения
ГДЕ
Движения.Регистратор <> &ИсключаемыйРегистратор
И Движения.Период <= &Период
СГРУППИРОВАТЬ ПО
Движения.Номенклатура
Почему это плохо? В СУБД для таблицы регистра накопления обычно существует индекс Период + Регистратор. Когда мы используем условие Регистратор <> ..., СУБД чаще всего не может эффективно использовать индекс и переходит к полному сканированию таблицы (Index Scan). Это создает колоссальную нагрузку на дисковую подсистему и память сервера.
Проанализируем технические детали, упомянутые опытными разработчиками. При выполнении первого метода (корректировка через ОБЪЕДИНИТЬ ВСЕ) важно понимать, как 1С работает с типами данных в SQL.
Поле Регистратор является составным типом. Когда вы передаете конкретную ссылку в параметр &ИсключаемыйРегистратор, 1С формирует запрос, который эффективно ищет запись в физической таблице по индексу. Это происходит очень быстро (Index Seek).
Также стоит упомянуть проблему «вымывания кэша планов запросов», которая обсуждалась в профессиональном сообществе. Если вы формируете текст запроса динамически, подставляя условия по типам документов, SQL-сервер может каждый раз генерировать новый план выполнения. Рекомендуется всегда использовать параметризацию (передачу ссылки через &Параметр), чтобы СУБД могла повторно использовать уже готовый план запроса.
Если задача по исключению конкретных документов из остатков возникает регулярно, стоит задуматься: а правильно ли спроектирована система? Рассмотрим альтернативы:
ТипОперации. В этом случае мы сможем наложить фильтр прямо в параметрах виртуальной таблицы Остатки, и это будет работать максимально быстро, используя стандартный механизм итогов 1С.Подведем итоги нашего исследования. Чтобы красиво и эффективно исключить движения одного регистратора из остатков, мы используем следующую стратегию:
1. Используем временные таблицы для разделения логики получения остатков и получения корректирующих движений.
2. В первой временной таблице получаем данные из РегистрНакопления...Остатки.
3. Во второй временной таблице получаем движения по конкретному Регистратору, обязательно ограничивая выборку периодом, равным дате остатков.
4. Выполняем математическую инверсию: приход превращаем в расход (минус), расход — в приход (плюс).
5. Объединяем результаты и выполняем финальную группировку.
Такой подход позволяет сохранить высокую скорость работы системы, задействовать механизм итогов и избежать ресурсоемкого сканирования физических таблиц за весь период ведения учета.