Accessing data with stored procedures - sql

One of the "best practice" is accessing data via stored procedures. I understand why is this scenario good.
My motivation is split database and application logic ( the tables can me changed, if the behaviour of stored procedures are same ), defence for SQL injection ( users can not execute "select * from some_tables", they can only call stored procedures ), and security ( in stored procedure can be "anything" which secure, that user can not select/insert/update/delete data, which is not for them ).
What I don't know is how to access data with dynamic filters.
I'm using MSSQL 2005.
If I have table:
CREATE TABLE tblProduct (
ProductID uniqueidentifier -- PK
, IDProductType uniqueidentifier -- FK to another table
, ProductName nvarchar(255) -- name of product
, ProductCode nvarchar(50) -- code of product for quick search
, Weight decimal(18,4)
, Volume decimal(18,4)
)
then I should create 4 stored procedures ( create / read / update / delete ).
The stored procedure for "create" is easy.
CREATE PROC Insert_Product ( #ProductID uniqueidentifier, #IDProductType uniqueidentifier, ... etc ... ) AS BEGIN
INSERT INTO tblProduct ( ProductID, IDProductType, ... etc .. ) VALUES ( #ProductID, #IDProductType, ... etc ... )
END
The stored procedure for "delete" is easy too.
CREATE PROC Delete_Product ( #ProductID uniqueidentifier, #IDProductType uniqueidentifier, ... etc ... ) AS BEGIN
DELETE tblProduct WHERE ProductID = #ProductID AND IDProductType = #IDProductType AND ... etc ...
END
The stored procedure for "update" is similar as for "delete", but I'm not sure this is the right way, how to do it. I think that updating all columns is not efficient.
CREATE PROC Update_Product( #ProductID uniqueidentifier, #Original_ProductID uniqueidentifier, #IDProductType uniqueidentifier, #Original_IDProductType uniqueidentifier, ... etc ... ) AS BEGIN
UPDATE tblProduct SET ProductID = #ProductID, IDProductType = #IDProductType, ... etc ...
WHERE ProductID = #Original_ProductID AND IDProductType = #Original_IDProductType AND ... etc ...
END
And the last - stored procedure for "read" is littlebit mystery for me. How pass filter values for complex conditions? I have a few suggestion:
Using XML parameter for passing where condition:
CREATE PROC Read_Product ( #WhereCondition XML ) AS BEGIN
DECLARE #SELECT nvarchar(4000)
SET #SELECT = 'SELECT ProductID, IDProductType, ProductName, ProductCode, Weight, Volume FROM tblProduct'
DECLARE #WHERE nvarchar(4000)
SET #WHERE = dbo.CreateSqlWherecondition( #WhereCondition ) --dbo.CreateSqlWherecondition is some function which returns text with WHERE condition from passed XML
DECLARE #LEN_SELECT int
SET #LEN_SELECT = LEN( #SELECT )
DECLARE #LEN_WHERE int
SET #LEN_WHERE = LEN( #WHERE )
DECLARE #LEN_TOTAL int
SET #LEN_TOTAL = #LEN_SELECT + #LEN_WHERE
IF #LEN_TOTAL > 4000 BEGIN
-- RAISE SOME CONCRETE ERROR, BECAUSE DYNAMIC SQL ACCEPTS MAX 4000 chars
END
DECLARE #SQL nvarchar(4000)
SET #SQL = #SELECT + #WHERE
EXEC sp_execsql #SQL
END
But, I think the limitation of "4000" characters for one query is ugly.
The next suggestion is using filter tables for every column. Insert filter values into the filter table and then call stored procedure with ID of filters:
CREATE TABLE tblFilter (
PKID uniqueidentifier -- PK
, IDFilter uniqueidentifier -- identification of filter
, FilterType tinyint -- 0 = ignore, 1 = equals, 2 = not equals, 3 = greater than, etc ...
, BitValue bit , TinyIntValue tinyint , SmallIntValue smallint, IntValue int
, BigIntValue bigint, DecimalValue decimal(19,4), NVarCharValue nvarchar(4000)
, GuidValue uniqueidentifier, etc ... )
CREATE TABLE Read_Product ( #Filter_ProductID uniqueidentifier, #Filter_IDProductType uniqueidentifier, #Filter_ProductName uniqueidentifier, ... etc ... ) AS BEGIN
SELECT ProductID, IDProductType, ProductName, ProductCode, Weight, Volume
FROM tblProduct
WHERE ( #Filter_ProductID IS NULL
OR ( ( ProductID IN ( SELECT GuidValue FROM tblFilter WHERE IDFilter = #Filter_ProductID AND FilterType = 1 ) AND NOT ( ProductID IN ( SELECT GuidValue FROM tblFilter WHERE IDFilter = #Filter_ProductID AND FilterType = 2 ) )
AND ( #Filter_IDProductType IS NULL
OR ( ( IDProductType IN ( SELECT GuidValue FROM tblFilter WHERE IDFilter = #Filter_IDProductType AND FilterType = 1 ) AND NOT ( IDProductType IN ( SELECT GuidValue FROM tblFilter WHERE IDFilter = #Filter_IDProductType AND FilterType = 2 ) )
AND ( #Filter_ProductName IS NULL OR ( ... etc ... ) )
END
But this suggestion is littlebit complicated I think.
Is there some "best practice" to do this type of stored procedures?

For reading data, you do not need a stored procedure for security or to separate out logic, you can use views.
Just grant only select on the view.
You can limit the records shown, change field names, join many tables into one logical "table", etc.

First: for your delete routine, your where clause should only include the primary key.
Second: for your update routine, do not try to optimize before you have working code. In fact, do not try to optimize until you can profile your application and see where the bottlenecks are. I can tell you for sure that updating one column of one row and updating all columns of one row are nearly identical in speed. What takes time in a DBMS is (1) finding the disk block where you will write the data and (2) locking out other writers so that your write will be consistent. Finally, writing the code necessary to update only the columns that need to change will generally be harder to do and harder to maintain. If you really wanted to get picky, you'd have to compare the speed of figuring out which columns changed compared with just updating every column. If you update them all, you don't have to read any of them.
Third: I tend to write one stored procedure for each retrieval path. In your example, I'd make one by primary key, one by each foreign key and then I'd add one for each new access path as I needed them in the application. Be agile; don't write code you don't need. I also agree with using views instead of stored procedures, however, you can use a stored procedure to return multiple result sets (in some version of MSSQL) or to change rows into columns, which can be useful.
If you need to get, for example, 7 rows by primary key, you have some options. You can call the stored procedure that gets one row by primary key seven times. This may be fast enough if you keep the connection opened between all the calls. If you know you never need more than a certain number (say 10) of IDs at a time, you can write a stored procedure that includes a where clause like "and ID in (arg1, arg2, arg3...)" and make sure that unused arguments are set to NULL. If you decide you need to generate dynamic SQL, I wouldn't bother with a stored procedure because TSQL is just as easy to make a mistake as any other language. Also, you gain no benefit from using the database to do string manipulation -- it's almost always your bottleneck, so there is no point in giving the DB any more work than necessary.

I disagree that create Insert/Update/Select stored procedures are a "best practice". Unless your entire application is written in SPs, use a database layer in your application to handle these CRUD activities. Better yet, use an ORM technology to handle them for you.

My suggestion is that you don't try to create a stored procedure that does everything that you might now or ever need to do. If you need to retrieve a row based on the table's primary key then write a stored procedure to do that. If you need to search for all rows meeting a set of criteria then find out what that criteria might be and write a stored procedure to do that.
If you try to write software that solves every possible problem rather than a specific set of problems you will usually fail at providing anything useful.

your select stored procedure can be done as follows to require only one stored proc but any number of different items in the where clause. Pass in any one or combination of the parameters and you will get ALL items which match - so you only need one stored proc.
Create sp_ProductSelect
(
#ProductID int = null,
#IDProductType int = null,
#ProductName varchar(50) = null,
#ProductCode varchar(10) = null,
...
#Volume int = null
)
AS
SELECT ProductID, IDProductType, ProductName, ProductCode, Weight, Volume FROM tblProduct'
Where
((#ProductID is null) or (ProductID = #ProductID)) AND
((#ProductName is null) or (ProductName = #ProductName)) AND
...
((#Volume is null) or (Volume= #Volume))

In SQL 2005, it supports nvarchar(max), which has a limit of 2G, but virtually accepting all string operations upon normal nvarchar. You may want to test if this can fit into what you need in the first approach.

Related

Stored procedure : Select data from Table "A" and insert data on Table "B"

I haven't touched stored procedures and functions for a very long time.
So I decided to make a mini-training database for myself.
At the moment, I'm trying to create a procedure that comes in and selects data from one table (Test_trigger) and says that if the sum for a colour exceeds a threshold, then I write an alert row to another table (Test_alm_trigger)
Here are my 2 tables creation script :
create table Test_trigger (
id INT IDENTITY(1,1) PRIMARY KEY,
Couleur VARCHAR(50),
Horodate DATETIME,
Nombre DECIMAL(6,2),
Seuil_fixe INT
);
create table Test_alm_trigger (
id_alm INT IDENTITY(1,1) PRIMARY KEY,
Label VARCHAR(100),
Horodate DATETIME,
Seuil DECIMAL(6,2)
);
To be more precise, the goal is :
when a colour ([Couleur]), such as "Blue", has a sum of the [Nombre] column above the threshold entered ([Seuil_fixe]), then the procedure runs and inserts a row in the Test_alm_trigger table with a [Label], the date the addition was made ( SYSDATETIME() ) and the sum of the [Nombre] column.
I have created this procedure but I am not sure how it works or if it is good or not
CREATE PROCEDURE ajoutL_triggerAlm
(
#Couleur nvarchar(50),
#Label nvarchar(200) = 'Dépassement de seuil',
#Seuil float(4),
#Seuil_fixe float(4),
#Msg nvarchar(200)
)
AS
BEGIN
IF EXISTS (
SELECT [Couleur]
FROM Test_trigger
GROUP BY [Couleur], [Nombre], [Seuil_fixe]
HAVING [Couleur] = #Couleur AND
SUM([Nombre]) = #Seuil AND
[Seuil_fixe] = #Seuil_fixe AND
#Seuil > #Seuil_fixe
)
BEGIN
SET #Msg = 'Debug'
END
ELSE
BEGIN
INSERT INTO Test_alm_trigger
VALUES (#Label, SYSDATETIME(), #Seuil)
END
END
If you have any answers, tips... I'll take them.
Thank you in advance
The main change I would suggest is making your procedure set-based rather than procedural. Relational databases are optimised for set-based operations so you should try and get into the mindset of operating that way yourself.
I've added other best practices.
CREATE PROCEDURE ajoutL_triggerAlm
(
#Couleur nvarchar(50)
, #Label nvarchar(200) = 'Dépassement de seuil'
-- Almost never use float, its not a precise numeric amount and should only be used when specifically required
, #Seuil decimal(8,4)
-- Almost never use float, its not a precise numeric amount and should only be used when specifically required
, #Seuil_fixe decimal(8,4)
, #Msg nvarchar(200)
)
AS
BEGIN
-- Best practice (I'll leave the meaning as an exercise)
-- SQL Server recommendation is to ";" terminate all statements
SET NOCOUNT, XACT_ABORT ON;
-- Best practice, always list the columns you are inserting into
-- Use set-based operations instead of procedural where you can
INSERT INTO dbo.Test_alm_trigger (Label, Horodate, Seuil)
SELECT #Label, SYSDATETIME(), #Seuil
-- Question states when SUM([Nombre] above the threshold #Seuil_fixe - so I think this is the logic
-- Although its not clear where the column Seuil_fixe comes into it
WHERE (
SELECT SUM([Nombre])
FROM Test_trigger
WHERE [Couleur] = #Couleur
) > #Seuil_fixe;
-- Detect failure using ##ROWCOUNT
SET #Msg = CASE WHEN ##ROWCOUNT = 0 THEN 'Debug' ELSE NULL END;
-- Return a status, can use other values to indicate errors
RETURN 0;
END;

3 tables, 2 DBs, 1 Stored Procedure

I'm a novice when it comes to Stored Procedures in SQL Server Management Studio. I have an application that I was told to make the following changes to using a stored procedure:
Step 1. User types in an item number.
Step 2. Customer name, address, etc. displays in the other fields on the same form.
There are 3 tables: Bulk orders, Small orders, and Customer information.
Bulk orders and small orders are in Database_1 and Customer information is in Database_2.
The primary key for small orders is the order number. A column in small orders contains the customer number for each order. That customer number is the primary key in the customer table.
The bulk orders table is similar.
I want to include a conditional statement that says: if order number is found in small orders table, show data from customer table that coorelates with that order number. I've attempted this multiple ways, but keep getting a "The multi-part identifier.... could not be bound" error.
I.E:
SELECT DB1.db.Customer_Table.Customer_Column AS CustomerNumber;
IF(CustomerNumber NOT LIKE '%[a-z]%')
BEGIN
SELECT * FROM db.small_orders_table;
END
ELSE
BEGIN
SELECT * FROM db.buld_orders_table;
END
Please help.
Sounds like it's 2 databases on the same server...in that case, you'll need to specify the fully qualified table name (database.schema.table) when referencing a table on the other database from where your stored procedure is found.
Database_1.db.small_orders_tables
first of all, you cannot use aliases as variables. If you want to assign a value to a variable in order to test it, you have to do a SELECT statement like SELECT #var = DB1.db.Customer_Table.Customer_Column FROM <YourTableFullName> WHERE <condition>. Then you can use the #var (which must be declared before) for your test.
About the error you're experiencing, youre using fully qualified names in a wrong way. If you're on the same server (different databases), you need to specify just the database name on the top and then the schema of your objects. Suppose to have the following database objects on the Database1:
USE Database1;
GO
CREATE TABLE dbo.Table1
(
id int IDENTITY(1, 1) NOT NULL PRIMARY KEY CLUSTERED
, val varchar(30)
);
GO
INSERT INTO dbo.Table1 (val) VALUES ('test1');
GO
INSERT INTO dbo.Table1 (val) VALUES ('test2');
GO
INSERT INTO dbo.Table1 (val) VALUES ('test3');
GO
And the following ones on Database2:
USE Database2;
GO
CREATE TABLE dbo.Table2
(
id int IDENTITY(1, 1) NOT NULL PRIMARY KEY CLUSTERED
, val varchar(30)
);
GO
Now, suppose that you want to read from the first table the value with id = 2, and then to apply your IF. Let's declare a variable and test it:
USE Database1;
GO
DECLARE #var varchar(30);
-- since you're on Database1, you don't need to specify full name
SELECT #var = val FROM dbo.Table1 WHERE id = 2;
IF #var = 'test2'
BEGIN
SELECT id, val FROM dbo.Table1;
END
ELSE
BEGIN
-- in this case the database name is needed
SELECT id, val FROM Database2.dbo.Table2;
END
GO
Does it help?

Conditional INSERT subquery of larger insert

I have a set of tables which track access logs. The logs contain data about the user's access including user agent strings. Since we know that user agent strings are, for all intents and purposes, practically unlimited, these would need to be stored as a text/blob type. Given the high degree of duplication, I'd like to store these in a separate reference table and have my main access log table have an id linking to it. Something like this:
accesslogs table:
username|accesstime|ipaddr|useragentid
useragents table:
id|crc32|md5|useragent
(the hashes are for indexing and quicker searching)
Here's the catch, i am working inside a framework that doesn't give me access to create fancy things like foreign keys. In addition, this has to be portable across multiple DBMSs. I have the join logic worked out for doing SELECTS but I am having trouble figuring out how to insert properly. I want to do something like
INSERT INTO accesslogs (username, accesstime, ipaddr, useragentid)
VALUES
(
:username,
:accesstime,
:ipaddr,
(
CASE WHEN
(
SELECT id
FROM useragents
WHERE
useragents.crc32 = :useragentcrc32
AND
useragents.md5 = :useragentmd5
AND useragents.useragent LIKE :useragent
) IS NOT NULL
THEN
THAT_SAME_SELECT_FROM_ABOVE()
ELSE
GET_INSERT_ID_FROM(INSERT INTO useragents (crc32, md5, useragent) VALUES (:useragentcrc32, :useragentmd5, :useragent))
)
)
Is there any way to do this that doesn't use pseudofunctions whose names i just made up? The two parts i'm missing is how to get the select from above and how to get the new id from a subquery insert.
You will need to do separate inserts to each of the tables. You can not do insert into both at the same time.
If you use MS SQL Server once you inserted you can get inserted id by SCOPE_IDENTITY(), and then use it in another table insert.
I'm not sure there is a cross platform way of doing this. You may have to have a lot of special cases for each supported back end. For Example, for SQL Server you'd use the merge statement as the basis of the solution. Other DBMSs have different names if they support it at all. Searching for "Upsert" might help.
Edt - added the second query to be explicit, and added parameters.
-- SQL Server Example
--Schema Defs
Create Table Test (
id int not null identity primary key,
UserAgent nvarchar(50)
)
Create Table WebLog (
UserName nvarchar(50),
APAddress nvarchar(50),
UserAgentID int
)
Create Unique Index UQ_UserAgent On Test(UserAgent)
-- Values parsed from log
Declare
#UserName nvarchar(50) = N'Loz',
#IPAddress nvarchar(50) = N'1.1.1.1',
#UserAgent nvarchar(50) = 'Test'
Declare #id int
-- Optionally Begin Transaction
-- Insert if necessary and get id
Merge
Into dbo.Test as t
Using
(Select #UserAgent as UserAgent) as s
On
t.[UserAgent] = s.[UserAgent]
When Matched Then
Update Set #id = t.id
When Not Matched Then
Insert (UserAgent) Values (s.UserAgent);
If #id Is Null Set #id = scope_identity()
Insert Into WebLog (UserName, IPAddress, UserAgentID) Values (#UserName, #IPAddress, #id)
-- Optionally Commit Transaction

SQL Search using case or if

Everyone has been a super help so far. My next question is what is the best way for me to approach this... If I have 7 fields that a user can search what is the best way to conduct this search, They can have any combination of the 7 fields so that is 7! or 5040 Combinations which is impossible to code that many. So how do I account for when the User selects field 1 and field 3 or they select field 1, field 2, and field 7? Is there any easy to do this with SQL? I dont know if I should approach this using an IF statement or go towards a CASE in the select statement. Or should I go a complete different direction? Well if anyone has any helpful pointers I would greatly appreciate it.
Thank You
You'll probably want to look into using dynamic SQL for this. See: Dynamic Search Conditions in T-SQL and Catch-all queries for good articles on this topic.
Select f1,f2 from table where f1 like '%val%' or f2 like '%val%'
You could write a stored procedure that accepts each parameter as null and then write your WHERE clause like:
WHERE (field1 = #param1 or #param1 is null)
AND (field2 = #param2 or #param2 is null) etc...
But I wouldn't recommend it. It can definitely affect performance doing it this way depending on the number of parameters you have. I second Joe Stefanelli's answer with looking into dynamic SQL in this case.
Depends on:
how your data looks like,
how big they are,
how exact result is expected (all matching records or top 100 is enough),
how much resources has you database.
you can try something like:
CREATE PROC dbo.Search(
#param1 INT = NULL,
#param2 VARCHAR(3) = NULL
)
AS
BEGIN
SET NOCOUNT ON
-- create temporary table to keep keys (primary) of matching records from searched table
CREATE TABLE #results (k INT)
INSERT INTO
#results(k)
SELECT -- you can use TOP here to norrow results
key
FROM
table
-- you can use WHERE if there are some default conditions
PRINT ##ROWCOUNT
-- if #param1 is set filter #result
IF #param1 IS NOT NULL BEGIN
PRINT '#param1'
;WITH d AS (
SELECT
key
FROM
table
WHERE
param1 <> #param1
)
DELETE FROM
#results
WHERE
k = key
PRINT ##ROWCOUNT
END
-- if #param2 is set filter #result
IF #param2 IS NOT NULL BEGIN
PRINT '#param2'
;WITH d AS (
SELECT
key
FROM
table
WHERE
param2 <> #param2
)
DELETE FROM
#results
WHERE
k = key
PRINT ##ROWCOUNT
END
-- returns what left in #results table
SELECT
table.* -- or better only columns you need
FROM
#results r
JOIN
table
ON
table.key = r.k
END
I use this technique on large database (millions of records, but running on large server) to filter data from some predefined data. And it works pretty well.
However I don't need all matching records -- depends on query 10-3000 matching records is enough.
If you are using a stored procedure you can use this method:
CREATE PROCEDURE dbo.foo
#param1 VARCHAR(32) = NULL,
#param2 INT = NULL
AS
BEGIN
SET NOCOUNT ON
SELECT * FROM MyTable as t
WHERE (#param1 IS NULL OR t.Column1 = #param1)
AND (#param2 IS NULL OR t.COlumn2 = #param2)
END
GO
These are usually called optional parameters. The idea is that if you don't pass one in it gets the default value (null) and that section of your where clause always returns true.

How to reuse code in SQL stored procedures?

We use SQL Server 2005. All our data access is done through stored procedures. Our selection stored procedures always return multiple result sets.
For instance:
CREATE PROCEDURE hd_invoice_select(#id INT) AS
SELECT * FROM Invoice WHERE InvoiceID = #id
SELECT * FROM InvoiceItem WHERE InvoiceID = #id
SELECT * FROM InvoiceComments WHERE InvoiceID = #id
RETURN
Our application's data access layer builds an object graph based on the results (O/R Mapper style).
The problem I have is that we have many different invoice selection stored procs. They all return the same structure, only for different selection criteria. For instance, I also have:
CREATE PROCEDURE hd_invoice_selectAllForCustomer(#customerID INT) AS
SELECT * FROM Invoice WHERE CustomerID = #customerID
SELECT * FROM InvoiceItem WHERE InvoiceID IN
(SELECT InvoiceID FROM Invoice WHERE CustomerID = #customerID)
SELECT * FROM InvoiceComments WHERE InvoiceID = #id
(SELECT InvoiceID FROM Invoice WHERE CustomerID = #customerID)
RETURN
and I have many others including:
hd_invoice_selectActive()
hd_invoice_selectOverdue()
hd_invoice_selectForMonth(#year INT, #month INT)
and I have the same pattern for a lot of concepts (Customers, Employees, etc)
We end up copying a lot of code and maintenance is really hard. When the "structure" of a concept changes, we have to go and fix all procs and it's very error prone.
So my question is: What is the best way to reuse the code in the scenario?
We came up with a solution that uses temp tables. But it's not very elegant. I'll let you share your ideas and if necessary I will post the detail of my solution in an upcoming post to get your comments on that approach.
Thanks
Posting this as a second answer because it is a different approach. If you are using SQL Server 2008:
CREATE TYPE InvoiceListTableType AS TABLE
(
InvoiceId INT
);
GO
CREATE PROCEDURE hd_invoice_selectFromTempTable
(
#InvoiceList InvoiceListTableType READONLY
)
AS
BEGIN
SELECT * FROM Invoice WHERE InvoiceID IN
(SELECT InvoiceId FROM #InvoiceList)
SELECT * FROM InvoiceItem WHERE InvoiceID IN
(SELECT InvoiceId FROM #InvoiceList)
SELECT * FROM InvoiceComments WHERE InvoiceID IN
(SELECT InvoiceId FROM #InvoiceList)
RETURN
END
GO
CREATE PROCEDURE hd_invoice_select(#id INT) AS
BEGIN
DECLARE #InvoiceList AS InvoiceListTableType;
SELECT id AS ID
INTO #InvoiceList
EXEC hd_invoice_selectFromTempTable(#InvoiceList)
RETURN
END
GO
CREATE PROCEDURE hd_invoice_selectAllForCustomer(#customerID INT) AS
BEGIN
DECLARE #InvoiceList AS InvoiceListTableType;
SELECT invoiceID as ID
INTO #InvoiceList
FROM Invoice WHERE CustomerID = #customerID
EXEC hd_invoice_selectFromTempTable(#InvoiceList)
RETURN
END
GO
CREATE PROCEDURE hd_invoice_selectAllActive AS
BEGIN
DECLARE #InvoiceList AS InvoiceListTableType;
SELECT invoiceID as ID
INTO #InvoiceList
FROM Invoice WHERE Status = 10002
EXEC hd_invoice_selectFromTempTable(#InvoiceList)
RETURN
END
GO
The "best" way for this specific scenario would be to use some sort of code generation. Come up with some sort of convention and plug it into a code generator.
Have you tried putting more than 1 query parameter type in the list of parameters for your main proc? I only wrote the proc to cover the Invoice table, you will need to extend it for your additional tables.
CREATE PROCEDURE hd_invoice_select
(
#id INT = NULL
, #customerId INT = NULL
) AS
BEGIN
SELECT *
FROM Invoice
WHERE
(
#id IS NULL
OR InvoiceID = #id
)
AND (
#customerId IS NULL
OR CustomerID = #customerId
)
RETURN
END
This proc can be called wide open by sending #id and #customerId as NULLs, for a specific InvoiceID based on #id with #customerId as NULL (or just leave it off all together), or for a specific customer based on #customerId leaving #id as NULL or exclude it from the query.
You also should look at views and Table-Valued User-Defined Functions. You can put these in your procs to wrap up some of the logic away from the procs so they can be shared and maintained in a single place. Having some of the logic in views/functions also allows you to deal with the data in a query window as if it were a table.
I'm the person who asked this question in the first place. I'm answering my own question here to let you know the code reuse solution I use and to get your comments on that approach. If this answer gets a lot of up votes, I will select it as the final answer.
This approach works and is simple to use. I don’t know if it has a performance impact because it relies heavily on temporary tables.
For each concept in my application, I have one storec proc like this:
CREATE PROCEDURE hd_invoice_selectFromTempTable AS
/* Get the IDs from an existing #TempInvoiceIDs temporary table */
SELECT * FROM Invoice WHERE InvoiceID IN
(SELECT ID FROM #TempInvoiceIDs)
SELECT * FROM InvoiceItem WHERE InvoiceID IN
(SELECT ID FROM #TempInvoiceIDs)
SELECT * FROM InvoiceComments WHERE InvoiceID IN
(SELECT ID FROM #TempInvoiceIDs)
RETURN
Then I create as many selection stored proc as I need:
CREATE PROCEDURE hd_invoice_select(#id INT) AS
/* Fill #TempInvoiceIDs with matching IDs */
SELECT id AS ID INTO #TempInvoiceIDs
EXEC hd_invoice_selectFromTempTable
RETURN
CREATE PROCEDURE hd_invoice_selectAllForCustomer(#customerID INT) AS
/* Fill #TempInvoiceIDs with matching IDs */
SELECT invoiceID as ID
INTO #TempInvoiceIDs
FROM Invoice WHERE CustomerID = #customerID
EXEC hd_invoice_selectFromTempTable
RETURN
CREATE PROCEDURE hd_invoice_selectAllActive AS
/* Fill #TempInvoiceIDs with matching IDs */
SELECT invoiceID as ID
INTO #TempInvoiceIDs
FROM Invoice WHERE Status = 10002
EXEC hd_invoice_selectFromTempTable
RETURN
What do you think of this approach? It is somewhat similar to AlexKuznetsov's answer but I use temp tables instead of a BLOB parameter.
This is one of the main problems with stored procedures and why people don't like them.
I've never found or seen a way around it.
I have started to use stored procedures generated by Code Generators for my basic CRUD. I use stored procs for reports or complex SQL work.
I have a suggestion unrelated to your question as well - instead of using the IN clause, use the EXISTS clause in your SQL statements.
I've inherited an application that used the temp table approach before and I agree that it's very messy.
On that project we were able to remove a lot of the temp tables by replacing them with Views that contained the desired 'objects' we needed then we updated our stored procedures to query off of those views.
Perhaps that may work in your situation as well.
In some cases I use VIEWS to reuse "code". In cases as filters, active items, outdated things, and so on...
Maybe you should learn to use joins. You could put the basic join of the three tables in a view and just query that with the sp handing the different parameters. Also, you should not in general use select * ever in production code. Only return the few columns you actually need in the circumstances and your whole system will be better performing. Plus you won't have unintended results when people change the structure on you.
I sometimes do it in two steps:
I come up with a list of InvoiceID. Then I call my stored procedure with this list as a parameter.
On 2005 we don't have table valued parameters, so I pack my list of IDs in a binary BLOB and submit it to SQL Server, as described here: Arrays and Lists in SQL Server 2005
You can also submit a list of IDs as a comma-separated list (somewhat slow) or as a concatenation of fixed width string representations (much faster).