How to dynamically specify a column in stored procedure - sql

I need to write a stored procedure which will update into a particular column, based on parameters. This will be called from a C# app.
The table to be updated is like this:
TableA: Id, Col1, Col2, Col3.
Something like this:
Create Procedure UpdateByCol
#col_num int,
#value string,
#id int
as
Begin
Update TableA Set *How do I specify the column here* = #value where Id = #id
End

Declare #SQL varchar(max)
Set #SQL = 'Update ' + #TableName + ' Set ' + #Col1 + ' = ' + #Col1Val....
Exec (#SQL)
This exposes you to SQL injection though...

There is a way to do this without dynamic sql. Create individual stored procs for each column that is eligible for updating. Then, in your master stored proc, call the applicable stored proc based on the paramter received.

If you really want to do this it can be done with dynamic sql. http://technet.microsoft.com/en-us/library/ms188001.aspx

It is not good practice to do this but if you really wanted to you'd have to use dynamic sql to create the string and execute it like so:
Create Procedure UpdateByCol
#col_num int,
#value string,
#id int
as
Begin
declare #str varchar(2000)
set #str = 'Update TableA Set Col' + convert(varchar,#col) + ' = ''' + #value + ''' where Id = ' + convert(varchar, #id )
exec ( #str )
End
This is a very bad thing to do because it creates a security hole.

Related

Does sp_executesql support multiple values in one parameter and return multiple records?

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

dynamic SQL update command

I am using MSSQL 2016,
I need to be able to update a row on a table dynamically.
I got a stored procedure :
CREATE PROCEDURE sp_lookupData_UpdatelookupValues
(
#FullTableName nvarchar(50),
#Id nvarchar(10),
#Name nvarchar(50),
#Description nvarchar(50)
)
AS
BEGIN
DECLARE #Cmd nvarchar(150) = N'UPDATE ' + #FullTableName + ' SET Name = ' + #Name + ', Description = ' + #Description + ' WHERE ID = ' + #Id + '';
EXECUTE sp_executesql #Cmd;
END
The problem is that Name and Description values are passed into the #Cmd like this :
UPDATE TABLE_NAME SET Name = Private, Description = Default WHERE ID = 1
Instead of 'Private' and 'Default'.
The result is an error where Private is being counted as a column which doesnt exist ( because of the bad format ).
Invalid column name 'Private'.
Put the quotes yourself
Use single quotes around Private and Default.
And since you are using dynamic querying, you have to double the single quotes to escape them.
DECLARE #Cmd nvarchar(150) = N'UPDATE ' + #FullTableName + ' SET Name = ''' + #Name + ''', Description = ''' + #Description + ''' WHERE ID = ' + #Id + '';
Also make sure you try the next solution, since the first one is SQL Injection compatible.
Use sp_executesql parameters
You can also use the parameters inside your #Cmd without doing the concatenation yourself but by passing the parameters to sp_executesql
Also I suggest you to QUOTENAME the #FullTableName parameter in case of spaces inside table's name.
DECLARE #Cmd nvarchar(150) = N'UPDATE QUOTENAME(#FullTableName) SET Name = #Name, Description = #Description WHERE ID = #Id;'
EXEC sp_executesql #Cmd, #FullTableName, #Name, #Description, #Id;
The advantage doing so, is you avoid any parameters not checked by the application to be able to do SQL Injection.
Reference :
https://learn.microsoft.com/en-us/sql/relational-databases/system-stored-procedures/sp-executesql-transact-sql
You have to add the quotes yourself. I prefer to use QUOTENAME to keep all the quotes recognizable:
QUOTENAME(#FullTableName, '''')
You cannot use parameters for identifiers. But, you can use parameters for values:
DECLARE #Cmd nvarchar(150) = N'
UPDATE ' + #FullTableName + '
SET Name = #Name,
Description = #Description
WHERE ID = #Id';
EXECUTE sp_executesql #Cmd,
N'#Name nvarchar(50), #Description nvarchar(50), #Id nvarchar(10)',
#Name = #Name, #Description = #Description, #Id = #id;
Unfortunately, the dynamic table name still poses risks, both in terms of SQL injection and syntax errors. I am guessing this is "controlled" code, not code with user input, so the risks might be acceptable. However, you probably should use quotename().
That brings up another issue which is probably the crux of the problem. Why do you have multiple tables with the same columns? Could these -- should these -- all be stored in a single table? This type of code calls into question aspects of the data model.

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.

Don't display dynamic query in result

Is it possible to hide a dynamic query from the result sets provided from a Stored Procedure?
I am using the ##rowcount of the dynamic query to set a variable that is used to determine whether another query runs or not.
The other query is used by code that I cannot change - hence why I am changing the Stored Procedure. The dynamic query returns as the first result set from the Stored Procedure is now the result of the dynamic query which currently is "breaking" the calling code.
Thanks in advance
I have managed to solve this by inserting the result of the dynamic query into a temporary table and then retrieving the rowcount from the temporary table.
-- Create query
declare #query nvarchar(max)
set #query = 'select ' + #entityname + 'id from ' + #entityname + ' where ' + #entityname + 'id = ' + cast(#entityid as nvarchar(100))
-- Insert into to temp table - no new result set displayed!
declare #tbl table (EntityID int not null primary key)
insert into #tbl
exec (#query)
-- Retrieve variable from temporary table
declare #count int
select #count = count(*) from #tbl
Above is the code I ended up using.
Try wrapping the dynamic query like this:
Set #query = 'SELECT COUNT(*) FROM (' + #Query + ') t'

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