The objective of the code is to run a query dynamically and return 0 if there are no rows with data present in the columns and to return 1 if there are rows with data in the columns. This is my code for the stored procedure:
ALTER proc [dbo].[usp_ColumnFieldValidator]
(
#TblName nvarchar(30),
#ColumnName nvarchar(30),
#RetVal bit output
)
as
begin
declare #CountOfRowsQuery as nvarchar(300)
set #CountOfRowsQuery = 'select count('+quotename(#ColumnName)+') from '+quotename(#TblName)+' having count(' +quotename(#ColumnName)+') = nullif(count('+quotename(#ColumnName)+'),0)'
execute sp_executesql #CountOfRowsQuery
select #RetVal = dbo.fn_ColumnValidator(#CountOfRowsQuery)
end
As you can see, a user-defined function is being called to set the value of #RetVal. This is my code for the user-defined function.
ALTER function [dbo].[fn_ColumnValidator]
(
#NullChecker as nvarchar(max)
)
returns bit
as
begin
declare #returnVar as bit
if #NullChecker is null
set #returnVar = 0
else
set #returnVar = 1
return #returnVar
end
The output of #RetVal is always 1 and I have attributed this error to #CountOfRowsQuery storing the entire string rather than the value of the query ie: #CountOfRowsQuery = null if the count of rows is zero else, #CountOfRowsQuery = the number of rows present in the column. To make things clearer I am attaching screenshots of the output when I run the program.
Output of a table that contains rows with data
Output of a table that contains no rows with no data
As you can see in list item.2, the sp returns null but the function_returned_value is being set to 1 instead of 0.
The objective of the code is to run a query dynamically and return 0 if there are no rows with data present in the columns and to return 1 if there are rows with data in the columns.
Man, if this is not an over-complication I don't know what is.
Here's a much simpler (and more efficient) query that does the work:
SELECT CAST(IIF(EXISTS(
SELECT 1
FROM TableName
WHERE ColumnName IS NOT NULL
), 1, 0) As Bit)
Now, to change that to a procedure using dynamic SQL in a way that will not expose you to SQL Injection threats you can do this:
ALTER PROCEDURE [dbo].[usp_ColumnFieldValidator]
(
#TblName sysname,
#ColumnName sysname,
#RetVal bit output
)
AS
BEGIN
IF NOT EXISTS(
SELECT 1
FROM Information_Schema.Columns
WHERE Table_Name = #TblName
AND Column_Name = #ColumnName
)
RETURN;
DECLARE #Sql nvarchar(1000) =
N'SELECT #RetVal = CAST(IIF(EXISTS(
SELECT 1
FROM '+ QUOTENAME(#TblName) + N'
WHERE '+ QUOTENAME(#ColumnName) + N' IS NOT NULL
), 1, 0) As Bit)'
EXEC sp_executesql #Sql, N'#RetVal bit output', #RetVal OUTPUT;
END
Key notes:
I've changed the #TblName and #ColumnName variables to data type sysname instead of your original nvarchar(30) - since that is the data type SQL Server use internally to store identifiers.
Since identifiers can't be parameterized, I've white-listed them.
I'm using sp_executeSql to get back the value of the dynamic query directly into my output parameter.
For more tips and tricks on dynamic SQL, you can read my blog post entitled The do’s and don’ts of dynamic SQL for SQL Server
Related
I have created a stored procedure as shown below, but it's returning only one row instead of 3:
CREATE PROCEDURE [dbo].[tempsp]
(#RecycleIds NVARCHAR(MAX) = NULL)
AS
BEGIN
DECLARE #Err INT
DECLARE #WhereClause NVARCHAR(MAX)
DECLARE #SQLText1 NVARCHAR(MAX)
DECLARE #SQLText NVARCHAR(MAX)
SET #SQLText1 = 'SELECT FROM dbo.SKU '
IF #RecycledSkuIds IS NOT NULL
BEGIN
SET #SQLText = 'SELECT FROM dbo.SKU WHERE SKU.SkuId IN (#RecycleIds)'
EXEC sp_executesql #SQLText, N'#RecycleSkuIds nvarchar', #RecycleIds
END
ELSE
BEGIN
EXEC(#SQLText1)
END
SET #Err = ##ERROR
RETURN #Err
END
-------end of stored procedure--------
EXEC tempsp #RecycleIds = '5,6,7'
After running this SQL statement, it only returns one row instead of 3, with the id's of 5, 6, 7.
Can anyone tell me what I am doing wrong?
i wanted to use sp_executesql, so that it can be safe against sql injection with strong type defined.
Use a table type parameter, with a strongly typed column:
CREATE TYPE dbo.IDs AS table (ID int);
GO
CREATE PROCEDURE [dbo].[tempsp] #RecycleIds dbo.IDs READONLY AS
BEGIN
IF EXISTS (SELECT 1 FROM #RecycleIds)
SELECT * --Replace with needed columns
FROM dbo.SKU S
--Using EXISTS in case someone silly puts in the same ID twice.
WHERE EXISTS (SELECT 1
FROM #RecycleIds R
WHERE R.ID = S.SkuID);
ELSE
SELECT * --Replace with needed columns
FROM dbo.SKU S
END;
GO
Then you could execute it like so:
EXEC dbo.tempsp; --All Rows
GO
DECLARE #RecycleIds dbo.IDs;
INSERT INTO #RecycleIds
VALUES(1),(40),(182);
EXEC dbo.tempsp #RecycleIds;
I was trying to retrive the rows whose id matches within the IN clause.
SET #INClauseIds='''' + replace(#Ids, ',', ''',''') + ''''
Above statement would convert the ID's ='1,2,3' to '1','2','3' which i can directly place in the IN clause.
SET #SQLText1 ='EXEC(''SELECT Name,SEOFriendlyName FROM SKU Where Id IN ( ''+ #Ids+'' ) )'
EXEC sp_executesql #SQLText1 ,N'#INClauseIds nvarchar(max)',#Ids=#INClauseIds
If you want to avoid the usage of Temp Table which would add extra caliculation time. you can you the above strategy to retrive n number of records. Safe with strongly coupled with sp_executesql and without any sql injection.
You cannot use IN. Or, more accurately, you have a string and you are confusing it with a list. One method is to instead use LIKE:
SET #SQLText = '
SELECT *
FROM dbo.SKU
WHERE CONCAT('','', #RecycleIds, '','') LIKE CONCAT(''%,'', SKU.SkuId, '',%'')
';
My table has column names m1,m2,m3...,m12.
I'm using iterator to select them and insert them one by one in another table.
In this iterator I'm trying to generate filed names with:
'['+concat('m',cast(#P_MONTH as nvarchar))+']'
where #P_MONTH is incrementing in each loop.
so for #P_MONTH = 1 this suppose to give [m1] which works fine.
But when I run query I get:
Conversion failed when converting the nvarchar value '[m1]' to data
type int.
And if I put simply [m1] in that select it works ok.
How to concat filed name so it can be actually interpreted as filed name from certain table?
EDIT
Here is full query:
DECLARE #SQLString nvarchar(500),
#P_YEAR int,
#P_MONTH int = 1
set #P_YEAR = 2018
WHILE #P_MONTH < 13
BEGIN
SET #SQLString =
'INSERT INTO [dbo].[MASTER_TABLE]
(sector,serial,
date, number, source)'+
'SELECT ' + '[SECTOR],[DEPARTMENT]' +
QUOTENAME(cast(CONVERT(datetime,CONVERT(VARCHAR(4),#P_YEAR)+RIGHT('0'+CONVERT(VARCHAR(2),#P_MONTH),2)+'01',5) as nvarchar))+
QUOTENAME ('M',cast(#P_MONTH as nvarchar)) +
'EMPLOYED' +
'FROM [dbo].[STATS]'+
'where YEAR= #P_YEAR'
EXECUTE sp_executesql #SQLString
SET #P_MONTH = #P_MONTH + 1
END
It's still not working. It executes successfully but it does nothing.
Good day,
Let's create a simple table for the sake of the explanation
DROP TABLE IF EXISTS T
GO
CREATE TABLE T(a1 INT)
GO
INSERT T(a1) VALUES (1),(2)
GO
SELECT a1 FROM T
GO
When we are using a query like bellow, the server parse the text as a value and not as a column name
DECLARE #String NVARCHAR(10)
SELECT #String = '1'
--
SELECT '['+concat('a',cast(#String as nvarchar))+']'
FROM T
GO
This mean that the result will be 2 rows with no name for the column and the value will be "[a1]"
Moreover, the above query uses the brackets as part of the string.
One simple solution is to use the function QUOTENAME in order to add brackets around a name.
Another issue in this approach is the optional risk of SQL Injection. QUOTENAME might not be perfect solution but can help in this as well.
If we need to use entities name dynamically like in this case the column name then for most cases using dynamic query is the best solution. This mean to use the Stored Procedure sp_executesql as bellow
DECLARE #String INT
SELECT #String = 1
DECLARE #SQLString nvarchar(500);
SET #SQLString =
'SELECT ' + QUOTENAME(concat('a',cast(#String as nvarchar))) + ' FROM T'
EXECUTE sp_executesql #SQLString
GO
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.
My COLUMNS can contain only three values or var chars - economy, basic, luxury. I want to select a ROW and display only those COLUMNS which contain luxury. The problem is that there are many such columns - about 50. I don't want to type the names of all those columns in my select query. Is there a shorter and simpler alternative to this ? Which query should I use ?
I am thinking of something like this (this is a FAKE query) -
#declare Column_Name varchar(30)
select Column_Name where Column_Value = 'luxury'
from ATable
where rowId = 'row 5';
Table structure -
rowId | Column1 | Column2 | Column3.....
I've created a stored procedure for you.
This procedure examines the MSSQL meta to build a dynamic SQL string that returns a result containing column names N and their values V, and the corresponding row key K from which that value was retrieved, for a specified table.
When this is executed, the results stored in a global temporary table called ##ColumnsByValue, which can then be queried directly.
Create the GetColumnsByValue stored procedure, by executing this script:
-- =============================================
-- Author: Ben Roberts (sepster#internode.on.net)
-- Create date: 22 Mar 2013
-- Description: Returns the names of columns that contain the specified value, for a given row
-- =============================================
SET ANSI_NULLS ON
GO
SET QUOTED_IDENTIFIER ON
GO
IF OBJECT_ID ( 'dbo.GetColumnsByValue', 'P' ) IS NOT NULL
DROP PROCEDURE dbo.GetColumnsByValue;
GO
CREATE PROCEDURE dbo.GetColumnsByValue
-- Add the parameters for the stored procedure here
#idColumn sysname,
#valueToFind nvarchar(255),
#dbName sysname,
#tableName sysname,
#schemaName sysname,
#debugMode int = 0
AS
BEGIN
-- SET NOCOUNT ON added to prevent extra result sets from interfering with SELECT statements.
SET NOCOUNT ON;
DECLARE #SQL nvarchar(max);
DECLARE #SQLUnion nvarchar(max);
DECLARE #colName sysname;
DECLARE #dbContext nvarchar(256);
DECLARE #Union nvarchar(10);
SELECT #dbContext = #dbName + '.' + #schemaName + '.sp_executeSQL';
SELECT #SQLUnion = '';
SELECT #Union = '';
IF OBJECT_ID ( 'tempdb..##GetColumnsByValueIgnoreList') IS NULL -- no columns to ingore have been specified, need to create an empty list.
BEGIN
CREATE TABLE ##GetColumnsByValueIgnoreList (column_name nvarchar(255));
END
DECLARE DBcursor CURSOR FOR
SELECT
COLUMN_NAME
FROM
INFORMATION_SCHEMA.COLUMNS
WHERE
TABLE_NAME = #tableName
AND
TABLE_SCHEMA = #schemaName;
OPEN DBcursor;
FETCH DBcursor INTO #colName;
WHILE (##FETCH_STATUS = 0)
BEGIN
IF (
#colName != #idColumn
AND
#colName NOT IN (SELECT column_name FROM ##GetColumnsByValueIgnoreList)
)
BEGIN
SELECT #SQL = 'SELECT '+#idColumn+' as K, '''+#colName+''' as N, ' +#colName+ ' as V FROM ' + #dbName + '.' + #schemaName + '.' + #tableName;
--PRINT #SQL;
SELECT #SQLUnion = #SQL + #Union + #SQLUnion;
SELECT #Union = ' UNION ';
END
FETCH DBcursor INTO #colName;
END; -- while
CLOSE DBcursor; DEALLOCATE DBcursor;
IF (#debugMode != 0)
BEGIN
PRINT #SQLUnion;
PRINT #dbContext;
END
ELSE
BEGIN
-- Delete the temp table if it has already been created.
IF OBJECT_ID ('tempdb..##ColumnsByValue') IS NOT NULL
BEGIN
DROP TABLE ##ColumnsByValue
END
-- Create a new temp table
CREATE TABLE ##ColumnsByValue (
K nvarchar(255), -- Key
N nvarchar(255), -- Column Name
V nvarchar(255) -- Column Value
)
-- Populate it with the results from our dynamically generated SQL.
INSERT INTO ##ColumnsByValue EXEC #dbContext #SQLUnion;
END
END
GO
The SP takes several inputs as parameters, these are explained in the following code.
Note also I've provided a mechanism to add an "ignore list" as an input:
This allows you to list any column names that should not be included
in the results.
You do NOT need to add the columnn that you're using as your key, ie the row_id from your example structure.
You MUST include other columns that are not varchar as
these will cause an error (as the SP just does a varchar comparison
on all columns it looks at).
This is done via a temp table that you must create/populate
Your example table structure suggests
the table contains only columns of interest, so this may not apply to
you.
I've included example code for how to do this (but only do this if you need to):
IF OBJECT_ID ( 'tempdb..##GetColumnsByValueIgnoreList') IS NOT NULL
BEGIN
DROP TABLE ##GetColumnsByValueIgnoreList;
END
CREATE TABLE ##GetColumnsByValueIgnoreList (column_name nvarchar(255));
INSERT INTO ##GetColumnsByValueIgnoreList VALUES ('a_column');
INSERT INTO ##GetColumnsByValueIgnoreList VALUES ('another_column');
INSERT INTO ##GetColumnsByValueIgnoreList VALUES ('yet_another_column');
Now, to fire off the procedure that build your temp table of results, use the following code (and modify as appropriate, of course).
-- Build the ##ColumnsByValue table
EXEC dbo.GetColumnsByValue
#idColumn = 'row_id', -- The name of the column that contains your row ID (eg probably your PK column)
#dbName = 'your_db_name',
#tableName = 'your_table_name',
#schemaName = 'dbo',
#debugMode = 0 -- Set this to 1 if you just want a print out of the SQL used to build the temp table, to 0 if you want the temp table populated
This leaves you with ##ColumnsByValue, on which you can perform whatever search you need, eg:
select * from ##ColumnsByValue WHERE v = 'luxury' and k = 5 --some_row_id
You'd need to re-execute the stored procedure (and if relevant, create/modify the ignore list table prior to it) for each table you want to examine.
A concern with this approach is the nvarchar length might get exceeded in your case. You'd prob. need to use different datatype, reduce the column name lengths etc. Or break it up into sub-steps and union the results together to get the resultset you're after.
Another concern I have is that this is complete overkill for your particular scenario, where a one-off script-to-query-window will give you the basis of what you need, then some clever text editing in eg Notepad++ will get you all the way there... and hence this problem will likely (and quite reasonably) put you off doing it this way! But it is a good general-case question, and so deserves an answer for anyone interested in future ;-)
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