Как решить проблему лавинообразного роста индексного файла CDX в 1С 7.7 при работе с длинными реквизитами?

Программист 1С v7.7 Управленческий учет Торговля и дистрибуция
← На главную

При активной работе со справочниками в файловой версии платформы 1С:Предприятие 7.7 разработчики часто сталкиваются с неприятной технической особенностью: стремительным ростом размера индексных файлов (с расширением .cdx). Особенно остро эта проблема проявляется при попытке проиндексировать длинные строковые поля (например, коды маркировки Честный Знак, акцизные марки ЕГАИС длиной до 150 символов). При массовой загрузке данных индексный файл может буквально за несколько минут вырасти до критического объема в 2 Гигабайта, после чего работа системы аварийно завершается с ошибками базы данных.

В этой статье мы подробно разберем, почему происходит такое раздувание индексов, как устроены механизмы хранения данных в файловой 1С 7.7, и рассмотрим эффективные практические способы решения этой проблемы — от изменения архитектуры данных до использования внешних компонент.

Выясняем причину: почему раздувается файл индексов CDX?

Для того чтобы понять природу лавинообразного роста файлов, проанализируем внутреннее устройство файлового движка 1С 7.7, который базируется на СУБД CodeBase. Индексные файлы .cdx используют древовидную структуру данных, известную как B-дерево (B-Tree). Вся информация в этой структуре разбита на страницы фиксированного размера (как правило, 512 или 1024 байта).

Когда мы включаем у строкового реквизита галочку "Сортировка" (что эквивалентно созданию индекса), платформа выделяет под каждый ключ в индексе строго определенное количество байт. Если длина реквизита составляет 150 символов, то размер одного индексного ключа в B-дереве получается огромным. Это приводит к следующим последствиям:

  1. Крайне низкая плотность страниц: На одной физической странице индекса может поместиться всего 3–5 записей ключей. В результате дерево становится очень "ветвистым" и глубоким.
  2. Постоянные расщепления страниц (Page Splits): При добавлении новых записей (особенно если они идут хаотично, а не в алфавитном порядке) страницы индекса переполняются. Движку приходится делить страницу пополам, создавая новые узлы дерева.
  3. Отсутствие автоматического сжатия (дефрагментации): Это ключевой недостаток СУБД CodeBase. При расщеплении страниц или удалении записей старые физические блоки на диске практически не переиспользуются. Движок просто запрашивает у операционной системы выделение новых блоков в конце файла. Файл растет "пустотой" изнутри.

Именно поэтому при массовой вставке 10–15 тысяч записей с длинными строками файл индекса раздувается до 2 Гб. Но стоит нам удалить файлы .cdx и запустить полную переиндексацию, как дерево перестраивается заново последовательно (Sequential Packing). Все ключи укладываются максимально плотно, пустые блоки исчезают, и размер файла падает до реальных 7 Мегабайт.


Способ 1. Хеширование длинного реквизита (Наиболее рекомендуемый метод)

Если нам необходимо выполнять быстрый поиск по длинному строковому реквизиту (например, по коду акцизной марки), оптимальным архитектурным решением будет отказаться от прямой индексации этой строки. Вместо этого мы будем строить индекс по фиксированному и короткому значению — хешу.

Разберем этот метод по шагам:

  1. В конфигураторе находим нужный справочник и у длинного реквизита (например, Марка, длина 150) снимаем галочки "Сортировка" и "Отбор по реквизиту".
  2. Создаем новый реквизит справочника, назовем его ХешМарки. Тип значения — строка, длина — 32 символа (если планируем использовать MD5) или 8 символов (для CRC32).
  3. Для нового реквизита ХешМарки включаем признак "Сортировка".
  4. При записи элемента справочника мы будем программно вычислять хеш-сумму от длинной марки и записывать её в реквизит ХешМарки.

Посмотрим на пример реализации данного подхода на встроенном языке 1С 7.7:


// Пример записи нового элемента справочника с вычислением хеша
Процедура ПриЗаписи()
    // Предполагаем, что функция глВычислитьMD5 реализована в Глобальном модуле
    ХешМарки = глВычислитьMD5(СокрЛП(Марка));
КонецПроцедуры

