I have a stored procedure which has several kind of parameters, INT, VARCHAR, DateTime, etc... This sp inserts a record into a log table with the parameters passed in. There are differents log tables, three exactly, which are called for example, LogTbl1, LogTbl2 and LogTbl3. This sp writes to LogTbl1, LogTbl2 or LogTbl3 depending on a sp parameter that indicates where to write to. I have set this parameter as tinyint and this takes as values 0, 1 or 2. Then depending on the value passed in, I build a dynamic query to write to the appropiate Log Table as below:
CREATE PROCEDURE [dbo].[spTraceLog]
#LogId int,
#param2 int,
#param3 int,
#param4 varchar(100),
#DateSent datetime,
#TargetTable tinyint = 0
AS
BEGIN
DECLARE #sqlCommand nvarchar(max)
DECLARE #tblName nvarchar(100)
SET #tblName = CASE #TargetTable
WHEN 0 THEN '[dbo].[LogTbl1]'
WHEN 1 THEN '[dbo].[LogTbl2]'
WHEN 2 THEN '[dbo].[LogTbl3]'
ELSE ''
END
IF #tblName <> ''
BEGIN
SET #sqlCommand =
'INSERT INTO ' + #tblName +
'([LogId]' +
',[param2]' +
',[param3]' +
',[param4]' +
',[Date]) ' +
'VALUES' +
'(#LogId' +
',#param2' +
',#param3' +
',#param4' +
',#DateSent)'
EXECUTE sp_executesql #sqlCommand
END
END
So is there any other better elegant way to do it? Not possible using an enumeration in the sp parameter #TargetTable?
Building up dynamic SQL then executing it means that Sql Server can't do a very good job of building an execution plan. This may impact efficiency and will matter most if the SP is called very frequently, which it looks like this SP will be.
Although the code would be less elegant I think it would be more efficient if your code had an if #TargetTable statement which contained three separate inserts which are all identical except for the table name you're inserting into.
But that doesn't answer the question. There is no enum and I don't think there is a problem with the type you've used to identify the log you want to write to. If you want the code to be more readable you could split it into three SPs and call them spTraceLog1, spTraceLog2 etc. and not pass in the TargetTable. I would avoid the dynamic SQL if possible.
Related
As described in title, I am trying to systematically get stored procedure pararameter names and their corresponding values inside the execution of the proper stored procedure.
First point, which is taking stored procedure parameter names, is easy using table [sys].[all_parameters] and the stored procedure name. However, getting the actual values of these parameters is the difficult part, specially when you are not allowed to use table [sys].[dm_exec_input_buffer] (as a developer, I am not allowed to read this table, since it is a system administrator table).
Here is the code I have so far, which I am sure can serve you as a template:
CREATE PROCEDURE [dbo].[get_proc_params_demo]
(
#number1 int,
#string1 varchar(50),
#calendar datetime,
#number2 int,
#string2 nvarchar(max)
)
AS
BEGIN
DECLARE #sql NVARCHAR(MAX);
DECLARE #ParameterNames NVARCHAR(MAX) = ( SELECT STRING_AGG([Name], ',') FROM [sys].[all_parameters] WHERE OBJECT_ID = OBJECT_ID('[dbo].[get_proc_params_demo]') )
SET #sql = N'SELECT ' + #ParameterNames;
DECLARE GetParameterValues CURSOR FOR
SELECT DISTINCT [Name] FROM [sys].[all_parameters] WHERE OBJECT_ID = OBJECT_ID('[dbo].[get_proc_params_demo]');
OPEN GetParameterValues;
DECLARE #param_values NVARCHAR(MAX) = NULL
DECLARE #StoredProcedureParameter NVARCHAR(MAX)
FETCH NEXT FROM GetParameterValues INTO #StoredProcedureParameter;
WHILE ##FETCH_STATUS = 0
BEGIN
SET #param_values = 'ISNULL('+#param_values+','')'+#StoredProcedureParameter+','
EXEC(#param_values)
FETCH NEXT FROM GetParameterValues INTO #StoredProcedureParameter;
END;
CLOSE GetParameterValues;
DEALLOCATE GetParameterValues;
SET #param_values = LEFT(#param_values, LEN(#param_values) - 1)
EXEC sp_executesql #sql,#ParameterNames,#param_values;
END
EXEC [dbo].[get_proc_params_demo]
#number1=42,
#string1='is the answer',
#calendar='2019-06-19',
#number2=123456789,
#string2='another string'
This is my approach trying to dynamically get parameter actual values inside a cursor, but it does not work, and I am clueless so far. I know it is quite rudimentary, and I am happy to hear other approaches. To be fair, I don't know if this problem is even possible to solve without system tables, but it would be great.
EDIT: This is an attempt to get a generic code that works on any stored procedure. You do not want to hardcode any parameter name. The only input you have is the stored procedure name via OBJECT_NAME(##PROCID)
I'm trying to pass user-inputted data to a SQL 'sp_executesql' (Dynamic SQL) statement in order to build a string for the 'SELECT','FROM', and 'WHERE' statements.
I know that SQL Server won't accept a table name or a column name as a parameter. However I was wondering if it was possible to take user-inputted values, store them in a locaL-SQL variable and then use the local variable in the 'FROM' clause?
I know this code would work:
set #tableName = 'SalesData'
set #monthNo = 2
set #sql = N'
select SalesPerson
from ' + #tableName + '
where mon = #monthNo'
exec sp_executesql #sql, N'#monthNo int', #monthNo
But, would this code run?
set #tableName = #ValueTypedByUser
set #monthNo = 2
set #sql = N'
select SalesPerson
from ' + #tableName + '
where mon = #monthNo'
exec sp_executesql #sql, N'#monthNo int', #monthNo
How many tables are you possibly dealing with? If it's a small number you have two better choices:
Use multiple stored procedures instead of one, and call them based on the table you need. You can use a parameter in your calling routine to indicate which SP you want.
Use a parameter to specify the table you want, but then, instead of using a variable to change the table name in your SP, use the following conditionals:
IF #table = 'SalesPersonTable'
BEGIN
SELECT SalesPerson
FROM SalesPersonTable
WHERE mon = #monthNo
END
IF #table = 'OtherTable'
BEGIN
SELECT SalesPerson
FROM OtherTable
WHERE mon = #monthNo
END
This avoids the SQL injection issues, but again, only works if the number of tables is "small" (with "small" being what you want it to be!)
I was wondering if I can make a stored procedure that insert values into dynamic table.
I tried
create procedure asd
(#table varchar(10), #id int)
as
begin
insert into #table values (#id)
end
also defining #table as table var
Thanks for your help!
This might work for you.
CREATE PROCEDURE asd
(#table nvarchar(10), #id int)
AS
BEGIN
DECLARE #sql nvarchar(max)
SET #sql = 'INSERT INTO ' + #table + ' (id) VALUES (' + CAST(#id AS nvarchar(max)) + ')'
EXEC sp_executesql #sql
END
See more here: http://msdn.microsoft.com/de-de/library/ms188001.aspx
Yes, to implement this directly, you need dynamic SQL, as others have suggested. However, I would also agree with the comment by #Tomalak that attempts at universality of this kind might result in less secure or less efficient (or both) code.
If you feel that you must have this level of dynamicity, you could try the following approach, which, although requiring more effort than plain dynamic SQL, is almost the same as the latter but without the just mentioned drawbacks.
The idea is first to create all the necessary insert procedures, one for every table in which you want to insert this many values of this kind (i.e., as per your example, exactly one int value). It is crucial to name those procedures uniformly, for instance using this template: TablenameInsert where Tablename is the target table's name.
Next, create this universal insert procedure of yours as follows:
CREATE PROCEDURE InsertIntValue (
#TableName sysname,
#Value int
)
AS
BEGIN
DECLARE #SPName sysname;
SET #SPName = #TableName + 'Insert';
EXECUTE #SPName #Value;
END;
As can be seen from the manual, when invoking a module with the EXECUTE command, you can specify a variable instead of the actual module name. The variable in this case should be of a string type and is supposed to contain the name of the module to execute. This is not dynamic SQL, because the syntax is not the same. (For this to be dynamic SQL, the variable would need to be enclosed in brackets.) Instead, this is essentially parametrising of the module name, probably the only kind of natively supported name parametrisation in (Transact-)SQL.
Like I said, this requires more effort than dynamic SQL, because you still have to create all the many stored procedures that this universal SP should be able to invoke. Nevertheless, as a result, you get code that is both secure (the #SPName variable is viewed by the server only as a name, not as an arbitrary snippet of SQL) and efficient (the actual stored procedure being invoked already exists, i.e. it is already compiled and has a query plan).
You'll need to use Dynamic SQL.
To create Dynamic SQL, you need to build up the query as a string. Using IF statements and other logic to add your variables, etc.
Declare a text variable and use this to concatenate together your desired SQL.
You can then execute this code using the EXEC command
Example:
DECLARE #SQL VARCHAR(100)
DECLARE #TableOne VARCHAR(20) = 'TableOne'
DECLARE #TableTwo VARCHAR(20) = 'TableTwo'
DECLARE #SomeInt INT
SET #SQL = 'INSERT INTO '
IF (#SomeInt = 1)
SET #SQL = #SQL + #TableOne
IF (#SomeInt = 2)
SET #SQL = #SQL + #TableTwo
SET #SQL = #SQL + ' VALUES....etc'
EXEC (#SQL)
However, something you should really watch out for when using this method is a security problem called SQL Injection.
You can read up on that here.
One way to guard against SQL injection is to validate against it in your code before passing the variables to SQL-Server.
An alternative way (or probably best used in conjecture) is instead of using the EXEC command, use a built-in stored procedure called sp_executesql.
Details can be found here and usage description is here.
You'll have to build your SQL slightly differently and pass your parameters to the stored procedure as arguments as well as the #SQL.
I am attempting to make a stored procedure that uses sp_executesql. I have looked long and hard here, but I cannot see what I am doing incorrectly in my code. I'm new to stored procedures/sql server functions in general so I'm guessing I'm missing something simple. The stored procedure alter happens fine, but when I try run it I'm getting an error.
The error says.
Msg 1087, Level 15, State 2, Line 3
Must declare the table variable "#atableName"
The procedure looks like this.
set ANSI_NULLS ON
set QUOTED_IDENTIFIER ON
go
ALTER PROCEDURE [dbo].[sp_TEST]
#tableName varchar(50),
#tableIDField varchar(50),
#tableValueField varchar(50)
AS
BEGIN
SET NOCOUNT ON;
DECLARE #SQLString nvarchar(500);
SET #SQLString = N'SELECT DISTINCT #aTableIDField FROM #atableName';
EXEC sp_executesql #SQLString,
N'#atableName varchar(50),
#atableIDField varchar(50),
#atableValueField varchar(50)',
#atableName = #tableName,
#atableIDField = #tableIDField,
#atableValueField = #tableValueField;
END
And I'm trying to call it with something like this.
EXECUTE sp_TEST 'PERSON', 'PERSON.ID', 'PERSON.VALUE'
This example isn't adding anything special, but I have a large number of views that have similar code. If I could get this stored procedure working I could get a lot of repeated code shrunk down considerably.
Thanks for your help.
Edit: I am attempting to do this for easier maintainability purposes. I have multiple views that basically have the same exact sql except the table name is different. Data is brought to the SQL server instance for reporting purposes. When I have a table containing multiple rows per person id, each containing a value, I often need them in a single cell for the users.
You can not parameterise a table name, so it will fail with #atableName
You need to concatenate the first bit with atableName, which kind defeats the purpose fo using sp_executesql
This would work but is not advisable unless you are just trying to learn and experiment.
ALTER PROCEDURE [dbo].[sp_TEST]
#tableName varchar(50),
#tableIDField varchar(50),
#tableValueField varchar(50)
AS
BEGIN
SET NOCOUNT ON;
DECLARE #SQLString nvarchar(500);
SET #SQLString = N'SELECT DISTINCT ' + quotename(#TableIDField) + ' FROM ' + quotename(#tableName);
EXEC sp_executesql #SQLString;
END
Read The Curse and Blessings of Dynamic SQL
You cannot use variables to pass table names and column names to a dynamic query as parameters. Had that been possible, we wouldn't actually have used dynamic queries for that!
Instead you should use the variables to construct the dynamic query. Like this:
SET #SQLString = N'SELECT DISTINCT ' + QUOTENAME(#TableIDField) +
' FROM ' + QUOTENAME(#TableName);
Parameters are used to pass values, typically for use in filter conditions.
Create Procedure [dbo].[spGenerateID]
(
#sFieldName NVARCHAR(100),
#sTableName NVARCHAR(100)
)
AS
BEGIN
SELECT ISNULL(MAX(ISNULL(#sFieldName, 0)), 0) + 1 FROM #sTableName
END
In the above procedure I supply the field name and table name and I want the max number of this field .Why this not work?I also want to check if those fields are null than it's not work.. This procedure must have a return parameter of the field that I supplied which contain the max number.Please help me to fixed it.
Why does this not work.
How to check input parameter are not null.
How to set output parameter
You can't have field names and table names as parameters without wrapping the entire SELECT statement in an EXEC statement:
EXEC ('select isnull(max(isnull([' + #sFieldName + '],0)),0)+1
from [' + #sTableName + '] ')
You cannot supply the tablename and fieldname as parameters to a stored procedure.
You need to create a dynamic query and execute using sp_executesql.
You should read The Curse and Blessings of Dynamic SQL
If this is always to be used for identity columns you can use a variable
SELECT ISNULL(IDENT_CURRENT(#sTableName),0)+1
Otherwise you need to use dynamic SQL (The usual caveats about SQL injection apply.)
Additionally I'm somewhat dubious about the reasons behind this anyway unless you don't have any concurrency to worry about.
I've changed the type of your parameters to sysname as this is more appropriate.
CREATE PROCEDURE [dbo].[spGenerateID]
(
#sFieldName sysname,
#sTableName sysname,
#id int output
)
AS
BEGIN
DECLARE #dynsql NVARCHAR(1000)
SET #dynsql = 'select #id =isnull(max([' + #sFieldName + ']),0)+1 from [' + #sTableName + '];'
EXEC sp_executesql #dynsql, N'#id int output', #id OUTPUT
END
Example Usage
DECLARE #id int
EXECUTE [dbo].[spGenerateID]
'id'
,'MYTABLE'
,#id OUTPUT
SELECT #id
1) This won't work because of the way the table name was passed.
2) You only have to check for ISNULL one time, you have a redundant number of calls there.
3) You need not necessarily declare an output, just catch the return value when you execute the stored procedure.
If you're trying to generate a unique Id this is not the best way to do it because you could run into race conditions and generate a duplicate ID for one of the calls. Ideally the ID is already declared as an IDENTITY column, but if you can't do it that way then it's better to create a special table that just returns an ID as an IDENTITY column. Then you can access that table to get the latest version with assurance that you will get a unique ID.
Here is how your stored procedure could work without the redundant IsNull().
Create Procedure [dbo].[spGenerateID]
#sFieldName NVARCHAR(100),
#sTableName NVARCHAR(100)
AS
BEGIN
Exec ( 'SELECT max(isnull(' + #sFieldName + ',0))+1 FROM ' + #sTableName)
END