Execute SQL arithmetic stored as string (stored procedure? c#?) - c#

I'm trying to figure out the best way of doing this....SQL SERVER stored procedure? also contemplating doing it in c#, but either way I'm sortof at a stand still.
Basically what I have is a parts table and a column with qty. This qty column could be a fixed number but it could also be dependent on other parameters. (length, width, size etc...). This was originally a very basic project and I cheated with a few if statements, however an increasing number of parts have a calculated quantity. I want to be able to execute a function stored as a string when a certain part should be selected.
so then based on the parts needed, a table would be created with part and its corresponding numeric quantity.
I was reading into sp_executesql, EXEC, but they still aren't making sense (havent found an comparable example)
Table:
PART QTY
==========
X 'CASE WHEN #FinWidth >=124 THEN ROUND(1.5 + (#FinHeight-#FinWidth)/2.2,0) ELSE 10 END'
Y '2'
Query:
DECLARE #sqlCommand nvarchar(1000)
DECLARE #qty decimal(18,3)
DECLARE #finHeight decimal(18,3)
DECLARE #finWidth decimal(18,3)
DECLARE #part varchar(80)
SET #finHeight = 120
SET #finWidth = 100
sp_executesql....something??
EXEC(something)??

Something like this can get you to use string from table and calculate it. Since it's dynamic SQL you can't use it as function, so I am not sure how useful it can be... you can maybe try with stored procedure:
DECLARE #sqlCommand nvarchar(MAX)
DECLARE #finHeight decimal(18,3)
DECLARE #finWidth decimal(18,3)
DECLARE #part varchar(80)
SET #part = 'X'
SET #finHeight = 124
SET #finWidth = 400
SELECT #sqlCommand= 'SELECT ' + QTY FROM dbo.Table1 WHERE PART = #part
SET #sqlCommand = REPLACE(#sqlCommand, '#finHeight', #finHeight)
SET #sqlCommand = REPLACE(#sqlCommand, '#finWidth', #finWidth)
EXEC (#sqlCommand)
SQLFiddle DEMO

I would create a function to return quantity based on the business logic related to misc fields. Example function would be
create dbo.fn_GetQuantity(#Qty int, #width decimal(18, 3), #Height decimal(18,3))
returns int
as
begin
-- TODO Apply all business logic related to #width & #height here and calculate new #Qty
return #Qty
end
then in the proc I would just call this new function with needed parameters.
create proc dbo.sGetParts()
as
begin
select Part, dbo.fn_GetQuantity(Qty, finWidth, finWidth)
from parts
end

Related

Stored procedure with parameters to return specific multiple columns, not all (*)

Currently, I have a stored procedure that returns all the columns (Select *). I am using this stored procedure to retrieve data from multiple tables, each table with different number of columns and obviously different column names, so Select * is convenient. However, now I need to retrieve only specific columns from each table so I want to pass the parameters something like this:
SELECT #param1, #param2, #param3, etc.
FROM #tableName
WHERE columnName = #tableId
The problem here is the number of parameters to be passed for the columns isn't set, as the table can have any number of columns. Is there a way to use some kind of loop or dynamic assignment so that I can pass any number of parameters as column names?
I know that I can filter out only the columns I want to use, and just leave out the rest, but that doesn't work in my case. I need the stored procedure to NOT return some specific columns with sensitive data.
I am using SQL Server 2008, ASP.NET MVC 4, and C# in my application.
If you are able to modify your stored procedure, you can easily put the required columns definitions as a parameter and use an auto-created temporary table:
CREATE PROCEDURE sp_GetDiffDataExample
#columnsStatement NVARCHAR(MAX) -- required columns statement (e.g. "field1, field2")
AS
BEGIN
DECLARE #query NVARCHAR(MAX)
SET #query = N'SELECT ' + #columnsStatement + N' INTO ##TempTable FROM dbo.TestTable'
EXEC sp_executeSql #query
SELECT * FROM ##TempTable
DROP TABLE ##TempTable
END
In this case you don't need to create a temp table manually - it is created automatically.
Hope this helps.
You can just pass one parameter that is a comma-delimited string of the columns you want to select and build a dynamic sql string.
#sql = 'SELECT ' + #param + ' FROM MyTable...';
EXECUTE (#sql);
If you use a dynamic sql solution, you should take care to guard against sql injection attacks.
You might also consider continuing to get all columns from the stored procedure, and showing only the columns the user wants in the front end.
Although I really do not like the stored procedure approach for this problem (I agree with #Gusman that building the query in C# is a better approach) you can make a stored procedure work without opening yourself to SQL Injection attacks. The following example is one simple way to do this:
Let's say that the table in question has the columns named COL1, COL2, and COL3. The stored procedure would accept a varchar(max) parameter named #IncludeCols and have code like:
SELECT CASE WHEN #IncludeCols LIKE '%#COL1#%' THEN COL1 ELSE '' END AS COL1,
CASE WHEN #IncludeCols LIKE '%#COL2#%' THEN COL2 ELSE '' END AS COL2,
CASE WHEN #IncludeCols LIKE '%#COL3#%' THEN COL3 ELSE '' END AS COL3
FROM <Table name>
WHERE <Where clause>
Yes, a column of every name will be returned, but data will only come from columns whose names are in the parameter. For example, if you wanted COL1 and COL3, the parameter value would be #COL1#COL3#. The # is important and must be on each side of every column name or any LIKE clause could get a false positive match.
If this is to export data to Excel then the best approach is likely to have a stored procedure that returns all of your columns minus the ones that should never be exported (passwords, other protected data, timestamps perhaps) and then filter out any additional unwanted columns in your front end.
You should never be using SELECT * for several reasons. Define what your application needs and program to that.

Multiple inserts in a single stored procedure?

TableName: Information
Stored procedure that inserts data into the above table.
CREATE PROCEDURE sp_insert_information
(
#profileID as int,
#profileName as varchar(8)
#profileDescription as varchar(100)
)
AS
BEGIN
INSERT INTO information(profileid, profilename, profiledescription)
VALUES (#profileID, #profileName, #profileDescription);
END
I call this procedure from .NET, is there a way to do multiple inserts if I pass profileID's as a comma separated parameter? (can I use split function?)
I can either loop through the profileID's and send 1 by 1 to procedure, however my data is going to be the same except different profileID.
Table data (with 3 columns):
1 profileUnavailable User Error
2 profileUnavailable User Error
3 profileUnavailable User Error
4 profileUnavailable User Error
5 profileUnavailable User Error
Any other approaches that I can try to do this in a single shot?
You have a couple options:
SqlBulkInsert - You can create a dataset that you can dump to the table. This is useful for many inserts. This will bypass the procedure altogether.
Table Valued Parameters - You can use a table value parameter as a parameter of the stored procedure, again manipulating data using a dataset.
The CSV Parameter with string split IS an option, but I would recommend one of the above over it.
Nope. That sproc does one insert at a time as it is written presently. You have to invoke it separately.
You might also consider wrapping that up into a transaction so if one fails, all of them won't be committed.
My favourite technique up to some years ago was to have an arsenal of splitting functions, that could split a delimited list of homogeneous values (e.g. all integers, all booleans, all datetimes, etc.) into a table variable. Here is an example of such a function.
CREATE FUNCTION [dbo].[fn_SplitInt](#text varchar(8000),
#delimiter varchar(20) = '|')
RETURNS #Values TABLE
(
pos int IDENTITY PRIMARY KEY,
val INT
)
AS
BEGIN
DECLARE #index int
SET #index = -1
-- while the list is not over...
WHILE (LEN(#text) > 0)
BEGIN
-- search the next delimiter
SET #index = CHARINDEX(#delimiter , #text)
IF (#index = 0) -- if no more delimiters (hence this field is the last one)
BEGIN
IF (LEN(#text) > 0) -- and if this last field is not empty
INSERT INTO #Values VALUES (CAST (#text AS INT)) -- then insert it
ELSE -- otherwise, if this last field is empty
INSERT INTO #Values VALUES (NULL) -- then insert NULL
BREAK -- in both cases exit, since it was the last field
END
ELSE -- otherwise, if there is another delimiter
BEGIN
IF #index>1 -- and this field is not empty
INSERT INTO #Values VALUES (CAST(LEFT(#text, #index - 1) AS INT)) -- then insert it
ELSE -- otherwise, if this last field is empty
INSERT INTO #Values VALUES (NULL) -- then insert NULL
SET #text = RIGHT(#text, (LEN(#text) - #index)) -- in both cases move forward the read pointer,
-- since the list was not over
END
END
RETURN
END
When you have a set of functions like these, then your problem has a solution as simple as this one:
CREATE PROCEDURE sp_insert_information
(
#profileID as varchar(2000),
#profileName as varchar(8)
#profileDescription as varchar(100)
)
AS
BEGIN
DECLARE #T TABLE (Id int)
INSERT INTO #T (Id)
SELECT val FROM dbo.fn_SplitInt(#profileID)
INSERT INTO information(profileid, profilename,profiledescription)
SELECT Id, #profileName, #profileDescription
FROM #T
END
But today it might be quicker to execute, and even require less coding, to generate an XML representation of the data to insert, then pass the XML to the stored procedure and have it INSERT INTO table SELECT FROM xml, if you know what I mean.
WHILE len(#ProfileId) > 0
BEGIN
DECLARE #comm int= charindex(',',#ProfileId)
IF #comm = 0 set #comm = len(#ProfileId)+1
DECLARE #Profile varchar(1000) = substring(#ProfileId, 1, #comm-1)
INSERT INTO Information(ProfileId,ProfileName,ProfileDescription)
VALUES (#ProfileId,#ProfileName,#ProfileDescription)
SET #ProfileId= substring(#ProfileId, #comm+1, len(#ProfileId))
END

SQL Server multiple EXEC Stored procedure and C# with XML

I have a few stored procedures which I am trying to call from another stored procedure like this:
SET NOCOUNT ON;
DECLARE #x1 XML;
DECLARE #x2 XML;
DECLARE #x3 XML;
EXEC p1 0, 2, #x1 OUTPUT
EXEC p2 0, 0, #x2 OUTPUT
EXEC p3 1, #x3 OUTPUT
DECLARE #x XML;
SET #x = (SELECT #x1, #x2, #x3) FROM XML PATH('root')
When I execute procedure in management studio, it returns 4 outputs in the result window. #x1, #x2, #x3 and #x respectively.
But when try to read this output in .NET C# (xmlReader = cmd.ExecuteXmlReader();), it only reads the first xml from #x1.
I don't know how can I limit the stored procedure to only return the output of #x. Or do I need something else?
Any help will be appreciated
EDIT:
Here is the first procedure:
#VersionID AS INT,
#xml AS XML = NULL OUTPUT
SET NOCOUNT ON;
SET #xml = (
SELECT
COALESCE(VersionID, 0) AS VersionID,
COALESCE(VersionName, '') AS VersionName
FROM
Versions v
WHERE
v.VersionID = #VersionID
FOR XML PATH ('Version')
);
SELECT #xml;
The rest look exactly the same with different tables
The SELECT #xml; at the end of the Stored Proc is being returned as the first result set (out of several result sets). You should remove that SELECT as you are returning the value via OUTPUT param.
If the SELECT #xml; is required by other callers of this proc, then you can add another input param for #SelectOutput BIT = 1 and wrap the SELECT at the end around a test of the new input param, such as:
IF (#SelectOutput = 1)
BEGIN
SELECT #xml;
END
Then in your outer proc (as shown at the top of your question), pass in a 0 for the new input param. No other callers of the proc need to be updated since the default value is to operate as it currently does.
EXEC p1 0, 2, #x1 OUTPUT, 0
Repeat the above steps (i.e. either remove final SELECT or at least wrap in an IF condition based on new, optional input param) for all sub-proc calls (i.e. p1, p2, and p3).
Finally:
Be sure to set the value of #x correctly. What is posted in the question doesn't work (might even cause the proc to error) and should be: SET #x = (SELECT #x1, #x2, #x3 FOR XML PATH('root'))
Make sure to actually do a SELECT #x in that outer proc as simply setting #x (assuming that it is an OUTPUT param) isn't a result set and hence not what XmlReader is looking for.

Problem in stored procedure

create procedure InsertQuestionEntry
#round_name varchar(40),
#question varchar(100),
#answer varchar(40),
#option1 varchar(20),
#option2 varchar(30),
#option3 varchar(30)
as
begin
insert into QuestionEntry(Question,Round_Name) values(#question,#round_name);
declare #quesion_id int
exec #quesion_id= select Question_ID from QuestionEntry;
insert into Answer(Question_ID,Answer,Option1,Option2,Option3) values(#quesion_id,#answer,#option1,#option2,#option3);
end
Here I want to retrieve the Question_ID from table QuestionEntry and use that Question_ID to another table Answer
But this didn't work.
So how can I use above way?
please help me
Instead of
insert into QuestionEntry(Question,Round_Name) values(#question,#round_name);
declare #quesion_id int
exec #quesion_id= select Question_ID from QuestionEntry;
use the following:
DECLARE #quesion_id int
INSERT INTO QuestionEntry(Question,Round_Name) values(#question,#round_name)
SET #quesion_id = SCOPE_IDENTITY()
You should not use "exec" there.
What exec does is:
Executes a command string or character
string within a Transact-SQL batch, or
one of the following modules: system
stored procedure, user-defined stored
procedure, scalar-valued user-defined
function, or extended stored
procedure.
You should use "set" or "select" instead of exec.
SET can only assign one variable at
a time, SELECT can make multiple
assignments at once. When assigning
from a query if there is no value
returned then SET will assign
NULL, where SELECT will not make
the assignment at all (so the variable
will not be changed from it's previous
value)
You can find more info about when to use SET or SELECT here: SET vs SELECT when assigning variables
Sample:
set #quesion_id = (select Question_ID from QuestionEntry)
select #quesion_id = (select Question_ID from QuestionEntry)
But that's also wrong way to get identity value from inserted record. If you have N users execute a same procedure at a same time it can happen that you will get wrong value (from last inserted record).
To do this properly you should use ##IDENTITY or even better SCOPE_IDENTITY(). More info: here.
After INSERT you can simply call:
SELECT #quesion_id = ##IDENTITY
--or
SELECT #quesion_id = SCOPE_IDENTITY()
Also, check your Question_ID is configured properly. It should be set to auto increment.
Sample:
Question_ID int IDENTITY(1,1)PRIMARY KEY CLUSTERED
The 1's following the IDENTITY keyword indicate the SEED number (value for first record in table) and increment property (0 or 1).
If your server's version is SQL Server 2005 or higher, you could also try something like this:
create procedure InsertQuestionEntry
#round_name varchar(40),
#question varchar(100),
#answer varchar(40),
#option1 varchar(20),
#option2 varchar(30),
#option3 varchar(30)
as
begin
insert into QuestionEntry(Question,Round_Name)
output inserted.Question_ID, #answer, #option1, #option2, #option3
into Answer (Question_ID, Answer, Option1, Option2, Option3)
values(#question,#round_name);
end

Creating a SQL table from a comma concatenated list

I am running SQL Server and I have a stored procedure. I want do a select statement with a WHERE IN clause. I don't know how long the list will be so right now I have tried something as follows
SELECT * FROM table1 WHERE id IN (#idList)
in this solution #idList is a varChar(max). but this doesn't work. I heard about passing in table values, but I am confused about how to do that. Any help would be great
I would suggest using a function to split the incoming list (use the link that Martin put in his comment).
Store the results of the split function in a temporary table or table variable and join it in your query instead of the WHERE clause
select * into #ids from dbo.Split(',', #idList)
select t.*
from table1 t
join #ids i
on t.id = i.s
The most efficient way would be to pass in a table valued parameter (if you're on SQL Server 2008), or an XML parameter (if you're on SQL Server 2005/2000). If your list is small (and you're on SQL Server 2005/2000), passing in your list as a comma (or otherwise) delimited list and using a split function to divide the values out into rows in a temporary table is also an option.
Whichever option you use, you would then join this table (either the table parameter, the table resulting from the XML select, or the temporary table created by the values from the split) to your main query.
Here is a table valued function that takes a nvarchar and returns a table to join on:
Create function [ReturnValues]
(
#Values nvarchar(4000)
)
Returns #ValueTable table(Value nvarchar(2000))
As
Begin
Declare #Start int
Declare #End int
Set #Start = 1
Set #End = 1
While #Start <= len(#Values)
Begin
Set #End = charindex(',', #Values, #Start)
If #End = 0
Set #End = len(#Values) + 1
Insert into #ValueTable
Select rtrim(ltrim(substring(#Values, #Start, #End - #Start)))
Set #Start = #End + 1
End
Return
End
GO
Binding an #idList parameter as you suggested is not possible with SQL.
The best would be bulk inserting the ids into a separated table and than query that table by using an subselect, or joining the IDs.
e.g.
INSERT INTO idTable (id, context) values (#idValue, 1);
INSERT INTO idTable (id, context) values (#idValue, 1);
INSERT INTO idTable (id, context) values (#idValue, 1); // as often as you like
SELECT * FROM table1, idTable WHERE table1.id == idTable.id and idTable.context = 1
The context must be a unique value that identifies the Id Range. That is important for running the stored proc parallel. Without the context information, running the stored procecure in parallel would mix the values from different selections.
If the number of parameters are reasonably small (< 100) you can use several parameters
SELECT * FROM table1 WHERE IN id IN (#id1, #id2, #id3)
If it is longer, look for a split function.

Categories