SQL Server Reporting Services (SSRS) has come a long way since the initial release of SSRS in SQL Server 2000. Early on, many developers and technology managers viewed SSRS as an average report writer that lacked functionality and scalability compared to other established reporting solutions. Fortunately, Microsoft greatly enhanced the SSRS feature set over the last eight years (from SSRS 2008 to the present)-thereby empowering SSRS advocates to make a stronger and more compelling case that SSRS is indeed a prime-time player in the world of business reporting. In this article, I'll demonstrate some tips and reusable approaches in different areas of report authoring/generation that help make that case.

SQL Server Reporting Services: The Ultimate Bread and Butter of Reporting Tools

Recently, I heard someone say, “Third-party report and charting tools might have many bells and whistles, but SQL Server Reporting Services is still the bread and butter for many developers.”

From the late 1990s to 2007, I was a die-hard Crystal Reports fanatic and published a book on Crystal Reports development with .NET. Toward the end of that period, I took a serious look at SQL Server Reporting Services 2005 (I'll use the acronym SSRS for the rest of this article). I concluded that SSRS 2005 was a good but not outstanding product, and lacked functionality compared to Crystal Reports. That changed in SSRS 2008. Microsoft didn't try to match every single bell and whistle of Crystal Reports in SSRS 2008; instead, they expanded their existing reporting (and charting) functionality on their own terms, and greatly improved the scalability and performance of the reporting engine.

So instead of adding the bells and whistles, Microsoft fortified their bread and butter. Although Crystal Reports still had an edge on the total number of features, SSRS 2008 became a truly viable option for internal business reporting. Microsoft continued to expand SSRS in SSRS 2008R2, and has given certain UI areas of the product a much-needed make-over in the upcoming SQL Server 2016 release.

Still, SSRS suffers from a bit of a perception issue. To many, SSRS remains merely adequate, but lacks some of the power found in third-party tools, competing products, and even the quasi-reporting capabilities in Excel. One of my roles as a consultant is to show developers and managers the full power of SSRS and how experienced developers can extend it. Several of the tips in this article reflect those exercises to demonstrate the power of the product.

What's on the Menu?

For over a decade, I've been using the Baker's Dozen theme of giving you 13 tips. In the last year, I've even been writing articles as test or interview questions, and this article will also diverge in that I'm only covering the following eight items:

  1. Implementing column level security in reports
  2. Implementing drilldown functionality with options to set initial drilldown
  3. Creating cross-chart filter refresh effects
  4. Repeating column headings across multiple pages
  5. Dealing with page margins and blank extra pages
  6. Relating multiple tables in a report
  7. Implementing a nested “Page X of Y” on a report
  8. Implementing Running Aggregations in a report

I'll present many of these tips by saying, “You want to implement functionality/behavior ABC, but you're running into a limitation. Is there a solution or workaround?” This reflects many scenarios that I encounter in my consulting practice.

Tip #1: Implementing Column-Level Security in Reports

Suppose you build a report that both regular users and executives will view. In addition to other data, the report contains cost and profitability measures that only executives should see. The regular users should only see specified data and the space for the cost columns shouldn't appear. Users should see the report as if the cost and financial measures never existed. Does SSRS provide security features to hide columns based on domain accounts/groups?

The answer is that SSRS doesn't provide any built-in functionality to address column-level security. However, just like many developer products, SSRS doesn't stand in your way from manually implementing such functionality.

Just like many developer products, SSRS doesn't stand in your way from manually implementing column-level security functionality.

I'll keep the example very simple and will focus on the mechanics. Figure 1 shows a very basic report, with a column (“Modified Date”) that you want to configure for visibility based on the current user.

Figure 1: An SSRS report with a column that you only want certain users to see
Figure 1: An SSRS report with a column that you only want certain users to see

You can implement this in five steps. Essentially, you'll create a SQL Server table to define user/column visibility, and a stored procedure to return that information for the current authenticated user. In the SSRS report, you'll read this information into a hidden/internal SSRS parameter, and tap into the SSRS column visibility feature to show/hide the column based on the SSRS parameter.

Step 1: Create a database table in SQL Server called dbo.RightsToColumns. For simplicity, add a single row for the user domain account name and a flag for whether to see the Modified Date column (Figure 2).

Figure2: A basic SQL Server table that holds each user name and column visibility information
Figure2: A basic SQL Server table that holds each user name and column visibility information

Step 2: Create a basic stored procedure to return the table information for a specific User ID based on a parameter that the report will ultimately supply.

CREATE PROCEDURE [dbo].[GetRightstocolumns]
@UserID varchar(100)
as
select * from RightsToColumns
    where UserID  = @UserID

Step 3: In the report, create a dataset that reads the stored procedure based on the current authenticated user that SSRS recognizes. Figure 3 shows mapping the SSRS Globals variable called User!UserID to the SSRS stored procedure parameter of the same core name (@UserID). When the report executes, SSRS calls the procedure and pass in the current authenticated user as the parameter.

Figure3: An SSRS DataSet that calls the stored procedure and passes the current user as a parameter
Figure3: An SSRS DataSet that calls the stored procedure and passes the current user as a parameter

Step 4: Here is where you bring it all together. You need to collect the results of the procedure (in this case, whether SSRS allows the user to see the column). You can implement a common SSRS pattern: Create a hidden/internal SSRS parameter and map the results of this dataset to the parameter. In Figure 4, I've added an SSRS parameter called ShowModifiedDate (as an internal parameter, so that SSRS never prompts for it), and I've mapped the results of the ColumnVisible column from the dataset to the default value. In the next step, I'll use the value of this parameter to determine column visibility.

Figure 4: An internal SSRS parameter that holds the value for the column visibility
Figure 4: An internal SSRS parameter that holds the value for the column visibility

Step 5: Finally, in the SSRS designer, right-click on the column separator for the column you want to configure (the “ModifiedDate” column in this example), and select Column Visibility. In the Column Visibility dialog, enter an expression to define whether you should hide the column based on the value of the ShowModifiedDate report parameter. Note that the expression must return a Boolean value based on whether you want to hide the column, so in this case, you're returning the reverse (“not”) of the ShowModifiedDate parameter.

Figure 5: An internal SSRS parameter that will hold the value for the column visibility
Figure 5: An internal SSRS parameter that will hold the value for the column visibility

One final note that I didn't demonstrate here: Suppose the ModifiedDate column had another column to the right. If the user running the report doesn't have the rights to view the column, the SSRS engine won't even bother showing an empty space for the column. Instead, the engine shows the report as if the ModifiedDate column never existed. This makes the output appear clean for either scenario.

I've presented an overall approach, a pattern, for solving this problem. You might find that you'll need to greatly enhance this to work for your scenario. But hopefully I've provided a major start on the mechanics.

Tip #2: Implementing Drill-Down Functionality with Options to Set Initial Drill-Down

Sometimes users ask for the same type of functionality that's found in end-user tools like Microsoft Excel. Here's one example: Users want to show an aggregation at a high level (such as Sales by State) and then expand a state to show more detailed information (such as Sales by City for the selected state). Most people refer to this functionality as drill-down, although in practice, you're initially suppressing the display of lower levels of detail (like the city), and toggling the display of that lower level based on how a user clicks on a higher level (like the state).

Figure 6 shows an example of SSRS drilldown. Using the Microsoft AdventureWorks demo database, I've built a report that initially collapses sales by Shipper and Vendor. I need to create a report with two row groups that summarize by Ship Method and Vendor, and I need to create a grand total row (by summarizing all shippers). The user can expand the initial grand total to see the summarized ship methods, and then expand any ship method to see the summarized vendors for that ship method. And, although I haven't displayed it, if the user expands any vendor, the report will show individual purchase orders.

Figure 6: SSRS drill-down with options for whether to expand or collapse
Figure 6: SSRS drill-down with options for whether to expand or collapse

You can implement this in two major steps.

Step 1: In the SSRS Report, I've created two Boolean parameters (Figure 7) that allow the user to determine if SSRS should initially expand or collapse the Ship Methods group and the Vendors group. Although this first step isn't necessary for implementing basic drill-down, it does give the user the ability to define whether SSRS will initially expand or collapse a group (i.e., set each group's ToggleState).

Figure 7: Two SSRS parameters to define the initial ToggleState for the Ship Method and Vendor Groups
Figure 7: Two SSRS parameters to define the initial ToggleState for the Ship Method and Vendor Groups

Step 2: To implement drill-down at the Ship Method level, you need to define the initial visibility (based on the value from the ExpandShipMethods parameters in Step 1). You also need to allow the user to toggle the display of the ship methods by the textbox for the Grand Total (so that the user can expand or collapse the grand total to show or suppress the ship methods). In Figure 8, I right-clicked on the row group for ShipMethodName and loaded the Group Properties dialog.

To complete the example, you'd need to follow the same pattern for the Vendor row group, using the ExpandVendors parameter and setting the display toggle to the parent vendor textbox.

Figure 8: Using the SSRS Group Properties to define the visibility and the report element that toggles the display of that group
Figure 8: Using the SSRS Group Properties to define the visibility and the report element that toggles the display of that group

Tip #3: Creating Cross-Chart Filtering Refresh Effects

In the last few years, software vendors have released self-service Business Intelligence tools that allow users to create attractive and interactive dashboards and charts. These tools also boast the ability to create powerful visualizations without always needing a developer. Examples of these products include Microsoft Power BI, Tableau, QlikView, Domo, SpotFire, and others. Vendors of these tools often demonstrate (and even promote as a selling point) the ability to create a dashboard page with multiple charts, where the user can click on one data point of one chart within the page, and the tool instantly refreshes other charts to reflect the initial selection.

Here's the question: Does SSRS contain built-in functionality for this type of cross-chart filtering? Similar to Tip #1, the answer is no. SSRS doesn't provide this ability straight out of the box. However (and this becomes a familiar theme), SSRS provides enough open capabilities and hooks to allow you to create a functional workaround.

For example, look at the two charts in Figure 9. The first chart on the left plots sales as a vertical stacked bar chart by country and year. The second chart on the right plots sales by country and month. Suppose you wanted to click on a specific series of data in the first chart (sales in the United Kingdom in 2013) and force all charts on the page to highlight everything associated with U.K./2013 (Figure 10).

Figure 9: The initial report page with two charts.
Figure 9: The initial report page with two charts.
Figure 10: The report page after the user clicks on U.K. Sales in 2013 on the left
Figure 10: The report page after the user clicks on U.K. Sales in 2013 on the left

I'll throw in a pop-culture reference here. Do you remember the old TV show, “Name that Tune?” The signature response in that show was: “I can name that tune in (N) notes.” Well, I can sing the “cross-chart filter visualization” in three steps. Essentially, you'll tap into the SSRS Report Action feature for a plotted data series point, and re-launch the report. As you do that, you'll pass forward to the report a hidden parameter for the year/country that the user selected. In the reload, you'll set a Fill Color expression to dynamically color the current series in yellow if the current year/country matches the value of the hidden parameter.

I must warn readers that because you're essentially relaunching the same report, this won't perform as instantly as tools that provide this feature out of the box. So use and implement with caution.

Having said all of that, here are the three steps:

Step 1: Create a hidden/internal SSRS parameter called HotSelect. You don't need to provide any special prompts for the parameter, as the user will never see it.

Step 2: For each chart on the report page, go to the Series properties and then the Actions tab (Figure 10). Set the action to Go to report and specify the same report name (to relaunch the report). In the parameters area at the bottom of the dialog box shown in Figure 11, map the HotSelect parameter based on the current tear and country. If you haven't realized it already, this works because SSRS allows you to hook into the Actions area when the user clicks on a chart data point. It also works because SSRS keeps track of the current country/year series when you click on a specific series.

Figure 11: Setting the Report Actions
Figure 11: Setting the Report Actions

Step 3: For each chart, go to the Series Properties dialog box and the Fill tab (Figure 12). Set the Color Expression to yellow if the current year and country that SSRS is plotting happens to be the same value in the HotSelect parameter.

Figure 12: Setting the Report Series fill color based on whether the current plotted country/year matches the hidden parameter
Figure 12: Setting the Report Series fill color based on whether the current plotted country/year matches the hidden parameter

Before you go any further, let's stop and reflect that you've used hidden/internal parameters as a means to drive different SSRS behavior. It's not a stretch to say that you're implementing some SSRS design patterns here, ones that could be used for other report functions.

Let's stop and reflect that you've used hidden/internal parameters as a means to drive different SSRS behaviors.

Tip #4: Repeating Column Headings Across Multiple Pages

Even the best tools in the world have an Achilles heel, and this topic represents a very strange issue and even stranger workaround in SSRS.

Suppose you implement a standard tabular report (i.e., a fixed number of columns) that spans many pages. When the user views the report in a browser and navigates beyond page one, the SSRS viewer doesn't show the column headings. Although SSRS provides an option in the SSRS table properties to repeat column headings across each page, that option has no affect. So what's going on?

The truth is that the “Repeat header columns on each page” option in the Table/Tablix properties only works for matrix reports. The option has no effect on tabular reports. In order to repeat column headings, you need to tap into the Advanced Mode section at the bottom of the report designer. In Figure 13, I've clicked the down arrow on the far right of the report column groups. Once you go into advanced mode, you'll notice that the Row Groups area now shows several static sections (Figure 14). You'll want to click the F4 key to bring up the full property sheet for each static group above the first non-static group and set the RepeatOnNewPage property to TRUE (Figure 15).

Figure 13: Click the drop-down to the right of Column Groups to enter Advanced Mode
Figure 13: Click the drop-down to the right of Column Groups to enter Advanced Mode
Figure 14: SSRS Advanced Mode with Static Row Groups
Figure 14: SSRS Advanced Mode with Static Row Groups
Figure 15: Set the RepeatOnNewPage property to TRUE for each Static Group above the first actual group
Figure 15: Set the RepeatOnNewPage property to TRUE for each Static Group above the first actual group

This might go down in history as one of the more obscure workarounds. Even more interesting, this problem didn't exist prior to SSRS 2008. However, an obscure workaround trumps no workaround in any poker game I've ever played!

Tip #5: Dealing with Extra Blank Pages When Exporting Reports

When printing reports or exporting reports to PDF, sometimes the SSRS report engine produces an extra page at the end of the report, or (worse), inserts a blank page between each regular page. The former scenario might not anger users, but the latter scenario definitely requires immediate attention. The question is, why is SSRS doing this and how can you prevent it?

Let's take the latter scenario. This usually occurs because the width of the content body exceeds the allowable width. You can calculate the allowable content body width as the page width (based on the report property orientation: portrait or landscape), less the left/right margins. In Figure 16, the full content body is roughly 10.5 inches, whereas the total page width is 8.5 inches and the left/right margins are one inch each. So the maximum allowable content body is 6.5 inches. (In practice I'd never go beyond 6.3 inches to allow a margin of error).

Figure 16: Produces blank pages because the total ruler width (10.5 inches) exceeds allowable width less margins (6.5 inches)
Figure 16: Produces blank pages because the total ruler width (10.5 inches) exceeds allowable width less margins (6.5 inches)

Some people might address the issue in Figure 16 by simply stretching the report table inward to stay with the allowable width, but they might leave the outer design area at the original width (10.5 inches). As I've shown in Figure 17, you need to also stretch that outer edge of the design area inward to stay within the range, or SSRS generates a blank page at the end.

Figure 17: Also produces a blank page at the end because of the extra white space.
Figure 17: Also produces a blank page at the end because of the extra white space.

Tip #6: Relating Multiple Tables in a Report

Suppose you create an SSRS report that contains two datasets. The first dataset contains the primary report data and the second dataset contains lookup information. You'd like the SSRS designer to relate the two datasets based on some common field/key. Unfortunately, the SSRS designer doesn't contain any such feature. Furthermore, the SSRS table (Tablix) report control only allows you to specify one primary dataset to drive the content. Is there any way for the report to show related information from the second dataset?

Before I continue, someone might respond with a suggestion to modify the database code that returns the first result set, and add additional columns so that you don't need the second result set. For this example, assume you can't modify the database code and that you're stuck with two procedures/two datasets. Although this constraint isn't very common, it's certainly not unheard of either.

Prior to SQL Server 2008 R2, this was a very difficult challenge. Fortunately, Microsoft implemented three new functions in SSRS 2008R2 (released in spring 2010). They are Lookup, LookupSet, and MultiLookUp. These functions allow SSRS developers to return information from a secondary SSRS DataSet, so long as the secondary DataSet contains a column that you can relate from the primary dataset.

You can use Lookup to search a second dataset based on a specific common value and return a single column from the second dataset. You can use LookupSet when you want to return multiple columns from the second dataset. Finally, you can use MultiLookUp to lookup in a second dataset based on multiple values.

Here is an excellent blog entry that covers these three functions: http://www.sql-datatools.com/2015/07/lookup-functions-in-ssrs.html

Tip #7: Implementing a Nested “Page X of Y” on a Report

Nearly every report writer provides sufficient pagination features to show “Page X of Y” on reports. However, users might want a nested “Page X of Y”. For example, suppose you're generating a long report (160 pages) of detailed transactions by customer. You want to define overall pagination (Page 1 of 160, Page 2 of 160, etc.), but you also want to define pagination scoped to the customer (Page 1 of 2, if the current customer has two pages of transactions).

Similar to Tip #6, prior to SQL Server 2008R2, developers struggled to implement this. You needed to manually calculate page definitions as part of the result set. This meant determining exactly where page breaks would occur, which might become error-prone if someone changes any aspect of the report layout (report margins, orientation, etc.).

Fortunately, Microsoft implemented a new feature in SSRS 2008R2. Specifically, they redesigned the pagination to implement two sets of page numbering global variables: Globals!OverallPageNumber and Globals!OverallTotalPages, as well as Globals!PageNumber and Globals!TotalPages.

In Figure 18, I've created two textboxes in the page header to use both sets of pagination values. However, that in itself does nothing. SSRS still generates “Page 1 of 166,” “Page 2 of 166,” etc., for both sets of page counts. So what else do you have to do?

Figure 18: Report that shows an Overall Page X of Y and a nested Page X of Y for a business context (the vendor)
Figure 18: Report that shows an Overall Page X of Y and a nested Page X of Y for a business context (the vendor)

SSRS did exactly what you told it to do instead of what you wanted it do to. Even though the report contains a vendor grouping, the report could theoretically contain multiple groups, and SSRS doesn't know which group to use for the nested pagination. You need to provide that information!

In Figure 19, I've pulled up the full-blown property sheet for the Tablix control, and I've set the PageBreak properties (the dataset column that represents the grouping as the PageName property, and a value of TRUE for the ResetPageNumber property).

Figure 19: To set nested pagination, pull up the full property sheet for the Tablix control and set the Page Break properties.
Figure 19: To set nested pagination, pull up the full property sheet for the Tablix control and set the Page Break properties.

Tip #8: Implementing Running Aggregations

Even SSRS novices are aware that SSRS allows you to create report subtotals and grand totals very easily. However, does SSRS allow you to show running totals and running aggregations?

Fortunately, the answer is “yes.” Not only that, but SSRS provides some very nice functionality to make this task easy. Figure 20 shows a sample report with three sets of running averages for chronological months in a year.

Figure 20: An example report that shows running totals, running averages, and running maximum values (highest month to date)
Figure 20: An example report that shows running totals, running averages, and running maximum values (highest month to date)

At first glance, you might think that SSRS provides three separate functions for the three columns. As it turns out, SSRS provides just one function, called RunningValue. You provide the aggregation method as a parameter to the RunningValue function. Figure 21 shows an example.

Figure 21: RunningValue function with parameters
Figure 21: RunningValue function with parameters

You pass three parameters to RunningValue:

  • The column value you wish to aggregate (the Monthly sales amount)
  • The aggregation method (Sum, Avg, Max, etc.). Note that SSRS color-codes the aggregation method in blue, indicating that SSRS treats it as a keyword.
  • The scope of the aggregation (the name of the group). In this instance, you're aggregating months within a year. Every running aggregation contains a scope, so you need to provide the actual SSRS group name.

Final thoughts:

I hope I've provided some information to help you in building SSRS reports. In future articles, I'll continue to show different SSRS power tips. As a SQL Server/Business Intelligence contractor/consultant, I've always found that working near the report layer helps me to understand the breadth and depth of the client's business application. Users will always want functionality that the product doesn't provide out of the box, so the value of SSRS depends on whether the tool provides enough hooks or open architecture for developers to extend it. Overall, SSRS does a very good job here.