Reporting Services 2008

You are currently browsing articles tagged Reporting Services 2008.

Introduction

This article is aimed at report developers who are used to develop reports using relational databases and have gotten a first-time assignment to develop reports on OLAP cubes.

It demonstrates how to build a report using SQL Server Reporting Services 2008 with data coming from an OLAP cube running on SQL Server Analysis Services 2008.

The OLAP database used in the article is called “Adventure Works DW 2008”, available for download at CodePlex.

If you’re fairly new to Reporting Services (aka SSRS) and you find that this article is going a bit too fast, I’d like to point you to my other article which explains how to build a report that’s retrieving data using regular stored procedures.

OLAP <> OLTP

When people are talking about databases, what they are usually referring to are “regular” relational OLTP databases.  OLTP stands for Online Transaction Processing.  As the name implies, these types of databases are built to handle many simultaneous transactions (consisting of actions such as inserts, updates, deletes) in real-time.  I’m sure you’re familiar with these types of database so I won’t go further into them.

OLAP (Online Analytical Processing) on the other hand is a totally different story.  OLAP cubes are built to answer multi-dimensional analytical queries as fast as possible.  For that purpose, what you can find in such a database are measures (these are the numbers) stored in cubes, and dimensions which allow filtering the measures.  This filtering is often referred to as slicing and dicing.  Furthermore, OLAP cubes contain pre-aggregated data, again to be able to answer queries as fast as possible.

Let’s make this clear with an example.  Imagine the following request:

“Give me the sum of all sales of product X for period Y in country Z.”

Three dimensions can be recognized in that request: “product X” is found in the Product dimension, “period Y” in the Date dimension and “country Z” in the Geography dimension.  (I’ve used the actual dimension names as they are called in the Adventure Works OLAP database.)

Each dimension consists of attributes and attribute hierarchies and it’s those attributes that you’re actually referring to when building an MDX query.  MDX stands for Multidimensional Expressions and that is the language used to query an OLAP database, just like you use SQL to query a relational database.

Looking at our example, what we need is for the Product attribute in the Product dimension to be equal to X.  An attribute in a dimension can also be written as [Dimension].[Attribute], thus we also want [Date].[Date] to be equal to Y and [Geography].[Country] equal to Z.

As for the measure part, that’s what “the sum of all sales” is referring to.  When looking at the measures available in the Adventure Works cube, one of the measures that would fulfill the request is the Reseller Sales Amount in the Reseller Sales measure group.  The Analysis Services engine searches the cube and retrieves the aggregated number for [Measures].[Reseller Sales Amount] available at the intersection of [Product].[Product] X, [Date].[Date] Y and [Geography].[Country] Z.

OLAP cubes are usually, although not necessarily, build on top of a data warehouse.  In SQL Server, a data warehouse is still a relational database, unlike an OLAP cube, but the table structure is different from an OLTP database.  A data warehouse contains tables that represent dimensions and other tables that contain the facts.  The facts are the numbers, so the measures that were mentioned earlier.  This is called a dimensional model.  Dimensional modeling was invented by Ralph Kimball, one of the pioneers in data warehousing.  For completeness I’d like to mention that another data warehousing approach was described by Bill Inmon.  I’ll leave it up to you to do some research on both approaches and decide for yourself which one you prefer, possibly even a mix of both.

As far as the “Adventure Works DW 2008” OLAP database is concerned, it’s built on top of the AdventureWorksDW2008 dimensional database.

Okay, I believe this theoretical explanation was sufficient for now, let’s start with the report!

Your First Report

Business Requirements

You’ve gotten the assignment to create a report that shows the reseller sales numbers by region.  The highest level to be shown is Country, with drilldown through State/Province to City.

Creating The Shared Data Source

Just like when building reports on OLTP databases, we’re not going anywhere without a Data Source.  I’m going to create a Shared Data Source called OLAP_AdventureWorks.rds:

Shared Data Source connecting to Adventure Works OLAP Database

The Type that we need is Microsoft SQL Server Analysis Services, which is the SQL Server service that’s running the OLAP databases.  Furthermore I’ve selected the “Adventure Works DW 2008” database.

Connection Properties specifying the Adventure Works DW 2008 OLAP database

There’s no need to type the database name yourself.  After you’ve provided sufficient credentials in the Credentials page, you can just select it from the dropdown in the Connection Properties screen.  This screen is opened by clicking that Edit button on the Shared Data Source Properties window.

Your First OLAP Dataset

I’ve created a new report called FirstOLAPReport.rdl.  In that report I’ve specified that I’ll be using the Shared Data Source created earlier.  This source is known as srcAdventureWorksOLAP in my report.

Next step is to create the dataset.  I’m calling it dsResellerSalesByRegion.  As this is our first OLAP report, we’re not going to write the MDX ourselves but we will use the Query Designer which is opened by clicking the button that has the words Query Designer printed on them, how difficult can that be?!

How to open the MDX Query Designer

The BIDS knows that it should open the MDX Query Designer because our data source is connecting to an Analysis Services server.  All we need to do now is to drag the measures and dimension attributes that we require into the area marked with “Drag levels or measures here to add to the query.”.

Let’s start by dragging our measures into that area.  We need two measures, both located in the Reseller Sales measure group.  They are called Reseller Order Quantity and Reseller Sales Amount.  Following screenshot shows the situation after the first measure has been added.  The second measure was being dragged into it as well.  When dragging items into the area, a vertical blue line appears to indicate where the item can be added.

MDX Query Designer: dragging a measure into the query

Next I’m going to drag the Geography hierarchy, located in the Geography dimension, into the design area.

MDX Query Designer: dragging a hierarchy into the query

Now we’ve got all the data we need for our report.

As you have noticed, the Query Designer automatically executes the query each time it gets modified when you’re dragging an item into the design area.  If you don’t want this behaviour, it can be switched off by clicking the Auto Execute button in the toolbar (indicated by a red 1 in the screenshot below).

Query Designer toolbar

Another interesting button is the Design Mode button (indicated by a green 2).  This one allows you to toggle between the graphical designer and the text editor.  By clicking it you can see the actual MDX query that the designer has prepared for you.

As you can see, the query is nicely formatted using capitals for the keywords and so on.  Well, no, actually it’s the worst editor around!  No syntax coloring, no multi-line formatting, nothing.  So if you are going to take a close look at the query, I recommend you to use the Management Studio.  Connect to your Analysis Services server, locate your database and right-click it in the Object Explorer.  Then choose New Query > MDX and paste the query into that new window.  You’ll still need to manually break it down into different lines but at least you get syntax coloring.  Furthermore, if you’re going to make manual modifications to it, you’ve got some command completion and error indicators as well.

Please take into account that once you’ve made manual changes to your query, you cannot switch back to the graphical designer.  Well, you can, but you will lose all manual modifications.  Don’t worry about doing it accidentally though, a nice pop-up will warn you:

Warning message when switching back to design mode.

Something else that you’ll also notice is that the results displayed in the Query Designer and those displayed in the Management Studio are not exactly the same.  That’s because both environments interpret the results differently.  Remember, you’re not retrieving two-dimensional row/column data like with a SQL query.  You’re retrieving multi-dimensional data!

If you take a closer look at the query that we’ve produced above, it’s similar to this:

SELECT something ON COLUMNS,
    something_else ON ROWS
FROM [Adventure Works]
That query is selecting data on two axes: COLUMNS and ROWS.  But in fact, MDX supports up to 128 axes.  However, the client tools that we are using here are not able to visualize that kind of cellset (as the result set of an MDX query is also called).
 
Okay, enough about our dataset.  We’ve got the data, let’s put it on the report!
 

Displaying The Result Set

As a reference, these are the fields available in our dataset:

Fields available in OLAP dataset

Without going into too much detail – there’s no difference compared to reporting off a relational database – I’ve set up a table with three grouping levels on the rows.  I’ve also added some makeup like background colors and font modifications.

As shown in following screenshot, the highest-level group is Country, followed by State_Province and City to conclude, just as specified in the requirements mentioned at the start of this chapter.

Table with three groupings defined

Rendering the report in preview gives us something like this:

Report without any numeric formatting applied

What is still missing at this point is decent formatting for those numbers!  And here’s where we can take advantage of the fact that we’re retrieving data from an OLAP cube.  A cube developer has the possibility to define the format for the measures in the cube itself.  Doing that ensures that the same formatting is applied no matter what OLAP client tool is used.  Any client that supports this way of formatting will show the numbers using the same format.

As you’ve seen in that last screenshot, there’s no formatting applied at all.  Does this mean that there was no format defined in the cube?  Let’s find out!

A Little Walk Into The Analysis Project

We are going to open up the Analysis Services project that contains the cube definition.  If you don’t have any experience with SSAS, don’t worry!  We will just have a look at a couple of properties and that’s it, plus I’ll explain each step as needed.  In case you’ve forgotten where the sources are located, this is the default location: C:\Program Files\Microsoft SQL Server\100\Tools\Samples\AdventureWorks Analysis Services Project\.  I’m opening the project located under the \enterprise subfolder by double-clicking the Adventure Works.sln file.

