Thursday, July 23, 2009

SSRS Multi-Select Parameters for Stored Procedures in SQL 2005

[Intro]

Using SSRS multi-value parameters with stored procedures can be a bit of a pain. Multi-Valued parameters are sent to Stored Procedures as a comma-delimited string. What follows is the method I use to write and debug stored procedures for SSRS reports.
The web is full of tutorials on handling multi-valued parameters in stored procedures. What I’m hoping to show is a method to set them up so that you can easily debug the stored procedure later on down the road.

[Background]

I found some code on SQLServerCentral.com that does a good job of splitting a comma-delimited string into a table of individual values. I wish I could credit the person who originally posted this idea, but there are lots of variations of this code floating around, and I modified the version I originally found, so please accept my apologies if this looks familiar.
This function will break down a comma-delimited string into a table of individual values. The one I’m posting here returns a table of ints, but I have others that return a table of other values (i.e. strings):

----------------------------------------------------
-- [dbo].[fnc_split_int]
-- Parse a comma delimited string and insert the values into a table variable.
-- Useful for sending a comma-delimited string parameter to a stored proc that will
-- use it in a where IN clause.
--
-- Ex:
-- select * from customers
-- will not work: Where customer_id in (@parameter)
-- will work : Where customer_id in (select item from fnc_split_string(@parameter,','))
---------------------------------------------------------
ALTER FUNCTION [dbo].[fnc_split_int](
@list varchar(8000),
@Delimiter VARCHAR(10) = ','
)
RETURNS @tablevalues TABLE (
item int
)
AS
BEGIN

DECLARE @P_item varchar(255)

WHILE (DATALENGTH(@list) > 0)
BEGIN
IF CHARINDEX(@Delimiter,@List) > 0
BEGIN
SELECT @p_Item = SUBSTRING(@List,1,(CHARINDEX(@Delimiter,@List)-1))
SELECT @List = SUBSTRING(@List,(CHARINDEX(@Delimiter,@List) + DATALENGTH(@Delimiter)),DATALENGTH(@List))
END
ELSE
BEGIN
SELECT @p_Item = @List
SELECT @List = NULL
END

INSERT INTO @tablevalues
SELECT Item = convert(int, LTRIM(RTRIM(@p_Item)))
END

RETURN

END

Here’s a simple example of a stored procedure using this function:

create proc dbo.salesinfo (
@customers varchar(max)
)
as

Select * from dbo.sales
where customer_id in ( select item from fnc_split_int(@customers, ',') )

[Using Table Variables]


The method above works well, but can get kind of annoying if you ever have to debug the stored procedure. The stored procedure could be huge, and you might not care too much about which customers are displayed. However it now requires a comma-delimited string of customers anytime you want to run some tests.
To keep me from losing my mind, I add a few table variables in my stored procedure that are designed to handle the multi-value SSRS parameters.


This is probably best explained in an example:

create proc rpt.salesinfo (
@customers varchar(max) = null --set to null by default
)
as

--Build local customer table
declare @customerlist table (
customer_id int
)

if @customers is null
begin
--Load all customers (null)
insert into @customerlist
select customer_id from customers
end
else
begin
--Load only those customers chosen in @customers
insert into @customerlist
select item from fnc_split_string(@customers, ',')
end


--report query (using @customerlist)
select * from sales
where customer_id in (select customer_id from @customerlist)


Notes:
  • It might seem a little excessive to build a local table of values when you already have those values in comma-delimited form. But using this method has saved me a lot of headaches when I’m trying to debug something in the stored procedure.

  • I could use a join instead of putting the @customerlist table in the where string. However, I’ve found that performance hasn’t really been an issue for our setup. I also think it’s a little easier to read – a new developer doesn’t have to worry about finding a join that is only used to limit the resultset. From what I remember, the server does a lot to optimize queries anyway, so things like this usually do not matter as much.

[Final Thoughts]

This example shows a method I use to test and debug stored procedures for SSRS reports that use multi-valued parameters. There isn’t any ground breaking functionality introduced, just a bunch of smaller things strung together to hopefully make life easier. I should mention there are other ways of doing this, some might be better than this one. At the time of this writing I was only using SQL Server 2005.

Please feel free to ask questions or suggest improvements.

Labels: , ,

Thursday, July 3, 2008

Number of Decimal places and the SubString() function

I wanted a function that could give me the number of decimal places used in a given number. I really didn’t find any elaborate solutions on the web, so I decided to write my own by converting the number into a varchar and parsing it appropriately. While building the function, I discovered a neat little feature of the SubString() function.


Normally, SQL seems to work with an index of 1. So I if asked it the position of the “J” in “Jason”, it would return 1 (one). However, I was surprised to learn that the SubString() function accepts a 0 (zero) as a parameter for where to start the sub string.


This is an effective way to trim-off the last character of a string using SubString(@var, 0, len(@var)). For example, if I want to trim off the last character of “0040”, I can use the SubString() command to trim off the last “0”.

Here is an explanation with code:

--SubString(string, start, length)

