SELECT statement in while loop isn't working - sql

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,',')
)

Related

Using a variable through a Query located in a table

Basically I have a procedure that is supposed to take queries from a table and run them. This works fine except when I try to define a variable in the procedure that is referenced in the query. I simply define the variable using:
DECLARE #spcode as varchar(255)
SET #spcode = 'C'
And then in the table I reference it here:
...
where sp_flag = #spcode
...
My procedure then runs through the table and executes all of the queries in the table, this works if I simply set sp_flag = 'C', but if I try to set it to the variable defined in the procedure I get the following error:
Msg 137, Level 15, State 2, Line 7
Must declare the scalar variable "#spcode".
I have searched around but I have not been able to find a solution to this problem, perhaps someone has an idea of how I would go about fixing this problem?
Thanks,
Sam
Look at this example.
First snippet execute SQL statement without using variable. Second with.
create table tbl1 (id int identity(1,1), value varchar(100));
insert into tbl1 (value) values ('abc'), ('def'), ('xyz');
-- first snippet (without using variable)
declare
#sql varchar(max);
set #sql = 'select * from tbl1 where value = ''abc''';
exec(#sql);
-- second snippet (with variable)
declare
#sql nvarchar(max),
#param nvarchar(100);
set #param = N'#val varchar(100)'
set #sql = N'select * from tbl1 where value = #val';
exec sp_executesql #sql, #param, #val = 'abc';

Stored procedure get parameter list and current values

Not sure how to implement this, but I need a way to get the current list of parameters for a stored procedure as well as their passed in values (this code will be executed in the stored procedure itself).
I know I can use sys.parameters to get the parameter names, but how to get the actual values?
What I need to do with this is to make a char string of the form
#param_name1=#value1,#param_name2=#value2,...,#param_namen=#valuen
I have tried to use dynamic sql, but not having much joy with that.
Any ideas??
Edit:
Currently I am just going through all the parameters one-by-one to build the string. However I want a "better" way to do it, since there are quite a few parameters. And incase parameters are added later on (but the code to generate the string is not updated).
I tried using dynamic sql but gave up, since the sp_executesql sp requires parameters be passed into it...
You state '(this code will be executed in the stored procedure itself).' so assuming you are in the procedure you will already know the parameter names as you have to declare them when creating your procedure. Just do a select and put the names inside text fields
ALTER PROCEDURE procname
(
#param1 NVARCHAR(255)
,#param2 INT
...
)
SELECT [Parameters] = '#param1=' + #param1
+ ',#param2=' + CONVERT(NVARCHAR(MAX),#param2)...
The CONVERT is there as an example for non-char datatypes.
update
You will need to create a linked server that points to itself to use the OPENQUERY function.
USE [master]
GO
/****** Object: LinkedServer [.] Script Date: 04/03/2013 16:22:13 ******/
EXEC master.dbo.sp_addlinkedserver #server = N'.', #srvproduct=N'', #provider=N'SQLNCLI', #datasrc=N'.', #provstr=N'Integrated Security=SSPI'
/* For security reasons the linked server remote logins password is changed with ######## */
EXEC master.dbo.sp_addlinkedsrvlogin #rmtsrvname=N'.',#useself=N'True',#locallogin=NULL,#rmtuser=NULL,#rmtpassword=NULL
GO
Now you can do something like this cursor to get each parameter name and then use dynamic sql in OPENQUERY to get the value:
DECLARE curParms CURSOR FOR
SELECT
name
FROM sys.parameters
WHERE OBJECT_ID = OBJECT_ID('schema.procedurename')
ORDER BY parameter_id
OPEN curParms
FETCH curParms INTO #parmName
WHILE ##FETCH_STATUS <> -1
BEGIN
SELECT #parmName + '=' + (SELECT * FROM OPENQUERY('linkedservername','SELECT ' + #parmName))
FETCH curParms INTO #parmName
END
CLOSE curParms
DEALLOCATE curParms
Since SQL Server 2014 we have sys.dm_exec_input_buffer, it is a table valued function with an output column event_info that gives the full execution statement (including parameters).
We can parse the param values from sys.dm_exec_input_buffer and get the param names from sys.parameters and join them together to get the string you want.
For example:
create procedure [dbo].[get_proc_params_demo]
(
#number1 int,
#string1 varchar(50),
#calendar datetime,
#number2 int,
#string2 nvarchar(max)
)
as
begin
-- get the full execution statement
declare #statement nvarchar(max)
select #statement = event_info
from sys.dm_exec_input_buffer(##spid, current_request_id())
-- parse param values from the statement
declare #proc_name varchar(128) = object_name(##procid)
declare #param_idx int = charindex(#proc_name, #statement) + len(#proc_name)
declare #param_len int = len(#statement) - #param_idx
declare #params nvarchar(max) = right(#statement, #param_len)
-- create param values table
select value, row_number() over (order by current_timestamp) seq
into #params
from string_split(#params, ',')
-- get final string
declare #final nvarchar(max)
select #final = isnull(#final + ',','') + p1.name + '=' + ltrim(p2.value)
from sys.parameters p1
left join #params p2 on p2.seq = parameter_id
where object_id = ##procid
select #final params
end
To test it:
exec get_proc_params_demo 42, 'is the answer', '2019-06-19', 123456789, 'another string'
Returns the string you want:
#number1=42,#string1='is the answer',#calendar='2019-06-19',#number2=123456789,#string2='another string'
I have something similar wrapped as a UDF. I use it for error logging in catch blocks.

Pass string variable into procedure and add it to a query

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...

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

Is there a way to select a database from a variable?

Is there a way to select a database from a variable?
Declare #bob as varchar(50);
Set #bob = 'SweetDB';
GO
USE #bob
Unfortunately, no.
Unless you can execute the rest of your batch as dynamic SQL.
Using execute to dynamically execute SQL will change the context for the scope of the execute statement, but will not leave a lasting effect on the scope you execute the execute statement from.
In other words, this:
DECLARE #db VARCHAR(100)
SET #db = 'SweetDB'
EXECUTE('use ' + #db)
Will not set the current database permanently, but if you altered the above code like this:
DECLARE #db VARCHAR(100)
SET #db = 'SweetDB'
EXECUTE('use ' + #db + ';select * from sysobjects')
select * from sysobjects
Then the result of those two queries will be different (assuming you're not in SweetDB already), since the first select, executed inside execute is executing in SweetDB, but the second isn't.
declare #NewDB varchar(50)
set #NewDB = 'NewDB'
execute('use ' + #NewDB)
#TempTables will presist across GOs
you can create the table in the first batch, insert/select data as necessary in that or any following batch.
here is some sample syntax:
CREATE TABLE #YourTableName
(
col1 int not null primary key identity(1,1)
,col2 varchar(10)
)