В практике разработки на платформе 1С:Предприятие часто встречается парадоксальная ситуация: два практически идентичных запроса к одной и той же виртуальной таблице ОстаткиИОбороты возвращают разные итоговые значения остатков. Различие заключается лишь в том, что в одном запросе присутствует измерение Номенклатура, а в другом — нет, хотя итоговая группировка в обоих случаях выполняется только по складу. Проанализируем, почему так происходит, и разберем механизмы работы платформы «под капотом».
Рассмотрим ситуацию, описанную разработчиком. Есть два запроса к регистру накопления ТоварыНаСкладах. В обоих случаях используется периодичность Месяц. Однако в первом запросе мы выбираем только склад и остатки, а во втором добавляем в выборку номенклатуру, но затем все равно схлопываем результат до склада через СГРУППИРОВАТЬ ПО. Логично было бы ожидать, что сумма всех остатков по всей номенклатуре на складе должна быть равна общему остатку по складу. Но на практике это не так.
Выясним причину этого поведения. Она кроется в особенностях оптимизации SQL-запросов, которые генерирует платформа 1С. Когда мы обращаемся к виртуальной таблице ОстаткиИОбороты, система не просто читает данные из одной таблицы, а выполняет сложный расчет, основываясь на физических таблицах итогов и движений.
Номенклатура, платформа обращается к таблицам итогов, где данные уже агрегированы до уровня склада. Это максимально быстрый и точный способ получить остаток.Номенклатура, платформа обязана рассчитать остатки в разрезе каждой уникальной комбинации «Склад + Номенклатура» для каждого месяца внутри периода.Проанализируем самый критичный момент: как платформа формирует строки для промежуточных периодов (например, для второго и третьего месяца в квартальном отчете). Виртуальная таблица ОстаткиИОбороты по умолчанию работает по принципу: «есть движение — есть строка».
Представим ситуацию: на складе лежит «Товар А» с остатком 100 единиц. В октябре по нему были продажи, в ноябре — полная тишина (ни приходов, ни расходов), а в декабре снова начались движения.
Если мы делаем запрос с детализацией по номенклатуре, то за ноябрь платформа может просто не создать строку для «Товара А», так как по нему не было оборотов. В результате, когда во внешнем запросе вы применяете функцию СУММА(КоличествоНачальныйОстаток), этот товар просто не попадет в общую копилку за ноябрь. Итоговая сумма по складу «просядет» ровно на остаток этого товара.
Важно понимать, что начальный остаток на первую дату периода и конечный на последнюю обычно рассчитываются правильно (из таблиц итогов), но промежуточные точки внутри периода (начала месяцев) рассчитываются математически: Остаток_N = Остаток_Начальный + Обороты_Периодов_До_N. Если строка аналитики выпадает из расчета из-за отсутствия оборотов, расчет для этой аналитики в конкретном периоде обрывается.
Разберем еще одну серьезную ошибку, которую часто совершают начинающие разработчики. Использование СУММА к полю НачальныйОстаток во вложенном запросе — это методологически неверно в контексте временных периодов.
Остаток — это моментный показатель. Его нельзя просто складывать, если у него разные временные метки. Когда вы группируете данные по Складу и Периоду, суммируя начальные остатки номенклатуры, результат будет корректен только при условии, что в выборку попали абсолютно все товары, имеющие ненулевой остаток. Как мы выяснили выше, без специальных настроек (дополнения периодов) это условие практически никогда не соблюдается.
Чтобы заставить запрос «протягивать» остатки даже через те периоды, где не было движений, необходимо использовать конструкцию ИТОГИ с ключевым словом ПЕРИОДАМИ. Это меняет логику построения SQL-запроса: платформа начнет принудительно генерировать строки для всех интервалов времени.
Посмотрим на пример правильного построения запроса:
ВЫБРАТЬ
ТоварыНаСкладахОстаткиИОбороты.Склад КАК Склад,
ТоварыНаСкладахОстаткиИОбороты.Номенклатура КАК Номенклатура,
ТоварыНаСкладахОстаткиИОбороты.КоличествоНачальныйОстаток КАК ОстатокНачало,
ТоварыНаСкладахОстаткиИОбороты.Период КАК Период
ИЗ
РегистрНакопления.ТоварыНаСкладах.ОстаткиИОбороты(&НачалоПериода, &КонецПериода, Месяц, , Склад = &Склад) КАК ТоварыНаСкладахОстаткиИОбороты
ИТОГИ
СУММА(ОстатокНачало)
ПО
Период ПЕРИОДАМИ(МЕСЯЦ, &НачалоПериода, &КонецПериода),
Склад
В этом случае при обходе результата запроса (используя метод Выбрать с параметром ОбходРезультатаЗапроса.ПоГруппировкам и "ВСЕ"), вы получите корректные данные. Платформа сама «дорисует» недостающие остатки в тех месяцах, где номенклатура не двигалась.
Наиболее современным и правильным способом решения подобных проблем является использование СКД. Система компоновки данных специально спроектирована для работы с остатками. Рассмотрим, почему в СКД такой проблемы обычно не возникает:
Остаток, а для периода — роль Период с указанием соответствующего поля дополнения.Если вам необходимо получить гарантированно точный результат в «плоской» таблице без использования итогов, рассмотрим альтернативный подход. Вместо виртуальной таблицы ОстаткиИОбороты можно использовать объединение двух разных таблиц:
Остатки на начало периода.Обороты за период с детализацией по месяцам.Затем, используя математические операции в запросе, можно вычислить остаток на начало любого месяца как НачальныйОстаток + СуммаОборотовПредыдущихМесяцев. Это более трудоемкий путь, но он дает полный контроль над логикой формирования каждой строки.
Подводя итоги, можно сказать, что виртуальная таблица ОстаткиИОбороты — это мощный, но требующий понимания инструмент. Основная ошибка заключается в ожидании того, что она ведет себя как обычная физическая таблица.
Запомним основные правила:
СУММА() к начальным и конечным остаткам во вложенных запросах или временных таблицах без очень веских на то причин.ИТОГИ ... ПЕРИОДАМИ.Проанализировав ситуацию, мы видим, что «ошибки» платформы здесь нет — есть лишь специфический алгоритм работы с данными, направленный на высокую производительность при больших объемах информации.