Как исключить лишние регистраторы из запроса к регистру остатков и оборотов и правильно получить чистый приход в 1С

Программист 1С v8.3 (Обычные формы) Бухгалтерский учет IT и автоматизация бизнеса
← На главную

При разработке отчетов в среде «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. Использование функции СРЕДНЕЕ для суммовых показателей

Использование конструкции СРЕДНЕЕ(СтоимостьПриход) КАК Сумма приводит к непредсказуемым результатам. Если в документе оприходования одна и та же номенклатура разбита на несколько строк (например, по разным сериям или характеристикам), функция СРЕДНЕЕ вернет среднее арифметическое значение строк вместо реальной итоговой суммы прихода. Суммовые показатели всегда должны агрегироваться исключительно с помощью функции СУММА.

---

Вариант 1. Использование физической таблицы движений (Самый быстрый и правильный способ)

Если перед нами стоит задача получить исключительно «чистый приход» по конкретному документу оприходования без расчета остатков на начало и конец периода, обращаться к виртуальным таблицам вообще не нужно. Физическая таблица движений регистра накопления хранит готовые записи (проводки), которые СУБД может прочитать напрямую по индексам. Это дает колоссальный выигрыш в скорости выполнения запроса.

Проанализируем правильный шаблон запроса к физической таблице движений регистра:


ВЫБРАТЬ
    Партии.Номенклатура КАК Номенклатура,
    Партии.СерияНоменклатуры КАК СерияНоменклатуры,
    Партии.ДокументОприходования КАК ДокументОприходования,
    Партии.ДокументОприходования.Контрагент КАК Контрагент,
    СУММА(Партии.Количество) КАК Количество,
    СУММА(Партии.Стоимость) КАК Сумма,
    // Рассчитываем средневзвешенную цену корректно:
    ВЫБОР
        КОГДА СУММА(Партии.Количество) <> 0
            ТОГДА СУММА(Партии.Стоимость) / СУММА(Партии.Количество)
        ИНАЧЕ 0
    КОНЕЦ КАК Цена
ПОМЕСТИТЬ ВТПоступления
ИЗ
    РегистрНакопления.ПартииТоваровНаСкладахБухгалтерскийУчет КАК Партии
ГДЕ
    Партии.Период МЕЖДУ &Дата1 И &Дата2
    И Партии.ВидДвижения = ЗНАЧЕНИЕ(ВидДвиженияНакопления.Приход)
    И Партии.Регистратор ССЫЛКА Документ.ПоступлениеТоваровУслуг
    И Партии.СерияНоменклатуры.Импорт = ИСТИНА
СГРУППИРОВАТЬ ПО
    Партии.Номенклатура,
    Партии.СерияНоменклатуры,
    Партии.ДокументОприходования,
    Партии.ДокументОприходования.Contragent

Почему этот вариант оптимален:

  1. Мы явно указали ВидДвижения = Приход, что позволяет СУБД отсечь все расходные операции на уровне базы данных.
  2. Фильтрация по типу регистратора ССЫЛКА Документ.ПоступлениеТоваровУслуг выполняется быстро, так как поле Регистратор проиндексировано СУБД по умолчанию.
  3. Цена рассчитывается правильно: делением общей суммы сгруппированных записей на их общее количество.

---

Вариант 2. Использование виртуальной таблицы Обороты

Если архитектура вашего отчета (например, при использовании Системы компоновки данных — СКД) требует обращения именно к виртуальным таблицам, то для получения оборотов по документам необходимо использовать исключительно таблицу Обороты, а не ОстаткиИОбороты.

Виртуальная таблица оборотов работает в разы быстрее, поскольку СУБД не тратит ресурсы на расчет остатков на каждую границу периода проведения документов. Давайте посмотрим, как будет выглядеть такой запрос:


ВЫБРАТЬ
    ПартииОбороты.Номенклатура КАК Номенклатура,
    ПартииОбороты.СерияНоменклатуры КАК СерияНоменклатуры,
    ПартииОбороты.ДокументОприходования КАК ДокументОприходования,
    ПартииОбороты.ДокументОприходования.Контрагент КАК Контрагент,
    СУММА(ПартииОбороты.КоличествоОборот) КАК Количество,
    СУММА(ПартииОбороты.СтоимостьОборот) КАК Сумма,
    ВЫБОР
        КОГДА СУММА(ПартииОбороты.КоличествоОборот) <> 0
            ТОГДА СУММА(ПартииОбороты.СтоимостьОборот) / СУММА(ПартииОбороты.КоличествоОборот)
        ИНАЧЕ 0
    КОНЕЦ КАК Цена
