Changing SQL Server query based on stored procedure parameter - sql

I have a stored procedure which has a parameter #FilterToUse.
I have a query inside the stored procedure as shown below:
INSERT INTO #StatusCalculation
SELECT COUNT(*)
FROM Data
WHERE DataType = 'Bid'
--This part can change based on SP param
AND CreatedDate BETWEEN CONVERT(DATETIME, ('1/1' + '/' + #RequestYear))
AND CONVERT(DATETIME, ('12/31' + '/' + #RequestYear))
--end dynamic part
AND Services LIKE '%Streamline Payroll%'
AND Services LIKE '%GlobalView Payroll%'
So if I have a different filter I ll have to write
INSERT INTO #StatusCalculation
SELECT COUNT(*)
FROM Data
WHERE DataType = 'Bid'
--This part can change based on param
AND BidId BETWEEN 1 AND 10
--end dynamic part
AND Services LIKE '%Streamline Payroll%'
AND Services LIKE '%GlobalView Payroll%'
Now my issue is that based on the #FilterToUse parameter I might have to use a different query expression. The same query based on different options is used about 10 times. Is there a way that I can dynamically add the filter conditions to the query based on parameter without duplicating the whole query.
I was thinking if somehow I can do the below
declare #data nvarchar(max)
Set #data = 'and CreatedDate between convert(datetime,("1/1"+"/"+#RequestYear))
and convert(datetime,("12/31"+"/"+#RequestYear))'
and then just add that variable to the query
Insert into #BidStatusCalculation
select 'SL Payroll',Count(*) from DashboardData
where DataType = 'Bid'
#data

You need to use dinamic SQL for this purpose.
First define and assign the #DinamicPart but you may include this in the call to the SP and consider as a parameter.
DECLARE #DinamicPart AS NVARCHAR(MAX), -- This is the part that will be passed as parameter to the SP
#SQL AS NVARCHAR(MAX);
SET #DinamicPart = ' AND CreatedDate BETWEEN CONVERT(DATETIME, (''1/1/' + #RequestYear+'))
AND CONVERT(DATETIME, (''12/31/' + #RequestYear))';
SET #SQL = 'INSERT INTO #StatusCalculation
SELECT COUNT(*)
FROM Data
WHERE DataType = 'Bid'
--This part can change based on SP param ' +
#DinamicPart + '
--end dynamic part
AND Services LIKE ''%Streamline Payroll%''
AND Services LIKE ''%GlobalView Payroll%'';
-- This part if for debugging purposes as Dinamic SQL can be very tricky
PRINT #SQL;
-- And last part, you run the Dinamic SQL
EXEC SP_ExecuteSQL #SQL;

Proposed solution:
insert into #StatusCalculation
select COUNT(*)
from Data
where DataType = 'Bid'
--This part can change based on param
and 1 = (
case #FilterToUse
when 1 then
case when year(CreateDate) = #RequestYear then 1 else 0 end
when 2 then
case when BidId between 1 and 10 then 1 else 0 end
end
)
--end dynamic part
and Services like '%Streamline Payroll%'
and Services like '%GlobalView Payroll%'

Related

SQL set WHERE statement with CASE puts an error with add operator

In the following script, I want to set the WHERE statement only if the CASE is set.
DECLARE #cmdINSERT varchar(8000);
declare #dowhere binary;
declare #date varchar(10);
set #dowhere = 1;
set #date = '14.03.2020';
set #cmdINSERT =
'SELECT *
FROM [TABLE]
WHERE [Changed At] >=
CASE WHEN ' + #dowhere + '
when 1 THEN ''' + #date + '''
ELSE [Changed At]
end;'
EXEC(#cmdINSERT)
But I am getting the error:
The data types varchar and binary are incompatible in the add operator
It seems like you're overly complicating the matter. You don't need a dynamic statement here, nor do you need 2 parameters. Use a single parameter, and pass the value NULL if you don't want to consider it. This is what is known as a "catch up" or "kitchen sink" query:
DECLARE #Date date = '20200314'; --Notice it is NOT a varchar
SELECT {Your Columns} --Replace this with a list of your columns
FROM dbo.YourTable --Replace this with your actual schema and table
WHERE [Changed At] >= #Date --Ideally, don't use names that must be delimit Identified
OR #Date IS NULL
OPTION (RECOMPILE);
The OPTION (RECOMPILE) is there to stop caching of the wrong plan, as if #Date has a value of NULL a full table scan will be required, however, if not then (depending on your indexes) one may not be required, which could be far faster. As the query is incredibly simple, the expense of regenerating query plan will be negligible, and likely <=1ms in cost.
It seems that you have complicated the solution. Why you do not try this?
SELECT *
FROM [Table]
WHERE #dowhere = 0
OR [Changed At] >= #date;
However it would be better if you change the data type of #dowhere to BIT instead of BINARY
Your approach is silly. Get rid of the case in the where altogether if no filtering is desired:
set #cmdINSERT = '
SELECT *
FROM [TABLE]
[where]';
SET #cmdInsert = REPLACE(#cmdInsert,
'[where]',
(CASE WHEN #dowwhere = 1
THEN 'WHERE [Changed At] = #date'
ELSE ''
END)
);
exec sp_executesql #cmdInsert, N'#date date', #date=#date;
Note that this also properly passes in the date as a parameter rather than munging the query string. If the date parameter is not used, that is fine: SQL Server just ignores parameters that are not used.

How do you call a variable (that is a list of values) in a conditional statement?

I am trying to create a procedure where the conditional statement changes based on the value that the user inputs. A simplified example of what I'm trying to achieve is below.
Create Procedure [LossRatioReport] (#construction AS VARCHAR(2), #state AS VARCHAR(2)) AS
DECLARE #construction_condition AS NVARCHAR(MAX)
DECLARE #construction AS VARCHAR(2)
SET #construction = '01'
SET #construction_condition = CASE #construction WHEN '01'
THEN #construction_condition = '('01', 'Frame Construction', 'Frame')'
ELSE #construction_condition = '00'
END
BEGIN
SELECT year, SUM(loss)
FROM Database_1
WHERE id IN (SELECT DISTINCT id FROM Database_2 WHERE construction = #construction_condition)
END
GO
I want to do it this way because there is a list of both integer and strings for each type and I don't want to rewrite the code over and over for each condition. But when I try and run my code, I keep getting incorrect syntax messages for the variables in the WHERE statement.
You'll need to use dynamic sql for this:
DECLARE #sql NVARCHAR(MAX)
SELECT #sql = 'SELECT year, SUM(loss)
FROM Database_1
WHERE id IN (SELECT DISTINCT id FROM Database_2 WHERE construction IN( ' + #construction_condition
EXEC sp_executesql #sql
As Sean Lange stated, something should probably be mentioned on SQL Injection. Be sure you are aware of the danger before implementing a solution with dynamic SQL. Here is an overview, and Google can tell you much, much more.

SQL Server stored procedure if statement injection string

Using SQL Server I am trying to inject a string into a SQL statement based on an if statement, note that am trying to accomplish this inside a stored procedure.
I am currently getting an error for this code:
Declare #topString varchar(240)
IF #topRecords > 0
SET #topString = 'top 500'
ELSE
SET #topString = ''
SELECT #topString * FROM( //incorrect syntax near FROM
SELECT top 500 c.Id as [Customer Id],....
UNION
SELECT top 500 c.Id as [Customer Id],....
)as table1
Order by 1 desc
Edit
if somethingTrue
#whereCondition = '1 = 1 '
else
#whereCondition = branch = #branch
select * from table
where #whereCondition AND etc...
Correct
for injection inside an if statement go with Jodrell
but if you need a dynamic top then go with what was suggested by Kaf.
thanks both for the help!
If you want to decide number of top records depending on #topRecords, you can do it using an INT or BIGINT depending on the number of records needed.
DECLARE #top INT --This is declared as an int here
IF #topRecords > 0
SET #top = 500
ELSE
SET #top = 5000000 --Make it more than records if you need all
--or make it 0 if no records needed.
--#top has to be >=0
--How to use it
SELECT TOP (#top) * FROM YourTable
EDIT: Your question had only a top injection initially. However, If you need more injections (as per your recent question edit) then I would suggest to use a dynamic query as per #Jordell's answer.
You can't inject statement parts as variables like that, however you can change most values for parameters.
Having a stored procedure perform operations that may require different query plans, based on a parameter is a bad idea, the results of this SP could vary wildly based on the value of the #topRecords parameter. You would need to use the RECOMPILE option to warn the query engine, mitigating much of the benefit of SPs. Have you considered just having two stored procedures?
If you want to do it dynamically, you could build the whole statement dynamically, making one big string, then execute that.
You should investigate using sp_executesql to execute the string/VarChar. Then similar queries will benefit from query plan reuse.
As ever Sommarskog is a good reference.
Something like this
DECLARE #topString varchar(240);
DECLARE #statement varchar(max);
IF #topRecords > 0
SET #topString = 'TOP 500';
ELSE
SET #topString = '';
SET #statement = 'SELECT ' + #topString + ' * FROM
(
SELECT TOP 500 c.Id as [Customer Id], ....
UNION
SELECT TOP 500 c.Id as [Customer Id], ....
) table1
ORDER BY 1 DESC'
/* Then execute #statement */
EXEC sp_executesql #statement

Passing SQL stored procedure entirety of WHERE clause

I have a SQL stored procedure of the form
SELECT [fields] FROM [table] WHERE #whereSql
I want to pass the procedure an argument (#whereSql) which specifies the entire WHERE clause, but the following error is returned:
An expression of non-boolean type specified in a context where a condition is expected
Can this be done?
The short answer is that you can't do it like this -- SQL Server looks at the contents of a variable as a VALUE. It doesn't dynamically build up the string to execute (which is why this is the correct way to avoid SQL injection attacks).
You should make every effort to avoid a dynamic WHERE as you're trying to do, largely for this reason, but also for the sake of efficiency. Instead, try to build up the WHERE clause so that it short-circuits pieces with lots of ORs, depending on the situation.
If there's no way around it, you can still build a string of your own assembled from the pieces of the command, and then EXEC it.
So you could do this:
DECLARE #mywhere VARCHAR(500)
DECLARE #mystmt VARCHAR(1000)
SET #mywhere = ' WHERE MfgPartNumber LIKE ''a%'' '
SELECT #mystmt = 'SELECT TOP 100 * FROM Products.Product AS p ' + #mywhere + ';'
EXEC( #mystmt )
But I recommend instead that you do this:
SELECT TOP 100 *
FROM Products.Product AS p
WHERE
( MfgPartNumber LIKE 'a%' AND ModeMfrPartNumStartsWith=1)
OR ( CategoryID = 123 AND ModeCategory=1 )
I believe this can be done using Dynamic SQL. See below:
CREATE PROCEDURE [dbo].[myProc]
#whereSql nvarchar(256)
AS
EXEC('SELECT [fields] FROM [table] WHERE ' + #whereSql)
GO
That said, you should do some serious research on dynamic SQL before you actually use it.
Here are a few links that I came across after a quick search:
http://www.sommarskog.se/dynamic_sql.html
http://msdn.microsoft.com/en-us/library/aa224806%28SQL.80%29.aspx
http://www.itjungle.com/fhg/fhg100505-story02.html
Make sure you read this fully
www.sommarskog.se/dynamic_sql.html
Dynamic SQL listed in some of the Answers is definitely a solution. However, if Dynamic SQL needs to be avoided, one of the solutions that I prefer is to make use of table variables (or temp tables) to store the parameter value that is used for comparison in WHERE clause.
Here is an example Stored Procedure implementation.
CREATE PROCEDURE [dbo].[myStoredProc]
#parameter1 varchar(50)
AS
declare #myTempTableVar Table(param1 varchar(50))
insert into #myTempTableVar values(#parameter1)
select * from MyTable where MyColumn in (select param1 from #myTempTableVar)
GO
In case you want to pass in multiple values, then the comma separated values can be stored as rows in the table variable and used in the same way for comparison.
CREATE PROCEDURE [dbo].[myStoredProc]
#parameter1 varchar(50)
AS
--Code Block to Convert Comma Seperated Parameter into Values of a Temporary Table Variable
declare #myTempTableVar Table(param1 varchar(50))
declare #index int =0, #tempString varchar(10)
if charindex(',',#parameter1) > 0
begin
set #index = charindex(',',#parameter1)
while #index > 0
begin
set #tempString = SubString(#parameter1,1,#index-1)
insert into #myTempTableVar values (#tempString)
set #parameter1 = SubString(#parameter1,#index+1,len(#parameter1)-#index)
set #index = charindex(',',#parameter1)
end
set #tempString = #parameter1
insert into #myTempTableVar values (#tempString)
end
else
insert into #myTempTableVar values (#parameter1)
select * from MyTable where MyColumn in (select param1 from #myTempTableVar)
GO
http://sqlmag.com/t-sql/passing-multivalued-variables-stored-procedure
try this it works!!
CHARINDEX (',' + ColumnName + ',', ',' +
REPLACE(#Parameter, ' ', '') + ',') > 0
execute syntax set #Parameter= 'nc1,nc2'

SQL Stored Procedure Issue - Decimal Value Conversion Issue

I am running into a very strange problem in a SQL Server stored procedure.
I have two databases. One contains the database for my billing system. The other is a reporting system with summarized data. Within this database there is a table with summarized job information. When this data is created, one of the fields, BilledToDate, is null. I wrote a stored procedure that creates a cursor that goes through this table and gets all of the job numbers. I then go through each job number and run a query against the billing database to get the total amount of billing that has been charged against job. Once I have this total, I update the BilledToDate column with this value.
The problem is that after running the stored procedure, some of the results are correct and some aren't. There doesn't appear to be any logical explanation as to why one is right and the next one is isn't. I put some print statements in the stored procedure and all of the values were correct. As an example, for one record the correct sum was 99,218.25 but the update put a value of 14,700.70 into the BilledToDate field. I added a varchar column to the table and populated that field. They are all correct. This leads me to believe that it is a casting problem but I checked and double checked my datatypes and they all look correct. I am pulling my hair out on this one (what little that is left).
My stored procedure is below. The InvoiceAmt field is a decimal(16,2) in the invchead table and I have kept it consistent throughout the process so I don't undertand why this is happening.
ALTER PROCEDURE [dbo].[sp_CalculateBilledToDate]
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from
-- interfering with SELECT statements.
SET NOCOUNT ON;
DECLARE #JobID varchar(10)
DECLARE #RecordID int
DECLARE #BilledToDate decimal(16,2)
DECLARE c1 CURSOR FOR
SELECT JobID, RecordID
FROM StructuralOpenBilling
OPEN c1
FETCH NEXT FROM c1
INTO #JobID, #RecordID
WHILE ##FETCH_STATUS = 0
BEGIN
SELECT #BilledToDate = CONVERT(money, CASE WHEN SUM(invoiceamt) > 0 THEN SUM(InvoiceAmt) ELSE 0 END)
FROM mfgsys803.dbo.invchead
WHERE shortchar01 = RTRIM(#JobID)
PRINT 'Record ID: ' + CONVERT(varchar(10), #RecordID) + ' JobID: ' + RTRIM(CONVERT(varchar(10), #JobID)) + ' Billed: ' + CONVERT(varchar(10), #BilledToDate)
UPDATE StructuralOpenBilling
SET BilledToDate = #BilledToDate, BilledCheck = CONVERT(varchar(50), #BilledToDate)
WHERE RecordID = #RecordID
PRINT 'Record ID: ' + CONVERT(varchar(10), #RecordID) + ' JobID: ' + RTRIM(CONVERT(varchar(10), #JobID)) + ' Billed: ' + CONVERT(varchar(10), #BilledToDate)
FETCH NEXT FROM c1
INTO #JobID, #RecordID
END
CLOSE c1
DEALLOCATE c1
END
Any ideas would be appreciated.
Thanks.
John
I notice a few things you might look at. BTW -- you're really over-thinking this -- a few ideas about that here as well.
SELECT #BilledToDate = CONVERT(money, CASE WHEN SUM(ISNULL(invoiceamt,0)) > 0 THEN SUM(ISNULL(InvoiceAmt,0)) ELSE 0 END)
Is the same as
SELECT #BilledToDate = CONVERT(money, SUM(ISNULL(invoiceamt,0)))
*NOTE the use of ISNULL() in both -- this would be important, as you can't do math on nulls.
Not necessary to use a cursor. Just join your two tables together in a single update statement and work on it as a batch.
UPDATE StructuralOpenBilling
SET S.BilledToDate = I.BilledToDate
FROM
StructuralOpenBilling S
INNER JOIN
(SELECT shortchar01, CONVERT(money, SUM(ISNULL(invoiceamt,0))) as BilledToDate
FROM mfgsys803.dbo.invchead) I
ON
S.JobID = I.shortchar01
Does this do what you are trying to do?
WITH inv AS
(
SELECT shortchar01,
CONVERT(MONEY, CASE WHEN SUM(invoiceamt) > 0 THEN
SUM(InvoiceAmt)
ELSE 0 END) AS BilledToDate
FROM mfgsys803.dbo.invchead
GROUP BY shortchar01
)
UPDATE StructuralOpenBilling
SET BilledToDate = inv.BilledToDate,
BilledCheck = CONVERT(VARCHAR(50), inv.BilledToDate)
FROM StructuralOpenBilling sob
JOIN inv ON inv.shortchar01 = RTRIM(sob.JobID)