SQL SP: Dynamic Query - sql

I am trying get a col and value as a function parameter and planning to use them in a query.
Unfortunately, my value for #Col is being treated like a string and not a column identifier.
ie, if I specify name as a value for the #Col parameter, it will be treated like 'name' instead of name and hence, the call to the function always returns NULL as a result
Have you came across similar situations? Could you please recommand me a better way to deal with this situation?
Here is my Function for your reference:
CREATE FUNCTION [dbo].[FN_FindIdBy]
(
#Col NVARCHAR(255),
#Value NVARCHAR(255)
)
RETURNS INT
AS
BEGIN
DECLARE #Id INT = NULL
SET #Id = (SELECT ID FROM dbo.MYWORK WHERE (CONVERT(NVARCHAR(255), #Col) = #Value))
IF #Id IS NOT NULL RETURN #Id
RETURN NULL
END
Thanks a lot for looking into this.

The following works, but you have to use it as a procedure and create dynamic sql.
create table MYWORK (ID int identity, Name nvarchar(255))
insert into MYWORK(Name)
select 'a'
union select 'b'
union select 'c'
union select 'd'
union select 'e'
union select 'f'
CREATE procedure [dbo].[EPG_FN_FindIdBy]
#Col NVARCHAR(255),
#Value NVARCHAR(255)
AS
BEGIN
DECLARE #Id nvarchar(255)
, #ParmDefinition nvarchar(255)
, #sql nvarchar(max)
set #sql = N'SELECT #IdOUT = ID FROM dbo.MYWORK WHERE '+ #Col +' = ''' + #Value + ''''
set #ParmDefinition = N'#IdOUT nvarchar(255) OUTPUT'
PRINT #sql
EXECUTE sp_executesql #sql,#ParmDefinition, #IdOUT = #Id OUTPUT
SELECT #Id as ID
END
Run this and it'll return the matching row
Exec dbo.EPG_FN_FindIdBy #Col = 'Name', #Value = 'a'
And for a NULL
Exec dbo.EPG_FN_FindIdBy #Col = 'Name', #Value = 'g'

Yeah, there's almost always a better way to query than using dynamic SQL.
Check out this usage of the CASE operator.
SELECT id
FROM dbo.MYWORK
WHERE CASE #Col
WHEN 'a' THEN [a]
WHEN 'b' THEN [b]
WHEN 'c' THEN [c]
END = #Value
Where the table has columns [a], [b] and [c].

Related

T-SQL function in Select to Only Return Columns with Values