ПОМЕСТИТЬ ВТПоступления
ИЗ
    РегистрНакопления.ПартииТоваровНаСкладахБухгалтерскийУчет.Обороты(
        &Дата1, 
        &Дата2, 
        Регистратор, 
        СерияНоменклатуры.Импорт = ИСТИНА
    ) КАК ПартииОбороты
ГДЕ
    ПартииОбороты.Регистратор ССЫЛКА Документ.ПоступлениеТоваровУслуг
СГРУППИРОВАТЬ ПО
    ПартииОбороты.Номенклатура,
    ПартииОбороты.СерияНоменклатуры,
    ПартииОбороты.ДокументОприходования,
    ПартииОбороты.ДокументОприходования.Контрагент

Важные детали реализации:

---

Вариант 3. Объединение запросов (Когда нужны и остатки, и обороты с регистратором)

Иногда перед разработчиком стоит задача вывести полноценную ведомость, где должны присутствовать начальный остаток, конечный остаток, а между ними — детальные обороты в разрезе документов-регистраторов. Как мы уже выяснили, использование таблицы ОстаткиИОбороты с периодичностью Регистратор в данном случае недопустимо по соображениям производительности.

В стандартах разработки «1С» для этой задачи описан классический паттерн — разделение запросов через ОБЪЕДИНИТЬ ВСЕ. Мы пишем три отдельных простых запроса, результаты которых объединяем в одну временную таблицу:

  1. Первый запрос: получает начальные остатки на дату начала периода из виртуальной таблицы Остатки (поле Регистратор заполняется пустой ссылкой).
  2. Второй запрос: получает чистые обороты за выбранный период из таблицы Обороты с периодичностью Регистратор.
  3. Третий запрос: получает конечные остатки на дату окончания периода из таблицы Остатки (поле Регистратор также заполняется пустой ссылкой).

Давайте проанализируем структуру такого комплексного запроса:


ВЫБРАТЬ
    ОстаткиНачало.Номенклатура КАК Номенклатура,
    ОстаткиНачало.СерияНоменклатуры КАК СерияНоменклатуры,
    ЗНАЧЕНИЕ(Документ.ПоступлениеТоваровУслуг.ПустаяСсылка) КАК Регистратор,
    ОстаткиНачало.КоличествоОстаток КАК КоличествоНачальныйОстаток,
    0 КАК КоличествоПриход,
    0 КАК КоличествоРасход,
    0 КАК КоличествоКонечныйОстаток
ИЗ
    РегистрНакопления.ПартииТоваровНаСкладахБухгалтерскийУчет.Остатки(&Дата1, СерияНоменклатуры.Импорт = ИСТИНА) КАК ОстаткиНачало

ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
    Обороты.Номенклатура,
    Обороты.СерияНоменклатуры,
    Обороты.Регистратор,
    0,
    Обороты.КоличествоПриход,
    Обороты.КоличествоРасход,
    0
ИЗ
    РегистрНакопления.ПартииТоваровНаСкладахБухгалтерскийУчет.Обороты(&Дата1, &Дата2, Регистратор, СерияНоменклатуры.Импорт = ИСТИНА) КАК Обороты

ОБЪЕДИНИТЬ ВСЕ

ВЫБРАТЬ
    ОстаткиКонец.Номенклатура,
    ОстаткиКонец.СерияНоменклатуры,
    ЗНАЧЕНИЕ(Документ.ПоступлениеТоваровУслуг.ПустаяСсылка) КАК Регистратор,
    0,
    0,
    0,
    ОстаткиКонец.КоличествоОстаток
ИЗ
    РегистрНакопления.ПартииТоваровНаСкладахБухгалтерскийУчет.Остатки(&Дата2, СерияНоменклатуры.Импорт = ИСТИНА) КАК ОстаткиКонец

После объединения полученный пакет данных группируется по номенклатуре и сериям. Этот метод работает в десятки раз быстрее, чем прямое обращение к ОстаткиИОбороты с периодичностью по регистратору, и гарантирует абсолютную точность итоговых показателей в отчете.

Используйте рассмотренные паттерны проектирования запросов, всегда контролируйте математику агрегатных функций при расчете цен и избегайте тяжелых виртуальных таблиц там, где можно обойтись простым чтением физических таблиц движений.

← На главную