SQL Table valued parameters from C# - c#

Just a few years behind, but I discovered table valued parameters for stored procedures/UDFs today. They are the ideal solution to a problem I'm having, but I can't get them to work from C#.
I have a UDF:
CREATE FUNCTION GetSurveyScores
#Survey bigint,
#Question nvarchar(max),
#Area StringList READONLY,
#JobCode StringList READONLY
AS BEGIN
SELECT * FROM SurveyResults WHERE RespondentArea IN (Select val from #Area) AND RespondentJobCode IN (select val from #JobCodes)
END
(StringList is the type I created, it's just a table valued type with a single column called val, defined as nvarchar(256))
Then from SQL Server Management Studio, I can do this:
declare #area StringList;
insert into #area(val) values('NW'),('NE'),('SW');
// Get all survey respondent job codes from the SurveyRespondent table.
declare #jobcodes StringList;
insert into #jobcodes select distinct jobcode from dbo.SurveyRespondent;
select * from dbo.GetSurveyScores(3, 'Q3', #area, #jobcodes)
That works brilliantly.
From C#, I get no results (no exceptions), using this code: (I'm using DataTables because the actual code I intend to drop this into uses DataTables already)
DataTable areas = new DataTable("StringList");
areas.Columns.Add("val");
areas.Rows.Add("NW"); areas .Rows.Add("NE"); areas .Rows.Add("SW");
DataTable jobcodes = new DataTable("StringList");
jobcodes.Columns.Add("val");
jobcodes.Rows.Add("JC1"); jobcodes .Rows.Add("JC2");
SqlCommand cmd = new SqlCommand("SELECT * FROM dbo.GetSurveyScores(3, 'Q3', #area, #jc)", connection);
cmd.Parameters.AddWithValue("#area", areas);
cmd.Parameters["#area"].SqlDbType = SqlDbType.Structured;
cmd.Parameters["#area"].TypeName = "StringList";
cmd.Parameters.AddWithValue("#jc", jobcodes);
cmd.Parameters["#jc"].SqlDbType = SqlDbType.Structured;
cmd.Parameters["#jc"].TypeName = "StringList";
DataTable results = new DataTable();
using (SqlDataAdapter da = new SqlDataAdapter(cmd)) {
da.Fill(results);
}
Console.WriteLine(results.Rows.Count);
The final line prints 0. I'm sure I must be missing something simple, but after 6 hours of trying, I think I need a new set of eyes to look at it.

I believe that the issue is that you are not returning anything from your function, although to be honest I'm not sure how it was able to run in MGT studio because it looks like a syntax error. The function should look something like this
CREATE FUNCTION GetSurveyScores (
#Survey bigint,
#Question nvarchar(max),
#Area StringList READONLY,
#JobCodes StringList READONLY )
RETURNS #Results TABLE (RespondentArea NVARCHAR(256), RespondentJobCode NVARCHAR(256))
AS
BEGIN
INSERT #Results
SELECT * FROM SurveyResults WHERE RespondentArea IN (Select val from #Area) AND RespondentJobCode IN (select val from #JobCodes)
RETURN
END
go
Id' image that the syntax you actually used was slightly different, and the select statement is getting executed but the return value of the function has no results.

Related

SQL INNER JOIN, #Parameter - does not filter results

Thank you in advance for any advice you are able to give me.
What I'm trying to achieve: I have a combobox that I want to populate with table1 / columnC, sorted by matches in table1 / columnB and table2 / columnB.
For example: I want to display all of Roberts potatoes, not everyone's potatoes.
I have created a stored procedure for this. It currently looks like this:
CREATE PROCEDURE [dbo].[myProcedure]
#myParameter nvarchar
AS
SELECT columnA, columnB, columnC
FROM table1
INNER JOIN table2 ON table1.columnB = table2.columnB
WHERE table1.columnB = #myParameter
Data set:
DataSet MyDataSet()
{
DataSet ds = new DataSet();
using (SqlConnection con = new SqlConnection(connectionString))
using (SqlDataAdapter da = new SqlDataAdapater("myProcedure", con))
{
da.SelectCommand.CommandType = CommandType.StoredProcedure;
//the line below gives value to my parameter, which in this case is a WinForms label
//with the table1/columnB, table2/columnB value (the name that I wish to sort by)
da.SelectCommand.Parameters.Add("#myParameter", SqlDbType.NVarChar).Value = label.Text.ToString();
da.Fill(ds);
}
return ds;
}
Populating my combobox (code is in form load event):
try
{
DataSet ds2 = MyDataSet();
myComboBox.DataSource = ds2.Tables[0];
myComboBox.DisplayMember = "columnC";
myComboBox.ValueMember = "columnA"; // hidden ID row
}
catch (Exception ex)
{
// messageboxcode
}
Oh, and, if it matters, all my columns are nvarchar except for table1/columnA, which is int.
Hope I make sense and that someone is able to give me a hint or two.
In regards to what I've tried to fix this, being new to SQL, I have read documentation, watched tutorials and generally spent hours trying to figure this out. I just can't for some reason! :-)
I can say that your stored procedure is malformed because you don't have a length on nvarchar(). Never use char and related types in SQL Server without a length parameter.. The default varies by context and assuming the default does what you want just leads to hard to debug errors (ahem).
In the absence of other information, you can use (max), but I would suggest that you use an appropriate value for the length of the parameter:
CREATE PROCEDURE [dbo].myProcedure (
#myParameter nvarchar(max)
) AS
BEGIN
SELECT columnA, columnB, columnC
FROM table1 JOIN
table2
ON table1.columnB = table2.columnB
WHERE table1.columnB = #myParameter;
END;
Add the order like:
order by table1.columnB,table2.columnB

Where In Clause error on The server support a maximum of 2100 parameters only

I'm currently trying to try catch the insert SQL query from my simple C# application, but when i'm trying to insert the data into database with user id which is added into userIDList parameter, the error came out saying that
The incoming request has too many parameters. The server supports a
maximum of 2100 parameters.
The userIDList sometimes will contains like 60 arrays above then the error will popped out.
My SQL CommandText will contain of
"SELECT * FROM TIME_ATTENDANCE_REPORT WHERE TRXDATETIME = #Date AND USERID IN (001,002,003,004,....)
So i think if more then certain number then the error popped out
Here are my sample code :
List<string> userIDList = new List<string>();
using (SqlCommand sqlDBComm = new SqlCommand())
{
openConnection();
SqlDataReader sqlDBReader;
sqlDBReader = null;
sqlDBComm.CommandText = "SELECT * FROM TIME_ATTENDANCE_REPORT WHERE TRXDATETIME = #Date AND USERID IN (" + string.Join(",", userIDList) + ") ORDER BY USERID ASC ";
sqlDBComm.Parameters.Add("#Date", SqlDbType.DateTime);
sqlDBComm.Parameters["#Date"].Value = GetDateFrom;
sqlDBComm.Connection = sqlDB;
sqlDBComm.CommandType = CommandType.Text;
try
{
sqlDBReader = sqlDBComm.ExecuteReader();
t.Load(sqlDBReader);
sqlDBReader.Close();
if (t.Rows.Count > 0)
{
status = "Update";
}
else
{
status = "Insert";
}
}
catch (Exception errMsg)
{
MessageBox.Show("Error Code: " + errMsg.ToString());
}
finally
{
sqlDBReader.Close();
closeConnection();
}
}
Any other solution can resolve this?
Thanks
There are many ways to solve this problem.
Instead of sending a list of IDs as seperate parameters, you can send a single #IDList parameter as a single comma separated string and let it parsed into IDs at the server side. Here is a function that I use for this (borrowed and modified from Jeff Moden's code):
CREATE FUNCTION [dbo].[iSplitter] (#Parameter VARCHAR(MAX))
RETURNS #splitResult TABLE (number INT, [value] INT)
AS
BEGIN
SET #Parameter = ','+#Parameter +',';
WITH cteTally AS
(
SELECT TOP (LEN(#Parameter))
ROW_NUMBER() OVER (ORDER BY t1.Object_ID) AS N
FROM Master.sys.All_Columns t1
CROSS JOIN Master.sys.All_Columns t2
)
INSERT #splitResult
SELECT ROW_NUMBER() OVER (ORDER BY N) AS Number,
SUBSTRING(#Parameter,N+1,CHARINDEX(',',#Parameter,N+1)-N-1) AS [Value]
FROM cteTally
WHERE N < LEN(#Parameter) AND SUBSTRING(#Parameter,N,1) = ','
RETURN
END
With this function created once, I do it like:
sqlDBComm.CommandText = #"SELECT * FROM TIME_ATTENDANCE_REPORT tar
inner Join dbo.iSplitter(#UserIdList) ul on tar.USERID = ul.[value]
WHERE TRXDATETIME = #Date
ORDER BY USERID ASC ";
sqlDBComm.Parameters.AddWithValue("#UserIdList",string.Join(",", userIDList));
This works very well for 5-6K integer ids but times out if used with 20-30K or more IDs. Then I created another alternative as a CLR procedure, and that one parses the list server side in less than a second. But I think this one is sufficient for your needs.
Another way is to send the IDs as an XML parameter and parse server side again.
Yet another way is to send a table parameter.
PS: Here is a link that shows sample code for other ways. The site is in Turkish but the codes are crystal clear in C#, separate per approach.
EDIT: XML sample using Northwind Orders table:
void Main()
{
int[] IDList = { 10265,10266,10267,10268,10269,10270,10271,10272,10273,10274,10275, 10320, 10400 };
var idsAsXML = new XElement("IDS",
from i in IDList
select new XElement("Row", new XAttribute("Id", i)));
string sql = #"
DECLARE #hDoc int;
DECLARE #tbl TABLE (Id int);
exec sp_xml_preparedocument #hDoc OUTPUT, #XML;
INSERT #tbl
SELECT *
FROM OPENXML(#hDoc, #Nodename, 1) WITH (Id int);
EXEC sp_xml_removedocument #hDoc;
select * from Orders o
where exists (select * from #tbl t where t.Id = o.OrderId) ";
DataTable tbl = new DataTable();
using (SqlConnection con = new SqlConnection(#"server=.\SQLExpress;Trusted_Connection=yes;Database=Northwind"))
{
SqlCommand cmd = new SqlCommand(sql, con);
cmd.Parameters.AddWithValue("#XML", idsAsXML.ToString());
cmd.Parameters.AddWithValue("#NodeName", "/IDS/Row");
con.Open();
tbl.Load(cmd.ExecuteReader());
con.Close();
}
//tbl.Dump(); // linqPad luxury
}
You can create a Table-Valued-Parameter and pass it as a parameter. It requires you to create a new type in your database, and will enable you to pass an array to the query and let the database treat it as a table. If this is something you do a lot, it could come in handy.
I no longer have access to the project where I implemented this but everything is available in the blog post. The code below is not tested, but I hope it can get you in the right direction.
1. Create a new type in your database:
CREATE TYPE integer_list_tbltype AS TABLE (n int NOT NULL PRIMARY KEY)
2. Pass it as parameter:
sqlDBComm.Parameters.Add("#userIds", SqlDbType.Structured)
sqlDBComm.Parameters["#userIds"].Direction = ParameterDirection.Input
sqlDBComm.Parameters["#userIds"].TypeName = "integer_list_tbltype"
sqlDBComm.Parameters["#userIds"].Value = CreateDataTable(userIDList)
3. Method for creating the parameter:
private static DataTable CreateDataTable(IEnumerable<int> ids) {
DataTable table = new DataTable();
table.Columns.Add("n", typeof(int));
foreach (int id in ids) {
table.Rows.Add(id);
}
return table;
}
4. Use it in your SQL:
... AND USERID IN (SELECT n FROM #userIds)
CreateDataTable from here:
How to pass table value parameters to stored procedure from .net code
Rest from here:
http://www.sommarskog.se/arrays-in-sql-2008.html#introduction

Return select statement and out parameter from stored procedure simultaneously in C#

My SQL Server stored procedure returns a result set from a SELECT statement and has output parameters. When I call this one from C# (connection in made through SqlConnection class) I have a problem: the out parameter returns null. If I comment select statement in procedure than it gets proper result.
CREATE PROC test
#Out INT OUT
AS
BEGIN
SET #Out=4
SELECT * FROM temp_table
END
Does out parameter wait to select statement?
How to fix it?
Added ----->
#huMptyduMpty is right i mentioned only sql procedure for experiment. Now I show changed procedure and code from C# side.
I write this procedure.
CREATE PROC T_Proc
#Count INT = NULL OUT
AS
BEGIN
SET #Count = (SELECT COUNT(*) FROM T_Table AS tt)
--SELECT * FROM T_Table AS tt
END
and this one in C#
SqlConnection conn = new SqlConnection("Data Source=127.0.0.1;Initial Catalog=test;Integrated Security=True");
SqlCommand cmd = new SqlCommand("T_Proc", conn);
cmd.CommandType = CommandType.StoredProcedure;
SqlDataAdapter MyDataAdapter = new SqlDataAdapter();
MyDataAdapter.SelectCommand = cmd;
SqlParameter a1 = cmd.Parameters.Add("#Count", SqlDbType.Int);
a1.Direction = ParameterDirection.Output;
DataSet Mydataset = new DataSet();
conn.Open();
cmd.ExecuteReader();
textBox1.Text = a1.Value.ToString();
conn.Close();
In this situation T_Proc returns result correctly, but if I uncheck this snip from SQL procedure --SELECT * FROM T_Table AS tt than #Count returns null
You can get both a dataset and parameter values, you just need to switch the order: first the dataset, then the parameters.
From MSDN:
Output, InputOut, and ReturnValue parameters returned by calling ExecuteReader cannot be accessed until you close the SqlDataReader.
"A SQL Server stored procedure that you can call is one that returns one or more OUT parameters, which are parameters that the stored procedure uses to return data back to the calling application. "
Read more about Using a Stored Procedure with Output Parameters
So you can do something like
CREATE PROC test
#Out INT OUT
AS
BEGIN
SELECT #Out = FieldName FROM temp_table
END
I suggest you (which i prefer), not to use out put parameter if you going to select multiple columns. You can use a SqlDataReader from your c# code
Also have a look at Stored Procedure, when to use Output parameter vs Return variable
Update
To get the row count, just remove your output parameter and get the count from c# code
i.e.
var reader = cmd.ExecuteReader();
var rowCount = reader.Cast<object>().Count();
Not quite sure what you after
check this one
CREATE PROC T_Proc
AS
BEGIN
Declare #Count INT
SET #Count = (SELECT COUNT(*) FROM T_Table AS tt)
SELECT *, #Count as TotalCount
FROM T_Table AS tt
Where xxxx---condition if you need any
END

Displaying data from SQL database in C# WinForm controls (textBox, comboBox, label)

I have a performance problem with displaying data from an SQL database in my application. The problem is that I have a large number of parameters that I need to display (customers personal data, his current statistics etc.).
So far I've used either SqlCommand.ExecuteScalar (for single parameters), or DataTable.Rows[].ItemArray.GetValue() (for multiple parameters - I fill the DataTable with SqlDataAdapter whose query withdraws the necessary data from the database) and assigned their values to the appropriate control. Assuming that command is an SqlCommand type:
For single parameter
command.CommandText = "SELECT Parameter1 FROM MyTable WHERE Condition = Value";
textBox1.Text = command.ExecuteScalar().ToString();
For multiple parameters (SDA is a SqlDataAdapter):
command.CommandText="SELECT Parameter1 - ParameterN FROM MyTable WHERE Condition = Value";
SDA.SelectCommand = command;
SDA.Fill(MyDataTable);
textBox1.Text = MyDataTable.Rows[0].ItemArray.GetValue(0).ToString();
comboBox1.Text = MyDataTable.Rows[0].ItemArray.GetValue(1).ToString();
/*
I repeat similar lines of code for each parameter and display it in the appropriate control.
*/
This approach works correctly but when I have a large number of parameters (20+), it works very slowly.
Is there a more efficient way to display these amounts of data, and how would I implement it?
Thank you
Probably, with the second example, a SqlDataReader will perform better because you read the values just one time, while with a DataAdapter, you need to load the DataTable and then loop over the rows of the table (Effectively reading data two times).
command.CommandText="SELECT Field1,...,FieldN FROM MyTable WHERE Condition = Value";
SqlDataReader reader = command.ExecuteReader();
while(reader.Read())
{
// Of course this works correctly just if your query returns one row....
textBox1.Text = reader.GetString(0);
comboBox1.Text = reader.GetString(n);
}
You could also try with the Field<T> extension for the DataRow
command.CommandText="SELECT Field1,...,FieldN FROM MyTable WHERE Condition = Value";
SqlDataAdapter SDA = new SqlDataAdapter(command);
SDA.Fill(MyDataTable);
textBox1.Text = MyDataTable.Rows[0].Field<string>("Field1");
comboBox1.Text = MyDataTable.Rows[0].Field<string>("FieldN");
However, I think that the real performance gain would be in the query that you submit to the database engine and in the correct working of indexes on your tables.
Try to retrieve the minimun number of rows possible, search on indexed fields and/or change to a stored procedure.
here i had write sample stored procedure in wich you can get idea...
you can pass as amny parameter as you can in xml format and insert into temp table...
now you have table with value Name/value pair means Paramater name /value....
now you can do your furteher work...
/*
EXEC wa_TempGetDaya '<SampleXML>
<tblXML><AccountID>3</AccountID><Code>11</Code><Description>Leptospiral infect NEC</Description></tblXML>
</SampleXML>'
*/
CREATE PROCEDURE wa_TempGetDaya
(
#ParaXML NVARCHAR(MAX)
)
AS
SET NOCOUNT ON
BEGIN
DECLARE #AccountID INT
DECLARE #MyXML XML
SET #MyXML = #ParaXML
IF OBJECT_ID('tempdb..#TempData') IS NOT NULL
DROP TABLE #TempData
SELECT * INTO #TempData
FROM (
SELECT
Parse.value('(AccountID)[1]', 'INT') AS 'AccountID',
Parse.value('(Code)[1]', 'Varchar(100)') AS 'Code',
Parse.value('(Description)[1]', 'varchar(1000)') AS 'Description'
FROM
#MyXML.nodes('/SampleXML/tblXML') AS YourData(Parse)
) AS tbl
declare #para1 varchar(20)
declare #para2 varchar(20)
declare #para3 varchar(20)
SELECT #para1 =AccountID ,#para2 =Code,#para3 =Description from #TempICD
END

calling a stored procedure from C# using SqlDataAdapter

I have a stored procedure which has been well tested and works perfectly from SQL Server Management Studio. All the procedure does is check for the existence of a record in a table, return it if it exists, or create it and then return it if it doesn't.
The procedure looks like this:
CREATE proc [dbo].[spInsertSerialBatch]
#MOS_JOB varchar(12), --PASSED COMMAND LINE
#MOS_LOT varchar(4) = NULL, --PASSED COMMAND LINE
#MES_USER varchar(12) = 'NOT PASSED',--PASSED COMMAND LINE
#COMPUTERNAME varchar(100) = 'NOT PASSED' --ENVIRONMENT VARIABLE
as ....
I use a SqlDataAdapter, which I have used repeatedly without any problems. The setup looks like this:
using (SqlCommand sqlComm = new SqlCommand("dbo.spInsertSerialBatch", serialBatchDataConnection))
{
if (serialBatchDataConnection.State != ConnectionState.Open)
{
serialBatchDataConnection.Open();
}
sqlComm.CommandType = CommandType.StoredProcedure;
sqlComm.Parameters.AddWithValue("#MOS_JOB", options.jobNumber);
sqlComm.Parameters.AddWithValue("#MOS_LOT", options.lotNumber);
sqlComm.Parameters.AddWithValue("#MES_USER", options.userId);
sqlComm.Parameters.AddWithValue("#COMPUTERNAME", System.Environment.MachineName);
SqlDataAdapter sda = new SqlDataAdapter(sqlComm);
DataTable dt = new DataTable();
int rowsAffected = sda.Fill(dt);
}
I then examine the results of the table after Fill is executed. It works fine when the row exists in the table, but if it doesn't, and the stored proc needs to generate it, Fill returns 0 rows and the data table remains empty. No errors/exceptions are thrown, I just get no results.
I suppose I could change the code to use ExecuteNonQuery and not use the DataAdapter, but I see no reason why this shouldn't just work; I prefer having a data table (which may result in more than a single row in some cases) than using a data reader and looping over the data to get the results.
Any suggestions as to why this might fail? I've looked over several posts on this and other sites that are similar, but haven't found a satisfactory answer. Any suggestions are greatly appreciated.
Thanks,
Gary
The entrire sp is quite large and probably too proprietary to publish...
--return inserted rows
SELECT 'CREATED' as [spRESULT], o.*
FROM #output o
END
/*
* Return existing SerialBatch(s)
*/
BEGIN
SELECT 'RETRIEVED' as [spRESULT], s.*
FROM SerialBatch s
WHERE SerialBatchId = #formattedId
UNION
/*
* pull in products that have components that need labels as well
*/
SELECT 'RETRIEVED' as [spRESULT],s.*
FROM SerialBatch s
WHERE SerialBatchParentId = #formattedId
END
This is the end of the stored procedure. I tried executing a DataReader instead and the result is the same...I get no results for the case when the sp has to create it, but again it runs perfectly stand-alone in SQL Server Management Studio.
Problem solved. Turns out that the OpenQuery string passed to Oracle was converting an empty string to a NULL and preventing the new row from being returned. All I need to add was a check for both NULL and empty string:
if #MOS_LOT IS NULL or #MOS_LOT = ''
set #MOS_LOT = ' ' --EMPTY STRINGS BEING EQUIVALENT TO NULLS

Categories

Resources