Типичный разговор с руководителем отдела продаж:
— Нужно, чтобы каждый менеджер видел в отчёте свой rank среди всех менеджеров. Чтобы мотивировать.
— Ок. А менеджер должен видеть других менеджеров?
— Нет, только своего rank и может — топ-5 анонимно. А выручку и клиентов других — ни в коем случае, это конфиденциально.
В этом и есть суть «Running RLS» — гибрид Row-Level Security и DAX, где пользователь считает метрики от невидимой для него базы данных. Он не видит строки других, но видит свой относительный показатель, свою позицию, свой процент от общего.
Обычный RLS этого не умеет: если строки чужих менеджеров отфильтрованы, DAX-мера RANKX ранжирует только видимое, и у каждого пользователя rank = 1. Бесполезно. Решение — разделить физические таблицы: «агрегированные метрики» (видны всем, в обезличенной форме) и «детали» (под обычным RLS).
Ниже — пять паттернов с кодом, типичные ошибки и вопросы безопасности, которые важно закрыть до продакшна.
Модель данных: двухслойная архитектура
Базовый принцип Running RLS: одни и те же факты лежат в двух таблицах:
fact_sales— детализированная таблица продаж: date, manager_id, customer_id, product_id, выручка. На ней стандартный RLS: менеджер видит только свои строки.fact_sales_anon— та же выручка, но агрегированная и обезличенная. Структура: date, manager_surrogate_id, выручка. Имена и ID менеджеров заменены на суррогатные числа (не раскрывающие личность). RLS не применяется — таблицу видят все.
Плюс справочник dim_manager под RLS (каждый видит только свою строку — через фильтр user_principal_name = USERPRINCIPALNAME()) и скрытая служебная таблица rls_manager_mapping (manager_id → surrogate_id), которая связывает две ветки.
Архитектурно:
dim_manager (RLS: only me)
│
├→ fact_sales (RLS через dim_manager)
│
└→ rls_manager_mapping (hidden, no RLS)
│
└→ fact_sales_anon (NO RLS, visible to all)
Пользователь видит:
- Свои детальные данные через fact_sales + dim_manager
- Весь «обезличенный» фрейм через fact_sales_anon — для построения ранжирования, долей, бенчмарков
- Но не может соединить surrogate_id с реальными именами (mapping скрыт, RLS оставляет только его собственную строку)
Если mapping видят все — схема обходится через прямое отображение суррогатный → manager_id, и running RLS разваливается.
Пять паттернов Running RLS
Мой ранг среди всей команды
«Покажите мне, какое я место занимаю по выручке среди всех менеджеров в этом месяце»
Классика. Считается на обезличенной таблице, где видны все. На выходе — одно число, индивидуальное для каждого.
My Surrogate Id =
LOOKUPVALUE(
rls_manager_mapping[surrogate_id],
rls_manager_mapping[manager_id],
LOOKUPVALUE(
dim_manager[manager_id],
dim_manager[user_principal_name], USERPRINCIPALNAME()
)
)
My Rank =
VAR _my_sid = [My Surrogate Id]
VAR _my_revenue =
CALCULATE(
SUM(fact_sales_anon[revenue]),
fact_sales_anon[manager_surrogate_id] = _my_sid
)
VAR _my_rank =
COUNTROWS(
FILTER(
SUMMARIZE(
fact_sales_anon,
fact_sales_anon[manager_surrogate_id]
),
CALCULATE(SUM(fact_sales_anon[revenue])) > _my_revenue
)
) + 1
RETURN _my_rank
Как это работает: SUMMARIZE строит список менеджеров по surrogate_id с их выручкой, FILTER оставляет тех, у кого выручка больше моей, COUNTROWS + 1 = мой ранг. Обратите внимание: всё работает на fact_sales_anon — там нет RLS, вся команда видна.
Почему не RANKX: RANKX требует явной итерации по таблице и в сочетании с CALCULATE на каждой строке медленнее. Для больших команд (> 500 менеджеров) разница в скорости существенная.
Защита от угадывания: surrogate_id — это просто число 1, 2, 3... Пользователь знает свой, может перебирать остальные. Поэтому surrogate_id сам по себе не должен быть доступен в визуалах — колонка скрыта, используется только в мерах.
В каком я процентиле по команде
«Моя выручка лучше, чем у X% менеджеров»
Мягче чистого ранга — перцентиль. «Вы в топ-15%» звучит мотивирующе и не раскрывает численность команды. А ранг «2 из 3» демотивирует. Перцентиль универсальнее.
My Percentile =
VAR _my_sid = [My Surrogate Id]
VAR _my_revenue =
CALCULATE(
SUM(fact_sales_anon[revenue]),
fact_sales_anon[manager_surrogate_id] = _my_sid
)
VAR _total_managers =
DISTINCTCOUNT(fact_sales_anon[manager_surrogate_id])
VAR _below_me =
COUNTROWS(
FILTER(
SUMMARIZE(
fact_sales_anon,
fact_sales_anon[manager_surrogate_id]
),
CALCULATE(SUM(fact_sales_anon[revenue])) <= _my_revenue
)
)
RETURN
DIVIDE(_below_me, _total_managers)
Показывается как «Вы обходите 87% команды». Доля ниже (или равная) меня от общего числа. Менеджер в топ-3 из 50 получит 94% — читается как «вы в топ-6%». Работает как мотиватор.
Моя выручка vs медиана/средняя/топ по команде
«Какая у меня выручка относительно средней и лучшей в команде?»
Для более глубокого анализа нужны абсолютные бенчмарки — но анонимные. Считаем несколько квантилей общей команды:
Team Median Revenue =
VAR _by_manager =
SUMMARIZE(
fact_sales_anon,
fact_sales_anon[manager_surrogate_id],
"rev", CALCULATE(SUM(fact_sales_anon[revenue]))
)
RETURN
MEDIANX(_by_manager, [rev])
Team Top Revenue =
VAR _by_manager =
SUMMARIZE(
fact_sales_anon,
fact_sales_anon[manager_surrogate_id],
"rev", CALCULATE(SUM(fact_sales_anon[revenue]))
)
RETURN
MAXX(_by_manager, [rev])
Team Top 10% Revenue =
VAR _by_manager =
SUMMARIZE(
fact_sales_anon,
fact_sales_anon[manager_surrogate_id],
"rev", CALCULATE(SUM(fact_sales_anon[revenue]))
)
RETURN
PERCENTILEX.INC(_by_manager, [rev], 0.9)
Me vs Median % =
DIVIDE(
[My Revenue] - [Team Median Revenue],
[Team Median Revenue]
)
В визуалах рядом отображаются «Вы — 1.2 млн₽», «Медиана команды — 800 тыс», «Топ-10% — от 2 млн». Менеджер видит своё место относительно команды абсолютно, но не знает кто конкретно делает 2 млн.
Моя доля в общем пуле команды
«Какую долю в общей выручке отдела делаю я?»
Простая и безопасная метрика: знаменатель — общая выручка отдела (не раскрывает отдельных менеджеров), числитель — моя. Деление даёт долю.
My Share of Team =
VAR _my_sid = [My Surrogate Id]
VAR _my_revenue =
CALCULATE(
SUM(fact_sales_anon[revenue]),
fact_sales_anon[manager_surrogate_id] = _my_sid
)
VAR _team_total = SUM(fact_sales_anon[revenue])
RETURN
DIVIDE(_my_revenue, _team_total)
«Я делаю 8.5% всей выручки отдела». Можно дополнить динамикой: сравнение с прошлым месяцем, динамика за квартал, тренд. Это тоже работает на обезличенной таблице, никаких нарушений RLS.
Расширение — доля в категории. Если структура сложнее (отдел делится на команды, команда на пары), можно показывать «моя доля в моей команде» и «моя доля в отделе». Для этого в fact_sales_anon добавляются колонки team_id, department_id (НЕ personally-identifiable), и в мерах используется соответствующий контекст.
Топ-5 анонимных менеджеров — показываю числа, но не имена
«Покажите топ-5 по выручке без имён, просто "Менеджер A, B, C..."»
Самый хитрый из пяти. Визуал показывает список топ-5 с ранжированными значениями, но вместо имён — условные метки «Менеджер A», «Менеджер B». Текущий пользователь видит свою строку подсвеченной как «Вы (место #3)».
Для этого нужна вспомогательная таблица dim_anon_labels:
dim_anon_labels:
anon_label anon_order
Менеджер A 1
Менеджер B 2
Менеджер C 3
...
Менеджер Z 26
Менеджер AA 27
...
А в DAX собираем рейтинг и мапим surrogate_id на label по позиции в сортировке:
// Виртуальная таблица: топ N менеджеров с их порядковым номером
Top N Managers =
VAR _top_count = 5
VAR _ranked_managers =
ADDCOLUMNS(
SUMMARIZE(
fact_sales_anon,
fact_sales_anon[manager_surrogate_id]
),
"rev", CALCULATE(SUM(fact_sales_anon[revenue])),
"rank_pos",
RANKX(
ALL(fact_sales_anon[manager_surrogate_id]),
CALCULATE(SUM(fact_sales_anon[revenue])),
, DESC, DENSE
)
)
VAR _top_n =
FILTER(_ranked_managers, [rank_pos] <= _top_count)
RETURN
SELECTCOLUMNS(
_top_n,
"position", [rank_pos],
"revenue", [rev],
"is_me",
IF([manager_surrogate_id] = [My Surrogate Id], TRUE, FALSE)
)
Визуализация — таблица с тремя колонками: position, выручка, is_me. Условное форматирование подсвечивает строку где is_me=TRUE. Лейбл можно сформировать как «Место #{position}» — и менеджер видит себя в топ-5 (если попадает), остальных видит анонимно.
Если пользователь не в топ-5, показываем отдельную строку «Ваше место: 14 из 50» под таблицей.
Пять паттернов в одной таблице
| Паттерн | Что видит пользователь | Риск деанонимизации |
|---|---|---|
| 01 · Personal Rank | «Вы на 3 месте из 50» | Низкий (на больших командах) |
| 02 · Percentile | «Вы лучше 87% команды» | Очень низкий |
| 03 · Anonymous Benchmarks | «Вы — 1.2 млн, медиана — 800 тыс» | Средний на малых командах |
| 04 · My Share | «Вы делаете 8.5% выручки отдела» | Очень низкий |
| 05 · Anonymous Top N | Список «Место #1 — 2.1 млн, #2 — 1.8 млн, ВЫ — 1.2 млн (#3)» | Средний на малых командах |
Типичные ошибки
- Забыть скрыть mapping-таблицу.
rls_manager_mappingдолжна быть hidden=true. Если пользователь увидит её, он соединит surrogate_id с manager_id. Обязательно тестируйте под реальным AD-логином, а не под админом. - Surrogate_id показан в визуале. Кажется безобидным числом, но если пользователь видит в одном визуале «я = surrogate_id 7» и в другом «у менеджера surrogate_id 12 выручка X» — он уже имеет таблицу соответствия для своих коллег. surrogate_id только в мерах.
- Малые когорты. При < 5 менеджерах в команде вся «обезличенность» проваливается: «топ-1» — это Петя, «я» — это Вася, а средний из 3 оставшихся — с высокой вероятностью считается однозначно. Либо делаем cutoff (не показываем для малых команд), либо используем общий бенчмарк по отделу/компании.
- Не синхронизировать fact_sales и fact_sales_anon. Если агрегированная таблица обновляется раз в сутки, а детальная — в real-time, пользователь видит расхождение: «моя выручка в детали 1.2М, но в бенчмарке 1.1М». Обе таблицы должны строиться из одного snapshot и обновляться согласованно.
- RLS на fact_sales_anon. Частая ошибка: админ решил «на всякий случай» применить RLS и к обезличенной таблице. Получаем пустой бенчмарк, все меры возвращают BLANK или «вы — 100% команды». Anon-таблица должна быть открыта.
- Показывать точный ранг на больших командах. «Вы на 347 месте из 500» — это демотивация. Для больших команд лучше перцентили или ступени (топ-10%, топ-25%, середина, нижние 25%).
- Не обновлять бенчмарки при изменении периода. Пользователь меняет срез «месяц → квартал», а бенчмарк остался месячным — получается ерунда. Все меры должны корректно реагировать на контекст даты.
Вопросы безопасности, которые нужно закрыть
- Ревью от ИБ. Running RLS — это информационная безопасность через анонимизацию. Покажите схему ИБ-команде, получите согласование. Особенно если данные чувствительные (зарплаты, коммерческая тайна).
- Audit логирование. Настройте логирование запросов к fact_sales_anon. Если пользователь 100 раз в день крутит фильтры и смотрит бенчмарки — это может быть попытка деанонимизации через перебор.
- Минимальный размер когорт. Внутри меры добавьте защиту: если количество менеджеров в фильтре < 5, возвращайте BLANK или «Недостаточно данных для бенчмарка».
- Юридическая сторона. Убедитесь, что показ чужой (даже обезличенной) выручки не нарушает NDA или трудовой договор. В большинстве компаний это ок, но лучше подстраховаться.
Производительность
Running RLS — тяжёлая история с точки зрения DAX: каждая мера пробегает по всей fact_sales_anon и делает агрегацию. Для команд < 200 менеджеров и 1-2 млн транзакций в месяц — работает за 1-3 сек без оптимизации. Для больших:
- Агрегационная таблица. Вместо fact_sales_anon по транзакциям держите
agg_manager_monthly(manager_surrogate_id + month + выручка). Для бенчмарков детализация до месяца обычно достаточна. - Материализованный rank в ETL. Считайте rank/percentile каждого менеджера в SQL раз в сутки, кладите в dim_manager. DAX только отображает. Работает мгновенно, теряется гибкость по фильтрам времени.
- Incremental aggregation tables + Aggregation mappings. Для больших моделей лучше через нативный механизм Power BI — SSAS сам выбирает куда идти.
Практические сценарии
Gamification продаж
Самый частый сценарий. Каждый менеджер в своём Power BI видит:
- Свою выручку за месяц (абсолют + % от плана)
- Свой rank по отделу (перцентиль)
- Бенчмарки: медиана, топ-10% команды
- «Стрелка роста/падения» относительно прошлого месяца
- Анонимный top-5 (для мотивации «догнать впередиидущих»)
Результат — здоровая конкуренция без утечки чьих-то цифр.
Бенчмаркинг филиалов
Руководитель филиала видит метрики своего филиала и сравнение с «медианным филиалом» сети + «топ-филиалом». Без детализации по другим филиалам. Полезно для операционной деятельности, когда филиалы конкурируют между собой.
HR-дашборды для сотрудников
«Ваша продуктивность vs средняя по отделу». В отделе 20 человек — вычислить конкретного коллегу по метрике невозможно. Полезно для 1-на-1 разговоров руководителя с сотрудником: «вы в нижнем квартиле, давайте разберёмся почему».
Self-service analytics для партнёров
В партнёрских программах (дистрибьюторы, франчайзи, агенты) каждый партнёр видит свои продажи плюс обезличенные бенчмарки по сети. «Ваш magazin Top 8% по выручке/кв.м в сети» — даёт контекст без раскрытия конкретных цифр соседей.
Тестирование: как проверить что всё работает
- Под админом — все меры работают, видны разумные числа. Sanity-check.
- Под тестовым менеджером через DAX Studio + EffectiveUserName. Проверьте: My Rank совпадает с реальной позицией этого менеджера в команде (сравните с SQL-запросом напрямую).
- Контрольная сумма. Sum(fact_sales_anon[выручка]) под менеджером должен быть равен общей выручке отдела (из SQL), а не его личной. Если равен его личной — RLS случайно применился к anon-таблице, чинить немедленно.
- Тест утечек. Под менеджером попытайтесь через визуал добраться до чужих surrogate_id, mapping-таблицы, имён. Если не получается — ок.
- Тест на малой когорте. Создайте команду из 3 тестовых менеджеров. Проверьте, что меры либо возвращают BLANK, либо бенчмарк недостаточно детализирован для деанонимизации.
Шаги внедрения
- Согласуйте с бизнесом и ИБ. Какие метрики показываем, какие нет. Какой минимальный размер когорты для бенчмарка.
- Постройте двухслойную модель: fact_sales (детали, под RLS) + fact_sales_anon (агрегаты, открыто) + скрытая mapping-таблица.
- Настройте RLS на dim_manager, fact_sales, mapping-таблице. На fact_sales_anon RLS не настраивайте.
- Напишите меры по паттернам 01-05. Начните с Personal Rank и My Share — самые безопасные и быстро окупают усилия.
- Визуализация. KPI-карточки сверху (мои цифры), бенчмарки сбоку, анонимный топ-5 ниже.
- Тестирование — см. раздел выше. Обязательно под реальным AD-логином, не под админом.
- Постепенное расширение. Начните с простых паттернов, по мере доверия бизнеса и ИБ добавляйте сложные (top-N, benchmarks).
Что делать дальше
- Нужен Running RLS под вашу оргструктуру — 30-минутный звонок. Обсудим уровни доступа, метрики, ИБ-ограничения. Типовой проект — 3-4 недели.
- Если RLS уже есть, но хотите добавить персонализацию — BI-аудит проверит текущую модель и даст план внедрения Running RLS-паттернов.
- Базовые RLS-паттерны — см. P14 про RLS на SSAS Tabular. Running RLS — надстройка над классическим RLS.
Связанные материалы:
- RLS на SSAS Tabular: 6 production-паттернов — база, на которой строится Running RLS
- DAX для управленцев: 15 формул — RANKX, PERCENTILE, DIVIDE
- RFM-сегментация на DAX — ещё один кейс с RANKX и квинтилями
- Рефакторинг Power BI в 10 раз — как ускорить тяжёлые RLS-меры