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