При работе с государственными классификаторами, такими как НСИ Территориального фонда ОМС (ТФОМС), клинико-статистические группы (КСГ) или справочники Минздрава, разработчики часто сталкиваются со стремительным ростом базы данных. Типичный сценарий: фонд ОМС несколько раз в год обновляет классификатор, содержащий более 500 000 записей. При стандартном проектировании, когда версия НСИ входит в состав измерений регистра сведений, база за пару лет раздувается до 40 000 000 записей и более.
Основная проблема заключается в том, что 98% этих записей дублируют друг друга от версии к версии, изменяясь лишь по ключевому реквизиту самой версии НСИ. Просто очищать регистр перед новой загрузкой нельзя, так как системе необходимо иметь возможность пересчитать или проверить медицинские случаи за прошлые периоды (год или два назад) по тем правилам, которые действовали на момент лечения.
В этой статье мы подробно разберем, как оптимизировать структуру хранения данных в 1С, избавиться от миллионов дубликатов и существенно ускорить работу системы.
Автор темы предложил следующее решение: создать справочник ВариацииНСИ, в табличную часть которого записываются версии НСИ, для которых актуален данный набор данных. Сам регистр сведений хранит только уникальные клинические параметры, а вместо измерения версий содержит реквизит (или измерение) связи со справочником вариаций.
Давайте посмотрим на алгоритм, который использовал автор для сопоставления вариаций при загрузке:
&НаСервереБезКонтекста
Процедура ЗаполнительНСИ()
// Создаем набор для записи данных в новый регистр
Набор = РегистрыСведений.НСИ_СоответствиеФКСГИДиагнозов1.СоздатьНаборЗаписей();
Набор.Записать();
Запрос = Новый Запрос;
Запрос.Текст =
"ВЫБРАТЬ
| НСИ_СоответствиеФКСГИДиагнозов.НСИ КАК НСИ,
| НСИ_СоответствиеФКСГИДиагнозов.ВидСтационара КАК ВидСтационара,
| НСИ_СоответствиеФКСГИДиагнозов.КСГ КАК КСГ,
| НСИ_СоответствиеФКСГИДиагнозов.Диагноз КАК Диагноз,
| НСИ_СоответствиеФКСГИДиагнозов.ДиагнозДоп КАК ДиагнозДоп,
| НСИ_СоответствиеФКСГИДиагнозов.Профиль КАК Профиль
|ИЗ
| РегистрСведений.НСИ_СоответствиеФКСГИДиагнозов КАК НСИ_СоответствиеФКСГИДиагнозов
|ИТОГИ ПО
| НСИ";
ВыборкаНСИ = Запрос.Выполнить().Выбрать(ОбходРезультатаЗапроса.ПоГруппировкам, "НСИ");
Пока ВыборкаНСИ.Следующий() Цикл
Выборка = ВыборкаНСИ.Выбрать();
Пока Выборка.Следующий() Цикл
Запись = РегистрыСведений.НСИ_СоответствиеФКСГИДиагнозов1.СоздатьМенеджерЗаписи();
ЗаполнитьЗначенияСвойств(Запись, Выборка);
Запись.Прочитать();
Если Запись.Выбран() Тогда
ВариацияНСИ = Запись.ВариацияНСИ;
ТаблицаНСИ = ВариацияНСИ.ДействуетДляНСИ.Выгрузить();
СтрокаНСИ = ТаблицаНСИ.Добавить();
СтрокаНСИ.ВерсияНСИ = ВыборкаНСИ.НСИ;
ВариацияНСИ = ПолучитьВариациюНСИ(ТаблицаНСИ);
Запись.ВариацияНСИ = ВариацияНСИ;
Запись.Записать();
Иначе
// Создание новой вариации, если комбинация параметров встретилась впервые
ВариацияНСИ = Справочники.ВариацииНСИ.СоздатьЭлемент();
ТаблицаНСИ = ВариацияНСИ.ДействуетДляНСИ.Выгрузить();
СтрокаНСИ = ТаблицаНСИ.Добавить();
СтрокаНСИ.ВерсияНСИ = ВыборкаНСИ.НСИ;
ВариацияНСИ = ПолучитьВариациюНСИ(ТаблицаНСИ);
ЗаполнитьЗначенияСвойств(Запись, Выборка);
Запись.ВариацияНСИ = ВариацияНСИ;
Запись.Записать();
КонецЕсли;
КонецЦикла;
КонецЦикла;
КонецПроцедуры
Для поиска существующей вариации автор использует вспомогательную функцию с запросом к табличной части справочника:
&НаСервереБезКонтекста
Функция ПолучитьВариациюНСИ(Знач ТаблицаНСИ)
Запрос = Новый Запрос;
Запрос.УстановитьПараметр("ТаблицаНСИ", ТаблицаНСИ);
Запрос.Текст =
"ВЫБРАТЬ
| ТаблицаНСИ.ВерсияНСИ КАК ВерсияНСИ
|ПОМЕСТИТЬ втТаблицаНСИ
|ИЗ
| &ТаблицаНСИ КАК ТаблицаНСИ
|;
|
|ВЫБРАТЬ РАЗЛИЧНЫЕ
| ВариацииНСИДействуетДляНСИ.ВерсияНСИ КАК ВерсияНСИ,
| ВариацииНСИДействуетДляНСИ1.Ссылка КАК Ссылка1
|ПОМЕСТИТЬ втУмножение
|ИЗ
| втТаблицаНСИ КАК ВариацииНСИДействуетДляНСИ
| ЛЕВОЕ СОЕДИНЕНИЕ Справочник.ВариацииНСИ.ДействуетДляНСИ КАК ВариацииНСИДействуетДляНСИ1
| ПО (ИСТИНА)
|;
|
|ВЫБРАТЬ
| ВариацииНСИ.Ссылка КАК Ссылка
|ИЗ
| Справочник.ВариацииНСИ КАК ВариацииНСИ
| ЛЕВОЕ СОЕДИНЕНИЕ (ВЫБРАТЬ РАЗЛИЧНЫЕ
| ЕСТЬNULL(втУмножение.Ссылка1, ВариацииНСИДействуетДляНСИ.Ссылка) КАК Ссылка1
| ИЗ
| втУмножение КАК втУмножение
| ПОЛНОЕ СОЕДИНЕНИЕ Справочник.ВариацииНСИ.ДействуетДляНСИ КАК ВариацииНСИДействуетДляНСИ
| ПО втУмножение.Ссылка1 = ВариацииНСИДействуетДляНСИ.Ссылка
| И втУмножение.ВерсияНСИ = ВариацииНСИДействуетДляНСИ.ВерсияНСИ
| ГДЕ
| (ВариацииНСИДействуетДляНСИ.Ссылка ЕСТЬ NULL
| ИЛИ втУмножение.Ссылка1 ЕСТЬ NULL)) КАК Исключаемые
| ПО (Исключаемые.Ссылка1 = ВариацииНСИ.Ссылка)
|ГДЕ
| Исключаемые.Ссылка1 ЕСТЬ NULL";
РезультатЗапроса = Запрос.Выполнить();
ВыборкаВариаций = РезультатЗапроса.Выбрать();
Если ВыборкаВариаций.Следующий() Тогда
Возврат ВыборкаВариаций.Ссылка;
Иначе
НоваяВариацияНСИ = Справочники.ВариацииНСИ.СоздатьЭлемент();
НоваяВариацияНСИ.ДействуетДляНСИ.Загрузить(ТаблицаНСИ);
НоваяВариацияНСИ.Записать();
Возврат НоваяВариацияНСИ.Ссылка;
КонецЕсли;
КонецФункции
Критика данного подхода: Несмотря на то, что это решение позволяет физически избавиться от дубликатов в регистре сведений, оно обладает критическим недостатком — катастрофически низкой производительностью. Вызов метода Прочитать() и выполнение тяжелого запроса с ПОЛНОЕ СОЕДИНЕНИЕ внутри цикла на 500 000 итераций приведет к зависанию СУБД. Такую загрузку невозможно использовать в реальных условиях крупных медицинских информационных систем.
Рассмотрим более изящные, производительные и архитектурно правильные подходы к решению этой проблемы.
Этот подход успешно применяется в типовых решениях фирмы 1С (например, в подсистеме РАУЗ в ERP). Мы разделяем тяжелые клинические реквизиты и логику версионирования.
Создадим справочник КлючиАналитикиПравил. Реквизитами этого справочника (не табличной частью, а обычными реквизитами) станут все наши разрезы: Диагноз, Профиль, Пол, Возраст, ВидСтационара и т.д. На эти поля накладывается уникальный индекс.
Регистр сведений при этом проектируется максимально легким:
ВерсияНСИ (Ссылка на справочник версий)КлючАналитики (Ссылка на справочник КлючиАналитикиПравил)КСГ (Ссылка на справочник групп)При таком подходе количество записей в самом регистре останется прежним, но физический размер таблиц в базе данных сократится многократно. СУБД будет хранить лишь пары простых целочисленных идентификаторов (ссылок), а текстовые и ссылочные клинические данные будут храниться в единственном экземпляре в справочнике ключей аналитики.
Вместо того чтобы привязывать правила к дискретным версиям классификатора ТФОМС («Версия 1», «Версия 2»), мы можем использовать концепцию интервалов действия. Это классический способ хранения истории изменений в реляционных базах данных.
Спроектируем регистр сведений следующим образом:
Диагноз, Профиль, Пол и т.д.)ВерсияНачалаДействия (Ссылка на справочник версий)ВерсияОкончанияДействия (Ссылка на справочник версий, может быть не заполнено, что означает «действует по настоящее время»)КСГ (Итоговая группа)Как работает этот подход при загрузке новой версии НСИ:
ВерсияОкончанияДействия не заполнена), проставляем ей в качестве окончания предыдущую версию НСИ, и записываем новую строчку с заполненной ВерсияНачалаДействия = ТекущаяВерсия.Этот подход сокращает объем регистра с 40 000 000 до примерно 600 000 записей (объем одной версии плюс небольшая дельта изменений), полностью решая проблему дублирования.
Запрос для получения актуального правила на конкретную версию НСИ в этом случае выглядит очень просто и быстро:
ВЫБРАТЬ
Регистр.КСГ КАК КСГ
ИЗ
РегистрСведений.СоответствиеКСГ КАК Регистр
ГДЕ
Регистр.Диагноз = &Диагноз
И Регистр.Профиль = &Профиль
И &ВыбраннаяВерсия >= Регистр.ВерсияНачалаДействия.Очередность
И (Регистр.ВерсияОкончанияДействия ЕСТЬ NULL
ИЛИ &ВыбраннаяВерсия < Регистр.ВерсияОкончанияДействия.Очередность)
Для реализации высокой скорости загрузки НСИ ТФОМС (за секунды вместо часов) необходимо полностью отказаться от построчной работы с базой данных в цикле. Разберем по шагам правильный алгоритм пакетного импорта.
Для этого мы можем использовать хэширование комбинаций параметров на стороне СУБД. Шаг 1 — расчет хэш-функции (например, MD5) от строки всех измерений, шаг 2 — быстрое сопоставление.
Проанализируем пример схемы пакетной загрузки:
&НаСервере
Процедура ПакетныйИмпортНСИ(ТаблицаНовыхДанных, ТекущаяВерсия)
// Шаг 1. Помещаем новые данные во временную таблицу в запросе
МенеджерВТ = Новый МенеджерВременныхТаблиц;
Запрос = Новый Запрос;
Запрос.МенеджерВременныхТаблиц = МенеджерВТ;
Запрос.УстановитьПараметр("ТаблицаНовыхДанных", ТаблицаНовыхДанных);
Запрос.УстановитьПараметр("ТекущаяВерсия", ТекущаяВерсия);
Запрос.Текст =
"ВЫБРАТЬ
| Т.Диагноз КАК Диагноз,
| Т.Профиль КАК Профиль,
| Т.КСГ КАК КСГ,
| Т.ВидСтационара КАК ВидСтационара
|ПОМЕСТИТЬ втНовыеДанные
|ИЗ
| &ТаблицаНовыхДанных КАК Т";
Запрос.Выполнить();
// Шаг 2. Одним запросом сопоставляем существующие Ключи Аналитики
// и определяем, каких ключей еще нет в базе данных
Запрос.Текст =
"ВЫБРАТЬ
| Новые.Диагноз,
| Новые.Профиль,
| Новые.ВидСтационара
|ИЗ
| втНовыеДанные КАК Новые
| ЛЕВОЕ СОЕДИНЕНИЕ Справочник.КлючиАналитикиПравил КАК Ключи
| ПО Новые.Диагноз = Ключи.Диагноз
| И Новые.Профиль = Ключи.Профиль
| И Новые.ВидСтационара = Ключи.ВидСтационара
|ГДЕ
| Ключи.Ссылка ЕСТЬ NULL";
ТаблицаДляСозданияКлючей = Запрос.Выполнить().Выгрузить();
// Шаг 3. Записываем только отсутствующие ключи аналитики пакетом в транзакции
Если ТаблицаДляСозданияКлючей.Количество() > 0 Тогда
НачатьТранзакцию();
Для Каждого СтрокаКлюча Из ТаблицаДляСозданияКлючей Цикл
НовыйКлюч = Справочники.КлючиАналитикиПравил.СоздатьЭлемент();
ЗаполнитьЗначенияСвойств(НовыйКлюч, СтрокаКлюча);
НовыйКлюч.Записать();
КонецЦикла;
ЗафиксироватьТранзакцию();
КонецЕсли;
// Шаг 4. Записываем связи в регистр сведений одним пакетом
Запрос.Текст =
"ВЫБРАТЬ
| &ТекущаяВерсия КАК ВерсияНСИ,
| Ключи.Ссылка КАК КлючАналитики,
| Новые.КСГ КАК КСГ
|ИЗ
| втНовыеДанные КАК Новые
| ВНУТРЕННЕЕ СОЕДИНЕНИЕ Справочник.КлючиАналитикиПравил КАК Ключи
| ПО Новые.Диагноз = Ключи.Диагноз
| И Новые.Профиль = Ключи.Профиль
| И Новые.ВидСтационара = Ключи.ВидСтационара";
ТаблицаДляРегистра = Запрос.Выполнить().Выгрузить();
НаборЗаписей = РегистрыСведений.СоответствиеКСГ.СоздатьНаборЗаписей();
НаборЗаписей.Отбор.ВерсияНСИ.Установить(ТекущаяВерсия);
НаборЗаписей.Загрузить(ТаблицаДляРегистра);
НаборЗаписей.Записать(Истина); // Запись набора одной транзакцией
КонецПроцедуры
Такой подход демонстрирует колоссальную разницу в скорости. Благодаря тому, что сопоставление выполняется на уровне СУБД через механизмы временных таблиц и соединений, а запись справочников и регистров происходит пакетами в рамках единых транзакций, время импорта классификатора сокращается до нескольких минут.
Для эффективного сокращения записей в регистрах сведений при работе с версионными справочниками НСИ: