Creating custom drillthrough for Analysis Services with Reporting Services
Analysis Services has changed the way many businesses look at data. Although OLAP certainly existed before Microsoft began bundling Analysis Services with SQL Server, Microsoft's move has made OLAP affordable and accessible to companies of any size. All companies can now analyze their data in order to make decisions more quickly and based on better information.
Analysis Services includes a feature called drillthrough that, once enabled by cube designers, lets data analysts drill from the cube data into the underlying relational tables. Drillthrough provides a look at the records that make up the data you see in the cube. For example, if the cube record shows that a client bought $10,000 worth of a particular item, you could drill through on that record to discover whether the client placed a single order for $10,000 or 10,000 orders of $1 each.
Drillthrough works well in most cases, but it has a shortcoming: You can't modify a drillthrough query on the fly. The cube builder sets up the query once for the cube and it's static after that point. If you need to pass parameters into the drillthrough query, you can't use Analysis Services alone. Instead, you can create your own drillthrough report in Reporting Services, then use a cell-level action in Analysis Services to call the report and pass the appropriate parameters. This option is especially useful when you need to pass one of several levels of a dimension as a parameter to a drillthrough query but don't know ahead of time which level you'll have to pass. A drillthrough report also works when you're performing counts, if the field you're counting in the fact table can contain NULL values.
Defining the Scenario
Say your company performs cleanup projects for other companies, and you want to track projects as they flow through a six-step process. In this process, a potential client asks you to bid on a project, you bid on it if you want to pursue it, the client accepts or rejects the bid, you sign the deal, you perform the work, and (usually) you get paid. To keep this example simple, we'll look mainly at the bid phase and the completion phase. For projects that don't have Bid or Completion Phase values—projects that were input but never turned into bids—you have to deal with NULL values.
The project-tracking example involves a simple star schema consisting of the three tables that Figure 1 shows. The DateDim table merely holds a date field that Analysis Services' Dimension Wizard will break into its constituent pieces when you build the Date dimension in the cube. The ProjectDim table holds a ProjectID field and a description field. The StatusFact table shows the status of each project for a particular date.
Figure 2 shows all the data the system uses. Note that only five records have a BidPhase value, and only two have a CompPhase value. Also, one record has a CompPhase but no BidPhase. In theory, that shouldn't happen, but in reality some work might be performed without a bid. More important than this sample data, however, is the concept—you need to count values in the BidPhase and CompPhase fields, but the NULL values in those fields make drillthrough problematic.
The values in the BidPhase and CompPhase fields are integers representing the number of days a project was in each phase. A NULL value in the BidPhase or CompPhase field means that the project hasn't yet been bid or completed. A 0 value means that the bid was accepted or rejected by the client or the project was started and completed. A value greater than 0 indicates that the client accepted or rejected the bid or the work was completed in the number of days specified. Knowing the number of days is important so that you can calculate the average values over time. However, at any point, you might also want to determine how many projects have been through the bid phase, so you also need to be able to count the records with a value in the BidPhase field. Retrieving a count is simple to do inside a cube if you use Count as the measure's aggregate function.
Working with the Cube
You'll work with the cube in the Analysis Services cube editor. As Figure 1 shows, the cube design is simple, with two dimensions: Date and Project. Below the Date dimension's All level are Year, Quarter, Month, and Day levels. The Project dimension has just an All level and a Project level. The cube contains two measures, BidCount and CompCount. You set up those counts by choosing an aggregate function value of Count. Figure 3 shows the cube design, including the details of CompCount.
To enable drillthrough on the cube, click Tools and choose Drillthrough Options. You then enable drillthrough and choose the fields you want to display when someone drills through. Figure 4 shows drillthrough enabled and all fields selected.
Now, it's time to look at the data and attempt to drill through to the underlying data in the relational tables. After processing the cube, you can look at the data in a variety of tools, although as I show later, it's best to avoid the cube browser that comes with Analysis Services. Figure 5 shows the data as viewed with ProClarity 6. Here, you can see in the grid just what you'd expect: For All Date, BidCount is 5 and CompCount is 2, meaning that five records in BidPhase and two records in CompPhase have a numeric value of greater than or equal to 0. You also see a list of detail records—the result of using default drillthrough—far more than the five records that make up BidCount and two that make up CompCount. If you drilled through on the BidCount value, you'd expect to see the five records that make up that value. Instead, you'd see 28 records. This happens because, although the cube counts only the records that have values, drillthrough doesn't filter out the records that have NULLs for BidPhase.
At this point, the fix appears simple. In the Drillthrough Options dialog box is a Filter tab that lets you add a filter, or WHERE clause, to the query that retrieves the relational data. Because the drillthrough query is retrieving relational data, the statement is SQL, not the MDX you use for querying cubes. To fix the problem of excessive records in the drillthrough, you might try this filter (note that although the filter is a WHERE clause, you don't actually add the word WHERE):
BidPhase IS NOT NULL
Using this filter to drill through on BidCount works perfectly. Unfortunately, if you drill through on CompCount, you get the results that Figure 6 shows; in fact, these are the same results as if you had drilled through on BidCount. Only two records have a CompPhase, but the drillthrough displays five records—and only one of those five is correct. When drilling through on BidCount, you want to filter out NULL values for the BidPhase field. When you're drilling through on CompCount you'd want to change the filter:
CompPhase IS NOT NULL
However, you can't dynamically modify the filter. To achieve this dynamic filter effect, you must use a reporting tool to create your own drillthrough report and create an action that passes your current position in the cube to Reporting Services so that your query can retrieve the correct data.
Creating the Report
Reporting Services lets you easily create reports and pass parameters into those reports. Before you can create a custom drillthrough report, you need to know the values of the dimensions coming from the cube. Then you'll be able to apply the correct filter to weed out NULL values from the results.
The first step in creating a custom drillthrough is to use Visual Studio .NET to create a new Reporting Services report and decide which fields you want in the report. The second step is to create the parameters that you'll pass to the report. In this example, you need to create parameters for the project and the date. For the Date hierarchy, you have to create a value for each level—Year, Quarter, Month, and Day—because a drillthrough could occur at any of these levels. Likewise, if you had a Project hierarchy, you'd need to create a parameter for each level of that hierarchy.
NULL values are important because if you're at the top level of a hierarchy, you need to pass a NULL value to the report so that you get all the records. For example, if you're on the Date dimension's All level, you want to see data for all years, so you must pass a NULL instead of a date value. As you'll see when I show how to create the action, the action must determine what level in the hierarchy the user has drilled to. If the user is at the Month level in the Date dimension, the action will pass the month value into a Month parameter, so the Day parameter will be NULL. Fortunately, the action passes these values to the parameters in Reporting Services; the user need not do anything special to pass the parameters.
If you were creating a parameter for only the year, the initial query might look something like Listing 1. Notice the excessive checking for NULL. When you create the report parameters, it's essential to select the "Allow null value" and "Allow blank value" checkboxes, as Figure 7 shows. In this example, the Year and Proj parameters should have both checkboxes selected. Don't worry about the Meas parameter; you'll learn more about that in a moment.
Notice that the last line in the query is a hardcoded value, CompPhase IS NOT NULL. You want to use this value only when the user has drilled down on CompCount. So you need to determine the measure on which the user drilled down and adjust this final condition accordingly. At first glance, it might seem that a simple CASE statement in the WHERE clause would work, but I couldn't get this to work in Reporting Services without moving the query into a stored procedure. A simple approach is to pass in a parameter and use an IF statement to choose the correct SELECT statement. To make the switch, you have to pass the current measure from the action into a parameter in Reporting Services. Therefore, you'll create a parameter called Meas and in this case, don't let it accept NULL or blank values. The code in Listing 2 handles a drillthrough on either CompCount or BidCount and includes the correct WHERE clause.
Note that in this code, the Meas parameter determines which SELECT statement is chosen. If you had more than two possible drillthrough fields, you'd just add multiple IF statements to check for all of them. In addition, don't set the Meas parameter to "Allow null value" or "Allow blank value." Make Meas a required parameter that must be passed from Analysis Services with each call to the report. The action you create next will pass the parameters from Analysis Services to Reporting Services.
Running this report as it now stands gives you the results that Figure 8 shows. You can fill in a Meas value of either "comp" or "bid" and get the correct results. You're now ready to create in the Analysis Services cube an action that passes the appropriate parameters to the report.
Creating the Action
Analysis Services lets you add to your cube actions that you can tie to various cube elements: a dimension level, a dimension member, an individual cell, and so on. For this example, you need a cell-level action, the most granular level of action possible. You need to be able to pass the current value for the date, project, and measure the user is interested in, so you need the intersection of all the dimensions, which is an individual cell.
When creating a new action, you have options for where in the cube to place the action. The New Action Wizard will ask for the "target" of the action, which lets Analysis Services know from where in the cube the action will be callable. Set the target to "Cells in this cube." Analysis Services includes various types of actions; for this example, use a URL action.
You now need to create the action syntax. To work with the report you just created, your action will look like the code in Listing 3. Note that the first line simply points to the report, which is named Drillthrough in a project with the same name. Then, an IIF function determines which measure the user wants to drill on (if you had more measures, you'd just nest IIF statements) and sets the Meas parameter to the correct value. Another IIF statement then determines where the user is in the Project dimension hierarchy. If the user isn't at the top (All) level, the statement passes the actual project value. A similar IIF statement determines where the user is in the Date dimension hierarchy and sets the date parameter appropriately. The last phrase of the action turns off the report's parameters bar so that the report runs with the values your action has passed to it instead. Figure 9 shows the report after you've used your newly created action to run it, and the menu command in ProClarity that you'd use to run the action.
Let me add a couple of cautions. First, the Analysis Services Manager cube browser doesn't pass parameters correctly. If you change a field in a drop-down box at the top (e.g., changing the date from All to 2004), the browser doesn't detect this change, and the action behaves as if the Date dimension is still at the All level. So choose another tool to test your action.
Second, in your action, you might want to explicitly pass a NULL value rather than ignoring the parameter and passing nothing. The syntax for this is a little strange. For example, to force the Proj parameter to NULL on the URL line of Listing 3, the syntax looks like "&Proj:isnull=true".
Your Better Drillthrough
Drillthrough is a useful tool for letting analysts see the underlying relational data when they're working with a cube. When the default behavior of Analysis Services' drillthrough isn't sufficient, you have few options for modifying it. Replacing the inherent drillthrough with a custom solution that uses Reporting Services is one option for overcoming drillthrough's limitations. Creating the report is fairly simple, although it's complicated a bit by the convoluted query syntax to check for and handle NULL values.
When first presented with this problem, Microsoft told me that you shouldn't have NULL values in the fact table. But in some cases, it makes sense to have NULL values. (And you can't always control what your clients do when they build the warehouse.) In this case, no other value would have work; a 0 in a field meant that the project had gone through that phase.
The creation of the cell-level action is less straightforward, often consisting of many nested IIF statements because you need one nesting for each dimension level. Ultimately, however, you can build a URL that passes the appropriate values to the report and launches the report for the user.