При разработке систем учета часто возникает задача регистрации товаров с уникальными идентификаторами, например, бланков строгой отчетности, подарочных сертификатов или определенных серий продукции. Если записывать каждый номер отдельной строкой в РегистрНакопления, то при объемах в сотни тысяч или миллионы единиц база данных начинает стремительно расти, а производительность отчетов — падать. Рассмотрим, как решить проблему «раздувания» регистров, используя интервальное хранение и специализированные алгоритмы.
Представим ситуацию: на склад поступила пачка бланков с номерами от 1 до 1000. Классический подход 1С предполагает создание 1000 записей. Однако гораздо эффективнее хранить одну запись: [Товар 1, Серия А, Начало 1, Конец 1000, Кол-во 1000]. Проблема возникает в момент списания. Если клиент забирает бланк №50, наш красивый интервал 1–1000 должен превратиться в два новых: 1–49 и 51–1000. Проанализируем, как автоматизировать этот процесс и какие структуры данных для этого подходят.
Это наиболее практичный способ для реализации в среде 1С. Суть заключается в разделении количественного учета и детальной информации о номерах. Разберем этот подход по шагам:
Номенклатура, Серия, НомерНачала и ресурс НомерКонца.Рассмотрим пример кода для расщепления интервала при списании одного номера. Допустим, у нас есть процедура, которая вызывается при проведении документа расхода:
Процедура ОбработатьСписаниеНомера(Товар, Серия, СписываемыйНомер)
// Поиск интервала, в который входит номер
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| Интервалы.НомерНачала КАК Нач,
| Интервалы.НомерКонца КАК Кон
|ИЗ
| РегистрСведений.ИнтервалыНомеров КАК Интервалы
|ГДЕ
| Интервалы.Номенклатура = &Товар
| И Интервалы.Серия = &Серия
| И &СписываемыйНомер МЕЖДУ Интервалы.НомерНачала И Интервалы.НомерКонца";
Запрос.УстановитьПараметр("Товар", Товар);
Запрос.УстановитьПараметр("Серия", Серия);
Запрос.УстановитьПараметр("СписываемыйНомер", СписываемыйНомер);
Выборка = Запрос.Выполнить().Выбрать();
Если Выборка.Следующий() Тогда
Нач = Выборка.Нач;
Кон = Выборка.Кон;
// Удаляем старый интервал
МенеджерЗаписи = РегистрыСведений.ИнтервалыНомеров.СоздатьМенеджерЗаписи();
МенеджерЗаписи.Номенклатура = Товар;
МенеджерЗаписи.Серия = Серия;
МенеджерЗаписи.НомерНачала = Нач;
МенеджерЗаписи.Прочитать();
МенеджерЗаписи.Удалить();
// Создаем новые интервалы (если есть остатки)
Если СписываемыйНомер > Нач Тогда
ЗаписатьНовыйИнтервал(Товар, Серия, Нач, СписываемыйНомер - 1);
КонецЕсли;
Если СписываемыйНомер < Кон Тогда
ЗаписатьНовыйИнтервал(Товар, Серия, СписываемыйНомер + 1, Кон);
КонецЕсли;
КонецЕсли;
КонецПроцедуры
Проанализируем более продвинутый алгоритмический подход. DIET (Discrete Interval Encoding Trees) — это структура данных, специально созданная для хранения наборов целых чисел, образующих последовательности. В 1С это можно реализовать с помощью иерархического справочника, где каждый узел — это сбалансированный интервал.
Преимущества DIET:
Если ваши уникальные номера — это целые числа в определенном диапазоне, мы можем применить метод битовых карт. Рассмотрим ситуацию: для хранения информации о наличии 64 товаров нам достаточно одного числа типа Число (64-битное целое). Каждый бит в этом числе отвечает за конкретный номер: 1 — товар в наличии, 0 — продан.
Однако обычные битовые маски неэффективны, если номера распределены разреженно (например, есть номер 1 и номер 1 000 000). В этом случае стоит обратить внимание на технологию Roaring Bitmaps. Она разбивает пространство номеров на блоки. Если блок заполнен номерами подряд, он хранится как интервал. Если в блоке всего несколько номеров — как список. Если блок почти полон — как битовая маска. В 1С такую логику можно реализовать в виде зашифрованной строки в ХранилищеЗначения, обрабатывая её через COM-объект или внешнюю компоненту для максимальной скорости.
Иногда полезно не хранить интервалы в явном виде постоянно, а вычислять их в момент построения отчета. Мы можем хранить в РегистреНакопления только факты движения конкретных номеров. Чтобы превратить их в компактные интервалы в отчете, используется SQL-логика «Островов».
Разберем, как это работает на уровне логики запроса: мы нумеруем все имеющиеся номера товаров по порядку и вычитаем этот порядковый номер из самого значения идентификатора. Для всех номеров, идущих подряд, эта разность будет одинаковой. Группируя данные по этой разности, мы мгновенно получаем начало и конец каждого «острова» (непрерывного диапазона).
При интервальном хранении получение классических оборотов за период (сколько раз списывался конкретный номер) становится сложнее. Выясним причину: в интервальном регистре мы видим только текущее состояние «свободных» номеров. Чтобы восстановить историю, необходимо:
[1, 5] в пять строк 1, 2, 3, 4, 5.Проанализировав ситуацию, можно сделать следующие выводы:
ХранилищеЗначения.Использование этих методов позволит вам сократить объем базы данных в десятки раз и значительно ускорить работу системы при массовом учете уникальных объектов.