Теперь для поиска элемента нам достаточно искать по короткому индексированному полю ХешМарки. В редких случаях возникновения коллизий (когда у разных марок совпали хеш-суммы), мы выполним дополнительную проверку оригинального поля:


// Функция быстрого поиска по хеш-сумме
Функция НайтиЭлементПоМарке(ИскомаяМарка) Экспорт
    Перем ВычисленныйХеш, Спр;
    
    ВычисленныйХеш = глВычислитьMD5(СокрЛП(ИскомаяМарка));
    Спр = СоздатьОбъект("Справочник.Марки");
    
    Если Спр.НайтиПоРеквизиту("ХешМарки", ВычисленныйХеш, 1) = 1 Тогда
        // Проверяем на возможное совпадение хешей (коллизию)
        Если Спр.Марка = ИскомаяМарка Тогда
            Возврат Спр.ТекущийЭлемент();
        Иначе
            // Если произошла коллизия, перебираем записи с таким же хешем
            Пока Спр.ПолучитьЭлемент() = 1 Цикл
                Если Спр.ХешМарки <> ВычисленныйХеш Тогда
                    Прервать; // Вышли за пределы индекса с данным хешем
                КонецЕсли;
                Если Спр.Марка = ИскомаяМарка Тогда
                    Возврат Спр.ТекущийЭлемент();
                КонецЕсли;
            КонецЦикла;
        КонецЕсли;
    КонецЕсли;
    
    Возврат ПолучитьПустоеЗначение("Справочник.Марки");
КонецФункции

Размер ключа в индексе уменьшился со 150 до 32 символов. Это мгновенно снизит скорость роста файлов .cdx в несколько раз и полностью устранит проблему переполнения дискового пространства.


Способ 2. Использование внешней компоненты SQLite для поиска

Если мы не хотим менять структуру данных конфигурации или добавлять новые реквизиты, мы можем воспользоваться замечательной альтернативой — внешней компонентой 1SQLite (библиотека 1sqlite.dll). Она позволяет выполнять прямые SQL-запросы к DBF-файлам базы данных "на лету", не требуя наличия штатных индексов 1С.

Рассмотрим пример создания функции поиска по длинному реквизиту без использования механизма НайтиПоРеквизиту:


Функция НайтиМаркуSqlLite(ШК) Экспорт
    Перем Запрос, БазаДанных, ТекстЗапроса, ТзРезультат, ИскомыйЭлемент;
    
    // Пытаемся инициализировать объект работы с SQLite
    Попытка
        БазаДанных = СоздатьОбъект("SQLiteBase");
    Исключение
        ЗагрузитьВнешнююКомпоненту("1sqlite.dll");
        БазаДанных = СоздатьОбъект("SQLiteBase");
    КонецПопытки;
    
    // Открываем базу данных в оперативной памяти
    БазаДанных.Открыть(":memory:");
    Запрос = БазаДанных.НовыйЗапрос();
    
    // Создаем виртуальную таблицу на основе физического dbf-файла справочника
    Запрос.ВыполнитьЗапрос("create virtual table Марки using dbeng(Справочник.Марки)");
    
    // Пишем SQL-запрос для поиска значения по длинному неиндексированному полю
    ТекстЗапроса = "
    |SELECT 
    |   id [Марка :Справочник.Марки]
    |FROM Марки
    |WHERE ismark <> '*'
    |AND КодНовМарк = '" + СокрЛП(ШК) + "'";
    
    Попытка
        ТзРезультат = Запрос.ВыполнитьЗапрос(ТекстЗапроса);
    Исключение
        Сообщить(ОписаниеОшибки(), "!");
        Возврат ПолучитьПустоеЗначение("Справочник.Марки");
    КонецПопытки;
    
    // Если результат найден, возвращаем ссылку на элемент
    Если ТзРезультат.КоличествоСтрок() > 0 Тогда
        ИскомыйЭлемент = ТзРезультат.ПолучитьЗначение(1, "Марка");
        Возврат ИскомыйЭлемент;
    Иначе
        Возврат ПолучитьПустоеЗначение("Справочник.Марки");
    КонецЕсли;    
КонецФункции

Использование этого подхода позволяет полностью снять признак "Сортировка" с тяжелого реквизита. База данных больше не будет генерировать избыточные индексы при записи элементов, а скорость работы прямого SQL-запроса через SQLite приятно удивит высокой производительностью.


Способ 3. Программная переиндексация базы данных

Иногда архитектуру менять нельзя, а массовый импорт делать необходимо. В таком случае мы можем автоматизировать процесс сброса и перестроения индексов во время ночного технологического окна или непосредственно перед транзакцией загрузки данных.

Штатными средствами запущенного сеанса 1С 7.7 монопольно удалить или перестроить свои собственные файлы индексов невозможно. Но мы можем решить эту задачу с помощью внешнего сценария командной строки (батника).

Вариант А. Переиндексация через принудительное удаление CDX

Если запустить платформу 1С 7.7 в монопольном режиме при отсутствии индексных файлов, система автоматически начнет процедуру их пересоздания. Мы можем использовать этот простой скрипт автоматизации:


@echo off
echo Останавливаем службы и закрываем зависшие сессии 1С...
taskkill /f /im 1cv7s.exe

echo Удаляем поврежденные или раздутые файлы индексов...
del /q /f "C:\PathToBAse\*.cdx"

echo Запускаем 1С в монопольном режиме для автоматического восстановления индексов...
start "" /wait "C:\Program Files\1Cv77\BIN\1cv7s.exe" enterprise /D"C:\PathToBAse\" /M /N"РоботРобот" /P"ПарольРобота"

При этом под учетной записью РоботРобот в Глобальном модуле в процедуре ПриНачалеРаботыСистемы обязательно должен быть прописан вызов команды ЗавершитьРаботуСистемы(), чтобы робот закрыл программу сразу после успешного восстановления индексов.

Вариант Б. Использование пакетного режима запуска Конфигуратора

Для более безопасного и чистого исправления информационной базы рекомендуется использовать встроенный механизм тестирования и исправления через конфигуратор. Создадим файл параметров с именем reindex.prm следующего содержания:


[General]
Quit=1
CheckAndRepair=1

[CheckAndRepair]
Repair=1
Reindex=1
PhysicalIntegrity=0
LogicalIntegrity=0
RecalcSecondaries=0
RecalcTotals=0
Pack=0

Запуск автоматического процесса исправления индексов выполняется следующей командной строкой:


"C:\Program Files\1Cv77\BIN\1cv7.exe" config /d"C:\PathToBAse" /n"Администратор" /p"ПарольАдмина" /@"C:\PathToPrm\reindex.prm"

Способ 4. Оптимизация записи транзакциями

Проанализируем еще одну частую ошибку разработчиков при загрузке 10–15 тысяч записей: выполнение всей загрузки внутри одной огромной транзакции или, наоборот, запись каждого элемента в отдельности вообще без транзакций.

Если мы не используем транзакции, то при записи каждого отдельного элемента движок 1С вынужден физически обращаться к жесткому диску, дергать файлы .dbf и .cdx, что приводит к дикой фрагментации страниц индекса. Если же мы обернем все 15 000 записей в одну транзакцию, кэш транзакций переполнится, что спровоцирует лавинообразный рост временных файлов.

Оптимальным решением будет разбиение общего массива данных на порции (пакеты) по 500–1000 элементов:


Перем СчетчикТранзакций;

СчетчикТранзакций = 0;
НачатьТранзакцию();

Пока ЕстьДанныеДляЗагрузки = 1 Цикл
    // Код создания и заполнения элемента справочника
    НовыйЭлемент.Записать();
    
    СчетчикТранзакций = СчетчикТранзакций + 1;
    
    Если СчетчикТранзакций >= 1000 Тогда
        ЗафиксироватьТранзакцию();
        СчетчикТранзакций = 0;
        НачатьТранзакцию();
    КонецЕсли;
КонецЦикла;

Если СчетчикТранзакций > 0 Тогда
    ЗафиксироватьТранзакцию();
КонецЕсли;

Такой подход позволит СУБД своевременно сбрасывать кэшированные данные страниц B-дерева на жесткий диск порциями, что уменьшит частоту случайных расщеплений страниц и существенно сократит итоговый размер индексного файла.

← На главную