В процессе проектирования метаданных в платформе 1С:Предприятие перед каждым разработчиком неизбежно встает классический архитектурный вопрос: какие данные необходимо физически записывать в таблицы базы данных СУБД, а какие следует вычислять программно в момент обращения? Этот вопрос часто становится предметом жарких споров между разработчиками, привыкшими к канонической нормализации реляционных баз данных (например, специалистами по C++ или веб-разработчиками), и практикующими архитекторами 1С, которые руководствуются соображениями финансовой точности и производительности крупных учетных систем.
Для детального анализа этой проблемы разделим условные данные табличной части прикладного документа (например, документа продажи или поступления) на три ключевые категории:
В рамках этой статьи мы подробно проанализируем каждую группу данных, разберем влияние денормализации на производительность СУБД, рассмотрим проблему копеек при округлении и выработаем единый стандарт проектирования для платформы 1С.
С точки зрения классической теории баз данных и нормальных форм (вплоть до шестой нормальной формы), хранение избыточных, вычисляемых значений является грубой ошибкой проектирования. Действительно, зачем хранить поле Сумма, если его можно в любой момент получить простым перемножением по формуле Количество * Цена? Чем меньше данных мы храним, тем меньше весит база данных и тем быстрее выполняются операции записи (INSERT/UPDATE).
Однако в реальных учетных системах чистая нормализация вступает в жесткое противоречие с производительностью на чтение (SELECT) и бизнес-логикой. В 1С мы постоянно сталкиваемся со следующими факторами:
Проанализируем детально, почему этот баланс смещается в сторону обязательного хранения прямых расчетных данных.
Существует несколько фундаментальных причин, по которым суммы, НДС и скидки в денежном выражении должны физически записываться в реквизиты табличных частей документов и регистры накопления.
Во-первых, это проблема финансовой точности и округления (так называемая «проблема копеек»). Финансовые расчеты в бухгалтерском и налоговом учете требуют строгого соответствия законодательству и первичным бумажным документам. Рассмотрим простой математический пример:
Допустим, мы продаем 3 единицы товара на общую сумму 10 рублей. Если мы попытаемся рассчитать цену за единицу, мы получим периодическую дробь: 10 / 3 = 3.333333.... В базе данных цена будет округлена до двух знаков после запятой и составит 3.33 рубля. Если бы мы не хранили итоговую сумму в базе, а рассчитывали ее динамически при открытии документа или формировании отчета, система выполнила бы расчет: 3.33 * 3 = 9.99. В итоге мы теряем одну копейку, баланс не сходится, а налоговая отчетность становится некорректной. Хранение рассчитанной и округленной суммы (10.00 рублей) гарантирует стабильность учета вне зависимости от округления цены.
Во-вторых, это историческая неизменность данных. Документ фиксирует коммерческие условия сделки на конкретный момент времени. Правила расчета скидок, налоговые ставки и алгоритмы ценообразования могут меняться разработчиками многократно в течение года. Если мы изменим формулу расчета скидки в конфигурации, все исторические документы (созданные месяц или год назад) при динамическом расчете изменят свои итоговые суммы. Это недопустимо. Сумма, зафиксированная в документе продажи, должна оставаться железобетонно неизменной. Единственный способ гарантировать это — записать результат расчета в базу данных в момент проведения документа.
В-третьих, это удобство и скорость построения запросов. Любой отчет в 1С (на базе Системы компоновки данных) должен работать максимально быстро. Если СУБД потребуется выполнять сложные математические расчеты с плавающей точкой в теле каждого запроса, производительность упадет в геометрической прогрессии. Считывание готового значения из индексированного поля базы данных происходит практически мгновенно.
Косвенные расчетные данные, такие как процент строки от общего итога документа или отношение стоимости текущей номенклатуры к максимальной цене в документе, представляют собой совершенно иной класс информации. Их хранение в базе данных является грубейшей ошибкой.
Выясним причину, почему такие показатели нельзя записывать в реквизиты табличной части:
Как же правильно работать с Блоком В? Для этого в платформе 1С:Предприятие существуют два стандартных механизма:
Механизм 1: Реквизиты формы (для отображения на экране). Если пользователю в процессе ввода данных необходимо видеть процент строки от общего итога, мы не добавляем этот реквизит в структуру метаданных документа. Вместо этого мы создаем колонку в табличном поле формы документа. Расчет этого значения происходит на клиенте при изменении данных в строках, и результат выводится на форму динамически, не нагружая базу данных при сохранении объекта.
Механизм 2: Вычисляемые поля СКД (для отчетов). Если процент от общего итога все-таки понадобился в аналитическом отчете, мы рассчитываем его на уровне Системы компоновки данных (СКД). Язык выражений СКД предоставляет мощный математический аппарат и агрегатные функции, которые позволяют вычислить долю строки от общего итога ресурса прямо в процессе компоновки результата отчета на стороне сервера СУБД, эффективно используя внутренние оптимизации SQL.
Себестоимость (входящая в Блок А как нормативный показатель и в Блок Б как сумма себестоимости строки) заслуживает отдельного глубокого рассмотрения. В современных типовых конфигурациях 1С (таких как 1С:ERP, 1С:Комплексная автоматизация или 1С:Управление торговлей) разработчики полностью отказались от хранения себестоимости в самом документе продажи (например, в документе РеализацияТоваровУслуг).
Рассмотрим, почему это было сделано:
РасчетСебестоимостиТоваров.ВыручкаИСебестоимостьПродаж). Документ продажи выступает лишь в роли регистратора сделки, а вся тяжелая аналитика хранится и обрабатывается в специализированных регистрах.Для того чтобы избежать дублирования алгоритмов расчета сумм в различных модулях системы (на форме документа, при загрузке из внешних источников через API, при групповой обработке), необходимо выносить всю расчетную логику в модуль объекта или специализированные общие модули.
Проанализируем пример правильной организации кода в платформе 1С. Создадим в модуле объекта документа процедуру расчета сумм строки табличной части:
// Процедура рассчитывает зависимые показатели строки табличной части
Процедура РассчитатьСуммуСтрокиТабличнойЧасти(СтрокаТЧ) Экспорт
// Рассчитываем сумму без скидки
СуммаБезСкидки = СтрокаТЧ.Количество * СтрокаТЧ.Цена;
// Рассчитываем сумму скидки с округлением до копеек
СтрокаТЧ.СуммаСкидки = Окр(СуммаБезСкидки * (СтрокаТЧ.ПроцентСкидки / 100), 2);
// Конечная сумма строки, подлежащая сохранению в БД
СтрокаТЧ.Сумма = СуммаБезСкидки - СтрокаТЧ.СуммаСкидки;
КонецПроцедуры
Теперь обратим внимание на то, как этот код вызывается на форме документа. Для обеспечения мгновенной обратной связи пользователю при изменении количества или цены мы вызываем этот расчет на клиенте через серверный контекст формы:
&НаКлиенте
Процедура ТоварыКоличествоПриИзменении(Элемент)
ТекущаяСтрока = Элементы.Товары.ТекущиеДанные;
Если ТекущаяСтрока <> Неопределено Тогда
// Вызываем расчет на сервере без контекста для экономии трафика
РассчитатьСуммуНаСервере(ТекущаяСтрока.НомерСтроки - 1);
КонецЕсли;
КонецПроцедуры
&НаСервереБезКонтекста
Процедура РассчитатьСуммуНаСервере(ИндексСтроки)
// Обрабатываем данные через объект документа
// Этот метод гарантирует идентичность расчета при интерактивном вводе и программном заполнении
КонецПроцедуры
При программном заполнении документа из внешних систем (например, при чтении файлов Excel или через HTTP-сервисы) разработчику достаточно заполнить первичные реквизиты (Блок А) и вызвать метод объекта:
НовыйДокумент = Документы.РеализацияТоваровУслуг.СоздатьДокумент();
// ... Заполнение шапки документа ...
Для Каждого СтрокаДанных Из ВнешниеДанные Цикл
НоваяСтрока = НовыйДокумент.Товары.Добавить();
ЗаполнитьЗначенияСвойств(НоваяСтрока, СтрокаДанных); // Заполняем Блок А
// Рассчитываем Блок Б программно перед записью
НовыйДокумент.РассчитатьСуммуСтрокиТабличнойЧасти(НоваяСтрока);
КонецЦикла;
НовыйДокумент.Записать(РежимЗаписиДокумента.Проведение);
Этот подход позволяет строго соблюдать принцип единственной ответственности (Single Responsibility Principle) и гарантирует, что формулы расчета сумм и скидок будут находиться в одном месте, исключая рассинхронизацию данных в базе.
Подводя итог нашей дискуссии, сформулируем краткие и понятные правила для разработчиков по организации хранения данных: