При разработке и сопровождении конфигураций на платформе «1С:Предприятие 8» разработчики часто сталкиваются с крайне неприятной проблемой: в отчетах начинают отображаться некорректные остатки. Вроде бы товар или ресурс был полностью списан, но при детальной настройке отчета с выводом всех группировок обнаруживаются странные «зависшие» или «встречные» остатки — положительные по одной аналитике и отрицательные по другой. В нашей статье мы подробно разберем, почему возникает такая ситуация, как она влияет на базу данных и какими способами можно навсегда решить эту проблему.
Рассмотрим классический пример, который часто встречается на практике и обсуждается на форумах разработчиков. Представим, что у нас есть регистр накопления остатков со следующей структурой:
Склад, Контрагент, СотрудникКоличествоПри поступлении ТМЦ (документ прихода) проведение выполняется корректно по всем трем измерениям. Например, на склад «Основной» поступает товар от контрагента «Поставщик», а ответственным лицом (измерение Сотрудник) записывается «Иванов». Ресурс Количество увеличивается на 10 единиц.
При списании ТМЦ (документ расхода) разработчик по какой-то причине решает заполнить только измерения Склад («Основной») и Контрагент («Поставщик»), оставляя измерение Сотрудник пустым. Программа без проблем проводит документ, уменьшая количество на 10 единиц. Но что происходит, когда мы пытаемся получить остатки в отчете с выводом всех трех измерений? Мы видим давно списанные данные: по сотруднику «Иванов» висит остаток +10, а по пустому сотруднику — остаток -10.
Чтобы понять, почему остатки зависают, заглянем на физический уровень работы СУБД (например, MS SQL Server или PostgreSQL). Для регистра накопления остатков платформа 1С создает в базе данных две ключевые таблицы:
Для оптимизации расчета итогов СУБД группирует записи строго по уникальным наборам значений измерений. В MS SQL Server для этого часто рассчитывается специальный хэш комбинации измерений — DimHash. Если при приходе были заполнены измерения Склад, Контрагент и Сотрудник, СУБД создает строку итогов с уникальным идентификатором этой тройки. Когда же при расходе поле Сотрудник остается пустым, СУБД воспринимает эту запись как совершенно другую комбинацию измерений.
В итоге в таблице итогов физически будут находиться две строки:
Автоматически между собой они никогда не свернутся, потому что с точки зрения реляционной базы данных это абсолютно разные аналитические разрезы.
Наличие таких незакрытых встречных записей («плюс» по одному набору измерений и «минус» по другому) — это не просто визуальный дефект в отчете. Это серьезная техническая проблема, которая влечет за собой целый ряд негативных последствий:
.Остатки() платформа выполняет агрегацию (суммирование) данных из таблицы итогов. Чем больше в ней встречных записей, тем дольше СУБД будет рассчитывать актуальные остатки.Иногда разработчики намеренно оставляют измерения пустыми, предполагая, что система поймет это одинаково. Однако здесь есть неочевидные подводные камни:
СправочникСсылка.Сотрудники.ПустаяСсылка()), значение Неопределено и значение NULL (которое может возникнуть при левых соединениях в запросах) — это абсолютно разные значения! Если один документ записал в измерение Неопределено, а другой — пустую ссылку, остаток не закроется.Чтобы исправить данную проблему раз и навсегда, необходимо определиться с бизнес-логикой вашей задачи. Нужны ли вам остатки в разрезе сотрудников?
Если вам не требуется контролировать остатки по сотрудникам, а поле нужно исключительно для справочной информации (например, чтобы знать, какой сотрудник провел операцию), то хранить его в качестве измерения регистра остатков — это грубая архитектурная ошибка. По стандартам разработки фирмы «1С» измерения регистра остатков должны содержать только те разрезы, по которым действительно ведется учет остатков и которые гарантированно закрываются в ноль.
В такой ситуации правильным решением будет:
Сотрудник будет полноценным измерением. Регистры оборотов не хранят остатки и не раздувают таблицы итогов встречными записями.Если бизнес-логика требует жесткого контроля за тем, за каким именно сотрудником числятся ТМЦ, вы обязаны обеспечить симметричное закрытие регистра. Это означает, что при расходе вы должны указывать того же самого сотрудника, который был указан при приходе.
Для этого необходимо доработать модуль проведения расходного документа. Алгоритм проведения должен предварительно получать остатки из регистра по всем измерениям (включая сотрудника), а затем списывать их по определенной логике (например, по методу ФИФО, ЛИФО или по прямому выбору сотрудника в табличной части документа).
Просто исправить алгоритм проведения документов для будущих операций недостаточно — нужно скорректировать историю в базе данных. Сделать это можно тремя основными способами:
Если документов относительно немного, период открыт для редактирования и вы уже исправили модуль проведения расходных документов, то лучшим решением будет выполнить групповое перепроведение документов за весь период. Платформа 1С автоматически пересчитает все движения, и остатки схлопнутся.
Если период уже закрыт для редактирования (например, сдан бухгалтерский баланс) или документов слишком много и перепроведение займет слишком много времени, можно воспользоваться программным исправлением записей регистра через объект РегистрНакопленияНаборЗаписей.
Для этого пишется внешняя обработка, которая находит документы с незаполненными измерениями, программно открывает их набор записей регистра, заполняет недостающие измерения (или очищает их везде для симметрии) и перезаписывает набор без перепроведения самих документов. Рассмотрим пример кода:
// Пример программного исправления некорректных записей регистра
Процедура ИсправитьПустыеИзмерения() Экспорт
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ РАЗЛИЧНЫЕ
| ТоварыНаСкладах.Регистратор КАК Регистратор
|ИЗ
| РегистрНакопления.ТоварыНаСкладах КАК ТоварыНаСкладах
|ГДЕ
| ТоварыНаСкладах.ВидДвижения = ЗНАЧЕНИЕ(ВидДвиженияНакопления.Расход)
| И ТоварыНаСкладах.Сотрудник = ЗНАЧЕНИЕ(Справочник.Сотрудники.ПустаяСсылка)";
РезультатЗапроса = Запрос.Выполнить();
Выборка = РезультатЗапроса.Выбрать();
Пока Выборка.Следующий() Цикл
// Создаем набор записей для конкретного документа-регистратора
НаборЗаписей = РегистрыНакопления.ТоварыНаСкладах.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.Регистратор.Установить(Выборка.Регистратор);
НаборЗаписей.Прочитать();
Модифицирован = Ложь;
Для Каждого Запись Из НаборЗаписей Цикл
Если Запись.ВидДвижения = ВидДвиженияНакопления.Расход И Не ЗначениеЗаполнено(Запись.Сотрудник) Тогда
// Определяем сотрудника по логике прихода (например, первый доступный остаток)
Запись.Сотрудник = НайтиСотрудникаДляЗаполнения(Запись.Склад, Запись.Контрагент);
Модифицирован = Истина;
КонецЕсли;
КонецЦикла;
Если Модифицирован Тогда
// Записываем набор записей с флагом замещения
НаборЗаписей.Записать(Истина);
КонецЕсли;
КонецЦикла;
КонецПроцедуры
Это крайний метод. Ввод ручных корректировок позволяет сопоставить и «схлопнуть» остатки в ноль (сделать расход по сотруднику и приход без сотрудника на ту же сумму). Однако это лишь маскирует проблему в кодовой базе и часто приводит к путанице у бухгалтеров и аналитиков. Используйте этот способ только тогда, когда программное исправление движений невозможно.
После того как записи в регистре были скорректированы (через перепроведение или обработку наборов записей), таблицы итогов в СУБД могут по-прежнему содержать старые «фантомные» записи или некорректно рассчитанные итоги. Чтобы физически очистить базу данных и восстановить её производительность, необходимо выполнить следующие регламентные операции:
Только после выполнения пересчета итогов вы заметите реальный прирост скорости работы отчетов и проведение документов без задержек.