I would like to know something .
I try to retrieve 2 files .
One is register for a group 2 , and one for a group 10 .
So the field is Files.Group .
One user is register to the group 1 and the group 10.
This is the query I use to retrieve files .
SELECT Files.Id, Files.Name, Files.Date, Files.Path, Files.[Group] FROM Files WHERE Files.[Group] = " + param + "ORDER BY Files.Id DESC"
Param is a cookie who get the group, creating a chain like this 2|10 .
This doesn't work actually.. And i don't know how can I pass in the query the two groups. Should I separate them by a coma ? like Files.Group = 2,10 ?
Or is it something else ? To pass 2 parameters ?
Baseline Structure
I don't have your entire structure so I have created the following simplified version of it:
CREATE TABLE [dbo].[Files]
(
[ID] INT IDENTITY(1,1) NOT NULL PRIMARY KEY,
[Name] NVARCHAR(64) NOT NULL,
[Group] INT NOT NULL -- Probably have a non-unique index over this.
);
GO
INSERT INTO [dbo].[Files] ([Name], [Group]) VALUES (N'My File 1', 1);
INSERT INTO [dbo].[Files] ([Name], [Group]) VALUES (N'My File 2', 2);
INSERT INTO [dbo].[Files] ([Name], [Group]) VALUES (N'My File 3', 3);
INSERT INTO [dbo].[Files] ([Name], [Group]) VALUES (N'My File 4', 2);
INSERT INTO [dbo].[Files] ([Name], [Group]) VALUES (N'My File 5', 3);
INSERT INTO [dbo].[Files] ([Name], [Group]) VALUES (N'My File 6', 5);
Temp Table
You can insert the split values into a temp table and use a WHERE EXISTS against it - probably yielding decent performance.
-- This would be passed in from C#.
DECLARE #GroupsParam NVARCHAR(64) = N'2|3';
-- This is your SQL command, possibly a SPROC.
DECLARE #GroupsXML XML = N'<split><s>' + REPLACE(#GroupsParam, N'|', N'</s><s>') + '</s></split>';
-- Create an in-memory temp table to hold the temp data.
DECLARE #Groups TABLE
(
[ID] INT PRIMARY KEY
);
-- Insert the records into the temp table.
INSERT INTO #Groups ([ID])
SELECT x.value('.', 'INT')
FROM #GroupsXML.nodes('/split/s') as records(x);
-- Use a WHERE EXISTS; which should have extremely good performance.
SELECT [F].[Name], [F].[Group] FROM [dbo].[Files] AS [F]
WHERE EXISTS (SELECT 1 FROM #Groups AS [G] WHERE [G].[ID] = [F].[Group]);
Table-Values Parameters (SQL 2008+ Only)
SQL 2008 has a neat feature where you can send tables as parameters to the database. Clearly this will only work if you are using SqlCommands correctly (Executing Parameterized SQL Statements), unlike your example (appending user-created values to a SQL string is extremely bad practice - learn how to use parameters) - as you need to pass in a DataTable which you can't do with a simple string value.
In order to use this you first need to create the value type:
CREATE TYPE [dbo].[IntList] AS TABLE
([Value] INT);
GO
Next we will do things properly and used a stored procedure - as this is a static query and there are some performance implications of using a sproc (query plan caching).
CREATE PROCEDURE [dbo].[GetFiles]
#Groups [dbo].[IntList] READONLY
AS BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
SELECT [F].[Name], [F].[Group] FROM [dbo].[Files] AS [F]
WHERE EXISTS (SELECT 1 FROM #Groups AS [G] WHERE [G].[Value] = [F].[Group]);
END
GO
Next we need to hit this from C#, which is pretty straight-forward as we can create a table to do the call.
public static void GetFilesByGroups(string groupsQuery)
{
GetFilesByGroups(groupsQuery.Split('|').Select(x => int.Parse(x)));
}
public static void GetFilesByGroups(params int[] groups)
{
GetFilesByGroups((IEnumerable<int>)groups);
}
public static void GetFilesByGroups(IEnumerable<int> groups)
{
// Create the DataTable that will contain our groups values.
var table = new DataTable();
table.Columns.Add("Value", typeof(int));
foreach (var group in groups)
table.Rows.Add(group);
using (var connection = CreateConnection())
using (var command = connection.CreateCommand())
{
command.CommandType = CommandType.StoredProcedure;
command.CommandText = "[dbo].[GetFiles]";
// Add the table like any other parameter.
command.Parameters.AddWithValue("#Groups", table);
using (var reader = command.ExecuteReader())
{
// ...
}
}
}
Remember: Table-Valued Parameters are only supported on SQL 2008 and later.
Edit: I would like to point out that there is likely a cross-over point in terms of performance between dknaack's answer and the temp table approach. His will likely be faster for a small set of search-groups; where the temp table approach would probably be faster for a large set of search-groups. There is a possibility that table-valued parameters would nearly always be faster. This is all just theory based on what I know about how the SQL query engine works: temp table might do a merge or hash join where the TVP would hopefully do a nested loop. I haven't done any profiling (and haven't received enough upvotes to motivate me to do so) so I can't say for certain.
Description
You should use SqlParameter to prevent Sql injections. Use the IN Statetment to pass in a comma seperated list of you group ids.
Sample
// value from cookie
string groups = "2,10,99";
// Build where clause and params
List<string> where = new List<string>();
List<SqlParameter> param = new List<SqlParameter>();
foreach(string group in groups.Split(','))
{
int groupId = Int32.Parse(group);
string paramName = string.Format("#Group{0}", groupId);
where.Add(paramName);
param.Add(new SqlParameter(paramName, groupId));
}
// create command
SqlConnection myConnection = new SqlConnection("My ConnectionString");
SqlCommand command = new SqlCommand("SELECT Files.Id, Files.Name, Files.Date, " +
"Files.Path, Files.[Group] " +
"FROM Files " +
"WHERE Files.[Group] in (" + string.Join(",", param) + ")" +
"ORDER BY Files.Id DESC", myConnection);
command.Parameters.AddRange(param.ToArray());
More Information
MSDN - IN (Transact-SQL)
C# SqlParameter Example
You're probably (depending on your database) looking at using this:
IN (2, 10)
rather than an = operator.
Note that constructing SQL using string concatenation like this can expose your code to SQL injection vulnerabilities, and using a properly parameterised SQL query is generally better practice. However, in your case, where you have an indeterminate number of parameters, it is harder to achieve in practice.
You need to set Param in cookie to create a chain like 2,10.
Then, instead of using = you need to use in () like this:
SELECT Files.Id, Files.Name, Files.Date, Files.Path, Files.[Group] FROM Files WHERE Files.[Group] in (" + param + ") ORDER BY Files.Id DESC"
Another thing that you got wrong was missing a space in param + "ORDER part.
Related
My Motive is to pass long array of ID as parameter to stored procedure and select data on the basis of ID. So i created Type in SQL Server
CREATE TYPE [dbo].[CategoryIdArray] AS TABLE(
[CategoryId] [bigint] NULL
)
GO
and stored procedure
ALTER PROCEDURE [dbo].[GetNewestArticleByCatsPageWise]
#dt as [dbo].[CategoryIdArray] READONLY,
#PageIndex INT = 1
,#PageSize INT = 10
,#PageCount INT OUTPUT
AS
BEGIN
SET NOCOUNT ON;
SELECT ROW_NUMBER() OVER
(
ORDER BY [dateadded]
)AS RowNumber,[desid]
INTO #Results
FROM [DB_user1212].[dbo].[discussions] as d , [DB_user1212].[dbo].[CategoryMap] as c where d.desid=c.[Topic Id] and c.[Category Id] in (select CategoryId from [dbo].[CategoryIdArray]) and [TopicType]='1' order by [dateadded]
DECLARE #RecordCount INT
SELECT #RecordCount = COUNT(*) FROM #Results
SET #PageCount = CEILING(CAST(#RecordCount AS DECIMAL(10, 2)) / CAST(#PageSize AS DECIMAL(10, 2)))
PRINT #PageCount
SELECT * FROM #Results
WHERE RowNumber BETWEEN(#PageIndex -1) * #PageSize + 1 AND(((#PageIndex -1) * #PageSize + 1) + #PageSize) - 1
DROP TABLE #Results
END
Tried to use above stored procedure by Code below
public List<String> getNewestArticleByCategoryPageWise( long[] categoryId)
{
List<string> topicId= new List<string>();
try
{
DataTable dt_Categories = new DataTable();
dt_Categories.Columns.Add("Category", typeof(String));
DataRow workRow;
foreach(long cat in categoryId)
{
workRow = dt_Categories.NewRow();
workRow["Category"] = cat;
dt_Categories.Rows.Add(workRow);
}
int pageIndex = 1;
SqlCommand cmd = new SqlCommand("dbo.GetNewestArticleByCatsPageWise", con);
cmd.CommandType = CommandType.StoredProcedure;
cmd.Parameters.AddWithValue("#PageIndex", pageIndex);
cmd.Parameters.AddWithValue("#PageSize", 10);
cmd.Parameters.Add("#PageCount", SqlDbType.Int, 4).Direction = ParameterDirection.Output;
SqlParameter tvparam = cmd.Parameters.AddWithValue("#dt", dt_Categories);
tvparam.SqlDbType = SqlDbType.Structured;
con.Open();
sdr= cmd.ExecuteReader();
while(sdr.Read())
{
topicId.Add(sdr.GetString(0));
}
con.Close();
}
catch(Exception ex)
{
con.Close();
throw ex;
}
return topicId;
}
When i run above function exception is thrown Invalid object name 'dbo.CategoryIdArray'. But i created it as type. Help me out what i missed out. I refferred this.
Problem is with this line in stored procedure is with this line
select CategoryId from [dbo].[CategoryIdArray] .
We can not select from type like this, we should use
select CategoryId from #dt
The first thing that I do when I get these questions is to create a sample database. The code below creates the following.
1 - database named [test]
2 - table named [Discussions]
3 - table named [CategoryMap]
4 - user defined table type named [CategoryIdArray]
5 - load the tables with 100 records of data
--
-- Create a test db
--
USE [master];
go
CREATE DATABASE [Test];
GO
--
-- Create the user defined type
--
USE [Test];
go
CREATE TYPE [CategoryIdArray] AS
TABLE
(
[CategoryId] [bigint] NULL
);
--
-- Create skelton tables
--
create table Discussions
(
dis_id int identity (1,1),
dis_name varchar(64),
dis_added_dte datetime default getdate()
);
go
create table CategoryMap
(
cat_id int identity(1,1),
cat_topic_id int,
cat_topic_type char(1)
);
go
-- clear tables
truncate table Discussions;
truncate table CategoryMap;
go
--
-- Create 100 rows of dummy data
--
declare #cnt int = 0;
while #cnt < 100
begin
insert into Discussions (dis_name)
values ('sample discussion record # ' + str(#cnt, 2, 0));
insert into CategoryMap (cat_topic_id, cat_topic_type)
values (#cnt+1, '1')
set #cnt = #cnt + 1;
end;
go
--
-- Show the sample data
--
select * from Discussions;
go
select * from CategoryMap;
go
The second step is to re-write the stored procedure. If you are using below 2012, go with a window function rownumber(). In 2012, the offset and fetch clauses of the order by were included for paging.
http://technet.microsoft.com/en-us/library/ms188385(v=sql.110).aspx
--
-- Create my procedure
--
create procedure [GetArticlesByPage]
#Tvp as [CategoryIdArray] READONLY,
#PageIndex INT = 1,
#PageSize INT = 10,
#PageCount INT OUTPUT
AS
BEGIN
-- Declare variables
DECLARE #var_recs int = 0;
DECLARE #var_offset int = 0;
-- Do not count the records
SET NOCOUNT ON;
-- Start of paging
SET #var_offset = #var_offset + ((#PageIndex - 1) * #PageSize);
-- Set page count variable
SELECT #var_recs = count(*)
FROM
[dbo].[Discussions] as d
JOIN
[dbo].[CategoryMap] as c
ON
d.dis_id = c.cat_topic_id
JOIN
#TVP a
ON
c.cat_id = a.CategoryId
WHERE
cat_topic_type = '1';
set #PageCount = ceiling(cast(#var_recs as real) / cast(#PageSize as real));
--
-- Return the record set
--
SELECT
dis_id
FROM
[dbo].[Discussions] as d
JOIN
[dbo].[CategoryMap] as c
ON
d.dis_id = c.cat_topic_id
JOIN
#TVP a
ON
c.cat_id = a.CategoryId
WHERE
cat_topic_type = '1'
ORDER BY
dis_added_dte
OFFSET #var_offset ROWS
FETCH NEXT #PageSize ROWS ONLY;
END;
GO
I did leave the page count in place; However, I do not think it is needed since you can repeat the call until the result set is empty.
Please do not dump the record set into a temporary table since it could be quite large if you were return all the columns to display. I choose two separate calls. One for a total count. One for a single page.
The last TSQL part is to test the stored procedure from SSMS.
--
-- Call the stored procedure
--
-- instantiate tvp
DECLARE #my_tvp as [CategoryIdArray];
DECLARE #my_page_cnt as int;
-- add 25 entries
declare #cnt int = 25;
while #cnt < 50
begin
insert into #my_tvp (CategoryId)
values (#cnt + 1);
set #cnt = #cnt + 1;
end;
-- show the data in the tvp
select * from #my_tvp
-- call the function
exec [GetArticlesByPage] #my_tvp, 1, 10, #PageCount = #my_page_cnt OUTPUT;
-- show the data in the output
select #my_page_cnt as 'my_pages';
go
In my test example, I wanted rows 26 to 50 paged as 10 rows. Result 1 is the 25 rows, Result 2 is the 10 rows that were paged, and Result 3 is how many pages. Therefore, the TSQL part of the solution is sound.
Stay tuned for a C# program debug session later tonight.
http://www.mssqltips.com/sqlservertip/2112/table-value-parameters-in-sql-server-2008-and-net-c/
Take a look at this post. It is doing exactly what you are trying to do.
Here are some ideas to try.
1 - Make sure the connection properties, login's default database is [Test] for my example.
2 - Is the type defined in the [Test] database? Please double check this.
3 - Is this correct? The column name is [CategoryId] in the database type. You have the following - [Category]. Try changing the name in the C# code.
dt_Categories.Columns.Add("Category", typeof(String));
4 - Remove the [dbo]. from the type in the SP. It is not in the example from MS SQL Tips. Might be confusing the issue. SQL server will resolve the name.
5 - I noticed the type is defined as big int but the id in the tables is int? Make sure the data types are consistent.
Please try these suggestions. Get back to me on how you make out.
Can you get me a detailed call stack trace and error message if this is still an issue??
So here is a C# console application that I promised.
It works as expected.
You were mixing up some ideas that are the foundation of ADO.NET and data tables. You should get used to looking at the immediate window and local variables. This will help you track down issues.
Here is my sample call to the Stored Procedure.
1 - Setup data table (50 to 74)
2 - Page the data by 5's
3 - Look at second page
//
// Good Ref. - http://msdn.microsoft.com/en-us/library/ms254937(v=vs.110).aspx
//
// Basic stuff from C# console app
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
// Required for data table
using System.Data;
using System.Data.SqlClient;
// Standard stuff ...
namespace ConsoleApplication1
{
class Program
{
static void Main(string[] args)
{
// Debug info
Console.WriteLine("Test - Start");
// Create the table with one column
DataTable my_Table;
my_Table = new DataTable("Category");
my_Table.Columns.Add("CategoryId", typeof(string));
// Add data to table
for (int my_Cnt = 50; my_Cnt < 75; my_Cnt++)
{
DataRow my_Row = my_Table.NewRow();
my_Row["CategoryId"] = my_Cnt.ToString();
my_Table.Rows.Add(my_Row);
}
// Debug info
Console.WriteLine("Test - created data set");
// Create a connection
SqlConnection my_Conn;
string str_Conn = "Server=localhost;Database=Test;Trusted_Connection=True;";
my_Conn = new SqlConnection(str_Conn);
// Debug info
Console.WriteLine("Test - create connection");
// Create the command and set its properties.
SqlCommand my_Cmd = new SqlCommand();
my_Cmd.Connection = my_Conn;
my_Cmd.CommandText = "dbo.GetArticlesByPage";
my_Cmd.CommandType = CommandType.StoredProcedure;
// Add parameter 0
SqlParameter my_Parm0 = new SqlParameter();
my_Parm0.ParameterName = "#Tvp";
my_Parm0.SqlDbType = SqlDbType.Structured;
my_Parm0.Direction = ParameterDirection.Input;
my_Parm0.Value = my_Table;
my_Cmd.Parameters.Add(my_Parm0);
// Add parameter 1
SqlParameter my_Parm1 = new SqlParameter();
my_Parm1.ParameterName = "#PageIndex";
my_Parm1.SqlDbType = SqlDbType.Int;
my_Parm1.Direction = ParameterDirection.Input;
my_Parm1.Value = 2;
my_Cmd.Parameters.Add(my_Parm1);
// Add parameter 2
SqlParameter my_Parm2 = new SqlParameter();
my_Parm2.ParameterName = "#PageSize";
my_Parm2.SqlDbType = SqlDbType.Int;
my_Parm2.Direction = ParameterDirection.Input;
my_Parm2.Value = 5;
my_Cmd.Parameters.Add(my_Parm2);
// Add parameter 3
SqlParameter my_Parm3 = new SqlParameter();
my_Parm3.ParameterName = "#PageCount";
my_Parm3.SqlDbType = SqlDbType.Int;
my_Parm3.Direction = ParameterDirection.Output;
my_Parm3.Value = 5;
my_Cmd.Parameters.Add(my_Parm3);
// Open the connection
my_Conn.Open();
// Debug info
Console.WriteLine("Test - execute reader");
// Execute the reader
SqlDataReader my_Reader = my_Cmd.ExecuteReader();
if (my_Reader.HasRows)
{
while (my_Reader.Read())
{
Console.WriteLine("{0}", my_Reader[0].ToString());
}
}
else
{
Console.WriteLine("No rows found.");
}
// Close the reader
my_Reader.Close();
// Number of pages (output after reader - order is important)
Console.WriteLine("Pages = ");
Console.WriteLine(my_Cmd.Parameters["#PageCount"].Value.ToString());
// Close the connection
my_Conn.Close();
// Debug info
Console.WriteLine("Test - close connection");
// Debug info
Console.WriteLine("Test - End");
// Pause to view output
Console.Read();
}
}
}
Here is a snapshot of the correct output from the C# console application.
I have to thank you for your question!
It has been a while since I coded in C#. But like a bike, does not take long to get back on it. The T-SQL examples were done with SSMS 2012 and the C# program was done with VS 2013. The latest and greatest.
Good nite!
I make no claim about efficient or correct -- but readable modern syntax your base query can be written like this:
SELECT ROW_NUMBER() OVER (ORDER BY [dateadded]) AS RowNumber,[desid]
INTO #Results
FROM [DB_user1212].[dbo].[discussions] as d
JOIN [DB_user1212].[dbo].[CategoryMap] as c ON d.desid=c.[Topic Id]
JOIN [dbo].[CategoryIdArray] arr ON c.[Category Id] = arr.CategoryID
WHERE [TopicType]='1'
Here is your solution:
In your stored procedure, in your WHERE statement, you are selecting * from a "TYPE" rather than the actual parameter object being passed in. It is like doing "SELECT * FROM VARCHAR", which makes no sense. Try this:
...
and c.[Category Id] in (
select CategoryId from #dt -- select from the actual parameter, not its TYPE
)
...
Instead of:
workRow["Category"] = cat;
use
workRow["CategoryId"] = cat;
Check in the SQL server management studio if the user has default database set to the database you're trying to access. I had the same type of error and got stuck for days. Finally found out the user had Master set as its' default DB.
I have a Gridview in front end where Grid have two columns : ID and Order like this:
ID Order
1 1
2 2
3 3
4 4
Now user can update the order like in front end Gridview:
ID Order
1 2
2 4
3 1
4 3
Now if the user click the save button the ID and order data is being sent to Stored Procedure as #sID = (1,2,3,4) and #sOrder = (2,4,1,3)
Now if I want to update the order and make save I want to store it into database. Through Stored procedure how can update into the table so that the table is updated and while select it gives me the results like:
ID Order
1 2
2 4
3 1
4 3
There is no built in function to parse these comma separated string. However, yo can use the XML function in SQL Server to do this. Something like:
DECLARE #sID VARCHAR(100) = '1,2,3,4';
DECLARE #sOrder VARCHAR(10) = '2,4,1,3';
DECLARE #sIDASXml xml = CONVERT(xml,
'<root><s>' +
REPLACE(#sID, ',', '</s><s>') +
'</s></root>');
DECLARE #sOrderASXml xml = CONVERT(xml,
'<root><s>' +
REPLACE(#sOrder, ',', '</s><s>') +
'</s></root>');
;WITH ParsedIDs
AS
(
SELECT ID = T.c.value('.','varchar(20)'),
ROW_NUMBER() OVER(ORDER BY (SELECT 1)) AS RowNumber
FROM #sIDASXml.nodes('/root/s') T(c)
), ParsedOrders
AS
(
SELECT "Order" = T.c.value('.','varchar(20)'),
ROW_NUMBER() OVER(ORDER BY (SELECT 1)) AS RowNumber
FROM #sOrderASXml.nodes('/root/s') T(c)
)
UPDATE t
SET t."Order" = p."Order"
FROM #tableName AS t
INNER JOIN
(
SELECT i.ID, p."Order"
FROM ParsedOrders p
INNER JOIN ParsedIDs i ON p.RowNumber = i.RowNumber
) AS p ON t.ID = p.ID;
Live Demo
Then you can put this inside a stored procedure or whatever.
Note that: You didn't need to do all of this manually, it should be some way to make this gridview update the underlying data table automatically through data binding. You should search for something like this instead of all this pain.
You could use a table valued parameter to avoid sending delimiter-separated values or even XML to the database. To do this you need to:
Declare a parameter type in the database, like this:
CREATE TYPE UpdateOrderType TABLE (ID int, Order int)
After that you can define the procedure to use the parameter as
CREATE PROCEDURE UpdateOrder (#UpdateOrderValues UpdateOrderType readonly)
AS
BEGIN
UPDATE t
SET OrderID = tvp.Order
FROM <YourTable> t
INNER JOIN #UpdateOrderValues tvp ON t.ID=tvp.ID
END
As you can see, the SQL is trivial compared to parsing XML or delimited strings.
Use the parameter from C#:
using (SqlCommand command = connection.CreateCommand()) {
command.CommandText = "dbo.UpdateOrder";
command.CommandType = CommandType.StoredProcedure;
//create a table from your gridview data
DataTable paramValue = CreateDataTable(orderedData)
SqlParameter parameter = command.Parameters
.AddWithValue("#UpdateOrderValues", paramValue );
parameter.SqlDbType = SqlDbType.Structured;
parameter.TypeName = "dbo.UpdateOrderType";
command.ExecuteNonQuery();
}
where CreateDataTable is something like:
//assuming the source data has ID and Order properties
private static DataTable CreateDataTable(IEnumerable<OrderData> source) {
DataTable table = new DataTable();
table.Columns.Add("ID", typeof(int));
table.Columns.Add("Order", typeof(int));
foreach (OrderData data in source) {
table.Rows.Add(data.ID, data.Order);
}
return table;
}
(code lifted from this question)
As you can see this approach (specific to SQL-Server 2008 and up) makes it easier and more formal to pass in structured data as a parameter to a procedure. What's more, you're working with type safety all the way, so much of the parsing errors that tend to crop up in string/xml manipulation are not an issue.
You can use charindex like
DECLARE #id VARCHAR(MAX)
DECLARE #order VARCHAR(MAX)
SET #id='1,2,3,4,'
SET #order='2,4,1,3,'
WHILE CHARINDEX(',',#id) > 0
BEGIN
DECLARE #tmpid VARCHAR(50)
SET #tmpid=SUBSTRING(#id,1,(charindex(',',#id)-1))
DECLARE #tmporder VARCHAR(50)
SET #tmporder=SUBSTRING(#order,1,(charindex(',',#order)-1))
UPDATE dbo.Test SET
[Order]=#tmporder
WHERE ID=convert(int,#tmpid)
SET #id = SUBSTRING(#id,charindex(',',#id)+1,len(#id))
SET #order=SUBSTRING(#order,charindex(',',#order)+1,len(#order))
END
I'm trying to write a windows forms app in C# .Net 4 it connects to a SQL Server 2008 database and I want to Select highest number from a table where the number is stored as string!
string SQL = "select MAX(CONVERT(int, myField)) from myTable where myCode = '" + theCust + "'";
I have also tried Max(CAST(myField as Int)) in the select statement but both fail to return anything even though the Database has for the theCust two rows with 10001 and 10002. The Error i Get is "Enumeration yielded no results"
What am I doing wrong?
I'm using the in built System.Data.SqlClient and if I just do a
string SQL = "select myField from myTable where myCode = '" + theCust + "'";
it returns both numbers as strings. I know I could sort them in code but if the Database gets large that would not be a good approach!
I just tried it again with an int Field in the db and still got the same error! Is Max the wrong thing to be using?
You can try it like this:
SELECT TOP 1 CAST(MyColumn AS int) AS TheMax
FROM MyTable
ORDER BY TheMax DESC
So (using the sloppy method, always paramaterize!)
String sql = "SELECT TOP 1 CAST(MyColumn AS int) AS TheMax FROM MyTable WHERE MyParam = '" + param + "' ORDER BY TheMax Desc";
//Fill DataAdapter/DataReader etc.
Have this function in your database(s):
CREATE FUNCTION dbo.IsAllDigits (#MyString VARCHAR(8000))
RETURNS TABLE AS
RETURN (
SELECT CASE
WHEN #MyString NOT LIKE '%[^0-9]%'
THEN 1
ELSE 0
END AS IsAllDigits
)
because it's better than the in-build ISNUMERIC() in T-SQL.
Then you can use this query to get set of strings that convert to integer types without errors, and filter them like with TOP 1.
SELECT TOP 1 MyColumn AS TheMax
FROM MyTable
WHERE IsAllDigits(MyColumn)=1
ORDER BY MyColumn DESC
Scenario
I have a stored procedure that takes a single parameter. I want to update this stored procedure to take a VARIABLE NUMBER OF PARAMETERS - a number that I will never know.
I currently use SQLConnections through a C# interface in order to pass in a single parameter to the stored procedure and return a result.
The SQL Part
Lets say that I have a stored procedure that returns a list of results based on a single input parameter "#ccy" - (Currency). Now lets say that I want to update this stored procedure to take a list of Currencies instead of a single one, but that this number will be variable depending on the situation.
The SQL Code
ALTER PROCEDURE [dbo].[SEL_BootStrapperInstRICs]
(
#ccy varchar(10)
)
AS
SELECT DISTINCT i.CCY, i.Instrument, i.Tenor, r.RIC, r.[Server], r.RIType
FROM MDR.dbo.tblBootStrapperInstruments as i, MDR.dbo.tblBootStrapperRICs as r
WHERE i.Instrument = r.MurexInstrument
AND
i.Tenor = r.Tenor
AND i.CCY = r.CCY
AND i.CCY = #ccy
AND r.RIType NOT LIKE '%forward%'
The C# Part
This particular stored procedure is called from a C# WinForms application that uses the "SqlCommand.Parameters.AddWithValue()" method. As mentioned earlier this method currently passes in a single Currency as the parameter to the stored procedure and returns the result as a DataSet.
public DataSet GetBootStrapperInstRICsDS(List<string> ccys)
{
DataSet ds;
SqlConnection dbConn = null;
SqlCommand dbCmd = new SqlCommand();
try
{
dbConn = GetSQLConnection();
dbCmd = GetSqlCommand();
dbCmd.CommandType = CommandType.StoredProcedure;
dbCmd.CommandText = Utils.Instance.GetSetting ("SELBootStrapInsRics", "default");
foreach(string ccy in ccys)
dbCmd.Parameters.AddWithValue("#ccy", ccy);
dbCmd.CommandTimeout = 600;
dbCmd.Connection = dbConn;
SqlDataAdapter adapter = new SqlDataAdapter(dbCmd);
ds = new DataSet();
adapter.Fill(ds, "tblBootStrapperInstRICs");
dbCmd.Connection.Open();
return ds;
}
catch (Exception ex)
{
ApplicationException aex = new ApplicationException ("GetBootStrapperInstRICsDS", ex);
aex.Source = "Dal.GetBootStrapperInstRICsDS " + ex.Message;
MainForm.job.Log(aex.Source, Job.MessageType.Error);
Job.incurredErrors = true;
throw aex;
}
finally
{
if (dbCmd != null)
dbCmd.Dispose();
if (dbConn != null)
{
dbConn.Close();
dbConn.Dispose();
}
}
}
The Question
On the C# side I think my best option is to use a "foreach/for loop" in order to iterate through a list of parameters and dynamically add a new one to the SPROC. (I have already made this update in the C# code above).
HOWEVER - Is there some way that I can do this in the SQL Stored Procedure too? My thoughts are split with two potential options - Either create 20 or more parameters in the SPROC (each with the same name but with an incrementing number at the end e.g. - #ccy1,#ccy2 etc.) and use "for(int i=0;i
for(int i=0;i<NumberOfCurrenciesToAdd;i++)
dbCmd.Parameters.AddWithValue("#ccy"+i, currencyArray[i]);
Or the other option is to do something completely different and less rubbish and hack-esque. Help greatly appreciated.
EDIT - SQL Server 2005
EDIT2 - Must Use SPROCS - Company Specification Requirement.
You never specified SQL Server version, but for 2008 there are Table-Valued Parameters, which may help you:
Table-valued parameters are a new parameter type in SQL Server 2008. Table-valued parameters are declared by using user-defined table types. You can use table-valued parameters to send multiple rows of data to a Transact-SQL statement or a routine, such as a stored procedure or function, without creating a temporary table or many parameters.
I worked for a company that had to do this. It is much easier to just pass an nvarchar that is really a list that is comma delimited and then parse it when you get into the stored proc and insert the values into a temp table. The other option would be to have an xml parameter in your proc. That should also work. This is all for SQL 2005. 2008 does give you the table variable and that would be your best option.
I would try to stay away from dynamically changing your stored proc because I think that would be hard to maintain. At any given time if you try to look at the proc it could be different. Also, what happens when 2 people are trying to use your site and hit that proc at the same moment? One person's session will be modifying the procedure and the others will try to do it. This could cause a lock on the stored proc or it could cause other issues. Regardless it would be pretty inefficient.
Here is another option - though I think Anton's answer is better. You can pass in a csv string as a single parameter. Use a user-defined function to convert the csv string into a table of values, which you can join in your query. There are several csv parsing functions listed on SO and other places (though, sorry, I can't come up with a link right now).
edit: here is another option. Pass in the same csv string, then generate the sql query as a string in the procedure, and execute the string. Use the csv in an 'in' clause :
where i.ccy in (1,2,3,4)
I would not try to change the stored procedure, but (since you are on SQL Server 2005 and don't have table variable parameters) just pass in a comma separated list of values and let the procedure split them apart. You can change your C# loop to just build a CSV string and once you create a SQL split procedure, use it like:
SELECT
*
FROM YourTable y
INNER JOIN dbo.yourSplitFunction(#Parameter) s ON y.ID=s.Value
I prefer the number table approach to split a string in TSQL
For this method to work, you need to do this one time table setup:
SELECT TOP 10000 IDENTITY(int,1,1) AS Number
INTO Numbers
FROM sys.objects s1
CROSS JOIN sys.objects s2
ALTER TABLE Numbers ADD CONSTRAINT PK_Numbers PRIMARY KEY CLUSTERED (Number)
Once the Numbers table is set up, create this split function:
CREATE FUNCTION [dbo].[FN_ListToTable]
(
#SplitOn char(1) --REQUIRED, the character to split the #List string on
,#List varchar(8000)--REQUIRED, the list to split apart
)
RETURNS TABLE
AS
RETURN
(
----------------
--SINGLE QUERY-- --this will not return empty rows
----------------
SELECT
ListValue
FROM (SELECT
LTRIM(RTRIM(SUBSTRING(List2, number+1, CHARINDEX(#SplitOn, List2, number+1)-number - 1))) AS ListValue
FROM (
SELECT #SplitOn + #List + #SplitOn AS List2
) AS dt
INNER JOIN Numbers n ON n.Number < LEN(dt.List2)
WHERE SUBSTRING(List2, number, 1) = #SplitOn
) dt2
WHERE ListValue IS NOT NULL AND ListValue!=''
);
GO
You can now easily split a CSV string into a table and join on it:
select * from dbo.FN_ListToTable(',','1,2,3,,,4,5,6777,,,')
OUTPUT:
ListValue
-----------------------
1
2
3
4
5
6777
(6 row(s) affected)
Your can pass in a CSV string into a procedure and process only rows for the given IDs:
SELECT
y.*
FROM YourTable y
INNER JOIN dbo.FN_ListToTable(',',#GivenCSV) s ON y.ID=s.ListValue
I use this function to split CSV text into a table of numbers, it has great performance due to various optimizations (like returning a table with a primary key which greatly influence the query optimizer to produce good query plans ever for extremely large data sets).
Also it's not limited to 4000 characters, so you can pass in very large strings.
CREATE Function [dbo].[TextSplitToInt](#list text,
#delim char(1) = N',')
RETURNS #T TABLE (ID_T int primary key)
BEGIN
DECLARE #slices TABLE (slice nvarchar(4000) NOT NULL)
DECLARE #slice nvarchar(4000),
#textpos int,
#maxlen int,
#stoppos int
SELECT #textpos = 1, #maxlen = 4000 - 2
WHILE datalength(#list) / 2 - (#textpos - 1) >= #maxlen
BEGIN
SELECT #slice = substring(#list, #textpos, #maxlen)
SELECT #stoppos = #maxlen - charindex(#delim, reverse(#slice))
INSERT #slices (slice) VALUES (#delim + left(#slice, #stoppos) + #delim)
SELECT #textpos = #textpos - 1 + #stoppos + 2 -- On the other side of the comma.
END
INSERT #slices (slice)
VALUES (#delim + substring(#list, #textpos, #maxlen) + #delim)
INSERT #T (ID_T)
SELECT distinct Cast(str as int)
FROM (SELECT str = ltrim(rtrim(substring(s.slice, N.Number + 1,
charindex(#delim, s.slice, N.Number + 1) - N.Number - 1)))
FROM Numbers N
JOIN #slices s ON N.Number <= len(s.slice) - 1
AND substring(s.slice, N.Number, 1) = #delim) AS x
RETURN
END
I am currently working in C#, and I need to insert a new record into one table, get the new primary key value, and then use that as a foreign key reference in inserting several more records. The Database is MS SQL Server 2003. All help is appreciated!
The way to get the identity of the inserted row is with the SCOPE_IDENTITY() function. If you're using stored procedures then this would look something like the following to return the row identity as an output parameter.
CREATE PROCEDURE dbo.MyProcedure
(
#RowId INT = NULL OUTPUT
)
AS
INSERT INTO MyTable
(
Column1
,Column2
,...
)
VALUES
(
#Param1
,#Param2
,...
);
SET #RowId = SCOPE_IDENTITY();
You can then use this value for any subsequent inserts (alternatively, if you can pass the data all into the stored procedure, then you can use it in the remainder of the procedure body).
If you're passing the SQL in dynamically then you use much the same technique, but with a single string with statement delimiters (also ; in SQL), e.g.:
var sql = "INSERT INTO MyTable (Column1, Column2, ...) VALUES (#P1, #P2, ...);" +
"SELECT SCOPE_IDENTITY();";
Then if you execute this using ExecuteScalar you'll be able to get the identity back as the scalar result and cast it to the right type. Alternatively you could build up the whole batch in one go, e.g.
var sql = "DECLARE #RowId INT;" +
"INSERT INTO MyTable (Column1, Column2, ...) VALUES (#P1, #P2, ...);" +
"SET #RowId = SCOPE_IDENTITY();" +
"INSERT INTO MyOtherTable (Column1, ...) VALUES (#P3, #P4, ...);";
This may not be exactly the right syntax, and you may need to use SET NOCOUNT ON; at the start (my mind is rusty as I rarely use dynamic SQL) but it should get you on the right track.
The best way of doing this is the use SCOPE_IDENTITY() function in TSQL. This should be executed as part of the insert i.e.
SqlCommand cmd = new SqlCommand(#"
INSERT INTO T (Name) VALUES(#Name)
SELECT SCOPE_IDENTITY() As TheId", conn);
cmd.AddParameter("#Name", SqlDbType.VarChar, 50).Value = "Test";
int tId = (int)cmd.ExecuteScalar();
Alternatively you can assign SCOPE_IDENTITY() to a variable to be used in successive statements. e.g.
DECLARE #T1 int
INSERT INTO T (Name) VALUES('Test')
SELECT #T1 = SCOPE_IDENTITY()
INSERT INTO T2 (Name, TId) VALUES('Test', #T1)
If you are just using SQL then check Duncan's answer. If however you are using LINQ then you can create the entity, save it to the DB and the ID parameter will be populated automatically.
Given a user entity and a user table it might look like this:
using(var db = new DataContext()) {
var user = new User { Name = "Jhon" };
db.Users.InsertOnSubmit(user);
db.SubmitChanges();
/* At this point the user.ID field will have the primary key from the database */
}