Once the project is loaded into the BIDS, locate and open the Adventure Works.cube in the Solution Explorer.  You can find it in the Cubes folder of the Adventure Works DW project.

By default it will open the cube Design showing the first page called Cube Structure.  At the top-left, we’ve got the Measures pane.  The measures are shown in measure groups.  Open the group called Reseller Sales.  Now locate the measure called Reseller Sales Amount and select it.

Cube in Design with Reseller Sales Amount selected

Now that we’ve selected one of the measures that we are retrieving in our report, have a look at the Properties window.  In case it’s not open yet you can right-click the measure and select Properties.  The property that we’re interested in is called FormatString.

Properties of the Reseller Sales Amount measure showing Currency as format string

The cube developer has specified that this measure should be shown as being a Currency.

Now that you’re in the cube, have a look at the properties for our other measure, the Reseller Order Quantity.  This one is being formatted as #,#.

The FormattedValue Field Property

So why are we not seeing those formats in our report?  Because by default they are not applied in an SSRS report!  When dragging fields from the Report Data window onto the design area, what the BIDS is retrieving is the Value property of the field.  However, there’s also a property called FormattedValue.

(You may want to make a copy of your report before applying the following changes.)

Now, change the six table cells that are showing the numbers (so including the ones showing the totals) to retrieve the FormattedValue property instead of the Value property.  The expression for the totals of the Reseller Sales Amount looks like this:

=Sum(Fields!Reseller_Sales_Amount.FormattedValue)

Once you’ve done that, have a look at the Preview:

Report Preview showing no numbers after retrieving the FormattedValue property

That doesn’t look right, does it?  We’ve lost our numbers!

Now hit the Refresh button: Refresh button in Report Preview

This time we’ve got some numbers:

Report Preview showing formatted numbers, and errors!

But we’ve also got some errors for free!  Looking at the Output window we get some extra details on the reason for the error.  Here’s one of them:

[rsAggregateOfNonNumericData] The Value expression for the textrun ‘Reseller_Order_Quantity1.Paragraphs[0].TextRuns[0]’ uses a numeric aggregate function on data that is not numeric.  Numeric aggregate functions (Sum, Avg, StDev, Var, StDevP, and VarP) can only aggregate numeric data.

In short, what it says is that our data is not numeric.  And this poses an issue when it tries to apply the SUM() aggregate function.  Right, as our data now contains formatting, it became a string instead of a number, and strings can’t be added together using SUM().

So that’s not a good way to apply the formatting, not in this case anyway.  Luckily there’s another method to do that.

But first, undo those last changes and replace the FormattedValue with the Value property.

(Or switch back to the original report if you took a copy earlier.)

The Cell Properties

What exactly is our MDX query doing?  I’m taking a closer look at it by taking it from the Dataset Properties window and pasting it into a MDX query window in the Management Studio:

SELECT
NON EMPTY { [Measures].[Reseller Sales Amount], [Measures].[Reseller Order Quantity] }
ON COLUMNS,
NON EMPTY { ([Geography].[Geography].[Postal Code].ALLMEMBERS ) }
DIMENSION PROPERTIES MEMBER_CAPTION, MEMBER_UNIQUE_NAME
ON ROWS
FROM [Adventure Works]
CELL PROPERTIES VALUE, BACK_COLOR, FORE_COLOR, FORMATTED_VALUE,
FORMAT_STRING, FONT_NAME, FONT_SIZE, FONT_FLAGS

Besides retrieving the requested measures and dimension attributes, it’s retrieving several Cell Properties, including FORMATTED_VALUE and FORMAT_STRING.  I believe that the first one rings a bell by now.  What we’re going to do is to retrieve the second one and apply it as Format property for our numeric table cells.

In the report’s Design, select one of the table cells containing a number.  In the Properties window, one of the properties is called Format.  Click to select it, then in the dropdown choose Expression….  For each of the six numeric cells, create an expression similar to the following:

=Fields!Reseller_Order_Quantity("FORMAT_STRING")

The example above tells the BIDS to retrieve the FORMAT_STRING cell property from the Reseller_Order_Quantity field.

Tip: you don’t need to open up the Expression builder for each of the six cells.  You can just copy/paste the string from the Format field.  Just ensure that you’re retrieving the format from the same field as the one that the cell is displaying.

Now let’s have a look at the Preview again:

Format is working for the quantity amounts but not for Currency!

Hmm,  the quantities are fine now, but the currencies are not!  So, let’s try out yet another method for those cells.

For the three cells containing a currency measure, remove the Format property – it’s not working anyway!

Next, change the expression that’s retrieving the Value property to something similar as this one:

=Format(Sum(Fields!Reseller_Sales_Amount.Value),
    Fields!Reseller_Sales_Amount("FORMAT_STRING"))

This expression applies the value of the FORMAT_STRING property using the Format() function.  In this particular case it’s the expression used to produce the Reseller Sales Amount total.

Having modified all three currency cells, here’s another Preview look:

Both Currency and regular numeric cells are showing formatted values!

That certainly looks better doesn’t it?!

Okay, to conclude, let’s activate drilldown by setting the subgroup levels to a collapsed state by default.

I will not go into full detail on this.  To start, make sure that the cells that are going to contain the +/- toggle have gotten a decent name, such as txtCountry for the cell that shows the Country name.  Then edit the properties of the subgroups by setting Visibility to Hide.  Also, activate the Display can be toggled by this report item checkbox and select the textbox showing the label one level higher.  Shown below is how to configure the group on State_Province.

Group Properties showing how to activate drilldown

 

Let’s have another look at the report Preview:

Fully working drilldown report

By default all nodes were collapsed.  I’ve expanded a couple of them just to show that it’s all working.

The InitialToggleState Property

Okay, I will not let you go just yet.  To really conclude I’ll let you in on a little feature related to the drilldown.  Open up the group properties for the State_Province group and set the initial visibility to Show (leave the “Display can be toggled by this report item” checked!).  Then checkout Preview:

Visibility toggle is broken!

Wow, that’s weird, the country level is expanded and yet there’s a plus icon in front of the country’s name.  Clicking it will collapse the states and change the icon to minus.  If that isn’t mixed up then I don’t know what is!

Well, the solution to this problem is simple.  Select the textbox showing the country name and locate the InitialToggleState property.  By default this is set to False, which means collapsed or in other words, False shows the plus icon.  Change it to True and now your initial state icon will be a minus!

Conclusion

With this article I believe to have shown you how to get started with reporting off an OLAP cube while throwing in a couple of tips in the process.

Have a look at another article that I wrote earlier, it explains an issue which you may run into when taking OLAP reporting a step further: SSRS and MDX: Detecting Missing Fields

Happy Reporting!

Valentino.

References

BOL 2008: The Basic MDX Query

BOL 2008: Using Cell Properties (MDX)

MDX: Retrieving Cell Properties by Greg Galloway

  • Share/Bookmark

Tags: , , , , , ,

Ever since I upgraded to SQL Server 2008 Service Pack 1 I noticed that the Management Studio was reporting incorrect version numbers when connected to Integration or Reporting Services.  This incorrect version number is located to the right of the server instance in the Object Explorer.

As usual, a picture says so much more than … :

Object Explorer showing wrong version numbers

As I have posted earlier, 10.0.2531 is the version number for SP1, while 10.0.1600 is the original RTM version number.

I never really spent time looking for an answer to this.  It was obviously a bug but I could live with it and someone else would probably already have filed it as being a bug.  So recently I came across a post by Phil Brammer that mentioned this issue.  This post got a comment from Matt Masson, a developer on the SSIS team.  Have a look at the comment but in short: the version numbers that are being shown in the Object Explorer are actually the version numbers of the service’s .exe file!  And SSMS is now showing the wrong number because these files didn’t get an update in SP1.

After a little search I found the bug report on Microsoft Connect, reported on March 11, 2009, by Dan English.  Its status is Fixed but it seems that it isn’t.  At least, looking at the comments, CU5 (Cumulative Update) for SQL Server 2008 SP1 is still showing the problem.  So I guess you could go over to the Connect page and click on that Yes button if you’re interested in seeing this fixed.  After all, it could be quite misleading to novice DB guys and gals…

On this same subject, there’s another interesting post by Adam W. Saxton, a member of the Microsoft SQL Server Escalation Services Team.  In this post he takes a closer look at the SQL Server 2008 Reporting Services version number after having installed CU2.

Conclusion: if you need to find out what version your server is running, do not rely on the version numbers that you see in the Object Explorer.  As Adam explained, one way is to look at the version numbers of the files that were included in the upgrade.  But that may a bit of an overkill.  My favorite way, assuming that all components of the SQL Server installation have been upgraded to the same version, is to use the following query:

SELECT @@VERSION;

 

On my machine that comes back with the following result:

Microsoft SQL Server 2008 (SP1) – 10.0.2531.0 (Intel X86)   Mar 29 2009 10:27:29   Copyright (c) 1988-2008 Microsoft Corporation  Developer Edition on Windows NT 5.1 <X86> (Build 2600: Service Pack 3)

And remember, have fun!

  • Share/Bookmark

Tags: , , , , , , , ,

Introduction

In a previous article I announced that I would write a sequel covering how to pass multiple-value parameters from a SQL Server Reporting Services report to a stored procedure.  So that’s what I will be writing about in this article.

As usual, I will be using the AdventureWorks2008 sample database (running on SQL Server 2008 SP1), downloadable from CodePlex.

The examples in this article, Part 2, are building further on the result achieved when following the steps described in Part 1, so please refer to the previous article if needed.

Passing Multi-Value Parameter To Stored Procedure

As we’ve already seen in Part 1, parameters can be passed from a Reporting Services report to a stored procedure.  The parameter that was used was just a simple, single-valued parameter.  However, a report parameter can be defined as being multi-value.  Let’s set one up!

Our report currently shows a list of employees who were hired after the selected hire date.  One of the columns being shown is the department in which they’re active.  We will modify the report so that it’s possible to filter the data on department – only the selected departments are to be retrieved from the database.

Creating A Multi-Value Report Parameter

The first step is to create a new report parameter, so right-click the Parameters node in the Report Data pane and select Add Parameter….

Report Data pane - Add Parameter

I’m calling my parameter Department, and I want it to be of type Integer.  In case you’re wondering why Integer, it will become clear very soon.  I have also checked the Allow multiple values checkbox:

Report Parameter Properties - setting up multi-value param

We want to make the parameter user-friendly so that the user sees a list of departments and can just select those that he needs.  That means the parameter needs to be populated with that list of departments.  To be able to do that, we first need to create a new dataset that retrieves the list of departments.

So for now, close the Report Parameter Properties screen and use the following query to create a dataset called dsDepartmentList:

select D.DepartmentID, D.Name as DepartmentName
from HumanResources.Department D
order by D.Name asc
 

This is what our query returns:

Result of DepartmentList query - a list of departments

Once the dataset is created, open up the properties of the Department Report Parameter created just before and select the Available Values page.

On that page, select the Get values from a query radio button, choose dsDepartmentList in the Dataset dropdown, select DepartmentID as Value field and DepartmentName as Label field.  The Label field is what the user sees while the Value field is what Reporting Services will use as value.  After all, we want to pass the IDs of the selected departments to our stored procedure, not the department names.  And we want the user to see the department names, not their ID.

Report Parameter Properties - Available Values

By default, no values are selected.  To make it a bit more user-friendly, let’s select all departments by default when the report first loads.  This is done on the Default Values page.

Select the Get values from a query radio button, dsDepartmentList as Dataset and DepartmentID as Value field.

Report Parameter Properties - Default Values

That’s it, the multi-value report parameter is created!  Of course, at this moment it doesn’t have any effect on the report’s content yet (switch to Preview if you don’t believe me and have a look).  We’ll get to that next.

Discover What Is Being Passed To The Stored Procedure

To be able to handle the values passed into our stored procedure, let’s first find out what exactly our report is passing into it.  We’ll do this by temporarily creating a new stored proc that will just accept the parameter values and return them.

This is what our test SP looks like:

CREATE PROCEDURE MultiValueParam
    @MyParam varchar(1000)
AS
BEGIN
    SELECT @MyParam as TheParameterReturned;
END
 

It accepts one parameter and returns it in a field called TheParameterReturned.

Set up a new dataset that calls this SP, called dsMultiValueParamTest.  I’m sure you know how to do this by now :-)

When creating the dataset, on the Parameters page, select the new parameter [@Department] that we created earlier:

Dataset Properties - Parameters

To see what the field contains, drag it from the Report Data pane onto the report canvas, above the table that was created in Part 1.  Enlarge the textbox a bit and activate the Preview tab.  Select a hire date (doesn’t matter which one) and click the View Report button.  As we’ve set up the report to select all departments by default, we don’t need to select them manually.  But of course if you want you can have a look in the Departments dropdown to check if they are actually selected.  This is the result after clicking the View Report button:

Content Of Multi-Value Parameter

If you compare that list of numbers with the result that our dsDepartmentList query returns, you’ll see that these are the values from the DepartmentID field in exactly the same order as in the query’s result.  And separated by commas.  So in other words: it’s a comma-separated string of selected values.

Wrong Way To Implement The Parameter

Now that we know what exactly the multi-value parameter passes to a stored procedure, let’s modify our main procedure by adding the extra parameter to it.

Here’s the modified procedure:

ALTER PROCEDURE GetEmployeeData
    @HireDate date,
    @DepartmentList varchar(1000)
AS
BEGIN
    SELECT E.NationalIDNumber, E.JobTitle, E.BirthDate, E.MaritalStatus, E.Gender,
        E.HireDate, E.SalariedFlag, E.VacationHours, E.SickLeaveHours,
        D.GroupName as DepartmentGroupName, D.Name as DepartmentName,
        P.FirstName, P.MiddleName, P.LastName
    FROM HumanResources.Employee E
    INNER JOIN HumanResources.EmployeeDepartmentHistory EDH
        ON EDH.BusinessEntityID = E.BusinessEntityID
        AND EDH.EndDate IS NULL -- current active department does not have EndDate filled in
    INNER JOIN HumanResources.Department D
        ON D.DepartmentID = EDH.DepartmentID
    INNER JOIN Person.Person P
        ON P.BusinessEntityID = E.BusinessEntityID
    WHERE E.HireDate > @HireDate
        AND D.DepartmentID IN (@DepartmentList);
END
 

Since our parameter is a comma-separated list of our values, I’ve used the IN operator to filter on only the selected values.

Next we need to add the extra parameter to the dsEmployeeData dataset.  Double-click it in the Report Data pane to get its properties and click the Refresh Fields button to have it add the new parameter to the list.  Then switch to the Parameters page and select the [@DepartmentList] parameter as Parameter Value for the newly-added @DepartmentList parameter.

Close the properties popup and run the report by activating the Preview tab.

Oh no, an error!  More precisely this one (I will only mention the last line):

Conversion failed when converting the varchar value ‘12,1,16,14,10,9,11,4,7,8,5,13,6,3,15,2’ to data type smallint.

This error comes from our stored procedure.  It complains that it cannot convert the list of values from a string to a smallint.  Weird isn’t it?  Well, maybe not.  Let’s have a look at what’s going on.

The following query would work perfectly:

SELECT E.NationalIDNumber, E.JobTitle, E.BirthDate, E.MaritalStatus, E.Gender,
    E.HireDate, E.SalariedFlag, E.VacationHours, E.SickLeaveHours,
    D.GroupName as DepartmentGroupName, D.Name as DepartmentName,
    P.FirstName, P.MiddleName, P.LastName
FROM HumanResources.Employee E
INNER JOIN HumanResources.EmployeeDepartmentHistory EDH
    ON EDH.BusinessEntityID = E.BusinessEntityID
    AND EDH.EndDate IS NULL -- current active department does not have EndDate filled in
INNER JOIN HumanResources.Department D
    ON D.DepartmentID = EDH.DepartmentID
INNER JOIN Person.Person P
    ON P.BusinessEntityID = E.BusinessEntityID
WHERE D.DepartmentID IN (12,1,16,14,10,9,11,4,7,8,5,13,6,3,15,2); 
 

But that is not what is being executed by our SP!  In the query above, we are passing a list of numbers to the IN operator.  But our SP accepts a varchar, a string.  Sure, the report parameter passes a list of numbers, but they are stored in a string!  An equivalent query for what our SP actually executes is the following:

SELECT E.NationalIDNumber, E.JobTitle, E.BirthDate, E.MaritalStatus, E.Gender,
    E.HireDate, E.SalariedFlag, E.VacationHours, E.SickLeaveHours,
    D.GroupName as DepartmentGroupName, D.Name as DepartmentName,
    P.FirstName, P.MiddleName, P.LastName
FROM HumanResources.Employee E
INNER JOIN HumanResources.EmployeeDepartmentHistory EDH
    ON EDH.BusinessEntityID = E.BusinessEntityID
    AND EDH.EndDate IS NULL -- current active department does not have EndDate filled in
INNER JOIN HumanResources.Department D
    ON D.DepartmentID = EDH.DepartmentID
INNER JOIN Person.Person P
    ON P.BusinessEntityID = E.BusinessEntityID
WHERE D.DepartmentID IN ('12,1,16,14,10,9,11,4,7,8,5,13,6,3,15,2'); 
 

When executing that in the Management Studio, it will throw this error:

Msg 245, Level 16, State 1, Line 1

Conversion failed when converting the varchar value ‘12,1,16,14,10,9,11,4,7,8,5,13,6,3,15,2′ to data type smallint.

Doesn’t that look familiar?!

The reason for this error is the following.  The DepartmentID field is of type smallint.  Therefore SQL Server tries to convert the list of values to smallint.  In the first SELECT statement, each value gets converted to smallint and all works fine.  In the second SELECT, SQL Server sees just one value, a varchar(1000), and tries to convert that to a smallint.  It fails because the value that the string contains is not convertible to smallint.  If the string would contain only one value, it would actually work.

You can try it out by replacing the last line with this:

WHERE D.DepartmentID IN ('12'); 
 

Right Way To Implement The Parameter

We’ve seen that our first implementation of using the IN operator is not a good idea.  So we need to find another way to get this stored procedure working.

Note: it would actually be possible to use the previous method in combination with dynamic SQL but I’m not going to apply that technique here.  In case you are interested in that method, just construct a long string that contains the whole query as it is in the working SELECT statement above.  For more info on dynamic SQL I’d like to point you to this excellent article by SQL Server MVP Erland Sommarskog: The Curse and Blessings of Dynamic SQL.

The IN operator can take a subquery.  So now we need to find a way to “select” the values out of our comma-separated string of values.  I am not going to re-invent the wheel and use a function that’s mentioned in another great article by Erland Sommarskog.  The article is called Arrays and Lists in SQL Server 2005 but is also applicable to 2008 and mentions a function called iter$simple_intlist_to_tbl.

For this article’s readability purposes I’ve renamed the function to list_to_tbl.  Here’s the code to create it:

-- from http://www.sommarskog.se/arrays-in-sql-2005.html
-- original name: iter$simple_intlist_to_tbl
CREATE FUNCTION list_to_tbl (@list nvarchar(MAX))
   RETURNS @tbl TABLE (number int NOT NULL) AS
BEGIN
   DECLARE @pos        int,
           @nextpos    int,
           @valuelen   int 

   SELECT @pos = 0, @nextpos = 1 

   WHILE @nextpos > 0
   BEGIN
      SELECT @nextpos = charindex(',', @list, @pos + 1)
      SELECT @valuelen = CASE WHEN @nextpos > 0
                              THEN @nextpos
                              ELSE len(@list) + 1
                         END - @pos - 1
      INSERT @tbl (number)
         VALUES (convert(int, substring(@list, @pos + 1, @valuelen)))
      SELECT @pos = @nextpos
   END
  RETURN
END
 

It takes a list of comma-delimited integers and returns a resultset containing integers, just what we need!

When we implement this in our main procedure, this is what it looks like:

ALTER PROCEDURE GetEmployeeData
    @HireDate date,
    @DepartmentList varchar(1000)
AS
BEGIN
    SELECT E.NationalIDNumber, E.JobTitle, E.BirthDate, E.MaritalStatus, E.Gender,
        E.HireDate, E.SalariedFlag, E.VacationHours, E.SickLeaveHours,
        D.GroupName as DepartmentGroupName, D.Name as DepartmentName,
        P.FirstName, P.MiddleName, P.LastName
    FROM HumanResources.Employee E
    INNER JOIN HumanResources.EmployeeDepartmentHistory EDH
        ON EDH.BusinessEntityID = E.BusinessEntityID
        AND EDH.EndDate IS NULL -- current active department does not have EndDate filled in
    INNER JOIN HumanResources.Department D
        ON D.DepartmentID = EDH.DepartmentID
    INNER JOIN Person.Person P
        ON P.BusinessEntityID = E.BusinessEntityID
    WHERE E.HireDate > @HireDate
        AND D.DepartmentID IN (select * from list_to_tbl(@DepartmentList));
END
 

And indeed, if we now run our report again, it works perfectly!

Report using multivalue parameter

Note: for another great reference on how to deal with a delimited list as Stored Proc parameter, I’d like to point you to the following article by colleague Expert and SQL Server MVP angelIII: http://www.experts-exchange.com/articles/Database/Miscellaneous/delimited-list-as-parameter-what-are-the-options.html

Displaying The Filter On The Report

Another best practice as far as report readability goes is that it should be clear on your report what data has been filtered.  As the multi-value parameter is on focus here, I’ll demonstrate how you can show the selected values on your report.

In fact, it’s not really the parameter’s values that we are interested in now (those are DepartmentIDs, remember?).  No, it’s the labels.  And here’s how to get to them.  Add a new textbox above the main report table.  Make it the same width as the table and right-click it to add an Expression.  Enter the following expression:

=Join(Parameters!DepartmentList.Label, ", ")

 

It uses the Join function to join all members of the Label collection together into one string, using comma and space as the value separator.  This is what it looks like on the report:

Showing selected values of a multi-value parameter on the report

 

In case you would like to see the selected departments under each other instead of in a long string, that’s also quite easy to achieve.  The expression is based on Visual Basic, and in Visual Basic there’s a constant called vbCrLf – Visual Basic carriage-return line-feed.  Adapt the expression to the following and the values will be shown in a list instead of a long string:

=Join(Parameters!DepartmentList.Label, vbCrLf)
 
Let’s have another look at the effect:
 

Showing selected values under each other

 

Conclusion

With this article I believe I’ve demonstrated that it is possible to pass multi-value parameters from a SQL Server Reporting Services report to a stored procedure, while applying some best practices such as giving the users a nice list of values to select from. 

Happy reporting, thank you for reading my article, and should you feel like it: post a comment!

Valentino.

References

BOL 2008: the IN operator

The Curse and Blessings of Dynamic SQL by Erland Sommarskog, SQL Server MVP

Arrays and Lists in SQL Server 2005 by Erland Sommarskog, SQL Server MVP

BOL 2008: Expression Examples (Reporting Services)

delimited list as parameter, what are the options? by angelIII, SQL Server MVP

  • Share/Bookmark

Tags: , , ,

Introduction

The first step of creating a SQL Server Reporting Services (SSRS) report involves setting up a connection to the data source and programming a dataset to retrieve data from that data source.  The dataset can use a SELECT query, which is the most common way of retrieving data and one that you’re probably already familiar with.  But it can also use a Stored Procedure (aka stored proc or SP).

The purpose of this article is to demonstrate how data can be retrieved from a SQL Server database through Stored Procedures defined in that same database, and then displayed in a SQL Server 2008 Reporting Services report.

I’ll be using the AdventureWorks2008 sample database available for download at CodePlex.

What Are Stored Procedures?

There are actually different types of stored procedure in the context of SQL Server.  The type that I am using in this article is called a “Transact-SQL Stored Procedure”.  According to the Books Online, this type of stored procedure is:

“A saved collection of Transact-SQL statements that can take and return user-supplied parameters.”

If you have experience using a regular programming language such as Visual Basic or C#, I’m sure this sounds familiar.  You can think of a stored procedure as a method that takes any number of parameters, depending on its definition, and that possibly returns a result, again depending on its definition.  Instead of being stored in a compiled .exe or .dll, it is stored in the database.

In this article I will be using some stored procedures that return a dataset as result.  How stored procedures are written is not the purpose of this article.  For that I’d like to refer you to this Books Online page: Implementing Stored Procedures.

Reasons For Using A Stored Procedure

There are several reasons why it is more interesting to use datasets based on stored procedures as opposed to SELECT statements.

Performance: stored procedures perform faster than SELECT statements.  The reason for this is because they are compiled when they’re created and their execution plan gets stored by SQL Server so that it can be reused for each procedure call.

Maintenance: when a database needs to undergo some changes to its schema, the changes can be handled in the stored procedures.  This makes it transparent for the reports that are using these stored procedures.  The reports will keep functioning as expected without any modifications to them.

Reuse: imagine a situation where several reports are reporting on the same set of data.  If you wouldn’t use stored procedures, you may have to repeat a possibly complex query in each report.  With stored procedures you just need to define the query in the stored procedure and then call it in each report’s dataset.

Security: in environments where DBAs are responsible for the SQL Server databases, the report developers will possibly not get sufficient rights to retrieve data from the tables directly.  One of the ways to prevent this is to give them access to a bunch of stored procedures and views instead.

Simple Procedure Call

Okay, enough theory, time to show you how it’s done!

Setting Up The Stored Procedure

Our first procedure queries the AdventureWorks database to return a list of employees with their corresponding department.  Here’s the code for the SP:

CREATE PROCEDURE GetEmployeeData
AS
BEGIN
    SELECT E.NationalIDNumber, E.JobTitle, E.BirthDate, E.MaritalStatus, E.Gender,
        E.HireDate, E.SalariedFlag, E.VacationHours, E.SickLeaveHours,
        D.GroupName as DepartmentGroupName, D.Name as DepartmentName,
        P.FirstName, P.MiddleName, P.LastName
    FROM HumanResources.Employee E
    INNER JOIN HumanResources.EmployeeDepartmentHistory EDH
        ON EDH.BusinessEntityID = E.BusinessEntityID
        AND EDH.EndDate IS NULL -- current active department does not have EndDate filled in
    INNER JOIN HumanResources.Department D
        ON D.DepartmentID = EDH.DepartmentID
    INNER JOIN Person.Person P
        ON P.BusinessEntityID = E.BusinessEntityID;
END

Creating The Report

