I have published my .pbix, it works great, speed is fine. I consume from SSAS. Now, I have introduced some RLS security and for people accessing with limited permissions, it is awfully slow…
How can I troubleshoot this?
I have 3 roles.
Role1: (everyone has access):
RLS
Employee table: =[NT Username] = USERNAME()
Projects table:
=[Key_Project] IN
SELECTCOLUMNS(
FILTER(
'Engagement Role'
,'Engagement Role'[UserName] = USERNAME()
)
,"Key_project"
,'Engagement Role'[Key_project])
Role2: (everyone has access):
Employee table:
=[TeamLeadEmployeeID] IN
SELECTCOLUMNS(
FILTER(
'Employee Current'
,'Employee Current'[NT UserName] = USERNAME()
)
,"RLS_Ids"
,[Employee Number])
Projects table:
= VAR MyEmployeeNumber=
SELECTCOLUMNS(
FILTER(
'Employee Current'
,'Employee Current'[NT Username]=USERNAME()
)
,"EmployeeNumber"
,'Employee Current'[Employee Number]
)
VAR My_Employees =
SELECTCOLUMNS(
FILTER(
'Employee Current'
,'Employee Current'[TeamLeadEmployeeID] = MyEmployeeNumber
) ,"EmployeeNumber"
,'Employee Current'[Employee Number]
)
RETURN
[Key_project] IN
SELECTCOLUMNS(
FILTER(
'Engagement Role'
,'Engagement Role'[Employee Number] IN My_Employees
)
,"Key_project"
,'Engagement Role'[Key_project]
)
(the 3rd role doesnt really matter, it has no filters is Read all access).
You don't want to set up your security in this way if you can avoid it. Take your first example -- security filter iterates over every row of your 'Projects' table, so it calculates that SELECTEDCOLUMNS for every row of the table. Depending on the size of these tables, that could be a lot of extra work and it totally sidesteps the advantages of your columnar model.
You want to filter on the table that actually has the value to match USERNAME() in it, and then let that filter propagate throughout your model along existing relationships.
So for Role1, you wouldn't build an RLS filter for the 'Projects' table, you'd only put one on 'Engagement Role', and all it would look like is [UserName] = USERNAME()
There would also need to be a relationship between 'Engagement Role' and 'Projects' on [Key_Projects]. The filter direction on the relationship should go from 'Engagement Role' to 'Projects.'
I think you'll find that this is quite a bit more performant.
Related
A co-worker has asked me to help her with an issue she is having. Her data model looks as follows:
She is using SUMMARIZE to build the Jobs table. In part, the DAX looks like this:
Jobs = SUMMARIZE(
'MainSI Table',
'MainSI Table'[Market],
'MainSI Table'[Region],
'MainSI Table'[EmployeeId],
'EmployeeTable'[Business],
)
This works but she is not able to add any columns from WODetail (the error is that the column selected from WODetail cannot be found) and I do not know why when there is a 1:M relationship between MainSI and WODetail.
You need to use ADDCOLUMNS, RELATEDTABLE and then summarise the result from the detail table according to requirements. e.g.
ADDCOLUMNS (
SUMMARIZE(MainSI Table, MainSI Table'[Market]),
"Max",MAXX(RELATEDTABLE(WODetail),WODetail[Job Finish Date])
)
So I have a table with employees. Each employee has a manager, and using the PATH function, I can create another column, that traces the management of each employee to the CEO (using employee ID and manager's ID.) For example, if my boss reports to the ceo, my employee path would be "CEO ID | My boss ID | My ID" or "10001234|10002345|1000456."
Once I have this path selected, I need to be able to filter my data based on a list of people (business leaders.) If I select business leader A, then I want my data to only include people who have business leader A in their employee path, i.e., people who eventually report to him. I have a separate table of business leaders. Here's what Im doing:
Create a path for each employee: empPath = PATH(EmployeeTable[Employee ID], EmployeeTable[Manager ID])
Use business leader table as a slicer (using their IDs)
Create a variable, selectedLeader, that records the value of whatever selection has been made from the slicer: selectedLeader = IF(HASONEVALUE('LeaderTable'[Id]),VALUES('LeaderTable'[Id]),BLANK())
Put it all together using PATHCONTAINS to filter assign a binary indicator that will allow us to filter the data:
Filter =
var selectedLeader = IF(HASONEVALUE('LeaderTable'[Id]),VALUES('LeaderTable'[Id]),BLANK())
var empPath = PATH(EmployeeTable[Employee ID], EmployeeTable[Manager ID])
return IF(PATHCONTAINS(empPath, selectedLeader ),1,0)
This however, gives the error: Calculation error in measure: A table of multiple values was supplied where a single value was expected.
I've been trying to play around with the formula a lot, using VALUES, FIRSTNONBLANK and other aggregating functions on employeePath, but none are working as desired. Any help appreciated!
The issue is that you need row context for your PATH function to work as you're expecting. As you have it written, it doesn't know which row of the EmployeeTable you're referring to with empPath.
Try this instead:
Filter =
VAR selectedLeader = SELECTEDVALUE ( 'LeaderTable'[Id] )
VAR FilteredTable =
FILTER (
EmployeeTable,
PATHCONTAINS (
PATH ( EmployeeTable[Employee ID], EmployeeTable[Manager ID] ),
selectedLeader
)
)
RETURN
IF ( ISEMPTY ( FilteredTable ), 0, 1 )
You can make this more efficient if you define a calculated path column on the EmployeeTable first.
Path = PATH ( EmployeeTable[Employee ID], EmployeeTable[Manager ID] )
Then the measure simplifies to:
Filter =
VAR selectedLeader = SELECTEDVALUE ( 'LeaderTable'[Id] )
VAR FilteredTable =
FILTER ( EmployeeTable, PATHCONTAINS ( EmployeeTable[Path], selectedLeader ) )
RETURN
IF ( ISEMPTY ( FilteredTable ), 0, 1 )
Assume I have 2 dimension tables and 1 fact table.
DimUser
DimClients
DimCompany
FactSales
All the dims have a 1:M relation with filter flowing from the 1 -> M side.
Assume I have configured RLS filter on DimUser ([Username] = USERNAME()) so that the user of the report can see his own data based on email address, so user can see his own record entry in DimUser. Thus it will auto filter the FactSales also, effectively the user will be able to see only his sales.
However the user can see all clients and companies in the slicer. I want to setup filter on the DimClients and DimCompany so that the user can see only clients and conpanies for which he has made sales. How can I achieve this?
For example:
How to apply RLS to the DimClients and DimCompany so that it will filter only those ClientIds that appear in the sales table based on dynamic filter [Username] = USERNAME() applied on the DimUsers table?
The other option is to enable bidirectional filtering but this is not allowed for multiple tables.
You can create a measure that looks like this:
UserFlag =
CONTAINSSTRING (
CONCATENATEX ( FactSales, RELATED ( DimUsers[UserEmail] ), "," ),
USERNAME()
)
Create your RLS role and add this filter to each dimension.
[UserFlag] = True()
The result: If a row from a dimension is related to a fact row that is in turn related to the DimUser row whose email matches USERNAME(), then the measure returns 'True.' And so RLS is filtering only to those rows.
Say I have Page 'A' and I want to drill through to Page 'B'. Ordinarily I would set up the drill through by specifying the field in page B that "joins" the same field from page A.
I want to do something slightly different. I would like to somehow specify that the User Name column from a table on page A should be used to filter the Manager Name field, from the same table, in page B.
For example, assume on page A, the current User Name is John Smith. On page B I would like the value of John Smith to be used to filter the Manager Name field from the same table.
Is this possible?
This is an interesting prompt. I think one way to do this is to have your [User Name] and [Manager Name] fields in a table in your model with ONLY an inactive relationship on the [Manager Name]. Then, when you filter on the [User Name], you would use DAX calculations that look something like this:
YourMeasure =
IF (
ISFILTERED ( 'NewTable'[User Name] ) = TRUE (),
CALCULATE (
CALCULATE (
[ExistingMeasure],
ALLEXCEPT ( 'NewTable', [Manager Name] ),
USERELATIONSHIP ( 'Table1'[Manager Name], 'NewTable'[Manager Name] )
)
),
BLANK ()
)
The main consideration here is that you'd have to do this for each measure you want to display on Page B.
I have a table like this
Role Skills Resource
Data Analyst R A
Data Analyst Python A
Data Analyst SQL B
Business Analyst SQL A
My Skills are on filter. I have multiple visuals on the dashboard.
And If I select SQL and Python then the results of both Data Analyst and Business Analysts are getting displayed in the visual.
But, I want it to display only the Data Analyst results because only Data Analyst has all the selected skills.
To achieve this, I think of creating a measure and putting it on visual level filter in each of the visual might help.
Update :- If I select SQL here, I get 2 distinct resources on my card visual which is relevant to resources, but If I select SQL and Python - I get 0 Resources on my card visual which is relevant to resources and 1 role count which is relevant to roles on the Role measure.
Kindly help me with creating that measure.
Perhaps someone will suggest a more elegant way to do it; I came up with the following ideas.
Create a measure (I'll call your table "Data"):
Has All Selected Skills
=
VAR
Selected_Skills = ALLSELECTED ( Data[Skills] )
VAR
Role_Skills = CALCULATETABLE ( VALUES ( Data[Skills] ), ALL ( Data[Skills] ) )
VAR
Missing_Skills = COUNTROWS ( EXCEPT ( Selected_Skills, Role_Skills ) )
RETURN
IF ( NOT ( Missing_Skills ), 1 )
If the measure is placed in a visual against Roles, it'll produce the following results:
The way this code works:
First, we store all selected skills in a variable "Selected_Skills";
Second, we store all skills available for a role in a variable "Role_Skills". We must use ALL(Data[Skill]) to ignore the skill slicer selections;
Third, since both variables above are tables, we can use EXCEPT function to find how they are different. Here, we tell DAX to find what records in Selected_Skills don't exist in Role_Skills. Store the result in a variable "Missing_Skills".
Finally, if Missing_Skills is zero, it means that the role has all selected skills, and we flag it as 1 (although you might use True/False, etc).
The problem I see with this approach is that if Skill selector has no selection (shows "all skills"), then the formula might return blank for all roles, and all your visuals will be blank. Technically, it's correct - it's essentially saying that no roles have all skills. But if that's not the behavior you want, consider a slightly modified approach:
Missing Skills Count
=
VAR
Selected_Skills = ALLSELECTED ( Data[Skills] )
VAR
Role_Skills = CALCULATETABLE ( VALUES ( Data[Skills] ), ALL ( Data[Skills] ) )
VAR
Missing_Skills = COUNTROWS ( EXCEPT ( Selected_Skills, Role_Skills ) )
RETURN
Missing_Skills + 0
The formula uses the same logic, only returns the number of missing skills per role, instead of a true/false status. It will allow you to show a list of skills, sorted by the number of missing skills vs the selected skill set:
You can still use it to filter your visuals; the advantage is that it's never blank, even if all skills are selected:
It also gives you a capability to see what roles are the closest to meeting the requirement, even if none matches it perfectly; might be a desirable feature.
Final note: in all these reports, I have no subtotals and totals, assuming that they are not important. If you do need them, then the formulas might need to be modified to meet your requirements for the totals (depending on what you want to show there).