How to apply DRY principle to SELECT in SQL procedures? - sql

I have a number of SQL Stored Procedures querying the same table with different arguments and WHERE clauses. How can I define the columns in SELECT in a single place so they are shared across all similar procedures? Can you also share the performance or maintainability implication please?
CREATE PROCEDURE [dbo].[stp_Metadata_GetById]
#Id int = 0
AS
SELECT M.Id,
M.Name
-- A long list of column selections
FROM dbo.Metadata M
WHERE M.Id = #Id;
CREATE PROCEDURE [dbo].[stp_Metadata_GetByName]
#Name nvarchar(200)
AS
SELECT M.Id,
M.Name
-- A long list of column selections
FROM dbo.Metadata M
WHERE M.Name = #Name;

How can I define the columns in SELECT in a single place so they are shared across all similar procedures?
In short, you can't. SQL is not a procedural programming language, but a set based declarative one. Consequently, principles like DRY don't really hold up in the SQL world.
Your only option here is to follow #jarlh's advice and to make a single procedure that allows passing of several parameters to filter on different columns all within the same procedure. This will come with it's own issues though, as you now have a monolith procedure that you will need to monitor and maintain as your database grows and develops.

You just need to create one sp with 2 params along with default value is null.
After that, you can use boolean logic like #Id is null or M.Id = #Id
CREATE PROCEDURE [dbo].[stp_Metadata_GetBy]
#Id int = null,
#Name nvarchar(200) = null
AS
SELECT M.Id,
M.Name
-- A long list of column selections
FROM dbo.Metadata M
WHERE (#Id is null or M.Id = #Id)
and (#Name is null or M.Name = #Name);
How to use
exec stp_Metadata_GetBy #Id = 1 -- if you want to query by ID
exec stp_Metadata_GetBy #Name = 'abc' -- if you want to query by Name
Looks like this one 'CASE' expression whether to apply a WHERE condition to a query or not

Related

WITH RECOMPILE in stored procedure after setting parameters

I want to use the WITH RECOMPILE in a stored procedure after the declared parameter are filled with values. Where do I have to put it?
I want to do something like this:
CREATE PROCEDURE sp_dosomething
#AdrID INT = '32'
WITH RECOMPILE
AS
SELECT #AdrID = SELECT MAX(ID)
FROM Address
INSERT INTO TempStreet
SELECT Streetname FROM Workadress WHERE ID = #AdrID
INSERT INTO TempStreet
SELECT Streetname FROM Homeadress WHERE ID = #AdrID
OPTION(RECOMPILE)
UPDATE TempStreet Set
FROM TempStreet inner join AditionalData1...
UPDATE TempStreet Set
FROM TempStreet inner join AditionalData2...
SELECT * FROM TempStreet
GO
So I want to set the parameter and then recompile the execution plan for the rest of the stored procedure every time it is executed.
The stored procedure does the followings things:
The Procedure is doing two different INSERT INTO a Table. After that the table is beeing updated by muptiple statements with joins on other tables. At the end there is a SELECT on that table.
What would be the right way to do it?
Would it be better to use WITH RECOMPILE or rather OPTION(RECOMPILE)
Please help me.
This should work:
CREATE PROCEDURE sp_dosomething_sub #AdrID INT
WITH RECOMPILE
AS
Select Streetname from Workadress where ID = #AdrID
Select Streetname from Homeadress where ID = #AdrID
GO
CREATE PROCEDURE sp_dosomething #AdrID INT = '32'
AS
select #AdrID = Select max(ID) FROM Address
EXEC sp_dosomething_sub #ArdID
GO
Note that I am not saying that this is necessarily a good idea, I'm just saying that it should do what you want. It would take some pretty odd (or complex) circumstances for this to be a desirable approach.
I always say that everything is possible as long as you have the resources (which includes time, money, knowledge and more). This case is not different. In order to use this format, all that you need to do is to design and develop your own language, since this format is not a valid Transact SQL format.
Your question is like asking how can I calculate 1+1 and get 3. The answer is to build your own math, since is not fit for the common math language.
If I understand your needs (and not request) correctly then you can gain what you need by executing a stored procedure from another stored procedure.
You can write procedure X which configure the parameters and this procedure X will execute procedure Y which is configured using WITH RECOMPILE

Test for a column within a Select statement

Is it possible to test for a column before selecting it within a select statement?
This may be rough for me to explain, I have actually had to teach myself dynamic SQL over the past 4 months. I am using a dynamically generated parameter (#TableName) to store individual tables within a loop (apologize for the vagueness, but the details aren't relevant).
I then want to be able to be able to conditionally select a column from the table (I will not know if each table has certain columns). I have figured out how to check for a column outside of a select statement...
SET #SQLQuery2 = 'Select #OPFolderIDColumnCheck = Column_Name From INFORMATION_SCHEMA.COLUMNS Where Table_Name = #TABLENAME And Column_Name = ''OP__FolderID'''
SET #ParameterDefinition2 = N'#TABLENAME VARCHAR(100), #OPFolderIDColumnCheck VARCHAR(100) OUTPUT'
EXECUTE SP_EXECUTESQL #SQLQuery2, #ParameterDefinition2, #TABLENAME, #OPFolderIDColumnCheck OUTPUT
IF #OPFolderIDColumnCheck IS NULL
BEGIN
SET #OP__FOLDERID = NULL
END
ELSE
IF #OPFolderIDColumnCheck IS NOT NULL
BEGIN
...etc
but id like to be able to do it inside of a select statement. Is there a way to check and see if OP__FOLDERID exists in the table?
Id like to be able to do something like this:
SELECT IF 'OP__FOLDERID' EXISTS IN [TABLE] THEN 'OP__FOLDERID' FROM [TABLE]
Thank you for any help or direction you can offer.
I'm afraid there isn't any direct way to do this within a SELECT statement at all. You can determine if a column exists in a table, however, and construct your dynamic SQL accordingly. To do this, use something like this:
IF COL_LENGTH('schemaName.tableName', 'columnName') IS NOT NULL
BEGIN
-- Column Exists
END
You could then set a variable as a flag, and the code to construct the dynamic SQL would construct the expression with/without the column, as desired. Another approach would be to use a string value, and set it to the column name if it is present (perhaps with a prefix or suffix comma, as appropriate to the expression). This would allow you to save writing conditionals in the expression building, and would be particularly helpful where you have more than one or two of these maybe-columns in a dynamic expression.

Parameterized WHERE clause in SQL Server Stored Procedure

I have a Stored Procedure to get the details of Invoices
Some occasions I get the list of invoices by sending only the InvoiceID
But in some other occasions I need to get the list of invoices as per the search fields supplied by the user. To do this I send all the fields to the Stored Procedure and use those parameters as below. I included only 2 columns but there are more.
SELECT * FROM INVOICES I
WHERE
(#InvoiceNumber is null or I.InvoiceNumber = #InvoiceNumber)
and
(#PONo is null or I.PONo = #PONo)
Is there a way to send the condition for the WHERE clause as one parameter?
Yes, it is possible with Dynamic SQL, but I highly discourage to do that.
SELECT * FROM tbl WHERE #condition:
If you are considering to write the procedure
CREATE PROCEDURE search_sp #condition varchar(8000) AS
SELECT * FROM tbl WHERE #condition
Just forget it. If you are doing this, you have not completed the transition to use stored procedure and you are still assembling your
SQL code in the client.
It will also open your application to SQL Injection attacks.
You can use custom type to pass table as parameter https://msdn.microsoft.com/pl-pl/library/bb510489(v=sql.110).aspx or you can use default parameters
If you're using SQL Server 2016 or similar (check by calling select compatibility_level, name from sys.databases and seeing that your DB is 130 or higher) then you can use the string_split builtin function.
I found it works best like this (spread out for clarity)
CREATE PROCEDURE [dbo].[GetInvoices]
#InvoiceNumber int = NULL
#PONo nvarchar(1024) = NULL
AS
SELECT * from [Invoices] AS [i]
WHERE
i.InvoiceNumber = ISNULL(#InvoiceNunber, i.InvoiceNunber)
AND CASE
WHEN #PONo is null
THEN 1
ELSE (CASE
WHEN i.PONo IN (select value from string_split(#PONo, ','))
THEN 1
ELSE 0
END)
END
= 1
So if you pass in a null to either parameter it gets translated as where x = x which is always true, and if you pass in a CSV value, it selects it from a split table of values that, if present, results in the where clause being where 1=1, which is true or 0=1 if the value is not present in the input list.
So here you can pass in an invoice number, or PO number, or both, or neither and it should return what you expect.

Dynamically Select from different DB's based on input to sproc

I'm trying to alter a stored procedure in our DB from a hard-coded select from 1 specific DB to be able to select from any of our DB's based on an id that's passed into the sproc. Here's the stub of what the sproc is doing for us:
ALTER PROCEDURE [dbo].[GetByAdId]
(
#AdId int,
#UserCompanyId int
)
AS
SET NOCOUNT ON
SELECT
[User].[UserId],
UserMappings.IsActive,
IsAccountOwner = ( SELECT Count(*) FROM DB1_SetUp.dbo.ad Adv WHERE Adv.AdID = UserMappings.AdID AND Adv.PrimaryAccountOwnerID = [User].[UserId] )
FROM
[User] INNER JOIN UserMappings ON
(
UserMappings.UserID = [User].UserID
AND UserMappings.AdID = #AdId
AND UserMappings.UserCompanyId = #UserCompanyId
)
Basically the "IsAccountOwner" variable is hardcoded to select from DB1_SetUp every time, but we have a number of SetUp db's for different groups, so like DB2_SetUp, DB3_SetUp, etc. The UserCompanyId variable being passed into the sproc functions like a group Id and can be used to point to the particular SetUp DB I want it to select from, but I'm not sure how to do this. I basically wanted something on the ilk of:
SELECT * FROM (
CASE #UserCompanyId
WHEN 3 THEN 'DB3_SetUp'
WHEN 4 THEN 'DB4_SetUp'
)
Is there a clean way to do this, or will I have to setup this sproc on each group DB and call the specific one over on each DB?
I've done this in the past by dynamically building the SQL I wanted to execute (based on parameters passed in) and then executing the SQL using sp_executesql
see: http://msdn.microsoft.com/en-us/library/ms188001.aspx

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.