I have a query that will return only columns with values. How do I add that to a function so I can use that with any query? Would it be a function in the where clause.
create table test1
(
s_no int not null,
name varchar(10) not null,
address varchar(10) null,
emailid varchar(100) null
)
insert into test1 (s_no, name)
values (1,'A'),(2,'B'),(3,'C')
declare #column_list varchar(8000),
#counter int
set #column_list = ''
set #counter = 0
while (Select max(colid) from syscolumns where id = object_id('test1') and isnullable= 0) > #counter
begin
select #counter = min(colid)
from syscolumns
where id = object_id('test1')
and isnullable = 0
and colid > #counter
select #column_list = #column_list + ',' + (Select name from syscolumns where id = object_id('test1') and isnullable= 0 and colid = #counter)
end
select #column_list = SUBSTRING(#column_list, 2, len(#column_list))
declare #sql varchar(8000)
select #sql = 'select ' + #column_list + ' from test1'
print #sql
exec (#sql)
SELECT * FROM [dbo].[test1]
I guess you could make a stored procedure where you provide the table name as parameter, and then build your query like you are doing already
create procedure ShowOnlyFilledColumns (#tablename varchar(100)) as
begin
set nocount on
declare #column_list varchar(8000),
#counter int
set #column_list = ''
set #counter = 0
while (Select max(colid) from syscolumns where id = object_id(#tablename) and isnullable= 0) > #counter
begin
select #counter = min(colid) from syscolumns where id = object_id(#tablename) and isnullable= 0 and colid > #counter
select #column_list = #column_list + ',' + (Select name from syscolumns where id = object_id(#tablename) and isnullable= 0 and colid = #counter)
end
select #column_list = SUBSTRING(#column_list,2,len(#column_list))
declare #sql varchar(8000)
select #sql = 'select ' + #column_list + ' from ' + #tablename
--print #sql
exec (#sql)
end
and use it like this
exec ShowOnlyFilledColumns 'test1'
See the complete example in this DBFiddle
EDIT: The OP asked how he can add joins on this
There are a few tricks to join with a stored procedure, for example in these answers
However, this won't work on this solution, because it requires to create a temp table to store the result of the procedure.
The trick looks like this
-- create a temporary table to store the results of the procedure
CREATE TABLE #Temp (
s_no int not null,
name varchar(10) not null,
address varchar(10) null,
emailid varchar(100) null
)
-- call the procedure and store the result in the temporary table
INSERT INTO #Temp
exec ShowOnlyFilledColumns 'test1'
-- now I can query the temp table, and join on it and write a where clause, and I can do whatever I want
select * from #Temp
Now, this won't work in this case, because the stored procedure can return different columns every time you run it, and to make the insert into #Temp exec ShowOnlyFilledColumns 'test1' work, the table #Temp must have the same number and type of columns as the procedure returns. And you just don't know that.

Stored procedure table-valued variable without aliases in query string must declare scalar variable

I will pass a table-valued input parameter into a stored procedure, and also a variable that contains query string, so I made my sproc like this.
CREATE PROCEDURE [dbo].[SP_SelectData_View]
(
#Sort VARCHAR(MAX),
#CONDITION VARCHAR(MAX) = ''
#Values dbo.FlowStatus READONLY
)
AS
BEGIN
DECLARE #STRQUERY NVARCHAR(MAX)
IF #CONDITION IS NOT NULL AND #CONDITION != ''
BEGIN
SET #CONDITION = 'WHERE ' + #CONDITION
END
ELSE
BEGIN
SET #CONDITION = ''
END
IF #Sort IS NULL OR #Sort = ''
BEGIN
SET #Sort = 'Id Desc'
END
BEGIN
SET #STRQUERY = 'SELECT A.*
FROM ' + #Values + ' as FlowStatus'
JOIN Tbl_A as A
ON A.status = FlowStatus.StatusNowId AND B.flow = FlowStatus.FlowNowId
' + #CONDITION + '
Order By ' + #Sort
EXEC(#STRQUERY)
END
END
But in the code above, I got an error
must declare scalar variable #Values
I've searched for it and I think it is because the aliases is not detected because it's inside a string. But if I didn't put it in a string query, the #condition and #sort variable will be error. Is there a solution where I can do both calling the table-valued variable and query string variable together?
There are several things wrong with the approach you currently have, as I and others have commented, Brent Ozar has a good reference on dynamic SQL https://www.brentozar.com/sql/dynamic/
I would say don't pass in some SQL, construct it in the stored proc; passing in parameters such as name which is used in the where, hence I have put a full working example. This also shows how to pass the user defined table type into the stored proc and then also pass it into the dynamic SQL.
I hope this is a good enough example of the techniques, I had a bit of time so thought I would try and help as much as possible :)
/*
--------------------------------------------
Create a test table to run the stored proc against
*/
IF (NOT EXISTS (SELECT * FROM INFORMATION_SCHEMA.TABLES WHERE TABLE_SCHEMA = 'dbo' AND TABLE_NAME = 'MyTestTable'))
BEGIN
PRINT 'Creating table MyTestTable'
CREATE TABLE [dbo].[MyTestTable](
Id BIGINT NOT NULL IDENTITY(1,1) PRIMARY KEY,
[Name] NVARCHAR(50) NOT NULL
)
INSERT INTO dbo.MyTestTable ([Name])
VALUES ('Andrew'),
('Bob'),
('john')
-- SELECT * FROM MyTestTable
END
GO
/*
--------------------------------------------
Create the table type that we pass into the store proc
*/
IF NOT EXISTS (SELECT * FROM sys.types WHERE is_table_type = 1 AND name = 'FlowStatus')
BEGIN
PRINT 'Creating type [dbo].[FlowStatus]'
CREATE TYPE [dbo].FlowStatus AS TABLE (
MyId BIGINT PRIMARY KEY,
SomeText NVARCHAR(200)
)
END
GO
/*
--------------------------------------------
Create the stored proc with the User Defined table type
*/
CREATE OR ALTER PROCEDURE [dbo].[MyStoredProc]
(
#SortBy VARCHAR(50),
#SearchName VARCHAR(50),
#Values dbo.FlowStatus READONLY
)
AS
BEGIN
-- As your SQL gets more complex it is an idea to create seperate parts of the SQL
DECLARE #SqlToExecute NVARCHAR(MAX)
-- The primary data you want to get
SET #SqlToExecute = N'
SELECT T.Id, T.[Name], V.SomeText
FROM MyTestTable AS T
LEFT JOIN #Values AS V ON V.MyId = T.Id
WHERE 1 = 1' -- We do this so that we can have many AND statements which could be expanded upon
IF #SearchName IS NOT NULL
BEGIN
SET #SqlToExecute = #SqlToExecute + N'
AND T.[Name] LIKE ''%' + #SearchName + ''''
END
IF #SortBy IS NOT NULL
BEGIN
SET #SqlToExecute = #SqlToExecute + N'
ORDER BY ' +
CASE WHEN #SortBy LIKE 'Name%' THEN N'T.[Name]'
ELSE N'T.[Id]'
END
END
-- Print out the script that will be run, useful for debugging you code
PRINT #SqlToExecute
EXEC sp_executesql #SqlToExecute,
N'#Values dbo.FlowStatus READONLY', #Values
END
GO
/*
--------------------------------------------
Now lets test it
-- Test Andrew
*/
DECLARE #flowStatusType AS dbo.FlowStatus
INSERT INTO #flowStatusType(MyId, SomeText)
VALUES(1, 'Test1'),
(2, 'Test2')
EXEC [dbo].[MyStoredProc] #SearchName = 'Andrew', #SortBy = 'Name', #Values = #flowStatusType
GO
-- Test Bob
DECLARE #flowStatusType AS dbo.FlowStatus
INSERT INTO #flowStatusType(MyId, SomeText)
VALUES(1, 'Test1'),
(2, 'Test2')
EXEC [dbo].[MyStoredProc] #SearchName = 'Bob', #SortBy = 'Name', #Values = #flowStatusType
GO
Its also worth noting that if you can just join on the #Values without needing dynamic SQL then that is sure to be less work.

Convert string from column to parameter in dynamic SQL?

I created a dictionary table with 'Condition' column where I store conditions for particular customers. Table's name is CustomerConditions:
Then I created dynamic SQL where I want to use this Condition:
declare
#TableName as nvarchar(10),
#FieldName as nvarchar(20),
#CustName as Nvarchar(50),
#Condition as NVARCHAR(MAX)
set #Condition = (SELECT o.Condition FROM CustomerConditions o WHERE o.Group = #CustName)
declare #strSQL as NVARCHAR(MAX)
SET #strSQL =
'DECLARE #FieldName as nvarchar(20),
#CustName as nvarchar(50)
;WITH NewCTE AS (
SELECT Tab1.Group, '+#FieldName+'
FROM (
SELECT
'+#Condition+' AS Group,
'+#FieldName+'
FROM '+#TableName+' as c) as Tab1)
SELECT * FROM NewCTE'
EXEC(#strSQL)
The problem is that when I pass #Condition to dynamic SQL string from column 'Condition' does not become part of SQL syntax - it's passed as expression, in the result I got:
This is not what I want. I want this 'Case WHEN...' to become part of my SQL syntax.
On the other hand when I'm not using #Condition but pass 'CASE WHEN..' explicitly then everything works well (all declared parameters works well, all is good):
;WITH NewCTE AS (
SELECT Tab1.Group, '+#FieldName+'
FROM (
SELECT
CASE WHEN '+ #FieldName +' LIKE ''XY%'' OR '+ #FieldName +' LIKE ''XYZ%'' then '''+ #CustName +''' END AS Group,
'+#FieldName+'
FROM '+#TableName+' as c) as Tab1)
So how can I pass this 'Case when' condition into dynamic SQL using parameter?
While agreeing with the issue of dynamic prone to injection attacks, and assuming some random values for your non initialized variables, here is how I approach
The trick is
SELECT #Condition = REPLACE(#Condition, '#FieldName', #FieldName )
SELECT #Condition = REPLACE(#Condition, '#CustName', #CustName )
So the main Query will be
declare
#TableName as nvarchar(10) = 'MyTbl',
#FieldName as nvarchar(20) = 'FldName',
#CustName as Nvarchar(50) = 'C1' ,
#Condition as NVARCHAR(MAX)
SELECT #Condition = (SELECT o.Condition FROM CustomerConditions o WHERE o.[Group] = #CustName)
SELECT #Condition = REPLACE(#Condition, '#FieldName', #FieldName )
SELECT #Condition = REPLACE(#Condition, '#CustName', #CustName )
declare #strSQL as NVARCHAR(MAX)
SET #strSQL = 'DECLARE #FieldName as nvarchar(20),
#CustName as nvarchar(50)
;WITH NewCTE AS (
SELECT Tab1.Group, '+#FieldName+'
FROM (
SELECT
'+#Condition+'
'+#FieldName+'
FROM '+#TableName+' as c) as Tab1)
SELECT * FROM NewCTE'
Select(#strSQL)
First provide proper values to your variables and execute this.
Instead of executing the query, what I did was create the query to see whether it got created as you want.
To be certain, you can copy paste the outcome and execute that.
Then you can change the last line to Execute ...

How to store a dynamic SQL result in a variable in T-SQL?

I have a stored procedure which takes 'table name' as parameter. I want to store my 'exec' results to a variable and display using that variable.
Here is my T-SQL stored procedure..
create procedure DisplayTable( #tab varchar(30))
as
begin
Declare #Query VARCHAR(30)
set #Query='select * from ' +#tab
EXEC (#Query)
END
I want to do something like this..
SET #QueryResult = EXEC (#Query)
select #QueryResult
How do i achieve this.. Please help.. I am a beginner..
You can use XML for that. Just add e.g. "FOR XML AUTO" at the end of your SELECT. It's not tabular format, but at least it fulfills your requirement, and allows you to query and even update the result. XML support in SQL Server is very strong, just make yourself acquainted with the topic. You can start here: http://technet.microsoft.com/en-us/library/ms178107.aspx
alter procedure DisplayTable(
#tab varchar(30)
,#query varchar(max) output
)
as
BEGIN
Declare #execution varchar(max) = 'select * from ' +#tab
declare #tempStructure as table (
pk_id int identity
,ColumnName varchar(max)
,ColumnDataType varchar(max)
)
insert into
#tempStructure
select
COLUMN_NAME
,DATA_TYPE
from
INFORMATION_SCHEMA.columns
where TABLE_NAME= #tab
EXEC(#execution)
declare #ColumnCount int = (SELECT count(*) from #tempStructure)
declare #counter int = 1
while #counter <= #ColumnCount
BEGIN
IF #counter = 1
BEGIN
set #query = (SELECT ColumnName + ' ' + ColumnDataType FROM #tempStructure where pk_id= #counter)
END
IF #counter <> 1
BEGIN
set #query = #query + (SELECT ',' + ColumnName + ' ' + ColumnDataType FROM #tempStructure where #counter = pk_id)
END
set #counter = #counter + 1
END
END
When you execute the SP, you'll now get a return of the structure of the table you want.
This should hopefully get you moving.
If you want the table CONTENTS included, create yourself a loop for the entries, and append them to the #query parameter.
Remember to delimit the #query, else when you read it later on, you will not be able to restructure your table.
First of all you have to understand that you can't just store the value of a SELECTon a table in simple variable. It has to be TABLE variable which can store the value of a SELECTquery.
Try the below:
select 'name1' name, 12 age
into MyTable
union select 'name2', 15 union
select 'name3', 19
--declaring the table variable and selecting out of it..
declare #QueryResult table(name varchar(30), age int)
insert #QueryResult exec DisplayTable 'MyTable'
select * from #QueryResult
Hope this helps!

Parametrize the WHERE clause?

I'm need to write a stored procedure for SQL Server 2008 for performing a large select query and I need it to filter results with specifying filtering type via procedure's parameters. I found some solutions like this:
create table Foo(
id bigint, code char, name nvarchar(max))
go
insert into Foo values
(1,'a','aaa'),
(2,'b','bbb'),
(3,'c','ccc')
go
create procedure Bar
#FilterType nvarchar(max),
#FilterValue nvarchar(max) as
begin
select * from Foo as f
where case #FilterType
when 'by_id' then f.id
when 'by_code' then f.code
when 'by_name' then f.name end
=
case #FilterType
when 'by_id' then cast(#FilterValue as bigint)
when 'by_code' then cast(#FilterValue as char)
when 'by_name' then #FilterValue end
end
go
exec Bar 'by_id', '1';
exec Bar 'by_code', 'b';
exec Bar 'by_name', 'ccc';
I'm finding that this approach doesn't work. It's possible to cast all the columns to nvarchar(max) and compare them as strings, but I think it will cause performance degradation.
Is it possible to parametrize the where clause in stored procedure without using constructs like EXEC sp_executesql?
This may become a little more long winded, for large filter requirements, but I think it probably more performant/easier to read/maintain:
create procedure Bar
#Id int,
#Code nvarchar,
#name nvarchar
begin
select * from Foo as f
where (#id = -1 or f.ID = #id)
and (#Code = '' or f.Code = #Code)
and (#Name = '' or f.Name = #Name)
end
go
exec Bar 1, '', ''
exec Bar -1, 'code', ''
exec Bar -1, '', 'name'
This also allows you to filter by more than one item at the same time.
Try this
create procedure Bar
#FilterType nvarchar(max),
#FilterValue nvarchar(max) as
begin
select * from Foo as f
where
(#FilterType ='by_id' and f.id =cast(#FilterValue as bigint) )
OR
(#FilterType ='by_code' and f.code =cast(#FilterValue as char)
OR
(#FilterType ='by_name' and f.name =#FilterValue
end
go
declare #field varchar(20)
declare #value varchar(100)
set #field = 'customerid'
set #value = 'ALFKI'
set #field = 'Country'
set #value = 'Germany'
set #field = 'ContactTitle'
set #value = 'Owner'
select * from customers
where (customerid = (case when #field = 'customerid' then #value else customerid end)
and ContactTitle = (case when #field = 'ContactTitle' then #value else ContactTitle end)
and country = (case when #field = 'Country' then #value else country end))
Example is adapted for Northwind database.
Hope this helps.
EDIT: Comment any 2 out of the 3 values for #field and #value for above.
If you don't have many filtering criteria, you could consider creating delegate functions and dispatching the request to the appropriate one. E.g.,
create procedure Bar
#FilterType nvarchar(max),
#FilterValue nvarchar(max) as
begin
case #FilterType
when 'by_id' then FilterById(#FilterValue)
when 'by_code' then FilterByCode(#FilterValue)
when 'by_name' then FilterByName(#FilterValue)
end
end
go