При работе с информационными системами 1С:Предприятие, особенно в условиях крупных компаний с десятками тысяч сотрудников и подразделений, часто возникает задача ограничения доступа пользователей к данным. Если пользователи должны видеть только документы, связанные с определенными подразделениями и сотрудниками, а число таких доступных объектов достигает десятков тысяч, традиционные подходы к реализации RLS (Row-Level Security) могут приводить к значительным задержкам и медленной работе системы. Давайте рассмотрим, как решить эту проблему, используя методы материализации и оптимизации запросов, даже без полноценного использования стандартной библиотеки подсистем (БСП).
Представим типовую ситуацию: в вашей базе ведется учет по нескольким десяткам тысяч сотрудников и подразделений. Есть документы, содержащие реквизит Сотрудник, и регистр сведений, по которому определяется текущее подразделение сотрудника. Задача состоит в том, чтобы ограничить доступ пользователей к этим документам, показывая только те, которые относятся к сотрудникам из разрешенных подразделений. При этом список доступных подразделений для одного пользователя может достигать 10 тысяч, а соответствующих им сотрудников — до 30 тысяч.
Начальные попытки решения этой задачи, как правило, включают:
Все эти подходы могут приводить к неприемлемому времени открытия списков документов, исчисляемому минутами, даже при, казалось бы, "удачных" вариантах.
Основная причина низкой производительности заключается в том, что при каждом открытии списка документов или обращении к данным, защищенным RLS, система вынуждена выполнять сложный запрос, который динамически вычисляет список разрешенных объектов. Если этот список велик (десятки тысяч значений), а сам запрос содержит соединения, подзапросы или сложные условия, то СУБД (система управления базами данных) тратит огромное количество ресурсов на его выполнение. Особенно это актуально, когда используется оператор В (&МассивПараметров) с большим количеством элементов или сложные вложенные запросы без оптимизации.
Рассмотрим пример запроса, который был предложен в ходе обсуждения как "более или менее норм" (хотя и работал минуту):
ВыдачаНоменклатурыСотрудникам
ИЗ
Документ.ВыдачаНоменклатурыСотрудникам КАК ВыдачаНоменклатурыСотрудникам
ЛЕВОЕ СОЕДИНЕНИЕ (ВЫБРАТЬ
Сотрудники.Ссылка КАК Сотрудник
ИЗ
Справочник.Сотрудники КАК Сотрудники
ГДЕ
&ЕстьОграничениеПоПодразделениям
И Сотрудники.Ссылка В(&ДоступныеУволенныеСотрудники)
ОБЪЕДИНИТЬ ВСЕ
ВЫБРАТЬ
СотрудникиПредприятия_Интервалы.Сотрудник
ИЗ
РегистрСведений.СотрудникиПредприятия_Интервалы КАК СотрудникиПредприятия_Интервалы
ГДЕ
&ЕстьОграничениеПоПодразделениям
И НЕ СотрудникиПредприятия_Интервалы.Профессия = ЗНАЧЕНИЕ(Справочник.Профессии.ПустаяСсылка)
И СотрудникиПредприятия_Интервалы.Профессия В(&МассивРазрешенныхПодразделений)
И &ТекущаяДатаДень МЕЖДУ СотрудникиПредприятия_Интервалы.НачалоПериода И СотрудникиПредприятия_Интервалы.КонецПериода) КАК РазрешенныеСотрудники
ПО (&ЕстьОграничениеПоПодразделениям)
И (ВыдачаНоменклатурыСотрудникам.Сотрудник = РазрешенныеСотрудники.Сотрудник)
ГДЕ
ВыдачаНоменклатурыСотрудникам.Организация В (&МассивРазрешенныхОрганизаций)
И ВыдачаНоменклатурыСотрудникам.Склад В (&МассивРазрешенныхСкладов)
И ВыдачаНоменклатурыСотрудникам.ВендинговыйАппарат В (&МассивРазрешенныхСкладов)
И ВыдачаНоменклатурыСотрудникам.ОператорПостамата В (&МассивРазрешенныхСкладов)
И (НЕ &ЕстьОграничениеПоПодразделениям
ИЛИ ВыдачаНоменклатурыСотрудникам.Сотрудник = ЗНАЧЕНИЕ(Справочник.Сотрудники.ПустаяСсылка)
ИЛИ НЕ РазрешенныеСотрудники.Сотрудник ЕСТЬ NULL)
Этот запрос пытается в подзапросе определить разрешенных сотрудников, а затем соединить результат с основным документом. Проблема здесь в том, что СУБД может испытывать трудности с оптимизацией такого сложного подзапроса, особенно если параметры &ДоступныеУволенныеСотрудники и &МассивРазрешенныхПодразделений содержат большое количество значений. Каждый раз система вынуждена выполнять это вычисление.
Для достижения высокой производительности при ограничении доступа на больших объемах данных, необходимо применять следующие принципы:
Самым эффективным подходом, который показал отличные результаты в обсуждаемой ситуации, является материализация списка доступных сотрудников. Суть метода заключается в том, что для каждого пользователя мы заранее вычисляем полный список сотрудников, к данным которых он имеет доступ. Этот список сохраняется в специальном вспомогательном регистре сведений.
Создайте новый регистр сведений, например, РегистрСведений.ДоступныеСотрудники. Он должен быть независимым, непериодическим. В нем будут следующие измерения:
Пользователь (тип СправочникСсылка.Пользователи)ДоступныйСотрудник (тип СправочникСсылка.Сотрудники)Для этого регистра необходимо создать индексы по обоим измерениям, а также составной индекс Пользователь, ДоступныйСотрудник, чтобы обеспечить быстрый поиск по паре "пользователь – сотрудник".
Заполнение этого регистра должно производиться регламентным заданием или по подписке на события при изменении данных, влияющих на права доступа (например, изменение подразделения сотрудника, изменение настроек прав пользователя). Это гарантирует актуальность данных в регистре.
Процедура заполнения будет примерно следующей:
&ТекущаяДатаДень МЕЖДУ СотрудникиПредприятия_Интервалы.НачалоПериода И СотрудникиПредприятия_Интервалы.КонецПериода), вычисляем список сотрудников, работающих в этих подразделениях.РегистрСведений.ДоступныеСотрудники для данного пользователя.Пользователь - Сотрудник в регистр.Этот процесс может быть достаточно ресурсоемким при первом расчете или при массовых изменениях, но выполняется в фоновом режиме и не влияет на интерактивную работу пользователей.
После того как регистр сведений ДоступныеСотрудники заполнен, мы можем значительно упростить и ускорить запросы на ограничение доступа. Вместо сложных вычислений, мы просто выбираем из этого регистра сотрудников, доступных текущему пользователю.
Пример оптимизированного запроса из обсуждения, который отлично сработал:
ВыдачаНоменклатурыСотрудникам
ИЗ
Документ.ВыдачаНоменклатурыСотрудникам КАК ВыдачаНоменклатурыСотрудникам
ГДЕ
ВыдачаНоменклатурыСотрудникам.Организация В (&МассивРазрешенныхОрганизаций)
И ВыдачаНоменклатурыСотрудникам.Склад В (&МассивРазрешенныхСкладов)
И ВыдачаНоменклатурыСотрудникам.ВендинговыйАппарат В (&МассивРазрешенныхСкладов)
И ВыдачаНоменклатурыСотрудникам.ОператорПостамата В (&МассивРазрешенныхСкладов)
И (НЕ &ЕстьОграничениеПоПодразделениям
ИЛИ ВыдачаНоменклатурыСотрудникам.Сотрудник В
(ВЫБРАТЬ
ДоступныеСотрудники.ДоступныйСотрудник КАК ДоступныйСотрудник
ИЗ
РегистрСведений.ДоступныеСотрудники КАК ДоступныеСотрудники
ГДЕ
ДоступныеСотрудники.Пользователь = &ТекущийПользователь))
В этом запросе подзапрос максимально упрощен. Он просто выбирает ссылки на сотрудников из регистра ДоступныеСотрудники, отбирая их по текущему пользователю. Благодаря индексам по Пользователь и ДоступныйСотрудник, этот подзапрос выполняется очень быстро. Внешний запрос уже просто фильтрует документы по списку сотрудников, который был предварительно рассчитан.
Для дальнейшей оптимизации, некоторые эксперты рекомендуют использовать внутреннее соединение (ВНУТРЕННЕЕ СОЕДИНЕНИЕ) вместо оператора В (подзапрос). Это может быть более эффективно для СУБД, так как позволяет ей лучше оптимизировать порядок выполнения операций. Запрос мог бы выглядеть так:
ВЫБРАТЬ
ВыдачаНоменклатурыСотрудникам.*
ИЗ
Документ.ВыдачаНоменклатурыСотрудникам КАК ВыдачаНоменклатурыСотрудникам
ВНУТРЕННЕЕ СОЕДИНЕНИЕ РегистрСведений.ДоступныеСотрудники КАК ДоступныеСотрудники
ПО ВыдачаНоменклатурыСотрудникам.Сотрудник = ДоступныеСотрудники.ДоступныйСотрудник
ГДЕ
ДоступныеСотрудники.Пользователь = &ТекущийПользователь
И ВыдачаНоменклатурыСотрудникам.Организация В (&МассивРазрешенныхОрганизаций)
И ВыдачаНоменклатурыСотрудникам.Склад В (&МассивРазрешенныхСкладов)
И ВыдачаНоменклатурыСотрудникам.ВендинговыйАппарат В (&МассивРазрешенныхСкладов)
И ВыдачаНоменклатурыСотрудникам.ОператорПостамата В (&МассивРазрешенныхСкладов)
Обратите внимание, что условие (НЕ &ЕстьОграничениеПоПодразделениям ИЛИ ВыдачаНоменклатурыСотрудникам.Сотрудник = ЗНАЧЕНИЕ(Справочник.Сотрудники.ПустаяСсылка), если оно действительно необходимо, может усложнить внутреннее соединение, поэтому его нужно будет адаптировать или рассмотреть как отдельный сценарий.
Этот подход получил название материализация или кеширование и индексирование, и он является одним из наиболее эффективных способов заставить работать "цифровой антиквариат" (как было иронично замечено в обсуждении) с большими объемами данных.
Мы всегда должны анализировать планы запросов (на уровне СУБД) и следить за актуальностью статистики базы данных. Медленная работа RLS часто связана с неоптимальным планом выполнения запросов. Неактуальная статистика может приводить к тому, что оптимизатор СУБД выбирает неэффективные методы поиска данных. Регулярно обновляйте статистику и используйте инструменты СУБД для просмотра планов выполнения запросов, чтобы понимать, какие индексы используются и где возникают "узкие места".
Создание подходящих индексов является одним из основных условий для построения эффективного плана запроса и ускорения получения данных из таблиц. Индексы должны содержать все поля, перечисленные в условиях запроса, и эти поля должны находиться в самом начале индекса и следовать друг за другом подряд. Убедитесь, что все измерения регистра сведений ДоступныеСотрудники проиндексированы, а также реквизит Сотрудник в документе ВыдачаНоменклатурыСотрудникам.
Фирма "1С" не рекомендует использовать вложенные запросы без особой необходимости и предлагает заменять их временными таблицами или соединениями. Вложенные запросы могут приводить к тому, что оптимизатор СУБД не всегда может правильно определить размер выборки вложенного запроса и построить оптимальный план обращений к физическим таблицам. Перенос обращения к виртуальной таблице в отдельный запрос с сохранением результатов во временной таблице также может улучшить производительность. Временные таблицы можно использовать многократно в пакетном запросе и передавать через МенеджерВременныхТаблиц в другие запросы, что повышает читаемость кода и обеспечивает независимость от СУБД.
Рекомендуется соединять друг с другом только объекты метаданных или временные таблицы, избегая соединений с подзапросами в условии соединения. Как мы видели выше, прямое соединение с материализованным регистром сведений часто оказывается эффективнее, чем подзапрос в условии ГДЕ ... В (...).
Для часто используемых, но редко меняющихся данных, таких как списки разрешенных организаций или складов для пользователя, можно использовать кэширование в параметрах сеанса. Это уменьшает количество обращений к базе данных и может дать существенный прирост производительности, особенно на мелких, часто выполняющихся запросах. Перед открытием списка или выполнением отчета, мы можем один раз получить эти данные и хранить их в ПараметрыСеанса.НашиРазрешенныеСклады, а затем использовать этот параметр в запросах.
Начиная с версии платформы 8.3.18, в 1С появился механизм оптимизации RLS, который позволяет заменять подзапросы, не зависящие от защищаемой таблицы, на результат их работы. Этот результат кэшируется в оперативной памяти рабочего процесса. Учитывая, что в нашей задаче используется платформа 8.3.18, этот механизм потенциально мог бы быть полезен для определенных подзапросов. Однако, материализация, как мы выяснили, дает более контролируемый и значительный эффект, особенно для самых сложных случаев.
Важно постоянно отслеживать состояние системы прав доступа, чтобы своевременно выявлять и устранять проблемы с производительностью. Ускорение работы с большими базами данных в 1С требует комплексного подхода, включающего правильную настройку системы, оптимизацию запросов, использование мощных серверных решений и регулярное обслуживание базы данных. Проверяйте журнал регистрации, используйте технологический журнал для анализа проблемных запросов, профилируйте сервер СУБД.
Мы должны четко понимать разницу между ограничением доступа на уровне записей (RLS) и настройкой отборов в запросах к динамическим спискам. RLS — это механизм, который срабатывает на уровне платформы при любом обращении к данным, защищенным RLS. Он физически модифицирует запрос к СУБД, добавляя условия ограничения. Это касается не только динамических списков, но и всех форм, отчетов, обменов данными. Отборы в динамических списках — это фильтры, которые пользователь видит и может менять в интерфейсе, или которые заданы программно в самом запросе динамического списка. Они работают поверх RLS (если RLS есть) и являются частью пользовательского интерфейса. В нашем случае, мы использовали оптимизированный подход для основного запроса динамического списка, который по сути является аналогом RLS-запроса, встроенного в конфигурацию.
Применение метода материализации данных, то есть предварительного расчета и хранения списка разрешенных объектов в специализированном регистре сведений, в сочетании с правильным индексированием и оптимизацией запросов, позволяет добиться значительного прироста производительности при работе с ограничениями доступа на уровне записей в 1С:Предприятие 8.3. Этот подход доказал свою эффективность на практике и позволяет решать задачи, которые казались невыполнимыми при стандартных методах.