Declare @var varchar(10);

Set @var = '0040';

Select SubString(@var, 1, len(@var)) --will return ‘.0040’

Select SubString(@var, 0, len(@var)) --will return ‘.004’


Using 0 for the start point with the length of the string has the effect of trimming the last character from the string. I wasn’t expecting this behavior, but it certainly is nice!


Decimal Count Function

Here is the entire function in case you are looking for something similar. I built it for use with SQL Server 2005.


Here is my usual disclaimer:

I’m not sure if this was the best way to go about solving the problem, but it worked. I thought my code and comments might be useful for someone else who has a similar problem. This code is free and comes with no guarantees.


One more thing - apparently I lost my formatting (tabs/spaces/etc) when I pasted this code, and I'm too lazy to fix it. Should only take a couple of minutes to make it look the way you're used to seeing it.



/*

DecimalCount

7/3/2008 JMJ

Count of the number of decimal places used after decimal point.

Params:

@numString varchar(MAX) varchar representing a number, assumes 1 (one) decimal point

Returns:

int Number of places used after decimal point, 0 (zero) if no decimal point

is found in the number. Will not consider zeros on the end of the number

(i.e. .0040 will consider only .004, which will return 3).

*/

CREATE function [dbo].[DecimalCount] (

@numString varchar(MAX)

) returns int

as

begin

declare @decimalPlace bigint;

declare @subStr varchar(max);

set @decimalPlace = 0;

--find index of decimal place

set @decimalPlace = Charindex('.',@numString,0);

if (@decimalPlace > 0)

begin

-- @subStr = string without decimal (or anything left of decimal)

set @subStr = @numString;

set @subStr = Right(@numString, Len(@numString) - @decimalPlace);

-- remove any trailing zeros

while(charindex('0',@subStr,len(@subStr))) = len(@subStr)

begin

-- catch conditions for numbers like '100.' or '100.0'

if ( charindex('0',@subStr,len(@subStr)) = 0

and len(@subStr) = 0 )

begin

break;

end

--using 0 for beginning will trim the last char from string

set @subStr = SubString(@subStr, 0, len(@subStr));

end

if (len(@subStr) is null)

begin

return 0;

end

else

begin

return len(@subStr);

end

end

else

begin

--no decimal place

return 0;

end

return 0;

end

Labels: , , ,

Thursday, March 6, 2008

Scripting Database Objects from MS SQL Server 2000 to 2005 for Subversion

Our development team keeps track of Database objects in our environment by scripting changes from SQL Server to a text file, then using a Subversion repository to keep track of changes. The process works pretty well for our team size, and Subversion provides us with a fairly good history of each database object.

We started this process back in SQL Server 2000 using the scripting utilities provided to generate the text files, using their file name convention. However, after we upgraded to SQL Server 2005, Microsoft changed the file naming convention of the text files. I’ve included a small table showing some of the differences.

Database Object

SQL 2000

SQL 2005

View

dbo.ViewName.VIW

dbo.ViewName.View.sql

Table

dbo.TableName.TAB

dbo.TableName.Table.sql

Table Trigger

dbo.TriggerName.TRG

TriggerName.Trigger.sql

Stored Procedure

dbo.StoredProcName.PRC

dbo.StoredProcName.StoredProcedure.sql

The filename change was a big thorn in our side. Subversion was tracking the files by filename and now all the filenames were going to change! At first we toyed with the idea of writing our own code to export the objects using the old naming convention, but we didn’t want to have to maintain yet another program. In the end we decided we were just going to have Subversion rename all the files accordingly.

