Specifying 0 to N parameters for Stored Procedures with XML
While the topic has been discussed before, here is another look at the "Multiple but Optional Parameters" issue, when using TSQL. Sometimes you want to write a stored procedure which returns ResultSet(s), but you may or may not want to pass in some parameters. A common delimited list just does not seem to cut it anymore. And dynamic sql doesn't cut the mustard either.
All of us have been faced at times with a problems like these:
What if you want to ~sometimes pass in a parameter?
How do you handle multiple parameters?
I call this the "0:N Parameter" problem. If I pass in a parameter, I want the TSQL to consider it. If I don't pass it in, I want TSQL to ignore it. And of course, sometimes I want to have more flexibility than just 'no parameters or 1 parameter', or said another way, I want to specify N parameters, where N is 1 through infinity.
The below example uses the Northwind database to return Orders. It actually will get 3 ResultSets : Customer, Order, and Order Detail information. The example allows you to specify filters such as: By OrderID , By Customer, an OrderDate before a certain Date, an OrderDate after a Certain Date, and/or by Customer-Country (~any combination of these filters you can think of). The "How to Use" shows how you can set 0 parameters, 1 parameter, or N number of parameters.
This method is not a "cure all" method. There is a performance penalty for using this method.
If you have a procedure where performance is the main goal, and you have a lot of records to go through, this solution
may not be the answer.
I typically use this procedure for Report Generation, where the User may or may not specify input parameters.
If you're a DotNet developer, and like to create a strongly typed DataSets, you can create one and fill in values that way (And send in the .GetXml() of the DataSet as the parameter). You'll notice my syntax of "ParametersDS", that is not an accident.
Good luck. The method is useful ~at times. Please be aware of possible scalability issues, since there is a performance hit by making the procedure so flexible. I learned this method from a previous colleague. The example is an original one coded against the Northwind database.
-- START TSQL CODE
-- Stored Procedure Definition
Use Northwind
GO
if exists (select * from sysobjects
where id = object_id('uspOrderDetailsGetByXmlParams') and sysstat
& 0xf = 4)
BEGIN
drop procedure dbo.uspOrderDetailsGetByXmlParams
END
GO
CREATE Procedure dbo.uspOrderDetailsGetByXmlParams(
@parametersXML Text
)
AS
SET NOCOUNT ON
DECLARE @hdoc INT -- handle to XML doc
-- build a table (variable) to store the xml-based result set (for specific orderid's)
DECLARE @orderCount int
DECLARE @orders TABLE ( --used to track which specific OrderID's you want
OrderID int
)
DECLARE @customerCount int
DECLARE @customers TABLE ( --used to track which specific customers you want
CustomerID varchar(5)
)
declare @dateOrderDateAfter datetime --used to track with orders with OrderDate after you want
declare @dateOrderDateBefore datetime --used to track with orders with OrderDate before you want
-- build a table (table-variable) to store the xml-based result set (for specific Countries)
DECLARE @customerCountryCount int
DECLARE @customerCountry TABLE ( --used to track which specific Countries you want
CountryName varchar(15)
)
--Start XML usage --
-- Only incur the penalty of XML parsing, if XML was specified
if (@parametersXML IS NOT NULL) AND (Datalength(@parametersXML) > 10 )
-- Only process the xml If the xml exists, and it has at least 10 chars. 10 is just a somewhat
-- arbritrary number, saying, that an xml doc with <10 chars doesn't have a whole lot going for it
-- || DataLength is used for Text datatype
BEGIN
--Create an internal representation of the XML document.
EXEC sp_xml_preparedocument @hdoc OUTPUT, @parametersXML
--(Do not forget that XML (and xpaths below) are CASE SENSITIVE, no matter what your database collation happens to be.) --
INSERT INTO @orders
SELECT OrderID
FROM
OPENXML (@hdoc, '/ParametersDS/Order', 2) WITH (
OrderID int 'OrderID')
INSERT INTO @customers
SELECT CustomerID
FROM
OPENXML (@hdoc, '/ParametersDS/Customer', 2) WITH (
CustomerID varchar(5) 'CustomerID')
SELECT @dateOrderDateBefore = dateOfOrderOrigenBefore
FROM
OPENXML (@hdoc, '/ParametersDS/SingleValueParam', 2) WITH (
dateOfOrderOrigenBefore datetime 'OrderDateBefore')
SELECT @dateOrderDateAfter = dateOfOrderOrigenAfter
FROM
OPENXML (@hdoc, '/ParametersDS/SingleValueParam', 2) WITH (
dateOfOrderOrigenAfter datetime 'OrderDateAfter')
INSERT INTO @customerCountry
SELECT CountryName
FROM
OPENXML (@hdoc, '/ParametersDS/CustomerCountry', 2) WITH (
CountryName varchar(15) 'CountryName')
--Remove the handle to the XML document, since we're done with using the xmlDoc
EXEC sp_xml_removedocument @hdoc
END
--End XML usage
-------------------- These count variables help distinquish between when a parameter is and isn't specified
select @orderCount = count(*) from @orders
select @customerCount = count(*) from @customers
select @customerCountryCount = count(*) from @customerCountry
--Note, if the xml doesn't supply any dates, @dateOrderDateBefore and @dateOrderDateAfter will remain null
--Debugging queries
--select * from @orders
--select * from @customers
--print @dateOrderDateBefore
--print @dateOrderDateAfter
--select * from @customerCountry
-------------------- Above are the variables and variable-tables for parameters
DECLARE @ordersWhichMetCriteriaTable TABLE ( --used to track the orderid's we're interested in
OrderID int
)
-- A new variable table holds (just) the OrderID's which meet the input parmeters.
-- You'll see the use of the @ordersWhichMetCriteriaTable later.
Insert into @ordersWhichMetCriteriaTable
SELECT
OrderID
FROM
Orders o
--Note, this join to the Customers table is only necessary because of the Country...
-- if you didn't want to use Customer.Country, you could leave this join out
INNER JOIN Customers c
ON o.CustomerID = c.CustomerID
WHERE -- the parantheses play an important role, so be careful altering them
((@orderCount = 0) OR (o.OrderID IN ( Select OrderID from @orders ) ) )
AND
((@customerCount = 0) OR (o.CustomerID IN ( Select CustomerID from @customers ) ))
AND
(( @dateOrderDateBefore IS NULL ) OR (o.OrderDate <= @dateOrderDateBefore ))
AND
(( @dateOrderDateAfter IS NULL ) OR (o.OrderDate >= @dateOrderDateAfter ))
AND --CountryName is a string, so watch the case sensitivity
((@customerCountryCount = 0) OR (c.Country IN ( Select CountryName from @customerCountry ) ))
-- ORDER BY is unnecessary here
-------------------- Below are 3 queries/result sets we're interested in. Notice the piggyback
-- off the @ordersWhichMetCriteriaTable every time.
--ResultSet #1
--All Customer Information (for the specific orders in the @ordersWhichMetCriteriaTable table)
SELECT
c.CustomerID,
c.CompanyName,c.ContactName,c.ContactTitle,c.Address,c.City,c.Region,c.PostalCode,c.Country
,c.Phone,c.Fax
FROM
Customers c INNER JOIN
Orders o ON c.CustomerID = o.CustomerID
WHERE
o.OrderID in ( Select OrderID from @ordersWhichMetCriteriaTable
)
ORDER BY
c.CustomerID
--ResultSet #2
--All Order Information (for the specific orders in the @ordersWhichMetCriteriaTable table)
SELECT
o.OrderID,o.CustomerID,o.EmployeeID,o.OrderDate,o.RequiredDate,o.ShippedDate,o.ShipVia
,o.Freight,o.ShipName,o.ShipAddress,o.OrderID,o.CustomerID,o.EmployeeID,o.OrderDate
FROM
Orders o
WHERE
o.OrderID in ( Select OrderID from @ordersWhichMetCriteriaTable )
ORDER BY
o.CustomerID , o.OrderID
--ResultSet #3
--All Order Detail Information (for the specific orders in the @ordersWhichMetCriteriaTable table)
SELECT
od.OrderID,od.ProductID,od.UnitPrice,od.Quantity,od.Discount
FROM
[Order Details] od
WHERE
od.OrderID in ( Select OrderID from @ordersWhichMetCriteriaTable )
ORDER BY
od.OrderID
GO
-- The user stored procedure definition is above, the use of the user stored procedure is below.
-- (Put the below code in a new Query Analyser window)
-- "How to Use" the procedure above. (Put this code in a new Query Analyser window.)
Use Northwind
GO
--no parameters
print 'No Filters, Just Give me back all the Data'
EXEC uspOrderDetailsGetByXmlParams '
<ParametersDS>
</ParametersDS>
'
GO
--just CustomerID
print 'Filter on specific Customers'
EXEC uspOrderDetailsGetByXmlParams '
<ParametersDS>
<Customer>
<CustomerID>CENTC</CustomerID>
</Customer>
<Customer>
<CustomerID>GROSR</CustomerID>
</Customer>
</ParametersDS>
'
GO
--Order Dates (Before)
print 'Filter on the OrderDates being Before'
EXEC uspOrderDetailsGetByXmlParams '
<ParametersDS>
<SingleValueParam>
<OrderDateBefore>7/7/1996</OrderDateBefore>
</SingleValueParam>
</ParametersDS>
'
GO
--Order Dates (After)
print 'Filter on the OrderDates being After'
EXEC uspOrderDetailsGetByXmlParams '
<ParametersDS>
<SingleValueParam>
<OrderDateAfter>5/5/1998</OrderDateAfter>
</SingleValueParam>
</ParametersDS>
'
GO
--Order Dates (both)
print 'Filter on the OrderDates being (before and after) the input
dates'
EXEC uspOrderDetailsGetByXmlParams '
<ParametersDS>
<SingleValueParam>
<OrderDateBefore>12/31/1997</OrderDateBefore>
<OrderDateAfter>1/1/1997</OrderDateAfter>
</SingleValueParam>
</ParametersDS>
'
GO
print 'Filter on specific OrderIDs'
EXEC uspOrderDetailsGetByXmlParams '
<ParametersDS>
<Order>
<OrderID>10265</OrderID>
</Order>
<Order>
<OrderID>10267</OrderID>
</Order>
<Order>
<OrderID>10269</OrderID>
</Order>
</ParametersDS>
'
GO
--Specific Countries
print 'Filter on specific Countries'
EXEC uspOrderDetailsGetByXmlParams '
<ParametersDS>
<CustomerCountry>
<CountryName>Austria</CountryName>
</CustomerCountry>
<CustomerCountry>
<CountryName>Belgium</CountryName>
</CustomerCountry>
</ParametersDS>
'
GO
--Specific Countries
print 'Filter on specific Countries and OrderDate'
EXEC uspOrderDetailsGetByXmlParams '
<ParametersDS>
<CustomerCountry>
<CountryName>Austria</CountryName>
</CustomerCountry>
<CustomerCountry>
<CountryName>Belgium</CountryName>
</CustomerCountry>
<SingleValueParam>
<OrderDateBefore>2/28/1997</OrderDateBefore>
<OrderDateAfter>1/1/1997</OrderDateAfter>
</SingleValueParam>
</ParametersDS>
'
print 'Order ID that does not exist'
EXEC uspOrderDetailsGetByXmlParams '
<ParametersDS>
<Order>
<OrderID>-9999</OrderID>
</Order>
</ParametersDS>
'
GO
-- END TSQL CODE
Conclusions
This is my preferred method of handling 0:N parameters.
As I said, this is a good approach for needs like Reporting, where you know up front the 0:N parameter need.
Please heed my warning about scalability, and do not consider this a cure-all.