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