При проектировании метаданных в платформе «1С:Предприятие 8» разработчики часто сталкиваются с необходимостью хранения произвольной текстовой или бинарной информации, которая напрямую не участвует в запросах и поиске, но должна быть привязана к конкретному объекту (например, документу или элементу справочника). Традиционным инструментом для этой задачи выступает реквизит с типом ХранилищеЗначения. Однако при работе с ним возникает множество вопросов: нужно ли использовать сжатие, какую степень сжатия выбрать, и не навредит ли это производительности базы данных?
В этой статье мы подробно разберем, как устроен механизм СжатиеДанных, проанализируем физику хранения двоичных данных в СУБД, выясним, почему сжимать маленькие тексты не имеет смысла, и рассмотрим более изящные архитектурные альтернативы, такие как сериализация в JSON и вынос данных в связанные регистры.
Объект ХранилищеЗначения предназначен для сохранения любых данных, поддерживающих сериализацию (включая картинки, файлы, структуры, таблицы значений и обычный текст). Конструктор этого объекта выглядит следующим образом:
Новый ХранилищеЗначения(Значение, СжатиеДанных)
Параметр СжатиеДанных является необязательным. Если его не передавать, то по умолчанию данные будут помещены в хранилище без какого-либо сжатия (уровень сжатия 0). Если же мы хотим оптимизировать размер, мы можем передать туда объект Новый СжатиеДанных(Х), где параметр Х определяет степень сжатия.
Рассмотрим подробнее шкалу степеней сжатия:
Новый СжатиеДанных() без параметров, платформа использует значение по умолчанию. Внутренний алгоритм 1С базируется на открытой библиотеке zlib (алгоритм Deflate). Значение -1 соответствует уровню сжатия 6, что является признанным индустриальным стандартом баланса между нагрузкой на центральный процессор (CPU) сервера 1С и коэффициентом уменьшения размера данных.Проанализируем пример стандартного использования сжатия при записи текста:
// Пример создания хранилища со сжатием по умолчанию
СтепеньСжатия = Новый СжатиеДанных(-1);
ЭлементХранилища = Новый ХранилищеЗначения(МойТекст, СтепеньСжатия);
Многие разработчики совершают ошибку, пытаясь сжимать небольшие текстовые фрагменты (от нескольких десятков символов до 2-3 килобайт). Выясним причину, почему в таких сценариях архивация дает противоположный результат — итоговый объем сохраненных данных в СУБД становится больше, чем исходный незажатый текст.
Любой алгоритм сжатия семейства Deflate/ZIP для своей работы требует добавления служебной информации в заголовок архива. В этот заголовок помещаются таблицы частот символов (алфавит), маркеры начала и конца блоков, контрольные суммы (CRC32). Кроме того, сама платформа 1С оборачивает упакованный объект в собственный бинарный формат сериализации, добавляя служебные метаданные типа сохраненного значения.
Для наглядности проанализируем ситуацию:
Правило: Использовать сжатие данных (особенно с высокими коэффициентами 8 и 9) эффективно только тогда, когда размер исходного одиночного текстового блока или файла гарантированно превышает 15–20 Килобайт. Для меньших объемов накладные расходы на структуру архива нивелируют всю пользу.
Важно помнить о контекстных ограничениях платформы 1С. Объект ХранилищеЗначения спроектирован как серверный тип данных. Попытка вызвать конструктор Новый ХранилищеЗначения() непосредственно в модуле управляемой формы на клиенте (тонком или веб-клиенте) приведет к критической ошибке компиляции «Тип не определен».
Запись и чтение двоичных данных должны выполняться строго в серверном контексте. На клиенте мы можем оперировать только простыми типами (строками, числами), либо передавать двоичные данные через механизм временного хранилища (используя функции ПоместитьВоВременноеХранилище и ПолучитьИзВременногоХранилища).
При записи реквизита с типом ХранилищеЗначения в базу данных, СУБД выделяет под него специальное поле неограниченной длины. В MS SQL Server это обычно тип VARBINARY(MAX), а в PostgreSQL — BYTEA.
С точки зрения СУБД, эти данные являются так называемыми LOB (Large Object — большими объектами). Физически они хранятся вне основной страницы данных таблицы (механизм Out-of-row). В основной строке таблицы хранится лишь 24-байтовая ссылка-указатель на цепочку страниц, где фактически лежат бинарные данные.
Это приводит к следующим последствиям для производительности:
ХранилищеЗначения, СУБД все равно тратит дополнительные ресурсы на обслуживание указателей страниц. Если же вы случайно выберете это поле в тяжелом запросе (например, при соединении таблиц), СУБД будет вынуждена совершать хаотичные дисковые операции чтения (Random Read) для сборки двоичных данных из отдельных страниц LOB-хранилища.СжатиеДанных(9), СУБД воспримет эти данные как «белый шум» (высокая энтропия заархивированного потока байт) и не сможет сжать их на уровне диска. При этом нагрузка на CPU сервера 1С вырастет, а выигрыша в дисковом пространстве не произойдет.Рассмотрим задачу автора темы: необходимо хранить 8 текстовых полей (длиной до 200 символов каждое), которые могут заполняться хаотично, не участвуют в поиске и нужны только при открытии документа или выводе на печать.
Вместо использования сложного бинарного объекта ХранилищеЗначения, гораздо более красивым, легким и производительным решением будет использование реквизита с типом Строка (неограниченной длины), внутри которой данные будут храниться в структурированном текстовом формате JSON.
Преимущества данного подхода:
Разберем по шагам, как реализовать этот механизм в коде. Для свертки полей в одну строку мы будем использовать объект Структура и системные средства работы с JSON.
Код для свертки данных и записи в реквизит документа:
// Создаем структуру с нашими хаотичными полями
ДанныеДляСохранения = Новый Структура;
ДанныеДляСохранения.Вставить("Поле1", "Значение текстового поля 1");
ДанныеДляСохранения.Вставить("Поле2", "Второе значение");
ДанныеДляСохранения.Вставить("Поле8", "Восьмое значение");
// Настраиваем параметры записи JSON
ПараметрыЗаписиJSON = Новый ПараметрыЗаписиJSON(ПереносСтрокJSON.Нет);
ЗаписьJSON = Новый ЗаписьJSON;
ЗаписьJSON.УстановитьСтроку(ПараметрыЗаписиJSON);
// Сериализуем структуру в строку JSON
ЗаписатьJSON(ЗаписьJSON, ДанныеДляСохранения);
СтрокаДляХранения = ЗаписьJSON.Закрыть();
// Записываем полученную строку в реквизит документа (тип: Строка неограниченной длины)
ОбъектДокумента.ДополнительныеТекстовыеДанные = СтрокаДляХранения;
Код для извлечения данных при открытии формы:
// Получаем строку из реквизита
СтрокаJSON = ОбъектДокумента.ДополнительныеТекстовыеДанные;
Если Не ПустаяСтрока(СтрокаJSON) Тогда
// Инициализируем чтение JSON
ЧтениеJSON = Новый ЧтениеJSON;
ЧтениеJSON.УстановитьСтроку(СтрокаJSON);
// Восстанавливаем структуру данных
ВосстановленныеДанные = ПрочитатьJSON(ЧтениеJSON);
ЧтениеJSON.Закрыть();
// Заполняем реквизиты формы для отображения пользователю
ЗначениеПоля1 = ?(ВосстановленныеДанные.Свойство("Поле1"), ВосстановленныеДанные.Поле1, "");
ЗначениеПоля2 = ?(ВосстановленныеДанные.Свойство("Поле2"), ВосстановленныеДанные.Поле2, "");
КонецЕсли;
Использование JSON позволяет легко масштабировать систему. Если завтра вам потребуется добавить 9-е или 10-е поле, вам не придется менять структуру таблиц базы данных и запускать длительную реструктуризацию тяжелых таблиц документов. Вы просто добавите новые ключи в структуру JSON. При этом старые документы без проблем откроются — отсутствующие свойства структуры просто вернут значение по умолчанию.
Если документов создается действительно много (сотни ежедневно), хранение даже JSON-строк в самой таблице документов может привести к постепенному замедлению работы с этим списком. Всякий раз при чтении списка документов СУБД будет вынуждена работать с перегруженной таблицей.
В этом случае отличной практикой проектирования высоконагруженных систем является вынос справочной «тяжелой» информации в подчиненный **Регистр Сведений** (например, с именем ДополнительныеДанныеДокументов).
Проанализируем структуру такого регистра:
ДокументСсылка (тип: документ-владелец данных). Обязательно устанавливаем свойство «Ведущее», чтобы при удалении документа связанные данные в регистре зачищались автоматически силами платформы.ДанныеJSON (тип: Строка неограниченной длины). Сюда мы пишем сериализованную строку.При таком подходе таблица самого документа остается легкой и компактной. Все запросы по проведению, анализу остатков и построению списков работают максимально быстро. А тяжелые текстовые данные запрашиваются из регистра сведений только в один конкретный момент времени — когда пользователь физически открывает форму конкретного документа на своем экране.
Подводя итог нашему анализу, сформулируем ключевые правила для эффективной работы с текстами в базе данных 1С:
ХранилищеЗначения в пользу строки неограниченной длины с сериализацией в JSON. Это упростит код, уберет бинарные заголовки и позволит работать с данными на стороне клиента без вызова контекста сервера.