The next step is to create a new Report in a Report Server project using Business Intelligence Development Studio 2008 (aka BIDS).  To add a report my preferred way is to right-click the Reports folder in the Solution Explorer (this is assuming that you’ve already created a Report Server project) and then select Add > New Item… :
 
Creating a new Reporting Services report
 
Make sure that the Report template is selected.  I’m calling my report StoredProcDataset.
 
Add New Item - Report
 

Creating A Shared Data Source

Now that we’ve got an empty report we still need to get some data.  We’ll set up a Shared Data Source first.  Shared Data Sources are convenient when you’re planning to create several reports on the same database.  It removes the connection string from the report itself and puts it in a separate Shared Data Source object, making it easy to switch between databases.  (Imagine putting your reports into the production environment – instead of needing to modify each report to connect to the production database, you just need to modify the Shared Data Source).
 
In the Solution Explorer, right-click the Shared Data Sources folder and select Add New Data Source.
 
Solution Explorer Shared Data Sources - Add New Data Source
 
 
Shared Data Source Properties
 
I’m calling the Shared Data Source AdventureWorks2008.  A Data Source can connect to several different sources, such as Oracle or an Analysis Services cube.  In fact, it can connect to any source through OLE DB, as long as there’s an OLE DB provider that has all the expected functionality implemented.  For a good list of drivers, have a look at the Books Online: Data Sources Supported by Reporting Services.
 
The one that we’re interested in now is called Microsoft SQL Server, which is by default selected in the Type dropdown.
 

Creating The Data Source

Next we’ll add a Data Source to the report based on the Shared Data Source.
 
Report Data pane - New Data Source
 
In the Report Data pane, select New > Data Source….
 
If you don’t see the Report Data pane, you can open it by going to the View menu item and selecting Report Data right down at the bottom of the menu window.  Or hit CTRL + ALT + D.  That’s an interesting shortcut to memorize because the Report Data pane has a tendency to disappear, especially if you’ve closed and re-opened the BIDS.
 
Data Source Properties
 
I’ve called my Data Source srcAdventureWorks2008 and I’ve told it to use the existing Shared Data Source.
 

Creating The Dataset

So, now we’re ready to query our stored procedure.  In the Report Data pane, right-click the newly created Data Source and select Add Dataset….
 
Right-click Data Source to Add Dataset

 

Dataset Properties

I’m calling the Dataset dsEmployeeData.  The Data Source edit box is already prefilled with the right data source because we’ve right-clicked the data source to which we want to add a dataset, how easy can it be?

To query a stored procedure from a dataset is really very straightforward.  All you need to do now is select the Stored Procedure radio button.  Doing that replaces the bottom part of the window.  Instead of an edit box that expects a query, we now get a simple dropdown where we need to select our SP.

Once you’ve done that, select the Fields page on the left.

Dataset Properties - Fields

As you can see, the list of fields has been pre-filled.  The BIDS queries the stored procedure’s metadata so we do not need to manually specify the fields returned by our SP, saving us quite some time!

Once you click OK to close the Dataset Properties window, you’ll see that the Report Data pane gets populated with our list of fields.

Report Data pane - list of fields in dataset

Show Me The Data

To prove that everything is working as expected, let’s set up a quick report using a Table.  From the Toolbox pane, drag a Table object onto the report canvas:

Report with a Table

Next, from the Report Data pane, drag some fields into the cells in the Data part of the Table.  As we’re dealing with employee data, it would be interesting to see their names.  I’ve also added their job and department.

Resize the columns a bit so that everything will fit nicely, and color the Header cells different from the Data cells to easily distinguish them.

Table with some fields added and basic layout

Now we’re ready to run the report, so click the Report’s Preview tab.

Table showing some employee data

So, that was our first procedure.  Wasn’t too complicated, was it?  So, ready for another one?

Parameterized Procedure Call

This time we’ll make it a little more complicated.  As you know, stored procedures can take parameters.  And SSRS knows how to pass them into a stored proc.  So let’s do that!

Setting Up Stored Proc Version 2.0

The stored procedure shown below is based on our previous one.  Except now it takes one parameter: @HireDate.  The SP will only return employees that have been hired after given HireDate.

ALTER PROCEDURE GetEmployeeData
    @HireDate date
AS
BEGIN
    SELECT E.NationalIDNumber, E.JobTitle, E.BirthDate, E.MaritalStatus, E.Gender,
        E.HireDate, E.SalariedFlag, E.VacationHours, E.SickLeaveHours,
        D.GroupName as DepartmentGroupName, D.Name as DepartmentName,
        P.FirstName, P.MiddleName, P.LastName
    FROM HumanResources.Employee E
    INNER JOIN HumanResources.EmployeeDepartmentHistory EDH
        ON EDH.BusinessEntityID = E.BusinessEntityID
        AND EDH.EndDate IS NULL -- current active department does not have EndDate filled in
    INNER JOIN HumanResources.Department D
        ON D.DepartmentID = EDH.DepartmentID
    INNER JOIN Person.Person P
        ON P.BusinessEntityID = E.BusinessEntityID
    WHERE E.HireDate > @HireDate;
END

 

As the HireDate field in the HumanResources.Employee table is of type date, I chose to make the parameter that same data type.

Please note that the above script uses ALTER PROCEDURE instead of CREATE PROCEDURE.  This will only work if you’ve already created the previous procedure.  If not, just replace the word ALTER with CREATE.

Modify Report To Pass Parameter To Stored Procedure

The next step is to modify our existing Dataset so that it passes a date into our stored procedure.

Open up the Dataset’s properties by double-clicking it in the Report Data pane.  Then select the Parameters page:

Dataset Properties - Parameters empty

As you can see, it is still empty.

Now switch to the Query page and click the Refresh Fields… button:

Dataset Properties - Query page - Refresh Fields button

Then switch back to the Parameters page:

Dataset Properties - Parameters page populated

The parameter has been automatically added when we clicked the Refresh Fields… button!

The BIDS again used the stored proc’s metadata to do this.  No need for manual intervention.  For now, leave the Parameter Value box as it is, empty.  Click OK to close the properties window.

Clicking OK caused another action in our report!  If you open up the Parameters folder in the Report Data pane, you’ll see that it has gotten an item as well:

Report Data pane - Parameters

You can double-click the @HireDate parameter to get its properties:

Report Parameter Properties

The BIDS has created a report parameter and linked it to the parameter in our Dataset.  It automatically chooses the correct data type for the report parameter, Date/Time in this case.  (Unlike in T-SQL, there are no separate Date and Time data types in SSRS 2008.)

To confirm that the report parameter is linked to the parameter in the Dataset, close the Report Parameter Properties window and open up the Dataset Properties again, selecting the Parameters page:

Dataset Properties - Parameters page - Parameter Value filled in

The Parameter Value for our parameter has now gotten a value, more precisely it’s referring to the Report Parameter called @HireDate.  If you don’t believe that it is actually the report parameter (I admit, it looks similar to the parameter in the Dataset, except for the square brackets surrounding the report parameter’s name.), click the fx button to see the formula:

=Parameters!HireDate.Value
 

Indeed, it is referring to the parameter on the report.

Test The Changes

Now that the parameter has been set up, it’s time to test our report again.

As the report is now filtering on the HireDate, it would be interesting to actually show this field in the table.  Add an extra column and drag this field into the data cell.

Our newly added field’s type is datetime, but we’re only interested in the date part.  So we’ll set up a format code.  Select the HireDate data cell and locate the Format property in the Properties pane.  Enter a small d into it:

Formatting a datetime to only show the date

This instructs the BIDS to show the field using the “short date pattern”.  A list of possible codes is available on MSDN: Standard Date and Time Format Strings.  As you notice, SSRS is using the regular formatting strings as they are known in .NET.

Note also that the format used to display the data depends on the report’s Language property.  I’ll leave it at its default: en-US.

Right, time to have a look at the report.  Select the Preview tab.  You can now see the Hire Date parameter which is expecting a value.  You can either type a value or select it from the calendar which is shown when you click the button on the right of the textbox:

Calendar control for a datetime report parameter

To be sure that you’re using the correct format for the date, I suggest to select a date using the control.  Then you can always modify it to whatever day you’d like to select.

For our test I know that some employees started before 2000 and others later, so I’ll select a date somewhere in the year 2000.

Report with datetime filter

And indeed, our report shows only employees that started after our selected HireDate.

Conclusion

With this article I hope to have shown you that it’s fairly straightforward to report on data coming from stored procedures.

In case the above hasn’t fulfilled your appetite on this subject yet, watch out for part 2.  In that sequel I will build further on the example used in this article to show you how you can use multi-value parameters to filter your report’s data.

In the meantime: happy reporting, and thank you for reading my article!

References

BOL 2008 – How to: Create an Embedded or Shared Data Source

BOL 2008 – How to: Create a Dataset (Reporting Services)

BOL 2008 – How to: Refresh Fields for a Dataset

BOL 2008 – Implementing Stored Procedures