Since we’re mostly doing our development on Windows, we use TortoiseSVN (http://tortoisesvn.tigris.org/), which is a great tool. However, we had around 600+ script files to rename and I didn’t feel like individually renaming each. Instead, I decided to write a small program to break down each file, determine what type of object it was, and then call “svn rename” with the appropriate change.

To do this, I had to download the Subversion client tools for Windows (http://subversion.tigris.org/project_packages.html). After I had it installed and the command line tools were working, I wrote a small C# program in Visual Studio to do the work. I never formally finished the program; instead I simply used it in debug mode so that I could step through to watch the process in detail (It only had to work once).

The end result was a bunch of renamed script files that had their revision history kept intact. I’m not sure if this was the best way to go about solving the problem, but it worked. I thought my code and comments might be useful for someone else who has a similar problem. This code is free and comes with no guarantees. Good luck.


Download the code here


using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.Text;
using System.IO;


namespace RenameSVNFiles
{
class Program
{
static void Main(string[] args)
{

Console.WriteLine("Begin SVN Rename Script");

//Set to your directory
String sDir = "D:/Projects/dbscripts";

DirectoryInfo sourceFiles = new DirectoryInfo(sDir);
FileInfo[] destFiles = sourceFiles.GetFiles("*.*");

String sOrigFileName = "";
String sNewFileName = "";
String strCmdLine = "";
String sOrigFileExt = "";
int iFileExtLength = 0;
int iModifiedCount = 0;

//Declare and instantiate a new process component.
System.Diagnostics.Process process1 = new System.Diagnostics.Process();
process1.StartInfo.WorkingDirectory = sDir;

foreach (FileInfo fi in destFiles)
{

sOrigFileName = fi.Name;

//Find extension length and File name (assumes syntax of filename.extension)
iFileExtLength = sOrigFileName.Length - sOrigFileName.LastIndexOf(".");
sOrigFileExt = sOrigFileName.Substring(sOrigFileName.LastIndexOf("."), iFileExtLength);

//change each file's extention to the new naming standard
switch(sOrigFileExt)
{
case ".VIW":
sNewFileName = sOrigFileName.Substring(0, sOrigFileName.LastIndexOf(".")) + ".View.sql";
iModifiedCount++;
break;
case ".TAB":
sNewFileName = sOrigFileName.Substring(0, sOrigFileName.LastIndexOf(".")) + ".Table.sql";
iModifiedCount++;
break;
case ".TRG":
sNewFileName = sOrigFileName.Substring(0, sOrigFileName.LastIndexOf(".")) + ".Trigger.sql";

//Triggers do not have the "dbo." at the beginning of the file anymore - remove them
String tmpStr = "dbo.";
char[] trimChars = tmpStr.ToCharArray();
sNewFileName = sNewFileName.TrimStart(trimChars);
iModifiedCount++;
break;
case ".PRC":
sNewFileName = sOrigFileName.Substring(0, sOrigFileName.LastIndexOf(".")) + ".StoredProcedure.sql";
iModifiedCount++;
break;
default:
//default - skip file
continue;
}

strCmdLine = "rename " + sOrigFileName + " " + sNewFileName;

try
{
process1.StartInfo.FileName = "svn.exe";
process1.StartInfo.Arguments = strCmdLine;
process1.StartInfo.CreateNoWindow = true;
process1.Start();
process1.WaitForExit();
}
catch (Exception ex)
{
Console.WriteLine(
"Exception Occurred :{0},{1}",
ex.Message, ex.StackTrace.ToString()
);
Console.WriteLine("While trying: " + sOrigFileName + " -> " + sNewFileName);
}

Console.WriteLine(sOrigFileName + " -> " + sNewFileName);
}


process1.Close();

Console.WriteLine("");
Console.WriteLine("");
Console.WriteLine("Number of files in directory: " + destFiles.GetLength(0));
Console.WriteLine("Number of files modified: " + iModifiedCount);
Console.WriteLine("Press any key to exit...");
Console.ReadKey();
}
}
}




Labels: , , ,

Thursday, April 12, 2007

Show/Hide Results Pane -- SQL Server 2005

I've been using Microsoft SQL Server 2005 for a few months now, and I've noticed a few differences between the SQL Server 2000 Enterprise Manager / Query Analyzer tools and the new Management Studio. I'll post some quick things I did to feel more at home in the new development environment.

Show/Hide Results Pane

One of the simplest and most useful features when debugging a script is to able to run your query to see some results, then hide the results pane to continue tweaking the sql code. This feature was a button in Query Analyzer in SQL Server 2000, but for some reason disappeared from the default buttons available in SQL Server 2005's Management Studio. Well, the button may not exist as a default button, but it's easy to add it back in.


After running a script in a query window, the window splits between the script and the results (each referred to as a pane). See the snapshot above.



To add a button to hide the results pane, click on the little down-arrow at the end of a toolbar and choose "Add or Remove Buttons", then choose "Customize..."



The Customize dialog window will open with the "Commands" tab selected. Now we only need to find the command we are looking for. Under "Categories:", choose "Window". Then under "Commands:", find the command "Show Results Pane".




Once you have located the "Show Results Pane" command, you have to click and drag it to where you want it to be located on the toolbar.

You can now Hide the Results Pane



Or you can Show the Results Pane


Originally I had my button located on the Standard toolbar, but eventually I figured out it made more sense to add it to the SQL Editor toolbar. It's pretty easy to find the SQL Editor toolbar - it only displays when you are working in a query window.

Labels: ,

Monday, March 19, 2007

Tabbed vs. MDI Queries - SQL Server 2005

I've been using Microsoft SQL Server 2005 for a few months now, and I've noticed a few differences between the SQL Server 2000 Enterprise Manager / Query Analyzer tools and the new Management Studio. I'll post some quick things I did to feel more at home in the new development environment.

Tabbed vs. MDI (Toolbar vs individual windows)

This is extremely simple, but I'm often surprised how many developers are debugging an issue but only looking at one query at a time. They often forget the exact item they are looking for. I switched to a windowed view a long time ago and found it useful to hold information in the background.


The default view in the Management Studio is to use tabs for all the queries (see snapshot). I know some developers like to see one statement at a time, but I'm usually referencing more than one query at a time.

To change the layout of the queries, chose Tools > Options from the menu. The first item under Environment gives you the option to switch between Tabbed documents and MDI (windowed).




Here is the result, all the queries are no longer in a tabbed view:


Labels: ,