В процессе доработки конфигураций 1С разработчики нередко сталкиваются с ситуацией, когда бизнес требует детализировать остатки в отчетах по произвольному текстовому описанию — например, по полю Содержание из табличной части документа. Простейшее на первый взгляд решение — добавить в регистр накопления измерение типа Строка(400) — со временем приводит к резкому падению производительности и техническим сбоям. Разберем, почему длинные строки в измерениях опасны для СУБД, и рассмотрим архитектурно правильные способы решения этой задачи.
Регистры накопления в платформе 1С построены на механизме агрегации данных. Для каждого регистра остатков платформа формирует в СУБД две основные таблицы: таблицу движений и таблицу итогов. Проанализируем ограничения, с которыми мы сталкиваемся при добавлении длинных строковых полей в измерения:
Строка(400) в кодировке Unicode (тип nvarchar в СУБД) занимает 800 байт. Добавление к нему измерений номенклатуры, договора или организации гарантированно превышает 900 байт, что приводит к аппаратным ошибкам СУБД при проведении документов и падению реструктуризации базы данных.GROUP BY, JOIN и поиск по строкам переменной длины значительно медленнее, чем по фиксированным ссылочным типам (16 байт) или целочисленным идентификаторам.Рассмотрим наиболее надежный и принятый в типовых конфигурациях 1С подход. Если строка содержания формируется индивидуально в каждом документе реализации и требуется при списании остатков в отчете комитенту, нам не нужно сохранять сам текст в регистре накопления.
Вместо строкового поля добавим в регистр накопления два компактных измерения:
ДокументРеализации с типом ДокументСсылка.РеализацияТоваровУслуг (фиксированный размер UUID — 16 байт).НомерСтрокиДокумента с типом Число(5, 0) (занимает от 3 до 4 байт).При таком подходе регистр накопления хранит остатки в разрезе конкретных строк документов поступления/реализации. Когда формируется Отчет комитенту о продажах, остатки берутся по этим двум измерениям, а само исходное текстовое поле Содержание извлекается соединением с табличной частью документа:
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Остатки.ДокументРеализации КАК ДокументРеализации,
| Остатки.НомерСтрокиДокумента КАК НомерСтрокиДокумента,
| Остатки.КоличествоОстаток КАК Количество,
| ТЧ.Содержание КАК Содержание
|ИЗ
| РегистрНакопления.РеализованныеТоварыКомитентов.Остатки(&Период, ) КАК Остатки
| ЛЕВОЕ СОЕДИНЕНИЕ Документ.РеализацияТоваровУслуг.Услуги КАК ТЧ
| ПО Остатки.ДокументРеализации = ТЧ.Ссылка
| И Остатки.НомерСтрокиДокумента = ТЧ.НомерСтроки";
Этот метод полностью устраняет нагрузку на таблицу итогов, исключает дублирование строковых данных и не требует создания дополнительных справочников.
Если специфика учета требует схлопывания одинаковых услуг между разными документами, партионная привязка к строкам может не подойти. В этом случае правильным решением будет создание вспомогательного справочника (например, СодержанияУслуг), ссылка на который выступает измерением регистра накопления.
Однако поиск существующей длинной строки стандартным методом НайтиПоРеквизиту вызовет полное сканирование таблицы (Table Scan) в СУБД. Разберем, как оптимизировать эту операцию с помощью встроенного объекта ХешированиеДанных:
Хеш с типом Строка(32) или Строка(40) и включим свойство Индексировать.ПолноеСодержание сделаем строкой неограниченной длины.Посмотрим на пример функции получения ссылки на элемент справочника:
Функция ПолучитьСсылкуНаСодержание(ТекстСодержания) Экспорт
Хеширование = Новый ХешированиеДанных(ХешФункция.MD5);
Хеширование.Добавить(СокрЛП(ТекстСодержания));
СтрокаХеша = Хеширование.ХешСумма; // Возвращает hex-строку фиксированной длины
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ ПЕРВЫЕ 1
| Спр.Ссылка КАК Ссылка
|ИЗ
| Справочник.СодержанияУслуг КАК Спр
|ГДЕ
| Спр.Хеш = &Хеш
| И Спр.ПолноеСодержание = &ПолноеСодержание";
Запрос.УстановитьПараметр("Хеш", СтрокаХеша);
Запрос.УстановитьПараметр("ПолноеСодержание", ТекстСодержания);
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Следующий() Тогда
Возврат Выборка.Ссылка;
Иначе
НовыйЭлемент = Справочники.СодержанияУслуг.СоздатьЭлемент();
НовыйЭлемент.Наименование = Лев(ТекстСодержания, 100);
НовыйЭлемент.Хеш = СтрокаХеша;
НовыйЭлемент.ПолноеСодержание = ТекстСодержания;
НовыйЭлемент.Записать();
Возврат НовыйЭлемент.Ссылка;
КонецЕсли;
КонецФункции
Такой алгоритм работает мгновенно даже при миллионах записей: СУБД мгновенно находит запись по индексированному полю хеша, а проверка совпадения полного текста предотвращает редкие коллизии алгоритма хеширования.
Если создание отдельного справочника нежелательно, можно хранить сам хеш текста непосредственно в измерении регистра накопления. Для этого в измерение помещается поле ХешСодержания типа Число или Строка(32).
Параллельно в табличную часть документа РеализацияТоваровУслуг добавляется такое же скрытое поле хеша. При проведении документа остатки фиксируются по рассчитанному хешу, а при заполнении документа Отчет комитенту о продажах текст восстанавливается сопоставлением по паре полей: Регистратор + ХешСодержания.
Прямое удаление старого измерения из регистра через конфигуратор приведет к длительной блокирующей реструктуризации таблиц и потере истории. Чтобы выполнить переход без сбоев, применим следующую последовательность шагов:
Набор = РегистрыНакопления.РеализованныеТоварыКомитентов.СоздатьНаборЗаписей();
Набор.Отбор.Регистратор.Установить(ДокументСсылка);
Набор.Прочитать();
Для Каждого Запись Из Набор Цикл
// Заполняем новые реквизиты на основании данных документа
Запись.ДокументРеализации = ДокументСсылка;
Запись.НомерСтрокиДокумента = НомерСтроки;
КонецЦикла;
Набор.Записать();