BOL 2008 – Data Sources Supported by Reporting Services

  • Share/Bookmark

Tags: , , ,

A while ago I wrote an article called Chart Optimization Tips.  This article explained how to optimize a Column Chart.  Today I have returned to show you some Pie Chart implementation techniques.

As usual, I will be using the AdventureWorks2008 database, available at CodePlex.  The chart itself will be implemented using SQL Server 2008 Reporting Services.

Retrieving The Data

The dataset in our report uses the following query:

select SWD.*, SWA.City, SWA.StateProvinceName, SWA.PostalCode, SWA.CountryRegionName, SWA.AddressType
from Sales.vStoreWithDemographics SWD
inner join Sales.vStoreWithAddresses SWA on SWA.BusinessEntityID = SWD.BusinessEntityID

This query illustrates a bad coding practice: never use “SELECT *”.  Ideally you should only retrieve the columns that you need for the report.  That will optimize performance when generating the report.  But that is not the goal of this article so I’ll leave the query as it is.

A Basic Pie Chart

To get started with our Pie Chart I have selected the third icon in the list of Shape charts.  This adds a regular 3D pie chart to the report.

Select Chart Type window

To set up the chart I dragged AnnualSales from the Report Data pane into the “Drop data fields here” area and StateProvinceName into the “Drop category fields here”.

In case you don’t see the Report Data view (it has a tendency to disappear now and then), you can open it through the main menu: View > Report Data.

Report Data Pane

This is what our report looks like in Preview:

Basic Pie Chart

Wow, we’ve still got some work to do, this looks like a kids color book!  You wouldn’t say that this chart is showing the annual sales, would you?  There aren’t even any numbers on it!  Let’s get started on improving this.

Sorting The Numbers

A good implementation practice is to sort the slices from large to small.  If the slices are not sorted, it’s difficult to tell which state is performing better than another.  Just take a look at the previous image and compare the following two slices:

  • the pie shown in grey at 12:00
  • the pie shown in yellow at 03:00

Which one is the larger of the two?  Indeed, “I don’t know” is the right answer.

To implement the sorting you need to think about what you want to achieve.  What is it that we want to sort?  The states.  And these are shown as categories on the chart, so we should take a look at its properties.  As shown in the following screenshot, right-clicking on the [StateProvinceName] button gives a pop-up menu.  Select Category Group Properties.

Context menu for Category Group

Then we need to think about how we want the states to get sorted.  For that we should look at what is being shown as data of the chart.  In our case that is the sum of the AnnualSales field.

In the Category Group Properties, select the page called Sorting.  Clicking the Add button will add a line in the sorting options list.  Use the following expression for the “Sort by” field:

=Sum(Fields!AnnualSales.Value)

As we want to sort the largest values first, select “Z to A” for the Order dropdown.

Category Group Properties

Right, time to have another look at our report in Preview.

Sorted Pie Chart

So, we went from a colorful mess to an ordered colorful mess.  Chaos has been reduced a bit, but this is still one difficult-to-read report.

On to the next improvement!

Limiting The Pies

As you have noticed, a pie chart is not suitable to show that many categories.  We need to find a way to reduce the slices.  One way to do that is by adding a filter.  Another way is to add the smallest slices together into one slice.  This can be interesting in cases where we want to use all the data but we’re only interested in the larger slices.  Luckily, this can be done using standard pie chart properties.

Click on the pie itself, this will select the Chart Series.  One way to tell if you’ve selected the correct part of the chart is by looking at the Properties pane.  Its selection should show something like “AnnualSales Chart Series”, where AnnualSales is the name of the chart series.  Another way to tell is by the small white selector bulbs: they should be surrounding the pie.

Now, among the properties of the Chart Series you will find a property group called CustomAttributes.  Open this one by clicking the plus icon in front of it.  Change the CollectedStyle property to SingleSlice.  This tells the chart that we want to group the smallest slices into one slice.

Other interesting properties here are CollectedThreshold and CollectedThresholdUsePercent.  I’ve put CollectedThreshold to 2 and CollectedThresholdUsePercent to True (which is its default).  This means that any slice smaller than 2 percent of the pie will be added into the “collected slice”.

More useful properties are CollectedLabel, that’s the text that is shown on the slice itself, and CollectedLegendText, the text shown in the legend.

Chart Series - collected slice properties

The CollectedStyle property has another option besides the one I’ve shown, called CollectedPie.  Choosing that will generate a second pie next to the main one to represent all the small slices.  See the following screenshot for what it looks like.  In some cases this may be an interesting option but not in our example here.

Pie Chart showing a CollectedPie

The collected pie can also show labels by setting the CollectedChartShowLabels property to True, and the categories shown on the collected pie can be shown in the pie’s legend by setting CollectedChartShowLegend to True.

If you’d like the collected slice to jump out, there’s a property called CollectedSliceExploded.  Setting it to True will produce something like the following:

Exploded Collected Slice

As you have noticed, the previous screenshots have started to show text on the slices.  This can be easily activated by right-clicking the pie and selecting the Show Data Labels menu item.

Context menu on pie chart - Show Data Labels

And the next screenshot shows what our chart currently looks like.

Pie chart with collected slice

The small slices have been replaced by a really large one, and the text on the large slice is our customized version.  The other slices are showing some rather large numbers, so we still have some work to do.

Displaying Percentages

Let’s customize the label shown on the slices.  As the numbers are really large, I recommend to divide them by 1,000.  As long as it’s clearly mentioned on the report, it will make everything more readable.

Furthermore I’ll show you how to use built-in chart keywords (only available to ToolTips, custom legend text, and data point label properties), such as #PERCENT.

Right-click on one of the data labels and select Series Label Properties.

Context menu of data labels - Series Label Properties

Click the expression (fx) button on the General page and enter the following expression:

=FormatCurrency(Sum(Fields!AnnualSales.Value) / 1000, 0) & " (#PERCENT{P1})"

The first part divides the sum of AnnualSales by 1,000 and then applies the FormatCurrency function to the result.  The second parameter for FormatCurrency tells the function that we don’t want any decimals.  The result of this function call is concatenated with the second part using Visual Basic string concatenation (&).

The second part looks like a regular string but it contains a built-in keyword: #PERCENT.  This will show the percentage that the slice represents.  Furthermore, there’s a custom string formatter appended: P1.  By default the percentage would show 2 decimals.  This way it will only use one digit for the decimal fraction.

See here for a list of all built-in keywords and this page for more information on the available formatting options.

And following screenshot shows what our chart now looks like.

Pie chart showing percentages on slices

I’ve also given it a clear title, decreased the Data Label font size to 8 and moved the legend down.

To move the legend: right-click it, select Legend Properties and play with the radio buttons for the Legend Position.

Legend Properties

So, we’ve now got a fairly readable chart.  It’s not perfect, some labels are overlapping, but it’s doable.  However, we won’t rest here.  On to the next tip.

Rotating The Pie

Some people may ask you, “Why on earth does the first slice (the blue one representing 10.6% in our example) start at this weird angle at 4 o’clock?  Why can’t it start at 12:00?”.

Again we’re lucky because this can be controlled using a standard property.  Among the Chart Series CustomAttributes property group there are still some properties which haven’t been mentioned earlier.  One of them is called PieStartAngle.  By default it is set to zero.  Funny enough, zero stands for 30°.  Try it out and enter 30 for the property value.  Did you see the effect?  Indeed, nothing happens!  Now enter 90.  Did you see the chart rotate, even in Design mode?  Switch to Preview to get a better view of what the impact is.  As you can see, setting it to 90 will cause the first slice to start at 06:00.  To make it start at 12:00, we thus need to set the property to 270 degrees.

Pie Chart with customized rotation angle

Labels Outside Pie Chart

Other people may tell you, “But I don’t want all these labels on the pie itself, I want them next to it.”.

We’re still lucky because again this can be achieved using standard properties.  Still in the Chart Series CustomAttributes, there’s a property named PieLabelStyle.  Its default value is Inside.  Switching it to Outside will render the labels outside the pie, with lines attaching them to their respective slice.

Other interesting properties for the outside labeling are 3DLabelLineSize and MinimumRelativePieSize.

3DLabelLineSize defines the amount of space used for drawing the line between the label and its corresponding slice and is a percentage of its default size.  Values range from 30 to 200.  I’ve put it to 30 to get as much space as possible for the pie itself and the labels.

MinimumRelativePieSize represents a percentage of the chart area size and defines the minimum acceptable pie size.  Values range from 10 to 70.  I’ve put this one to 70 to maximize the size of the pie.

Chart Series properties with properties for outside labeling highlighted

With these modifications we’ve actually gotten some extra space for the labels.  Let’s take advantage of that and add extra information in the labels.  Change the Data Label expression to the following:

="#LEGENDTEXT" & vbcrlf &
FormatCurrency(Sum(Fields!AnnualSales.Value) / 1000, 0) & " (#PERCENT{P1})"

