Pass string variable into procedure and add it to a query - sql

I have an application written in C#, which connects to database and analyze its data, database stores information about execution of automated tests, what I would like to do is to retrieve those tests that fulfill the above given conditions. But we are having different projects and will be supporting more and more so I do not want to construct different procedure for each one, but pass the name - 2nd parameter deploy as the parameter so the query will depend on the project and return the data to the application, then I will send it in a report.
For the time being it looks like this:
CREATE PROCEDURE [dbo].[SuspectsForFalsePositive](#build_id INT, #deploy VARCHAR(25))
AS
BEGIN
SET NOCOUNT ON;
DECLARE #i int, #build int, #deployname varchar(25), #SQL varchar(max)
DECLARE #result table (tc int, fp float)
SET #i = 0
SET #build = #build_id
SET #deployname = #deploy
SET #SQL = 'insert '+#result+'select testcase_id, fail_percentage FROM [BuildTestResults].[dbo].['+#deployname+'TestCaseExecution]
where build_id = #build and fail_percentage >= 70'
--INSERT #result select testcase_id, fail_percentage FROM [BuildTestResults]
--.[dbo].[ABCTestCaseExecution]
--where build_id = #build and fail_percentage >= 70
--commented works
EXEC(#SQL)
WHILE (##rowcount = 0)
BEGIN
SET #build = #build - 1
EXEC(#SQL)
--INSERT #result select testcase_id, fail_percentage FROM [BuildTestResults].[dbo]. --[ABCTestCaseExecution]
--where build_id = #build and fail_percentage >= 70
--commented works
END
select * from #result order by fp DESC
END
GO
Thanks for any advice !

In your string you have #build - this is interpreted as a string. At the time you execute the #SQL it doesn't contain such a variable, so you get a failure.
You need to concatenate the value directly:
SET #SQL = 'insert '+#result+'select testcase_id, fail_percentage FROM [BuildTestResults].[dbo].['+#deployname+'TestCaseExecution]
where build_id = '+#build+' and fail_percentage >= 70'
You will need to do that between executions too.

There are a few issues with your example. This is, however, one over-arching consideration.
Variables (table and/or scalar) are only visible in the StoredProcedure they are defined in. And calling EXEC(#SQL) is calling a stored. This means that neither your #result table, not your other parameters are visible to the dynamic SQL you are executing.
In terms of the table, you can get around that by creating a temp table instead. And for the scalar variables, you can pass them around when using SP_EXECUTESQL instead of EXEC.
I don't have access to sql server at present, but maybe somethign like this can start you on your way...
CREATE PROCEDURE [dbo].[SuspectsForFalsePositive](#build_id INT, #deploy VARCHAR(25))
AS
BEGIN
SET NOCOUNT ON;
DECLARE
#i int,
#build int,
#deployname varchar(25),
#SQL varchar(max)
CREATE TABLE #result (
tc int,
fp float
)
SELECT
#i = 0,
#build = #build_id,
#deployname = #deploy
SET #sql = ''
SET #sql = #sql + ' INSERT INTO #result'
SET #sql = #sql + ' SELECT testcase_id, fail_percentage'
SET #sql = #sql + ' FROM [BuildTestResults].[dbo].['+#deployname+'TestCaseExecution]'
SET #sql = #sql + ' WHERE build_id = #build and fail_percentage >= 70'
SP_EXECUTESQL
#SQL,
'#build INT',
#build
WHILE (##rowcount = 0)
BEGIN
SET #build = #build - 1
SP_EXECUTESQL
#SQL,
'#build INT',
#build
END
SELECT * FROM #result ORDER BY fp DESC
END
GO
I also occures to me that ##rowcount may now see the rows being processes within SP_EXECUTESQL. In which case you may need to re-arrange things a little (using an output parameter, or embedding the loop in the #SQL, etc).
Overall it feels a bit clunky. With more information about your schema, etc, it may be possible ot avoid the dynamic SQL. This will have several benefits, but one in particular:
- Right now you're open to SQL Injection Attacks on the #deploy parameter
Anyone that can execute this SP, and/or control the value in the #deploy parameter could wreak havok in your database.
For example... Could you store all the TestCaseExecutions in the same table? But with an extra field: TestCaseID *(Or even TestCaseName)?
Then you wouldn't need to build dynamic SQL to control which data set you are processing. Instead you just add WHERE TestCaseID = #TestCaseID to your query...

Related

Cannot get output variable from stored procedure when procedure written in dynamic sql

I am writing a procedure to produce an int output variable, but I'm not sure how to do this using dynamic sql. If I execute the below procedure I get the #AnlyNum value displayed in the results screen, but I just want #AnlyNum variable set with a value so I can use it. Thank you.
Create procedure [dbo].[sp_test] #Db varchar(50), #RwNum int, #AnlyNum int output
As
Begin
Declare #Sql nvarchar(max) =
'Select ''#AnlyNum'' = (Select AnlyId From '+#Db+'..Test order by AnlyId desc OFFSET '+convert(varchar(10),#RwNum)+' rows fetch next 1 rows only)'
End
exec(#Sql)
This removes SQL injection concerns by properly escaping the database name and also dynamically executing against that database instead of embedding the database name in the command. Also, you don't need #RwNum to be dynamic.
CREATE PROCEDURE dbo.test
#Db sysname,
#RwNum int,
#AnlyNum int output
AS
BEGIN
SET NOCOUNT ON;
DECLARE #exec nvarchar(max) = QUOTENAME(#Db) + N'.sys.sp_executesql',
#sql nvarchar(max) = N'SELECT #AnlyNum = AnlyId
From dbo.Test order by AnlyId desc
OFFSET #RwNum rows fetch next 1 rows only);';
EXEC #exec #sql, N'#AnlyNum int output, #RwNum int',
#AnlyNum output, #RwNum;
END

SELECT statement in while loop isn't working

I have created the following Select statement which is working fine:
SELECT #Block = [Blok], #Year = [Aar]
FROM [PT99991_Result].[dbo].[Testheader]
WHERE N = #TestHeaderID
The problem is that this Select statement is used in a While loop where the database can change to another one during the loop. I have tried to modify the statement to the following but it's not working. I have also tried to use EXEC which takes care of my problem but then I'm facing a problem with the local variables #Block and #Year instead.
SET #DataBase = 'PT99991_RESULT' --This is only for test!
SELECT #Block = [Blok], #Year = [Aar]
FROM '[' + #DataBase + '].[dbo].[Testheader]'
WHERE N = #TestHeaderID
I'm not sure what I'm doing wrong?
First, generate a T-SQL template like this:
DECLARE #DynamicTSQLStatement NVARCHAR(MAX) = N'SELECT #Block = [Blok], #Year = [Aar]
FROM [#DataBase].[dbo].[Testheader]
WHERE N = #TestHeaderID';
Then, let's say that the variale #Datbase holds your current database name. If it is not extracted from sys.database you can perform additional validation to ensure nobody is doing something wrong.
IF NOT EXISTS(SELEFT 1 FROM sys.database WHERE [name] = #Datbase
BEGIN
....
END;
After the validation, you just replace the database name in the template:
SET #DynamicTSQLStatement = REPLACE(#DynamicTSQLStatement, '#Database', #Database);
Then, execute the code passing the parameters:
EXEC sp_executesql #DynamicTSQLStatement
,N'#TestHeaderID INT'
,N'#Block INT OUTPUT'
,N'#Year INT OUTPUT'
,#TestHeaderID
,#Block OUTPUT
,#Year OUTPUT;
of course on every loop iteration, reset the template.
Instead of while loop, You can go for undocumented stored procedure: ms_foreachdb and execute against the databases and finally apply filter for the specific database.
Caveat: Don't use this in production code, because, it uses undocumented stored procedure.
CREATE TABLE #test(dbname sysname, blok int, aar int)
DECLARE #db_list NVARCHAR(max) = 'DB1,DB2'
EXECUTE master.sys.sp_MSforeachdb 'USE [?]; declare #blok int, #aar int; INSERT INTO #test SELECT db_name(), blok, aar from [dbo].[Testheader] WHERE N = TestHeaderId;'
SELECT * FROM #test where dbname in
(
SELECT value FROM string_split(#db_list,',')
)

SQL Store a value from an EXECUTE Command into a variable

I'm attempting to store a value into a variable from a EXECUTE command. I know I'm suppose to use sp_executesql command, but all examples online are only making more confused. So here is what I'm trying to do.
I have a stored procedure that accepts two parameters (a table name, a room #). To have a dynamic table name, I use dynamic SQL style while using strings. I'm attempting to store a phone number that is either from multiple tables. I got this working so far.
DECLARE #Location varchar(MAX);
DECLARE #Room varchar(10);
DECLARE #Number char(8);
DECLARE #SQLString varchar(MAX);
SET #Location = N'CMPhone.dbo.GardenCottage';
SET #Room = N'202';
SET #SQLString ='SET #Number = (SELECT PhoneNumber FROM ' + #Location + ' WHERE Room = ''' + #Room + ''');';
PRINT(#SQLString);
OUTPUT
SET #Number = (SELECT PhoneNumber FROM CMPhone.dbo.GardenCottage WHERE Room = '202');
SET #Number = (SELECT PhoneNumber FROM CMPhone.dbo.GardenCottage WHERE Room = '202');
PRINT(#Number);
OUTPUT
123-4567
Which is the correct number. Now, here is where the problem comes in. I need to do another query using dynamic SQL so I can use multiple tables again. So in my stored procedure, I need to store my EXEC(#SQLString) into a variable (#Number) so I can use that value, and that's where I'm having problems. I can't get sp_executesql to store the value into #Number. The other query will look something like this
SET #SQLString = ' UPDATE PhoneNumbers SET Active = ''1'' WHERE
PhoneNumber = ''' + #Number + ''';';
EXEC(#SQLString);
If this is confusing in anyway, or you have questions, please ask. Any help is very much appreciated. Thanks
Update #1:
I have this new string now
#SQLString = 'SELECT PhoneNumber FROM ' + #Location ' + ' WHERE Room = ''' + #Room + ''';';
EXECUTE SP_EXECUTESQL #SQLString
gets the correct number, but I don't know how to set up a OUTPUT parameter.
I'm attempting to follow this example from Microsoft
DECLARE #SQLString NVARCHAR(500)
DECLARE #ParmDefinition NVARCHAR(500)
DECLARE #IntVariable INT
DECLARE #Lastlname varchar(30)
SET #SQLString = N'SELECT #LastlnameOUT = max(lname)
FROM pubs.dbo.employee WHERE job_lvl = #level'
SET #ParmDefinition = N'#level tinyint,
#LastlnameOUT varchar(30) OUTPUT'
SET #IntVariable = 35
EXECUTE sp_executesql
#SQLString,
#ParmDefinition,
#level = #IntVariable,
#LastlnameOUT=#Lastlname OUTPUT
SELECT #Lastlname
But I don't see how their declaring the lastlNameOUT variables.
use output variable in your EXECUTE sp_executesql like this:
EXECUTE sp_executesql #SQLString, N'#Number char(8) out',#Number out then you will get #Number value from inside dynamc sql, then you can use that value in other part of the query. hope this helps

How can I spot in what database is a stored procedure with name 'myStoredProcedure'?

There are bunch of databases to the SQL server I am connected.
How should I query the sysobjects in order to spot in what database a stored procedure with name 'myStoredProcedure' is located ?
The query should return the database name.
Thanks
I know you are not asking for this, but I'd really download RedGate's Sql Search add-in for SSMS and use that. It allows you to find any object (proc, table, view, column, etc) on any database easily.
And it's free!
I'd give this a try:
CREATE TABLE ##DatabaseList
(
DatabaseName varchar(50)
)
EXECUTE SP_MSForEachDB 'USE [?]; INSERT INTO ##DatabaseList SELECT DB_NAME() FROM [sys].[objects] WHERE name = "MyStoredProcedure" AND type_desc = "SQL_STORED_PROCEDURE"'
SELECT * FROM ##DatabaseList
DROP TABLE ##DatabaseList
That's using the undocumented/ unsupported system stored procedure SP_MSForEachDb and writing any hits to a global temp table, then outputting the contents to the Results window before dropping the table. If you just need to know which database (or databases - there may of course be more than one) has an appropriately named SP, this should do it. If you want to use the output elsewhere as a parameter, it may take a little more work.
By the way, I'm only learning this stuff myself over the last few months so if anyone can critique the above and suggest a better way to go at it I'm happy to receive feedback. Equally, I can answer any further questions posted here to the best of my ability.
Cheers
So out of curiosity I decided to try write this myself, especially since ADG mentioned his solution was using an unsupported, undocumented procedure. This could also be expanded to take a 2nd parameter so where it checks the type = P (stored Proc) you could probably change it to look for other things like views / tables etc.
My solution is a bit long but here goes:
CREATE PROCEDURE spFindProceduresInDatabases
(
#ProcedureName NVARCHAR(99)
)
AS
BEGIN
-- Get all the database names and put them into a table
DECLARE #Db TABLE (DatabaseName Varchar(99))
INSERT INTO #Db SELECT name FROM Sys.databases
-- Declare a table to hold our results
DECLARE #results TABLE (DatabaseName VARCHAR(99))
-- Make a Loop
-- Declare a variable to be incremented
DECLARE #count INT
SET #count = 0
-- Declare the end condition
DECLARE #endCount INT
SELECT #endCount = COUNT(*) FROM #Db
-- Loop through the databases
WHILE (#count < #endCount )
BEGIN
-- Get the database we are going to look into
DECLARE #dbWeAreChecking VARCHAR(99)
SELECT TOP 1 #dbWeAreChecking = DatabaseName FROM #Db
DELETE FROM #Db WHERE DatabaseName = #dbWeAreChecking
-- Create and execute our query
DECLARE #Query NVARCHAR(3000)
SET #Query = N'SELECT #outParam = COUNT(*) FROM '+#dbWeAreChecking+'.sys.sysobjects WHERE type = ''P'' and name = #ProcedureName'
Declare #outParam INT
print (#Query)
DECLARE #ParmDefinition NVARCHAR(500)
DECLARE #IntVariable INT
SET #ParmDefinition = N'#ProcedureName VARCHAR(99),#outParam INT OUTPUT'
SET #IntVariable = 35
EXECUTE sp_executesql
#Query ,
#ParmDefinition,
#ProcedureName,
#outParam = #outParam OUTPUT
-- If we have a result insert it into the results table
If (#outParam > 0)
BEGIN
INSERT INTO #results(DatabaseName) VALUES(#dbWeAreChecking)
END
-- Increment the counter
SET #count = (#count + 1)
END
-- SELECT ALL OF THE THINGS!!!
SELECT * FROM #results
END

MS SQL - High performance data inserting with stored procedures

Im searching for a very high performant possibility to insert data into a MS SQL database.
The data is a (relatively big) construct of objects with relations. For security reasons i want to use stored procedures instead of direct table access.
Lets say i have a structure like this:
Document
MetaData
User
Device
Content
ContentItem[0]
SubItem[0]
SubItem[1]
SubItem[2]
ContentItem[1]
...
ContentItem[2]
...
Right now I think of creating one big query, doing somehting like this (Just pseudo-code):
EXEC #DeviceID = CreateDevice ...;
EXEC #UserID = CreateUser ...;
EXEC #DocID = CreateDocument #DeviceID, #UserID, ...;
EXEC #ItemID = CreateItem #DocID, ...
EXEC CreateSubItem #ItemID, ...
EXEC CreateSubItem #ItemID, ...
EXEC CreateSubItem #ItemID, ...
...
But is this the best solution for performance? If not, what would be better?
Split it into more querys? Give all Data to one big stored procedure to reduce size of query? Any other performance clue?
I also thought of giving multiple items to one stored procedure, but i dont think its possible to give a non static amount of items to a stored procedure.
Since 'INSERT INTO A VALUES (B,C),(C,D),(E,F) is more performant than 3 single inserts i thought i could get some performance here.
Thanks for any hints,
Marks
One stored procedure so far as possible:
INSERT INTO MyTable(field1,field2)
SELECT "firstValue", "secondValue"
UNION ALL
SELECT "anotherFirstValue", "anotherSecondValue"
UNION ALL
If you aren't sure about how many items you're inserting you can construct the SQL query witin the sproc and then execute it. Here's a procedure I wrote to take a CSV list of groups and add their relationship to a user entity:
ALTER PROCEDURE [dbo].[UpdateUserADGroups]
#username varchar(100),
#groups varchar(5000)
AS
BEGIN
DECLARE #pos int,
#previous_pos int,
#value varchar(50),
#sql varchar(8000)
SET #pos = 1
SET #previous_pos = 0
SET #sql = 'INSERT INTO UserADGroups(UserID, RoleName)'
DECLARE #userID int
SET #userID = (SELECT TOP 1 UserID FROM Users WHERE Username = #username)
WHILE #pos > 0
BEGIN
SET #pos = CHARINDEX(',',#groups,#previous_pos+1)
IF #pos > 0
BEGIN
SET #value = SUBSTRING(#groups,#previous_pos+1,#pos-#previous_pos-1)
SET #sql = #sql + 'SELECT ' + cast(#userID as char(5)) + ',''' + #value + ''' UNION ALL '
SET #previous_pos = #pos
END
END
IF #previous_pos < LEN(#groups)
BEGIN
SET #value = SUBSTRING(#groups,#previous_pos+1,LEN(#groups))
SET #sql = #sql + 'SELECT ' + cast(#userID as char(5)) + ',''' + #value + ''''
END
print #sql
exec (#sql)
END
This is far faster than individual INSERTS.
Also, make sure you just a single clustered index on the primary key, more indexes will slow the INSERT down as they will need to update.
However, the more complex your dataset is, the less likely it is that you'll be able to do the above so you will simply have to make logical compromises. I actually end up calling the above routine around 8000 times.