Разработчики в среде 1С:Предприятие часто сталкиваются с загадочным поведением виртуальной таблицы ОстаткиИОбороты регистров накопления. Типичная ситуация: строится запрос с периодичностью (например, по месяцам), и при анализе итогов выясняется, что начальный остаток текущего месяца не равен конечному остатку предыдущего месяца. При этом за первый месяц выборки данные могут отображаться корректно, а в последующих периодах начинается расхождение сумм.
В этой статье мы подробно разберем внутренние механизмы работы платформы 1С с виртуальными таблицами остатков и оборотов, выясним фундаментальные причины возникновения этой проблемы и рассмотрим практические варианты её решения.
Рассмотрим ситуацию на примере запроса, который часто пишут начинающие разработчики. В нем данные выбираются из регистра накопления РасчетыСКлиентами.ОстаткиИОбороты, помещаются во временную таблицу с группировкой, а затем из этой временной таблицы выбираются с отбором по организации:
ВЫБРАТЬ
РН.Период КАК Период,
РН.АналитикаУчетаПоПартнерам КАК Ключ,
РН.АналитикаУчетаПоПартнерам.Организация КАК Организация,
СУММА(РН.СуммаНачальныйОстаток) КАК ОН,
СУММА(РН.СуммаКонечныйОстаток) КАК ОК
ПОМЕСТИТЬ втОстатки
ИЗ РегистрНакопления.РасчетыСКлиентами.ОстаткиИОбороты(&ДН, &ДК, МЕСЯЦ) КАК РН
СГРУППИРОВАТЬ ПО РН.Период, РН.АналитикаУчетаПоПартнерам
;
/////////////////////////////////////////////////////////////////////////////////
ВЫБРАТЬ
РН.Период КАК Период,
СУММА(РН.ОН) КАК ОН,
СУММА(РН.ОК) КАК ОК
ИЗ втОстатки КАК РН
ГДЕ РН.Организация = &орг
СГРУППИРОВАТЬ ПО РН.Период
При выполнении подобного запроса разработчик обнаруживает, что конечный остаток за Январь равен 100 000 рублей, а начальный остаток за Февраль внезапно становится равен 80 000 рублей. Куда исчезли 20 000 рублей? Ответ кроется в специфике СУБД и оптимизации запросов платформой 1С.
---
Когда мы указываем периодичность МЕСЯЦ в параметрах виртуальной таблицы, мы ожидаем, что СУБД вернет нам непрерывную цепочку данных по каждому ключевому значению. Однако СУБД работает иначе:
АналитикаУчетаПоПартнерам в условном «Месяце 2» не было никаких движений (оборотов), то СУБД не формирует строку для этого ключа за этот месяц.СУММА(РН.ОН) и СУММА(РН.ОК) в разрезе периодов, данные за «Месяц 2» по этому контрагенту просто не попадают в сумму. Соответственно, общий начальный остаток «Месяца 2» уменьшается на величину «зависшего» остатка этого контрагента.По умолчанию виртуальная таблица использует метод дополнения периодов ДвиженияИГраницыПериода. Платформа принудительно рассчитывает и выводит остатки на крайние границы запроса (начало &ДН и конец &ДК) для всех ключей аналитики, по которым есть остатки. Именно поэтому за первый месяц начальный остаток сходится. Но на промежуточные периоды (второй месяц и далее) это правило автоматического дополнения границ по умолчанию не распространяется.
---
Проанализируем строку отбора во втором запросе: ГДЕ РН.Организация = &орг. Поле Организация получено как РН.АналитикаУчетаПоПартнерам.Организация. Здесь допущены сразу две серьезные ошибки проектирования:
втОстатки, и только потом отсечь лишнее. Это приводит к колоссальному падению производительности.LEFT JOIN) таблицы регистра с таблицей справочника партнеров, чтобы получить организацию. Если в группировку итогового запроса не входит само физическое измерение АналитикаУчетаПоПартнерам, СУБД не сможет корректно сопоставить и спроецировать рассчитанные остатки на результат выборки. Происходит искажение математического ожидания итогов.---
Существует три основных архитектурных подхода к решению этой проблемы в зависимости от того, где и как выводятся данные.
Если вы разрабатываете отчет, наиболее простым и правильным решением будет переложить всю работу по расчету и протягиванию остатков на механизм СКД. Платформа 1С содержит встроенный мощный процессор компоновки, который умеет дополнять периоды автоматически.
Для этого выполните следующие шаги:
ВЫБРАТЬ
РН.Период КАК Период,
РН.АналитикаУчетаПоПартнерам КАК АналитикаУчетаПоПартнерам,
РН.СуммаНачальныйОстаток КАК СуммаНачальныйОстаток,
РН.СуммаКонечныйОстаток КАК СуммаКонечныйОстаток
ИЗ
РегистрНакопления.РасчетыСКлиентами.ОстаткиИОбороты(&ДН, &ДК, МЕСЯЦ, , АналитикаУчетаПоПартнерам.Организация = &Организация) КАК РН
СуммаНачальныйОстаток и СуммаКонечныйОстаток обязательно настройте Роли. Укажите, что это поля остатка, задайте имя группы и свяжите их между собой (для начального остатка укажите связь с конечным, и наоборот).Период укажите роль периода с соответствующим типом и точностью.---
Если результат необходимо получить строго в коде (например, для программной обработки или выгрузки в Таблицу Значений), СКД использовать не получится. В этом случае нам нужно самостоятельно «выровнять» периоды в запросе при помощи генерации координатной сетки (календаря периодов).
Рассмотрим детальный алгоритм реализации этого метода:
// Шаг 1: Получаем все уникальные ключи аналитики, по которым были движения или остатки
ВЫБРАТЬ РАЗЛИЧНЫЕ
РН.АналитикаУчетаПоПартнерам КАК АналитикаУчетаПоПартнерам
ПОМЕСТИТЬ втАналитики
ИЗ
РегистрНакопления.РасчетыСКлиентами.ОстаткиИОбороты(&ДН, &ДК, МЕСЯЦ, , АналитикаУчетаПоПартнерам.Организация = &Организация) КАК РН
;
// Шаг 2: Создаем сетку периодов (календарь месяцев).
// В реальной задаче периоды можно получить из производственного календаря или сгенерировать программно
ВЫБРАТЬ РАЗЛИЧНЫЕ
РН.Период КАК Период
ПОМЕСТИТЬ втПериоды
ИЗ
РегистрНакопления.РасчетыСКлиентами.ОстаткиИОбороты(&ДН, &ДК, МЕСЯЦ, , АналитикаУчетаПоПартнерам.Организация = &Организация) КАК РН
;
// Шаг 3: Перемножаем аналитики и периоды (Декартово произведение), чтобы получить полную сетку
ВЫБРАТЬ
втПериоды.Период КАК Период,
втАналитики.АналитикаУчетаПоПартнерам КАК АналитикаУчетаПоПартнерам
ПОМЕСТИТЬ втСетка
ИЗ
втПериоды КАК втПериоды
СОЕДИНИТЬ втАналитики КАК втАналитики ПО ИСТИНА
;
// Шаг 4: Соединяем сетку с реальными данными остатков и оборотов
ВЫБРАТЬ
втСетка.Период КАК Период,
втСетка.АналитикаУчетаПоПартнерам КАК АналитикаУчетаПоПартнерам,
ЕСТЬNULL(ДанныеРН.СуммаНачальныйОстаток, 0) КАК СуммаНачальныйОстаток,
ЕСТЬNULL(ДанныеРН.СуммаКонечныйОстаток, 0) КАК СуммаКонечныйОстаток
ИЗ
втСетка КАК втСетка
ЛЕВОЕ СОЕДИНЕНИЕ РегистрНакопления.FastОстатки КАК ДанныеРН
ПО втСетка.Период = ДанныеРН.Период
И втСетка.АналитикаУчетаПоПартнерам = ДанныеРН.АналитикаУчетаПоПартнерам
Этот метод гарантирует, что для каждого периода (даже «пустого», где не было движений) в результирующей таблице будет присутствовать строка с ключом аналитики. Однако, чтобы остатки правильно перетекали из месяца в месяц в пустых строках, потребуется дополнительно обработать полученную таблицу запросом либо программно на встроенном языке.
---
Если таблица данных очень большая, сложные соединения в запросах могут перегрузить СУБД. Существует элегантное и быстрое решение: мы получаем из базы данных только начальный остаток на самую первую дату и отдельно чистые обороты по всем месяцам. Затем в цикле обхода выборки мы самостоятельно рассчитываем конечные остатки.
Проанализируем этот алгоритм на примере кода:
// Получаем начальный остаток на &ДН и обороты в разрезе месяцев
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| втОбороты.Период КАК Период,
| втОбороты.АналитикаУчетаПоПартнерам КАК АналитикаУчетаПоПартнерам,
| втОбороты.СуммаОборот КАК Оборот
|ИЗ
| РегистрНакопления.РасчетыСКлиентами.Обороты(&ДН, &ДК, МЕСЯЦ, АналитикаУчетаПоПартнерам.Организация = &Организация) КАК втОбороты
|УПОРЯДОЧИТЬ ПО
| АналитикаУчетаПоПартнерам,
| Период";
Запрос.УстановитьПараметр("ДН", НачалоДня(ДатаНачала));
Запрос.УстановитьПараметр("ДК", КонецДня(ДатаКонца));
Запрос.УстановитьПараметр("Организация", ВыбраннаяОрганизация);
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
// Для хранения текущего остатка по каждой аналитике используем Соответствие
ТекущиеОстатки = Новый Соответствие;
// Заполняем первоначальные остатки на начало периода запросом к таблице Остатки...
// (код получения первоначального остатка опущен для лаконичности)
Пока Выборка.Следующий() Цикл
Аналитика = Выборка.АналитикаУчетаПоПартнерам;
Период = Выборка.Период;
ОборотЗаМесяц = Выборка.Оборот;
// Получаем накопленный остаток
НачОстаток = ТекущиеОстатки.Получить(Аналитика);
Если НачОстаток = Неопределено Тогда
НачОстаток = 0; // Или значение из таблицы первоначальных остатков
КонецЕсли;
КонОстаток = НачОстаток + ОборотЗаМесяц;
// Записываем новые данные обратно в соответствие для следующего месяца
ТекущиеОстатки.Вставить(Аналитика, КонОстаток);
// Выводим данные в результирующую таблицу
Сообщить(СтрШаблон("Период: %1 | Аналитика: %2 | Нач: %3 | Изм: %4 | Кон: %5",
Формат(Период, "ДФ=MM.yyyy"), Аналитика, НачОстаток, ОборотЗаМесяц, КонОстаток));
КонецЦикла;
Данный подход полностью исключает влияние платформенных особенностей виртуальных таблиц остатков и оборотов на конечный результат. Мы оперируем физическими таблицами оборотов (которые работают в СУБД значительно быстрее виртуальных таблиц с периодичностью) и самостоятельно контролируем непрерывность математических расчетов.