Our expression uses another built-in keyword: #LEGENDTEXT.  This will add the legend text to the label itself, which means the legend becomes obsolete.  So I’ve removed it.

And this is what our chart now looks like:

Pie chart showing labels on the outside

With Halloween coming up I thought it would be nice to create a spidery chart :-)

One More Custom Attribute

I’ve already mentioned several CustomAttribute properties of the Chart Series and I’d like to mention one more.  This property is called PieDrawingStyle and it will only appear in the list of properties when 3D is not enabled.  After disabling 3D I could set it to either SoftEdge or Concave.  I also noticed that labels outside of a pie chart will only have lines attached to them when rendered in 3D, so I’ve switched back to Inside for the PieLabelStyle property.

This is what SoftEdge looks like.  I think it’s rather nice.

Pie chart using SoftEdge drawing style

Custom Coloring

To conclude, there may be some people who tell you, “I don’t like those colours, and I don’t like any of the predefined sets.  I want to specify custom colours.”.

So again we’re lucky because even that is supported by default.

To get started with our color customization, select the chart object.  To know if you’ve made the correct selection, the Property pane should show “Chart” as non-bold part of the dropdown.  Alternatively you can just use that dropdown to select the Chart.  As Chart is a main object on the report, it is shown in the list (whereas parts of a Chart, such as Chart Series and Chart Area, are not shown in that list).

With the Chart selected, locate the Palette property.  By default it is set to BrightPastel.  In case you’re happy with one of the predefined palettes you can just select it here.  But we go for Custom, located at the bottom.

Next, locate the CustomPaletteColors property.  Selecting the property will show a button with an ellipsis as button text.  Click this button to get to the ReportColorExpression Collection Editor (what a name for a popup window!).  This window allows you to specify a list of colors.  I’ve specified the following 10 colors:

ReportColorExpression Collection Editor

And finally this is what our report looks like.  To stay in the theme, I’ve specified some colors which are suitable for Halloween-time charting.

Pie chart with custom colors - Halloween-style!

Coloring Consistency Using Dynamic Colors

And now to really conclude this article I’d like to mention one additional tip related to chart coloring.  In some occasions it may be interesting to have coloring consistency between different charting periods.  With that I mean that California would always show in grey, no matter whether it came first or not, Washington as brown, and so on.  This is currently not the case.  With the current implementation it’s the first pie that gets the grey color, the second pie is brown, and so on.

The best way to achieve that is to store the colors in the database and then fetch them in the same dataset that is used to retrieve the chart data.  The AdventureWorks database hasn’t got any color codes stored so I’ll just illustrate what I mean using a little cheat.

In order to get our dynamic coloring working, we will override the colors from the palette.  This is how it’s done.  Right-click on the pie and select Series Properties.  Select the Fill page and click the Expression (fx) button to define the color.  In the case where you’re selecting the color code as one of the database fields, your expression would look similar to this (assuming that colors are stored using their 6-digit hexadecimal representation with 000000 being black and FFFFFF being white):

="#" & Fields!ColourCode.Value

To imitate dynamic coloring I’ve used the following expression:

=Switch
(
    Fields!StateProvinceName.Value = "California", "Blue",
    Fields!StateProvinceName.Value = "Washington", "Red",
    Fields!StateProvinceName.Value = "Florida", "Green",
    True , "#888888"
)

The expression gives three states their own color and all the others will be colored a kind of grey.

This is what it looks like:

Pie Chart using dynamic colors

So, I hope you’ve enjoyed reading this article.  Feel free to post any comments should you wish to do so, and… happy charting!

References

SQL Server 2008 Books Online: Pie Charts

How to: Collect Small Slices on a Pie Chart

Formatting Data Points on a Chart

How to: Define Colors on a Chart Using a Palette

  • Share/Bookmark

Tags: , , , ,

Two commonly-used functions in Reporting Services are the IIF() and the Switch().  These are two functions of the Program Flow type, or Decision Functions as they are called on this MSDN page.  In case you’re wondering why it’s so difficult to find a function reference for the built-in functions of SSRS, it’s because these are actually Visual Basic functions and Microsoft refers to those for any detailed explanation.  Click this link for the IIF() function in the Visual Basic Language Reference, and this one for the Switch().

Anyone who’s done some programming most likely already knows the if <expression> then <some_code> else <other_code> statement.  If <expression> evaluates to true then <some_code> gets executed, else <other_code>  gets executed.

The IIF() works in the same way.  According to its description it “Returns one of two objects, depending on the evaluation of an expression.”.  This is its definition:

Public Function IIf( _
    ByVal Expression As Boolean, _
    ByVal TruePart As Object, _
    ByVal FalsePart As Object _
) As Object
 

Here’s a simple example.

=IIf(Fields!YearlyIncome.Value >= 60000,"High","Low")
 

Now have a look at the following example.  It has been nicely structured with indentation and line breaks to make reading easier.

=IIF
(
    Sum(Fields!LineTotal.Value) >= 100,
    "Violet",
    IIF
    (
        Sum(Fields!LineTotal.Value) < 25,
        "Transparent",
        "Cornsilk"
    )
)
 

As you see, it shows a nested IIF inside another one.  Imagine that there were several more nestings and that line breaks were not used by the coder.  Would be a nightmare to read, right?

That’s why the Switch() was invented.  The description for the Switch function reads “Evaluates a list of expressions and returns an Object value corresponding to the first expression in the list that is True.”.

And this is the function definition:

Public Function Switch( _
    ByVal ParamArray VarExpr() As Object _
) As Object
 

In Reporting Services, the VarExpr parameter is simply an even list of expressions and/or object references separated by commas.  Which comes down to something like this: Switch(<expr1>, val1, <expr2>, val2).

Here’s a simple example:

=Switch(Fields!State.Value = "OR", "Oregon", Fields!State.Value = "WA", "Washington")
 

Now, to get to the point of this blog post, the Switch function does not contain an ELSE part like the IIF does.  But I wouldn’t be writing this if there wasn’t a workaround, would I?  If you read the Switch’s description closely, it says that it will return the first expression in the list that is true.  So each expression is evaluated in the order that they are passed to the function.  To get ELSE-like behavior we would need an expression that evaluates to True but only when all other expressions are False.  So, why not use True as expression?  It’s the simplest expression that I can think of and it does the works!

Have a look at the following, it’s a rewrite of the last IIF example mentioned earlier.

=Switch
(
    Sum(Fields!LineTotal.Value) >= 100, "Violet",
    Sum(Fields!LineTotal.Value) < 25, "Transparent",
    True, "Cornsilk"
)
 

Quick tip for users of Report Builder 2.0: to be able to format your expression with line breaks and tabs, you need to use CTRL + ENTER or CTRL + TAB in the Expression Builder.  Just hitting ENTER will close the popup window.  It’s quite annoying if you’re used to the BIDS interface, but it works :-)

  • Share/Bookmark

Tags: , , ,

I came across an issue when playing around with Report Builder 2.0.  I had created a report using an embedded data source.  Once I’d published the report to the report server, I couldn’t get it to run anymore.  Instead it gave me the following error:

This report cannot be run in report builder because it contains one or more embedded data sources with credential options that are not supported.  Instead of embedded data sources use shared data sources or save and view the report on the server.

Okay, no problem I thought, let’s just create a shared data source and switch to that one then.  So I opened up the Data Source Properties in Report Builder and selected the Use a shared connection or report model radio button.

Unfortunately, when running the report it threw me that same error?!  And when I open the Data Source properties again, my change was undone!  It was still using the embedded data source.

As far as I’m concerned that should be a bug.

The only way that I could switch my data source to a shared connection was by creating a new data source, which means you also need to move all datasets connected to the original data source.

Quick tip: if you first rename the original data source and datasets to something like srcMyDataset_OLD, you can give the correct name to the new one straightaway.

So I guess that’s another workaround on my list :-)

This issue was encountered while using Report Builder 2.0 (10.0.2531.0).  I tried to reproduce it using Report Builder 3.0 (10.50.1092.20 – that’s the version of the SQL Server 2008 R2 August CTP) and I couldn’t.  Which means it has been fixed.  Good on you Microsoft!

  • Share/Bookmark

Tags: , , , , , ,

Here are some optimization techniques that can be used when creating charts in SQL Server 2008 Reporting Services.  These tips will probably be already known to experienced chart developers, but freshmen charters may spent some time searching how to achieve something before actually finding it, if finding it at all.  I know because I’ve been there myself when I started out and I also know because I’ve seen questions on forums related to this.

Okay, enough vague intro-words, let’s get concrete now.  In the following example I’ll be creating a regular column chart.

X-axis: show all labels

By default, a chart in SSRS will automatically position the labels on the X-axis as it best fits.  However, one of these options includes hiding labels when the chart feels there are too many to show.  As you can guess, this is not always what we want.

Column bar chart without any changes to its default settings

Have a look at the chart above and try to guess what you’re seeing.  Difficult, huh?  Let’s make some changes to it.

