При разработке отчетов в среде «1С:Предприятие» программисты регулярно сталкиваются с необходимостью получения детальной информации о движениях товаров в разрезе документов-регистраторов. Типичная задача — получить объем и стоимость поступления по конкретному документу оприходования (например, ПоступлениеТоваровУслуг). Однако неоптимальный выбор виртуальных таблиц и ошибки в агрегатных функциях могут привести не только к искажению данных в отчете, но и к катастрофическому падению производительности всей информационной базы.
В этой статье мы подробно проанализируем архитектурные и логические ошибки в запросах к регистрам накопления, разберем причины их возникновения и рассмотрим три надежных, протестированных на практике сценария решения проблемы в соответствии со стандартами разработки фирмы «1С».
Давайте пошагово разберем, какие подводные камни скрываются в стандартных запросах и как написать оптимальный код.
---
Для начала детально рассмотрим исходный запрос, с которым возникли трудности, и выясним, почему он возвращает некорректные данные или работает медленно.
Ошибка 1. Использование виртуальной таблицы ОстаткиИОбороты с детализацией по Регистратору
Виртуальная таблица ОстаткиИОбороты — одна из самых ресурсоемких в платформе 1С. Когда мы указываем в параметрах этой таблицы периодичность Регистратор, СУБД выполняет сложнейшую математическую задачу: она рассчитывает остатки (начальный и конечный) на момент проведения каждого документа за выбранный период. Если в базе ведутся тысячи документов в день, построение такого отчета вызовет колоссальную нагрузку на дисковую подсистему и процессор сервера СУБД, вплоть до полной блокировки работы пользователей. Более того, остатки в разрезе регистратора не имеют практического экономического смысла — это плавающая техническая величина.
Ошибка 2. Логическая ошибка расчета цены (Классическая «сумма дробей»)
В запросе автора присутствует следующий фрагмент расчета цены:
СУММА(ВЫБОР
КОГДА ЕСТЬNULL(ПартииТоваровНаСкладахБухгалтерскийУчетОстаткиИОбороты.КоличествоПриход, 0) <> 0
ТОГДА ЕСТЬNULL(ПартииТоваровНаСкладахБухгалтерскийУчетОстаткиИОбороты.СтоимостьПриход, 0) / ЕСТЬNULL(ПартииТоваровНаСкладахБухгалтерскийУчетОстаткиИОбороты.КоличествоПриход, 0)
ИНАЧЕ ЕСТЬNULL(ПартииТоваровНаСкладахБухгалтерскийУчетОстаткиИОбороты.СтоимостьПриход, 0)
КОНЕЦ) КАК Цена
Это грубейшая математическая ошибка при группировке данных. Запрос сначала делит стоимость на количество для каждой отдельной записи в базе данных, а затем складывает полученные результаты. Проиллюстрируем это простым примером:
При группировке по номенклатуре формула автора выполнит сложение цен: 10 + 20 = 30 рублей, что абсолютно неверно. Правильный математический подход заключается в том, чтобы сначала просуммировать всю стоимость, затем просуммировать все количество, и только после этого выполнить деление суммарных величин: (100 + 100) / (10 + 5) = 200 / 15 = 13.33 рублей.
Ошибка 3. Использование функции СРЕДНЕЕ для суммовых показателей
Использование конструкции СРЕДНЕЕ(СтоимостьПриход) КАК Сумма приводит к непредсказуемым результатам. Если в документе оприходования одна и та же номенклатура разбита на несколько строк (например, по разным сериям или характеристикам), функция СРЕДНЕЕ вернет среднее арифметическое значение строк вместо реальной итоговой суммы прихода. Суммовые показатели всегда должны агрегироваться исключительно с помощью функции СУММА.
---
Если перед нами стоит задача получить исключительно «чистый приход» по конкретному документу оприходования без расчета остатков на начало и конец периода, обращаться к виртуальным таблицам вообще не нужно. Физическая таблица движений регистра накопления хранит готовые записи (проводки), которые СУБД может прочитать напрямую по индексам. Это дает колоссальный выигрыш в скорости выполнения запроса.
Проанализируем правильный шаблон запроса к физической таблице движений регистра:
ВЫБРАТЬ
Партии.Номенклатура КАК Номенклатура,
Партии.СерияНоменклатуры КАК СерияНоменклатуры,
Партии.ДокументОприходования КАК ДокументОприходования,
Партии.ДокументОприходования.Контрагент КАК Контрагент,
СУММА(Партии.Количество) КАК Количество,
СУММА(Партии.Стоимость) КАК Сумма,
// Рассчитываем средневзвешенную цену корректно:
ВЫБОР
КОГДА СУММА(Партии.Количество) <> 0
ТОГДА СУММА(Партии.Стоимость) / СУММА(Партии.Количество)
ИНАЧЕ 0
КОНЕЦ КАК Цена
ПОМЕСТИТЬ ВТПоступления
ИЗ
РегистрНакопления.ПартииТоваровНаСкладахБухгалтерскийУчет КАК Партии
ГДЕ
Партии.Период МЕЖДУ &Дата1 И &Дата2
И Партии.ВидДвижения = ЗНАЧЕНИЕ(ВидДвиженияНакопления.Приход)
И Партии.Регистратор ССЫЛКА Документ.ПоступлениеТоваровУслуг
И Партии.СерияНоменклатуры.Импорт = ИСТИНА
СГРУППИРОВАТЬ ПО
Партии.Номенклатура,
Партии.СерияНоменклатуры,
Партии.ДокументОприходования,
Партии.ДокументОприходования.Contragent
Почему этот вариант оптимален:
ВидДвижения = Приход, что позволяет СУБД отсечь все расходные операции на уровне базы данных.ССЫЛКА Документ.ПоступлениеТоваровУслуг выполняется быстро, так как поле Регистратор проиндексировано СУБД по умолчанию.---
Если архитектура вашего отчета (например, при использовании Системы компоновки данных — СКД) требует обращения именно к виртуальным таблицам, то для получения оборотов по документам необходимо использовать исключительно таблицу Обороты, а не ОстаткиИОбороты.
Виртуальная таблица оборотов работает в разы быстрее, поскольку СУБД не тратит ресурсы на расчет остатков на каждую границу периода проведения документов. Давайте посмотрим, как будет выглядеть такой запрос:
ВЫБРАТЬ
ПартииОбороты.Номенклатура КАК Номенклатура,
ПартииОбороты.СерияНоменклатуры КАК СерияНоменклатуры,
ПартииОбороты.ДокументОприходования КАК ДокументОприходования,
ПартииОбороты.ДокументОприходования.Контрагент КАК Контрагент,
СУММА(ПартииОбороты.КоличествоОборот) КАК Количество,
СУММА(ПартииОбороты.СтоимостьОборот) КАК Сумма,
ВЫБОР
КОГДА СУММА(ПартииОбороты.КоличествоОборот) <> 0
ТОГДА СУММА(ПартииОбороты.СтоимостьОборот) / СУММА(ПартииОбороты.КоличествоОборот)
ИНАЧЕ 0
КОНЕЦ КАК Цена
ПОМЕСТИТЬ ВТПоступления
ИЗ
РегистрНакопления.ПартииТоваровНаСкладахБухгалтерскийУчет.Обороты(
&Дата1,
&Дата2,
Регистратор,
СерияНоменклатуры.Импорт = ИСТИНА
) КАК ПартииОбороты
ГДЕ
ПартииОбороты.Регистратор ССЫЛКА Документ.ПоступлениеТоваровУслуг
СГРУППИРОВАТЬ ПО
ПартииОбороты.Номенклатура,
ПартииОбороты.СерияНоменклатуры,
ПартииОбороты.ДокументОприходования,
ПартииОбороты.ДокументОприходования.Контрагент
Важные детали реализации:
Обороты мы указали периодичность Регистратор. Это позволяет получить доступ к полю Регистратор внутри выборки.---
Иногда перед разработчиком стоит задача вывести полноценную ведомость, где должны присутствовать начальный остаток, конечный остаток, а между ними — детальные обороты в разрезе документов-регистраторов. Как мы уже выяснили, использование таблицы ОстаткиИОбороты с периодичностью Регистратор в данном случае недопустимо по соображениям производительности.
В стандартах разработки «1С» для этой задачи описан классический паттерн — разделение запросов через ОБЪЕДИНИТЬ ВСЕ. Мы пишем три отдельных простых запроса, результаты которых объединяем в одну временную таблицу:
Остатки (поле Регистратор заполняется пустой ссылкой).Обороты с периодичностью Регистратор.Остатки (поле Регистратор также заполняется пустой ссылкой).Давайте проанализируем структуру такого комплексного запроса:
ВЫБРАТЬ
ОстаткиНачало.Номенклатура КАК Номенклатура,
ОстаткиНачало.СерияНоменклатуры КАК СерияНоменклатуры,
ЗНАЧЕНИЕ(Документ.ПоступлениеТоваровУслуг.ПустаяСсылка) КАК Регистратор,
ОстаткиНачало.КоличествоОстаток КАК КоличествоНачальныйОстаток,
0 КАК КоличествоПриход,
0 КАК КоличествоРасход,
0 КАК КоличествоКонечныйОстаток
ИЗ
РегистрНакопления.ПартииТоваровНаСкладахБухгалтерскийУчет.Остатки(&Дата1, СерияНоменклатуры.Импорт = ИСТИНА) КАК ОстаткиНачало
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
Обороты.Номенклатура,
Обороты.СерияНоменклатуры,
Обороты.Регистратор,
0,
Обороты.КоличествоПриход,
Обороты.КоличествоРасход,
0
ИЗ
РегистрНакопления.ПартииТоваровНаСкладахБухгалтерскийУчет.Обороты(&Дата1, &Дата2, Регистратор, СерияНоменклатуры.Импорт = ИСТИНА) КАК Обороты
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
ОстаткиКонец.Номенклатура,
ОстаткиКонец.СерияНоменклатуры,
ЗНАЧЕНИЕ(Документ.ПоступлениеТоваровУслуг.ПустаяСсылка) КАК Регистратор,
0,
0,
0,
ОстаткиКонец.КоличествоОстаток
ИЗ
РегистрНакопления.ПартииТоваровНаСкладахБухгалтерскийУчет.Остатки(&Дата2, СерияНоменклатуры.Импорт = ИСТИНА) КАК ОстаткиКонец
После объединения полученный пакет данных группируется по номенклатуре и сериям. Этот метод работает в десятки раз быстрее, чем прямое обращение к ОстаткиИОбороты с периодичностью по регистратору, и гарантирует абсолютную точность итоговых показателей в отчете.
Используйте рассмотренные паттерны проектирования запросов, всегда контролируйте математику агрегатных функций при расчете цен и избегайте тяжелых виртуальных таблиц там, где можно обойтись простым чтением физических таблиц движений.