Searching multiple SQL tables for data - c#

In my SQL database I have a table for every state in the USA. The user can create multiple pages in different states. Each table has a "UserID" column that will always match the logged in user. How do I search multiple tables for the "UserID"?
string sqlquery = "SELECT * FROM[all tables] WHERE #UserID ='" + userID.Text + "'";
SqlConnection conn1 = new SqlConnection(connString);
SqlCommand comm1 = new SqlCommand(sqlquery, conn1);
SqlDataReader DR1 = comm1.ExecuteReader();
if (DR1.Read())
{
Textbox1.Text = DR1.GetValue(0).ToString();
}

I haven't seen your database schema, but I can already tell you need to refactor it. There's no need to have a table for each state, and the maintenance on that could be rough. What you should have is a table holding all of the states, and then another table with a reference to the State table that holds whatever information you want (your "pages")
CREATE TABLE States
(
StateID int not null, --PK
StateName nvarchar(32) not null
)
CREATE TABLE Pages
(
PagesID int not null, --PK
StateID int not null, --FK
UserID int not null
//Whatever other columns you need
)
Now, you can query the Pages table based on a specific Page, State or User
SELECT * FROM Pages WHERE UserID = (userId)
SELECT * FROM Pages WHERE StateID IN (1, 5, 20)
SELECT * FROM Pages WHERE PageID = (pageID)

The right answer is that you need to change your database so that all this information is held in a single table. The easy answer is to take a few minutes to create a view that unions all the tables together so that you can access them as if they were all in one table.
The only difference in my answer from the others though is that I wouldn't union them in your c# code. I would just create an actual view on the database that unions them all together. The view will be simple (though long) and your code will be simple.

