From: Frank on
Hi,

I have the following SQL Server table valued function that I've
written to use as a parameterised view. It takes a bunch of parameters
and returns a table of results. Each of the parameters can contain a
value or can be NULL. If the parameter is NULL then no rows will be
filtered by that value. The function is called from an Excel
spreadsheet (using VBA) against a SQL Server 2005 database.

CREATE FUNCTION [dbo].[fnLoanData]
(
@loan_start_date VARCHAR(8) = NULL
, @loan_end_date VARCHAR(8) = NULL
, @loan_currency VARCHAR(3) = NULL
, @customer_number VARCHAR(8) = NULL
, @department_id VARCHAR(4) = NULL
, @loan_id VARCHAR(11) = NULL
, @product_type VARCHAR(3) = NULL
)
RETURNS TABLE AS
RETURN(

SELECT l.*
, c.strCustomer_Name
FROM dbo.Loan_History AS l
LEFT OUTER JOIN dbo.Customer AS c
ON l.Borrower_Id = c.Customer_ID
WHERE (l.Loan_Start_Date >= @loan_start_date OR
@loan_start_date IS NULL)
AND (l.Loan_Start_Date <= @loan_end_date OR @loan_end_date IS
NULL)
AND (l.Loan_Currency = @loan_currency OR @loan_currency IS
NULL)
AND (l.Borrower_Id = @customer_number OR
@customer_number IS NULL)
AND (l.Department_ID = @department_id OR @department_id IS
NULL)
AND (l.Loan_Id = @loan_id OR @loan_id IS NULL)
AND (l.Product_Type = @product_type OR @product_type IS
NULL)
);

I've now been asked to change this so that a user can enter a comma-
delimited string to represent a list of items.
So for instance, instead of a single department id of 999, a user can
now enter 997,998,999.
And the code above will need to change to handle this (presumably
using an IN statement).
I'm guessing that the only way to achieve what I want is to use
dynamic SQL, which apparently isn't possible from within a function.
So is there a better (or more idiomatic) way of handling this?

Many thanks,
Frank.
From: Bob Barrows on
Frank wrote:
> I've now been asked to change this so that a user can enter a comma-
> delimited string to represent a list of items.
> So for instance, instead of a single department id of 999, a user can
> now enter 997,998,999.
> And the code above will need to change to handle this (presumably
> using an IN statement).
> I'm guessing that the only way to achieve what I want is to use
> dynamic SQL, which apparently isn't possible from within a function.
> So is there a better (or more idiomatic) way of handling this?
>
Check out Erland's articles on using arrays and lists:
http://www.sommarskog.se/arrays-in-sql.html

--
HTH,
Bob Barrows


From: Eric Isaacs on
The easiest way to do it and keep it in a FUNCTION would be to create
multiple parameters for each instance of a department. It really
depends on the requirements if this is politically feasible. Assuming
that the spreadsheet builds the function call and that the user isn't
interacting with the T-SQL function call directly, the user could key
998, 999, 997 into a cell and the code could could parse it out into 3
(or X) parameters that get passed to the function. The limitation
here is that you have to set a max number of departments that can be
included by creating a @Department1 ... @DepartmentX parameters for
each possible department parameter. Currently you have a string of
length 4. Your existing function will also need to limit the number
of parameters as well, just based on the length of that string. The
question is, do you want that string to be able to handle 999
departments at a time or say 10 at a time?

Another option would be to do the department check outside of SQL.
Limit the departments after the data is returned in the result set.

Beyond this, you could use a stored procedure and then you have a lot
more flexibility in how you handle it. You can call a stored
procedure from a function.

You could also try something like this where you parse through the
department ids and insert the data into the return table with multiple
inserts...

CREATE FUNCTION [dbo].[fnLoanData]
(
@loan_start_date VARCHAR(8) = NULL,
@loan_end_date VARCHAR(8) = NULL,
@loan_currency VARCHAR(3) = NULL,
@customer_number VARCHAR(8) = NULL,
@department_ids VARCHAR(400) = NULL,
@loan_id VARCHAR(11) = NULL,
@product_type VARCHAR(3) = NULL
)
RETURNS @LoanData TABLE
(
CustomerName VARCHAR(100)
)
AS BEGIN
DECLARE
@DepartmentId VARCHAR(4),
@Position INT

IF @Department_ids IS NOT NULL
BEGIN

SET @department_ids = LTRIM(RTRIM(@department_ids)) + ','
SET @Position = CHARINDEX(',', @department_ids, 1)

IF REPLACE(@department_ids, ',', '') <> ''
BEGIN
WHILE @Position > 0
BEGIN
SET @DepartmentId =
LTRIM(RTRIM(LEFT(@department_ids, @Position - 1)))
IF @DepartmentId <> ''
BEGIN
INSERT INTO
@LoanData (CustomerName)
SELECT
--l.*,
c.strCustomer_Name
FROM
dbo.Loan_History AS l
LEFT OUTER JOIN
dbo.Customer AS c ON l.Borrower_Id = c.Customer_ID
WHERE
(l.Loan_Start_Date >=
@loan_start_date
OR @loan_start_date IS
NULL)
AND (l.Loan_Start_Date <=
@loan_end_date
OR @loan_end_date IS
NULL)
AND (l.Loan_Currency =
@loan_currency
OR @loan_currency IS
NULL)
AND (l.Borrower_Id =
@customer_number
OR @customer_number
IS NULL)
AND (l.Department_ID =
@departmentid)
AND (l.Loan_Id = @loan_id
OR @loan_id IS NULL)
AND (l.Product_Type =
@product_type
OR @product_type IS
NULL)

END
SET @department_ids =
RIGHT(@department_ids, LEN(@department_ids) - @Position)
SET @Position = CHARINDEX(',',
@department_ids, 1)
END
END
END

ELSE
BEGIN
INSERT INTO
@LoanData (CustomerName)
SELECT
--l.*,
c.strCustomer_Name
FROM
dbo.Loan_History AS l
LEFT OUTER JOIN dbo.Customer AS c ON l.Borrower_Id
= c.Customer_ID
WHERE
(l.Loan_Start_Date >= @loan_start_date
OR @loan_start_date IS NULL)
AND (l.Loan_Start_Date <= @loan_end_date
OR @loan_end_date IS NULL)
AND (l.Loan_Currency = @loan_currency
OR @loan_currency IS NULL)
AND (l.Borrower_Id = @customer_number
OR @customer_number IS NULL)
AND (l.Loan_Id = @loan_id
OR @loan_id IS NULL)
AND (l.Product_Type = @product_type
OR @product_type IS NULL)
END

RETURN
END

....That SQL is untested, but hopefully you get the idea.


-Eric Isaacs
From: Frank on
Hi Eric,

Amazing! And works straight out of the box as well.

Many thanks,
Frank.
From: Eric Isaacs on
Yeah, I find that testing is for amateurs. :o) No, Seriously, I'm
glad it worked out of the box. The multiple insert method isn't the
most efficient solution. A function that calls a stored procedure
would probably allow you to do it with one statement instead of
multiple statements. But if the multiple departments isn't very
common, then this approach is probably the most cost-effective from a
development time standpoint.

-Eric Isaacs