If you’ve ever required a dynamic data source in Power BI that can change based on who the user is, when they are querying the data source or if certain data elements have changed you can leverage the ability for Power BI to connect to a table value function in SQL Server.
Table-valued functions allow you to abstract complex business logic from the report author and encapsulate it into a database object. This simplifies report building and enables you to do things like hide hierarchy levels, filter data based on a certain point in time stored in the database or check for certain data conditions and alter the query results as appropriate.
- SQL Server 2016 or later. You can download the SQL Server 2017 developer edition HERE
- Wide World Importer sample database. A copy can be found HERE
- Power BI Desktop. You can download the latest version from HERE
In this series of articles I will step through several use cases for direct queries from SQL Server Table-valued Functions in Power BI.
Use case 1: Change results based on user
For this first use case we will cover how you can embed some simple logic in your table-valued function to hide lower levels of a hierarchy. This is useful if you want to prevent certain individuals from breaking down aggregated values but still allow them to use data at a summary level.
Create user accounts
For the purposes of simplicity, we will create some users in the database using SQL Server authentication. Connect to your SQL Server database and execute the following SQL code.
USE [master] GO --Create Bob CREATE LOGIN [Bob] WITH PASSWORD='Bob', DEFAULT_DATABASE=[WideWorldImportersDW], DEFAULT_LANGUAGE=[us_english], CHECK_EXPIRATION=OFF, CHECK_POLICY=OFF GO --Create Mary CREATE LOGIN [Mary] WITH PASSWORD='Mary', DEFAULT_DATABASE=[WideWorldImportersDW], DEFAULT_LANGUAGE=[us_english], CHECK_EXPIRATION=OFF, CHECK_POLICY=OFF GO USE [WideWorldImportersDW] GO --Grant Bob access to WideWorldImportersDW CREATE USER [Bob] FOR LOGIN [Bob] WITH DEFAULT_SCHEMA=[dbo] GO --Grant Mary access to WideWorldImportersDW CREATE USER [Mary] FOR LOGIN [Mary] WITH DEFAULT_SCHEMA=[dbo] GO --Grant Bob access to read access to WideWorldImportersDW ALTER ROLE db_datareader ADD MEMBER [Bob] GO --Grant Mary access to read access to WideWorldImportersDW ALTER ROLE db_datareader ADD MEMBER [Mary] GO
Next, we will create a function in SQL Server with the following code.
Use the code below to create a new Table-Valued function in SQL Server. The function is what we will directly connect to in Power BI.
CREATE FUNCTION dbo.ufn_Customer() RETURNS TABLE AS RETURN ( SELECT [Customer Key] ,[WWI Customer ID] ,[Customer] ,[Bill To Customer] ,[Category] ,CASE SYSTEM_USER WHEN 'Bob' THEN NULL WHEN 'Mary' THEN [Buying Group] ELSE [Buying Group] END AS [Buying Group] ,[Primary Contact] ,[Postal Code] ,[Valid From] ,[Valid To] ,[Lineage Key] FROM [WideWorldImportersDW].[Dimension].[Customer] ); GO
As you can see in the code above, I have created a function called dbo.ufn_Customer which returns the data from the Customer dimension table. In the code I have added a simple case statement that returns different data for the Buy Group based on who executing the function.
Next we will bring this function into Power BI and see the results.
Connect with Power BI
Open Power BI and get data from SQL Server. Enter in the server name and select DirectQuery.
Click on OK. Log in using a Database account. First, we will try using Bob.
Click on connect and select the function ufn_Customer from the list of available objects.
If you wanted to force the report author to use the function rather than the actual customer table you can use database security to only expose the function and not the table. I typically use custom database roles and schemas because it is easier to manage and allows me to enable “data discovery with guard rails”.
Load the data in and create a simple hierarchy using the fields Category and Buying Group.
Drop a Matrix onto the canvas of the report and use the Category Hierarchy you just created for the rows and the Customer Key for the values.
You should have a report that looks similar to the image below. Notice how the Buying Group is null because for Bob the function is not returning the Buyin Group value but the NULL value instead.
Now lets switch to Mary and see how the lower level values of the hierarchy appear in the report. Click on Home > Edit Queries > Data Source Settings. Select the data source that you are using for this report and click on Edit Permissions…
In the Edit Permission pop up menu click on Edit.. then in Database enter in Mary and Mary for the ID and PWD.
Click on Save and OK and Close. Refresh the report.
Notice how the lower levels of the hierarchy now appear in the matrix visual because for Mary the actual Buying Group value is being return as specified in the function.
NOTE: The reason why you need to refresh is because of caching. When using this technique to obfuscate lower levels of a hierarchy make sure to build your visuals so that the default view of the report is at an aggregated level and minimize the amount of caching which will force Power BI to re-query from the source and update the results appropriately.
If you want to add an enterprise semantic layer such as an Analysis Services Tabular model and still have the same dynamic results you will need to build your SSAS model using DirectQuery mode because the results of the model need to change and cannot be processed and stored in memory in advance of the user querying it.
In the next article I will cover how you can use a database function to curate the results based on a date time in your database.
Next we will look at using a database function to curate the result set based on time.
Hopefully you have found this to be another practical post.
Until next time.