If tables have the same columns I would go for something like. Using the sql UNION.
var statesTables = new[]{"NY, Texas, Oregano, Alabama}";
var qBuild = new StringBuilder();
qBuild.Append(string.Format("SELECT * FROM {0} WHERE UserId = #userId ", statesTables[0]));
for(int i=1;i<stateTables.Length;i++){
qbuild.Append(string.Format("UNION SELECT * FROM {0} WHERE UserId = #userId ", statesTables[i]))
}
SqlConnection conn1 = new SqlConnection(connString);
SqlCommand comm1 = new SqlCommand(qBuild.ToString(), conn1);
comm1.Parameters.Add(new SqlParameter("userId", userId));
It will generate SQL:
SELECT * FROM NY WHERE UserId = #userId
UNION
SELECT * FROM Texas WHERE UserId = #userId
UNION
SELECT * FROM Oregano WHERE UserId = #userId
UNION
SELECT * FROM Alabama WHERE UserId = #userId
If there are some different columns, replace * with column names tables have in common.
BUT, as other suggests, refactoring your DB schema would be the best!

Related

C# Dynamic full outer join on 2 datatables

I need to do a full outer join on 2 datatables dinamically, I don't know what columns are in the datatables but they will be the same in each table, I also only have the name of the column I need to do the join on in a variable. Is there a way of doing this?
What I need to do is join 2 datatables in a C# script. I'm using a Dataflow in an SSIS to get data from a couple of files, and at the end I need to compare the 2 final sets of data. I need to to this on whatever 2 datatables as long as they have the same columns, so I can't finish the process in an SSIS as I need to specify the columns.
The GetData() I just use it in case I need to compare 2 tables but donnesPROD and donnesDEV are filled from object variables in the SSIS.
Here's my code so far :
DataTable donnesPROD = GetData(connectionPROD, sql_request);
DataTable donnesDEV = GetData(connectionDEV, sql_request);
Here's the code for GetData :
DataTable GetData(string cs, string query)
{
OleDbConnection conn = new OleDbConnection(cs);
conn.Open();
OleDbCommand cmd = new OleDbCommand(query, conn);
DataTable dt = new DataTable();
dt.Load(cmd.ExecuteReader());
conn.Close();
return dt;
}
I have the list of the columns in another datatable, and I have the name of the primary key in a string variable key. From here I need to be able to do a fullouterjoin of donnesPROD and donnesDEV on key. Can this be done this way? Or is there a way of generating the script code it self dynamically and then execute it?
You have two options.
Conditional joins
If you don't know the specific column name, but you do have some idea what the column name might be, you could do a conditional join like this:
CREATE PROCEDURE ExampleDynamicJoin(#JoinColumn AS VarChar(40))
AS
BEGIN
SELECT *
FROM TableA
JOIN TableB ON (#JoinColumn = 'ColumnA' AND TableA.ColumnA = TableB.ColumnA)
OR (#JoinColumn = 'ColumnB' AND TableA.ColumnB = TableB.ColumnB)
OR (#JoinColumn = 'ColumnC' AND TableA.ColumnC = TableB.ColumnC)
END
You may not get the best performance out of this (the conditional joins will confuse the query engine and it may not pick the best index, if it picks one at all). If the table is very large you could also do something like this. It is a bit painful-looking but will get better performance:
CREATE PROCEDURE ExampleDynamicJoin(#JoinColumn AS VarChar(40))
AS
BEGIN
IF (#JoinColumn = 'ColumnA') BEGIN
SELECT *
FROM TableA
JOIN TableB ON TableA.ColumnA = TableB.ColumnA
END
IF (#JoinColumn = 'ColumnB') BEGIN
SELECT *
FROM TableA
JOIN TableB ON TableA.ColumnB = TableB.ColumnB
END
IF (#JoinColumn = 'ColumnC') BEGIN
SELECT *
FROM TableA
JOIN TableB ON TableA.ColumnC = TableB.ColumnC
END
END
If TableA or TableA are part of a larger query, and you'd end up duplicating tons of SQL, you could always extract the resultset for just TableA and TableB into a temporary table, then use the temporary table in the larger query.
Dynamic SQL
If you don't have the foggiest about the column name and there are tons of possibilities, you could construct the SQL as a string and join that way. You should validate the column name that is passed in; not only will that make sure the column actually exists, but it will prevent the dynamic SQL from being constructed when #JoinColumn contains an injection attack, since legal column names do not contain SQL statements. Example:
CREATE PROCEDURE ExampleDynamicJoin(#JoinColumn AS VarChar(40))
AS
BEGIN
DECLARE #Sql AS VarChar(MAX)
IF NOT EXISTS
(
SELECT 0
FROM syscolumns c
JOIN sysobjects o ON o.id = c.id
WHERE o.Name = 'TableA'
AND c.Name = #JoinColumn
)
RAISERROR (15600,-1,-1, 'ExampleDynamicJoin'); //Throw error if column doesn't exist
SET #Sql =
'SELECT *
FROM TableA
JOIN TableB ON TableA.' + #JoinColumn + ' = TableB.' + #JoinColumn
sp_ExecuteSql #Sql
END
Or, if you don't use stored procedures,
DataTable ExampleDynamicJoin(string joinColumn)
{
if (!ValidateColumn(joinColumn)) throw new ArgumentException();
var sql = String.Format(
#"SELECT *
FROM TableA
JOIN TableB ON TableA.{0} = TableB.{0}",
joinColumn
);
using (var connection = GetConnectionFromSomewhere())
{
using (var cmd = new SqlCommand
{
CommandText = sql,
CommandType = CommandType.Text,
Connection = connection
})
{
var reader = cmd.ExecuteReader();
var table = new DataTable();
table.Load(reader);
return table;
}
}
}
When using dynamic SQL you should always use parameters if possible. But you can't use parameters as a column name, so in this case you have to concatenate. When concatenating, ALWAYS white list the inputs. That is why I included a function named ValidateColumn which could look like this:
bool ValidateColumn(string columnName)
{
switch columnName.ToUpper()
{
case "COLUMNA":
case "COLUMNB":
case "COLUMNC":
return true;
default:
return false;
}
}

can I combine a select count(*) and sum(column) in a single select?

I am presently pulling two long values from my database with two selects:
// We need the number of users and the total bandwidth
long numUsers, totalBandwidth;
using (IDbCommand cmd = conn.CreateCommand())
{
cmd.CommandText = "select count(*) from [User] where [User].CompanyId = #CompanyId";
DbUtilities.AddParam(cmd, "#CompanyId", id);
numUsers = (long)(decimal)cmd.ExecuteScalar();
}
using (IDbCommand cmd = conn.CreateCommand())
{
cmd.CommandText = "select sum(BandwidthThisMonth) from [User] where [User].CompanyId = #CompanyId";
DbUtilities.AddParam(cmd, "#CompanyId", id);
totalBandwidth = (long)(decimal)cmd.ExecuteScalar();
}
I think logically this can be a single select that returns two numbers. But everything I've tried has given me errors. Can this be done?
select count(*) as count_all,
sum(BandwidthThisMonth) as sum_BandwidthThisMonth
from [User]
where [User].CompanyId = #CompanyId
but you'll get two columns back instead of one scalar. so you'll need to handle that...
If you want them in the same column, you can use a union. This scans the table 2 times, so not as efficient.
select "count" as key
, count(*) as value
from [User]
where [User].CompanyId = #CompanyId
union all
select "sum_BandwidthThisMonth" key
, sum(BandwidthThisMonth) as value
from [User]
where [User].CompanyId = #CompanyId
Use union all if you don't want a sort. Union does a sort. Not a big deal with 2 rows, but...

How to get data from one table based on a data from another table

I have a two tables
tblstudent (StudentID (PK), StudentNumber, Name & Surname)
and
tblfingerprint (ID (PK), StudentID (FK), Template)
I have a web service that selects a Template based on their ID. However, I want to be able to select the template from tblFingerprint based on the given StudentNumber and not StudentID in tblStudent.
I have the following code so far. I'm not sure if I need to use leftjoin or innerjoin?
[WebMethod]
public Verification StuVerification(Student student)
{
cn.Open();
SqlCommand com = new SqlCommand("SELECT Template FROM tblFingerprint WHERE ID = '"+ student.StudentNumber + "'", cn);
//SqlCommand com = new SqlCommand("SELECT Template FROM tblFingerprint WHERE ID = '" + ID + "'", cn);
//com.Parameters.AddWithValue("#Template", Template);
SqlDataReader sr = com.ExecuteReader();
while (sr.Read())
{
Verification verification = new Verification()
{
StudentID = sr.GetInt32(0),
StudentNumber = sr.GetString(1),
Name = sr.GetString(2),
Surname = sr.GetString(3),
};
cn.Close();
return verification;
}
cn.Close();
return new Verification();
}
Use an inner join between the two tables, but as usual use a parameterized query
string cmdText = #"SELECT s.StudentID, s.StudentNumber, s.Name, s.Surname, f.Template
FROM tblStudent s INNER JOIN tblFingerprint f
ON t.StudentID = f.StudentID
WHERE s.StudentNumber = #numb";
SqlCommand com = new SqlCommand(cmdText, cn);
com.Parameters.AddWithValue("#numb", student.StudentNumber);
In an INNER JOIN only the records of the first table that have corresponding records on the second table are returned (in your case only the students that have a corresponding record in the tblFingerprint table)
In a LEFT JOIN every records of the first table are returned with the corresponding values of the second table. If any student has no record in the tblFingerprint, the Template field will be null
Also, note that you try to read 4 fields from your datareader, but the actual query contains just the Template field. Thus I have added the missing fields to your query
First of all, if you did a little search for yourself you would find thousands of pages with information about it.
OT: you can join the table
SELECT Template FROM tblFingerprint WHERE ID = '"+ student.StudentNumber + "'
SELECT Template
FROM tblFingerprint F
INNER JOIN tblStudent S
ON F.StudentID = S.StudentID
WHERE S.StudentNumber = '"+ student.StudentNumber + "'"
Hint: Work with parameters, in this example there a major risk for SQL Injection
JOIN is equivalent to INNER JOIN on SQL Server so just use the following. I've added aliases to the table names to make it less verbose.
I've based it on your example so I have to assume that StudentNumber is a string field. If not then just remove the string delimiters from your query.
"SELECT Template FROM tblFingerprint fp
JOIN tblstudent s ON fp.StudentID = s.StudentID
WHERE s.StudentNumber = '"+ student.StudentNumber + "'"
Hope this helps.
Al.

c# database retrieving informations

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.

Trying to filter datagrid according to selections made in dropdown lists

I have a View Feedback page with two drop down lists to filter the gridview of answers to questions. The 1st ddList is Modules, once this is selected, the 2nd ddList of Questions becomes enabled and the user can select a question to see the answers for it in relation to the module selected, OR they can select to see all answers to all questions for the selected module.
I have it working if they select a question but if they select all I simply get all answers, not just the ones specific to the selected module.
Sorry I know thats not the clearest explanation but any help would be awesome.
My tables:
CREATE TABLE tblModules
(
Module_ID nvarchar(10) PRIMARY KEY,
Module_Title nvarchar(MAX) NOT NULL
);
CREATE TABLE tblQuestions
(
Q_ID int PRIMARY KEY IDENTITY(1,1),
Question_Text varchar(1000) NOT NULL
);
CREATE TABLE tblFeedback
(
Submission_ID int PRIMARY KEY IDENTITY(1,1),
Username varchar(100) NOT NULL,
Domain varchar(50) NOT NULL,
DateTime_Submitted datetime NOT NULL
Module_ID nvarchar(10)
FOREIGN KEY (Module_ID) REFERENCES tblModules (Module_ID);
);
CREATE TABLE tblAnswers
(
Q_ID int NOT NULL,
Submission_ID int NOT NULL,
Answer_Text varchar(max),
FOREIGN KEY (Q_ID) REFERENCES tblQuestions(Q_ID),
FOREIGN KEY (Submission_ID) REFERENCES tblFeedback(Submission_ID)
);
Here is the snippet of code I'm using to construct the Sql statement which is used to select the data which is then bound to my grid.
// If they have selected view all questions, get all answers for module
if (ddQuestions.SelectedItem.Value == "all")
{
//selectQuery = "SELECT * FROM tblAnswers ORDER BY Submission_ID";
selective = false;
//gridviewFeedback.Columns[3].Visible = true;
selectQuery = "SELECT * FROM tblAnswers A ";
selectQuery += "WHERE EXISTS (SELECT * FROM tblModules M JOIN tblFeedback F ON M.Module_ID = F.Module_ID ";
selectQuery += "WHERE F.Module_ID = '" + this.selectedModuleID + "')";
}
// Instead, if they have selected a specific question, get the information for the selected module and question
else
{
selectQuery = "SELECT * FROM tblAnswers WHERE Q_ID = '" + qID + "' ORDER BY Submission_ID";
selective = true;
//gridviewFeedback.Columns[3].Visible = false;
}
DataSet objDs = new DataSet();
SqlDataAdapter myCommand2;
myCommand2 = new SqlDataAdapter(selectQuery, myConnection);
myCommand2.SelectCommand.CommandType = CommandType.Text;
myCommand2.Fill(objDs);
gridviewFeedback.DataSource = objDs;
gridviewFeedback.DataBind();
I think its my SQL statement as I'm not very experienced at SQL and have kind of edited a statement I used elsewhere that was given to me by someone else.
UPDATE Just realised my other statement isnt working either - its not providing module specific answers either.
You don't seem to have correlated table A into Table M or Table F. This will give you all rows in Table A if there are any modules or feedback anywhere with the provided module ID. You need to add an AND statement to correlate table A with what's in your EXISTS clause.
selectQuery = #"
SELECT * FROM tblAnswers A
WHERE EXISTS (
SELECT * FROM tblModules M
JOIN tblFeedback F ON M.Module_ID = F.Module_ID
WHERE F.Module_ID = #ModuleID
AND A.Submission_ID = F.Submission_ID)";
Also please consider using parameterized queries instead of appending variables into your strings. This causes SQL Server to use the plan cache more efficiently, which is faster, and also eliminates the possibility of a SQL injection attack.
Try this query instead. I think this should do it.
This should help you get all the answers belonging to a specific module
SELECT ans.* FROM tblAnswers ans, tblFeedback fb
WHERE ans.Submission_ID = fb.Submission_ID
AND fb.Module_ID = 'selected module'

Categories

Resources