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