First we start with some basics like giving it a clear title.  This chart shows the annual revenue per state/province, split up by store specialty.  The states or provinces shown are those of the selected country.  I would call it “Annual Revenue per State/Province by Specialty for <SELECTED_COUNTRY>”.  As you probably know, the chart’s title can be edited by giving it a single-click.  However, this does not give you the option to build an expression so unless you want to type it all from memory, here’s another option.  You can right-click on the chart’s title.  This gives you a pop-up menu with Title Properties… as one of the options.

Right-click menu on chart title

Selecting that one will give you the Chart Title Properties where you have the familiar Expression Builder icon next to Title Text textbox.

Chart Title Properties

This is my expression:

=“Annual Revenue per State/Province by Specialty for ” & Parameters!Country.Value

Also, let’s move the legend to the upper middle to make extra horizontal space for all those bars.  This can be done through the Legend Properties.

Legend Properties

Lastly, now that we’ve freed up some space for the chart area, we’ll modify the X-axis properties so that it shows all labels.

When opening up the axis properties for the X-axis on a column chart you get the Category Axis Properties screen.  Like all other property screens, this is also one with several pages.  You see that one of the pages is called Labels and as you want it to show all labels, that’s were you start looking.  Well, stop looking, that’s the wrong place.  The option that you need is located in the first page, the Axis Options, and its called Interval.  This is the interval between each label on the axis, and by default it is set to Auto.  As we want all labels, change it to 1.

Category Axis Properties

Following screenshot shows what the result looks like:

Column chart showing all labels on category axis

Better, but we’re not quite there yet.

X-axis: rotate labels in all directions

Right now the labels on the X-axis are difficult to read unless we turn our screen 90 degrees clockwise.  Let’s dive again into the Category Axis Properties to put them diagonally.  On the Labels page there’s an option to specify the Label Rotation Angle.   To be able to do this you need to activate the Disable auto-fit radio button.  Putting 45 as value will give the following:

Labels rotated 45 degrees

The labels are rotated 45 degrees.  But not in the direction that I would prefer.  It’s nicer when they’re positioned from bottom-left to top-right.  So you start increasing the rotation value.  However, once you’re past 90 you notice that the labels stay vertically, so 135 degrees does not put them in the direction as you’d hoped.  The answer is quite simple, once you know it.  As we want the opposite of our 45 degrees, and it’s not 135, try –45 :-)

Category Axis Properties with Label rotation angle set to a negative value

Indeed, that gives the following result (I’ve also removed the X-axis title).

Column chart with optimized X-axis

To finalize the X-axis optimization, I’ve added sorting so that the States/Provinces are sorted alphabetically.  This is a recommended design practice to keep your different charts consistent.

To get the labels sorted, you should not look into the Axis properties but in the Category Group Properties:

Pop-up menu to get to the Category Group Properties

On the Sorting page just add the field that’s shown on the X-axis.

Category Group Properties with sorting

Y-axis: make numbers readable

So, now that we’re done with the X-axis, let’s move on to the next letter of the alphabet.  Our Y-axis (also known as Value Axis on a column chart) is not very readable at the moment.  The numbers are too large and there’s no formatting.  You don’t even see that this is a currency value.

This time we need the Number page of the Value Axis Properties.  We want:

  • no decimals
  • a thousands separator
  • the numbers divided by 1000 (that’s the “Show values in” setting)
  • a dollar sign in front of the value

To get all this, you can set the options as shown in the screenshot:

Value Axis Properties optimized for large currencies

And here’s our chart again:

Chart with optimized Y-axis

Additionally, the axis title has also gotten a clear value.  It indicates how the values should be read.

Y-axis: move to right-hand side

To conclude this article I’ll show you how to move the Y-axis to the other side of the chart.  This may seem like an odd thing to do, but sometimes the business people want to display a chart that way so you’d better have strong convincing skills or know how to do it.  Or both :-)

In fact, a chart has got two Value axes and two Category axes, a Primary and a Secondary.  The Primary axes are the ones used right now.  To move the visible axis to the other side, it’s not through the Value Axis Properties as you might think (I think you’re getting the hang of it now, right? ;-) .  No, all you need to do is move all data series to the secondary axis.  This can be done through the Series Properties.

Pop-up menu on data series

The Axis and Chart Area page is the one you’re after.

Series Properties with Secondary Value axis activated

That will give us the following end result:

Column chart with Y-axis on the right-hand side

BTW: you will need to re-do the axis formatting when you switch to the secondary axis so better start with axis activation before formatting it.

Happy charting!

  • Share/Bookmark

Tags: , , , ,

Now that the European Elections are over once again, I’d like to draw your attention to another request for voting.

Using Reporting Services 2008, at this moment it is not possible to link two datasets.  Linking datasets would be really interesting in certain cases.  Imagine you’ve got two datasets, used in two different report data regions.  However, your second data region needs data from the first dataset as well.  As you probably know, a data region can only be bound to one dataset.  Both datasets contain an identical identifier so in theory they could be perfectly linked, if only the IDE would allow it.  Linking two datasets should result in a third dataset that behaves exactly the same as a regular one, which would allow us to bind that one to a data region.

Here’s a small example to clarify the above:

Dataset 1 consists of col1, col2.

Dataset 2 consists of col1, col3.

col1 is an identifier.

If you could tell SSRS to join dataset 1 and dataset 2 on col1, resulting in:

Dataset 3: col1, col2, col3

that would be really great!

This has several benefits:

  • the data is already available in another dataset, why load it twice ?
  • performance: in certain cases combining the two queries into one would result in a slower query (I’m thinking of situations where the database design is not optimal and you’ve got no control over it – sounds familiar ?)
  • developing reports would become an even nicer experience (no need for workarounds such as this one)
  • if Crystal Reports and others can do it, why shouldn’t SSRS be able to do it as well ?

If you Google around (or should I say Bing around ?), you can see that I am certainly not the only one with this question in mind.  In fact, someone has already posted a request on Microsoft Connect and a Microsoft representative said the following:

Thank you for your suggestion.
We are indeed considering adding this kind of functionality in a future release of Reporting Services. We are also monitoring the customer vote count on this particular suggestion to gauge the relative community demand compared to other suggestions.
Thanks,
Reporting Services Team

So if you’d like to see this feature implemented in Reporting Services, click this link and vote!

  • Share/Bookmark

Tags: , , ,

A while back when I started using a preconfigured server with Windows Server 2008 and SQL Server 2008 I came across the following.

My reports were deployed to the server, they were working fine when using Internet Explorer directly on the server.  But when I tried to connect remotely to the Report Manager (http://server.domain/Reports) I got the well-known “Page cannot be displayed” message.  Then I tried the Report Server (http://server.domain/ReportServer) and that seemed to work fine.  Until I asked it to generate a report, same issue.

I then logged on to the server to have a look if anything was appearing in the SSRS-related log files.  I saw the GET requests with the ListChildren command from Report Server getting logged in the HTTP log, but no Render command.  Other log files didn’t show anything for the report render requests and neither did the ExecutionLog2 view in the ReportServer database.  Which is quite logical of course, if there’s no GET request, there’s no execution either.  But hey, I was exploring all options here.

Then I activated HttpWatch to have a look at what was happening with the GET request.  And guess what!  It was being redirected to HTTPS!  So the report server apparently did not accept remote unencrypted connections, it forced the user into HTTPS.  But that didn’t work either, probably because no one properly configured SSL.

In my situation there’s no need for the encrypted connection so I started looking for the switch to turn off SSL.  I had a look in the Reporting Services Configuration Manager, no checkbox that says “do not redirect to SSL” or similar.  Nothing in the Report Server properties either.

Apparently the only way to turn SSL off (or on for that matter) is by changing a setting in the RSReportServer.config file.  This setting is called SecureConnectionLevel and valid values are:

0 Least secure. The report server does not check for a secure connection when Web service methods are invoked. However, all calls to the Web service can still be made to the report server over a secure connection.
1 Minimally secure. All Web service calls that are made over an insecure connection and which might pass sensitive data such as user credentials are rejected. However, this setting does not guarantee security. It is still possible for sensitive data sent by the client to the report server to be exposed before the report server handles the request and rejects it.
2 Secure. All rendered reports and all Web service calls require a secure connection. This includes all calls to the Render method and requests for rendered reports made through URL access.
3 Most secure. All calls made to the Reporting Services SOAP API require a secure connection.

The descriptions in the table above are copied from the Using Secure Web Service Methods article, see there for more details.

In my situation the setting was set to 2.  Changing it to 0 solved my issue.  This also explains why the Report Server was partly working.  When the security level is set to 2, all calls to Render and other web service methods need to be secure while listing the children can be done unencrypted.

In case you’re interested in actually using SSL, have a look at the Configuring a Report Server for Secure Sockets Layer (SSL) Connections article.

I noticed that my blog item contains links to almost all relevant MSDN BOL pages related to Reporting Services configuration, interesting to keep in mind :-)

  • Share/Bookmark

Tags: , ,

« Older entries

© 2008-2010 A Developer's Blog All Rights Reserved