6

In my database there are 3 column which is Name, Age, Gender. In the program, I only want to use 1 search button. When the button is clicked, the program determine which 3 of the textbox has input and search for the right data.

How do you work with the query? For example if Name and Gender has text, the query :

"Select * from table Where (Name = @name) AND (Gender = @gender)"

And when only name is entered, I only query for the name. Must I check textbox by textbox whether there is user input and then write multiple query for each of them? Or is there a better way to do this?

Edit (29/5/16) : I tried doing this another way like this

myCommand = New MySqlCommand("Select * from project_record Where
                       (FloatNo = @floatNo OR FloatNo = 'None') AND 
                       (DevCompanyName = @devCompanyName OR DevCompanyName = 'None') AND 
                       (DevType = @devType OR DevType = 'None') AND 
                       (LotPt = @lotPt OR LotPt = 'None') AND
                       (Mukim = @mukim OR Mukim = 'None') AND
                       (Daerah = @daerah OR Daerah = 'None') AND
                       (Negeri = @negeri OR Negeri = 'None') AND
                       (TempReference = @tempRef OR TempReference = 'None')", sqlConn)

But as you can guess already it will not work efficiently as well because if I only enter input for DevType and leave other textboxes blank, the query will not pull up all the records for DevType only. It will just display as no records.

Rick James
  • 135,179
  • 13
  • 127
  • 222
Student
  • 432
  • 2
  • 10
  • 30
  • it is much more better tactic to filter your results directly in your MySQL database. And if this is done with a simple query it is much more preferable! I have not seen any database schema but i think you must have set up indexes. @RickJames check out how to use `OR` in your query but still get single results if needed. –  Jun 04 '16 at 00:12
  • i really can't understand something... you use multiple AND's in your query. Why? You can query for getting both single and multiple results by using `OR`. One more thing i can't understand is this (i hope i am not lost in translation), this is from your question `In my database there are 3 column which is Name, Age, Gender. In the program, I only want to use 1 search button. When the button is clicked, the program determine which 3 of the textbox has input and search for the right data.` From the above i understand that if an input value is null or 0 to not participate in query results,right? –  Jun 05 '16 at 22:57
  • @PeterDarmis Yes correct. And that is why I put the AND clause because I am trying to make the query determine which textboxes has value then only search in the database. Also the column above is just an example. Not an actual column name in my program. My mistake for not clarifying it earlier – Student Jun 06 '16 at 14:38
  • 1
    i was going to tell to check my answer but as i see, you already got votes for correct answers. Anyway do try the answer, i think it covers you better. –  Jun 06 '16 at 16:47
  • Will do! Thanks a lot too! Every answer counts! – Student Jun 06 '16 at 22:10

8 Answers8

4
Select * from table 
Where (Name = @name OR @name is Null) 
  AND (Gender = @gender OR @gender is Null)
 ...

it should be one query

StanislavL
  • 56,971
  • 9
  • 68
  • 98
  • Not sure why but the datagridview is not showing the records. Have a look here [link](http://stackoverflow.com/questions/36884228/vb-net-mysql-datagridview-is-empty) – Student Apr 27 '16 at 08:31
  • you can also try **OR @gender = ''** just check what is sent to DB in case of empty textfield – StanislavL Apr 27 '16 at 08:49
  • It doesn't work. Lets say my textbox input for name is 'John'. The query will be `Name = John OR John is Null`. – Student May 29 '16 at 14:27
  • yes. and what's wrong with this? if you use paratrized query. 'John' is null - false and name='John' is true. So the result all 'John' records are found – StanislavL May 30 '16 at 14:08
  • change the query to Name = John OR Name is Null – iLikeMySql May 31 '16 at 14:21
4

Other answers have explained how to simplify the query. It is especially important to get rid of the ORs, since they inhibit any use of indexes.

Once you have the query build cleanly, you need to think about the dataset and decide which columns are usually used for filtering. Then make a few INDEXes for them. You won't be able to provide 'all' possible indexes, hence my admonition that you think about the dataset.

When building indexes, you can have single-column or multiple-column indexes. For your type of data, I would suggest starting with several 2-column indexes. Make sure each index starts with a different column.

For Where (Name = @name) AND (Gender = @gender), here are some notes:

INDEX(gender) is useless because of low 'cardinality';
INDEX(gender, name) might be useful, but the following would be better:
INDEX(name)

Things like name and DevCompanyName are virtually unique, so a 1-column index is probably good.

If you had gender and age, then INDEX(age, gender) might be useful.

MySQL will almost never use two indexes for a single SELECT.

By the way, the construction of the WHERE could be done in a Stored Procedure. You would need CONCAT, PREPARE, etc.

Rick James
  • 135,179
  • 13
  • 127
  • 222
  • What is INDEX and how does it work? Also what do you mean by stored procedure for the Where clause? – Student May 30 '16 at 06:07
  • 3
    Cease all progress with databases until you read about "keys" and "indexes"! They are vital to performance of SQL queries. – Rick James May 30 '16 at 19:11
  • @RickJames couldn't have said it better! – Trevor Jun 02 '16 at 20:52
  • @RickJames yes setting up a database with the correct keys,indexes,foreign keys etc is best practice and the initial database schema should be like that. But besides that simplifying a query to get the results we need even if we can't re-design the database is also needed sometimes. Additionally i can't understand why someone can't get results using `OR` between the fields??? –  Jun 04 '16 at 00:29
  • The answer by iLikeMySql should work, but be slower than building the 'minimal' query, as Robin suggests. – Rick James Jun 04 '16 at 06:25
2

Original answer

(scroll down to see update)

Can you try the following:

  • build a list only including values of the textboxes that have an input
  • set a string of the join the items of that list together with the " AND " string
  • append that string to your standard SELECT statement

The code looks like this:

Private Sub Button1_Click(sender As Object, e As EventArgs) Handles Button1.Click

    Dim Predicate1 As String = Me.TextBox1.Text
    Dim Predicate2 As String = Me.TextBox2.Text
    Dim Predicate3 As String = Me.TextBox3.Text
    Dim PredicateList As New List(Of String)
    Dim WhereClause As String
    Dim Query As String

    If Predicate1 <> String.Empty Then
        PredicateList.Add("Name=""" & Predicate1 & """")
    End If
    If Predicate2 <> String.Empty Then
        PredicateList.Add("Age=""" & Predicate2 & """")
    End If
    If Predicate3 <> String.Empty Then
        PredicateList.Add("Gender=""" & Predicate3 & """")
    End If

    WhereClause = String.Join(" AND ", PredicateList.ToArray)
    Query = "SELECT * FROM TABLE WHERE " & WhereClause
    MessageBox.Show(Query)

End Sub

Update

Further to the comments re SQL injection, here is an updated sample.

Dim Command As SqlClient.SqlCommand
Dim Predicate1 As String = Me.TextBox1.Text
Dim Predicate2 As String = Me.TextBox2.Text
Dim Predicate3 As String = Me.TextBox2.Text
Dim ParameterList As New List(Of SqlClient.SqlParameter)
Dim PredicateList As New List(Of String)
Dim BaseQuery As String = "SELECT * FROM TABLE WHERE "

If Predicate1 <> String.Empty Then
    PredicateList.Add("name = @name")
    ParameterList.Add(New SqlClient.SqlParameter("@name", Predicate1))
End If
If Predicate2 <> String.Empty Then
    PredicateList.Add("age = @age")
    ParameterList.Add(New SqlClient.SqlParameter("@age", Predicate2))
End If
If Predicate3 <> String.Empty Then
    PredicateList.Add("gender = @gender")
    ParameterList.Add(New SqlClient.SqlParameter("@gender", Predicate3))
End If

Command = New SqlClient.SqlCommand(BaseQuery & String.Join(" AND ", PredicateList.ToArray))
Command.Parameters.AddRange(ParameterList.ToArray)
Robin Mackenzie
  • 18,801
  • 7
  • 38
  • 56
  • I did thought of this method too. But how would you know when to stop the AND at the end of which query? If only the last two textboxes have no input, the AND clause for the last two query would cause an error I think – Student May 29 '16 at 15:51
  • 1
    Please try it out - wherever some TextBox is empty then it won't be included in the list. Therefore the join method on the list only includes the non-empty inputs. – Robin Mackenzie May 29 '16 at 15:54
  • It did works out well enough. Though I personally do not understand quite well about your predicatelist. Do you mind to explain more about it? Also I made some changes for the `If` statement because if the input is a blank space, the query will pull up wrong records. – Student May 29 '16 at 16:04
  • 1
    Lists are collections from which you can add and remove items. For your requirements we are only adding non-empty items to the list. Once we have all the non-empty items we join them with the string " AND " and append to the SELECT. If you want to exclude items where the user accidentally input a space then use `If Trim(Predicate1) = String.Empty Then ...` etc – Robin Mackenzie May 29 '16 at 16:08
  • Thanks a lot. So far it is working fine from previous few debugging. I will continue to test it out further by continuing to add more of my original code and see how it goes. Once all is right I will accept your answer. – Student May 29 '16 at 16:20
  • Also if I enter `"` as input, it will give out error. Does this mean it is vulnerable to sql injection attacks? – Student May 29 '16 at 16:36
  • 2
    Yes it does. Moreover, try to filter/find a name like `O'Brian` or `D'Artagnan` and it will crash. – Ňɏssa Pøngjǣrdenlarp May 29 '16 at 20:37
  • @Plutonix So should I just remove the predicate1,2,... And replace it all with parameters string eg, $predicate1,$predicate2,... And using addwithvalue for those parameters? – Student May 29 '16 at 20:57
  • 3
    I cant believe this is accepted answer! You should *NEVER* concatenate query text with user input! This is serious security vulnerability as it opens doors for SQL injections. *Always use parameters to pass values to the command!* Wake up people, it is 2016!!! – Kaspars Ozols Jun 03 '16 at 23:35
  • @KasparsOzols I accepted this answer because this answer gave me the idea and ways to overcome my problem. Issue like sql injections in this answer does not really matter to me because I know how to fix it myself. So basically I accepted this answer because it fix my problem. Nothing to do about sql injection problem. – Student Jun 06 '16 at 22:13
  • 1
    @Student, I understand your reasons and this is great that you are just taking only good parts out of this, but there are so many people around that dont. They are blindly copy-pasting solutions and use in their code which goes into production. Answers like this make things like SQL injections still be around. This is very trivial error that should have been gone already by today. I would encorage answer owner to go and edit this answer and add parameters. – Kaspars Ozols Jun 07 '16 at 04:29
  • @KasparsOzols True. I am expecting the owner of this answer to fix the answer soon too. I just accept the answer based on what I questioned. So to the owner of the answer is responsible to amend his/her answer properly – Student Jun 07 '16 at 04:31
  • Update in the answer – Robin Mackenzie Jun 07 '16 at 11:14
2

COALESCE is your friend here. You can use it to make the where clause ignore comparisons where the parameter is NULL.

Select * from table Where (Name = COALESCE(@name,table.Name))
AND (Gender = COALESCE(@gender,table.Gender))

So, if the @name parameter is NULL, COALESCE(@name,table.Name) will return the value of the 'Name' column of the current row and (Name = COALESCE(@name,table.Name)) will always be true.

This assumes that if no value is entered in a textbox the corresponding parameter will be NULL. If instead it is a value such as 'None', you can use the NULLIF function to map 'None' to NULL

Select * from table Where 
(Name = COALESCE( NULLIF( @name, 'None'), table.Name)) 
AND (Gender = COALESCE( NULLIF( @gender, 'None'), table.Gender))
Blubberguy22
  • 1,344
  • 1
  • 17
  • 29
DCJones
  • 21
  • 2
1

How to implement a more efficient search?

The answer partly depends on what your definition of efficient is. I suspect you mean less code and fewer if blocks etc. But fundamentally, running a new SELECT * query to apply a filter is inefficient because your base data set can be all the rows and you just fiddle with the users View of it.

I have a DB with random data in columns for Fish, Color (string), Bird, Group (int) and Active which should be similar enough for Name, Age and Gender in the question - or that other long thing at the bottom.

DataTable

Fill a datatable and bind it to a DGV:

' form level object
Private dtSample As DataTable
...
' elsewhere
Dim sql = "SELECT  Id, Name, Descr, `Group`, Fish, Bird, Color, Active FROM Sample"
Using dbcon As MySqlConnection = New MySqlConnection(MySQLConnStr)
    ' create SELECT command with the Query and conn
    Dim cmd As New MySqlCommand(sql, dbcon)
    ...
    daSample.Fill(dtSample)
    daSample.FillSchema(dtSimple, SchemaType.Source)
End Using
dgv2.DataSource = dtSample

Going forward, we can filter the user's view of that table without issuing a new query.

Filter Controls

If some of the fields are limited to certain selections, for instance Gender, you can use a ComboBox instead of a TextBox. This is to help the user succeed and avoid typos (Make or Mael instead of Male; or here, correctly spelling Baracuda I mean Baraccuda, er Barracuda correctly.

For illustration purposes, Fish is something where the user can type in anything at all, but Bird is constrained to a set of choices. If there is a Bird table, cboBird can be bound or populated from it. But you may also be able to populate it from the master/base table:

Dim birds = dtSample.AsEnumerable.Where(Function(d) d.IsNull(5) = False).
            Select(Function(d) d.Field(Of String)("Bird")).
            Distinct.
            ToArray()
cboBird.Items.AddRange(birds)

If "Finch" is a legal choice but there are none in the database, it wont show in the list. Depending on the app, this can be a Good Thing:

  • If the user filters on Finch and there a no resulting records, you won't need a MessageBox or StatusBar message explaining the empty result set.
  • If something is not in the list, you are signalling up front that there are none of those. It then becomes a matter of training why a known element isnt in the list.
  • On the other hand, you'd have to repopulate those filter controls each time before they are used in case new records were added recently. If the controls are on a Dialog or different TabPage, this is easy to do as needed.
  • It isnt always applicable, but it can help the user avoid typos.

It depends on the app whether either method is of value.

DBNull / 'none'

I am not sure why you are adding 'none' to each clause. If someone want to see all the 'John` or all the 'Cod' records, it doesn't seem like they would also be interested in 'none'. Personally, Null/DBNull seems a better way to handle this, but it is easy to add or not add either form.

It would seem more valuable to filter to just those with DBNull/None. The code above for the Bird List filters out DBNull and I would do so for none as well. Then, before the result is added to the ComboBox, add a `None' item first so it is at the top.

Again it depends on what the app does; Or = 'None', may make perfect sense in this case.

Filter

Using a TextBox for Fish and Group, a ComboBox for Bird and Color and a CheckBox for Active, the code can form the filter thusly:

Dim filterTerms As New List(Of String)
Dim filterFmt = "{0} = '{1}' "
'   OR:
' Dim filterFmt = "{0} = '{1}' OR {0} Is Null"
'   OR:
' Dim filterFmt = "{0} = '{1}' OR {0} = 'none'"
    
If String.IsNullOrEmpty(tbSearchFish.Text) = False Then
    Dim txt = tbSearchFish.Text.Replace("'", "''")
    filterTerms.Add(String.Format(filterFmt, "Fish", txt))
End If

If cboBird.SelectedIndex > -1 Then
    filterTerms.Add(String.Format(filterFmt, "Bird", cboBird.SelectedItem.ToString))
End If

If String.IsNullOrEmpty(tbGroup.Text) = False Then
    Dim n As Int32
    If Int32.TryParse(tbGroup.Text, n) Then
        filterTerms.Add(String.Format(filterFmt, "[Group]", n))
    End If
End If

If cboColor.SelectedIndex > -1 Then
    filterTerms.Add(String.Format(filterFmt, "Color", cboColor.SelectedItem.ToString))
End If

If chkActive.Checked Then
    ' NOTE: I do not have TreatTinyAsBoolean turned on
    '  for some reason
    filterTerms.Add(String.Format(filterFmt, "Active", "1"))
End If

If filterTerms.Count > 0 Then
    Dim filter = String.Join(" AND ", filterTerms)
    dtSample.DefaultView.RowFilter = filter

    Dim rows = dtSample.DefaultView.Count
End If
  • Use whichever filterFmt is appropriate for what the app needs to do
  • A filter term is only added to the list if the related control has a value (as per above, this could include a 'None').
  • For the TextBox, it escapes any embedded ticks such as might be found in names like O'Malley or D'Artgnan. It replaces one tick with two.
  • Since Group is a numeric, a valid Int32 input is tested
  • If there are elements in the filterTerms list, a filter string is created
  • The filter is applied to the DefaultView.Filter (you can use also use a DataView or a BindingSource) so that the code need not query the database to provide filter capabilities.
  • Rows will tell you how many rows are in the current View.

The only halfway tricky one is a Boolean like Gender or Active because those actually resolve to three choices: {Any/Either, A, B}. For that, I would use a ComboBox and ignore it for SelectedIndex 0 as well. I didn't bother with this because the Combo concept is amply covered. Result:

enter image description here

Is it More "Efficient"?

It still depends.
enter image description here It doesn't re-query the database to get rows the app can already have.
enter image description here No new DBConnection, DBCommand or other DBProvider objects are created, just a list.
enter image description here No need to dynamically create a SQL statement with N parameters in a loop to avoid SQL injection/special words and chars.
enter image description here It doesn't even query the database for the items for the filter terms. If there is a static list of them in the DB, they could be loaded once, the first time they use the filters.
enter image description here It is easy to remove the filter, no need to query yet again without WHERE clauses.
enter image description here A ComboBox where applicable helps the user find what they want and avoid typos.
enter image description here Is the SQL "cleaner". more "efficient? The code doesn't really mess with new SQL, just some WHERE clauses.

Is there less code? I have no idea since we just see the result. It doesnt string me as a lot of code to do what it does.

Community
  • 1
  • 1
Ňɏssa Pøngjǣrdenlarp
  • 38,411
  • 12
  • 59
  • 178
  • Thanks so much for the expert answer. But I honestly do not get most part of it because I am still new and learning. Also, the application I am doing will be using by at least 3 different people and I want it to get latest updated records because one person maybe continuously entering records while another is searching for records.. – Student May 29 '16 at 20:51
  • Multiuser does not mean you need to requery the DB. If you have a fully configured DataAdapter, `myDA.Fill(myDT)` will **refresh** the datatable getting any new rows added by others and remove those deleted by others **without** building a whole new query. I suspected that there would be some knowledge gaps which is why there is lots of explanation there. The filter code is pretty simple, add it to some test project to see how it works. – Ňɏssa Pøngjǣrdenlarp May 29 '16 at 21:08
  • Think of the `.RowFilter` as magically adding a desired `WHERE` clause to the existing query - which is what it does. If the datatable starts with all the rows, you just need to add the desired filter. For multi-user, the only change would be to do `daSample.Fill(dtSample)` at the start to pick up any committed changes since the last time. See [this answer](http://stackoverflow.com/a/33702351/1070452) for setting up the DataAdapter to be smart – Ňɏssa Pøngjǣrdenlarp May 29 '16 at 21:31
  • This in general isn't good for large tables depending on the data that you want coming back. For example we have a few system tables that store geospatial images that add up very quickly (1 tables is around 12 gigs) and therefore doing a select all upfront would be very costly and useless. Although the solution is good, but honestly depends on case by case. – Trevor May 30 '16 at 02:15
  • If one could load everything in memory in one go, then he would not care about optimizing DB query as it would be blazingly fast anyways. – Kaspars Ozols Jun 03 '16 at 23:48
  • @KasparsOzols I believe the way the question is posed that the OP is talking about the way the code applies multiple filters from one button click. Elsewhere, I was told 2k rows. – Ňɏssa Pøngjǣrdenlarp Jun 04 '16 at 01:15
1

In my database there are 3 column which is Name, Age, Gender. In the program, I only want to use 1 search button. When the button is clicked, the program determine which 3 of the textbox has input and search for the right data.

And when only name is entered, I only query for the name. Must I check textbox by textbox whether there is user input and then write multiple query for each of them? Or is there a better way to do this?

SELECT * FROM `table`
WHERE (`name` = @name AND `name` IS NOT NULL)
OR (`age` = @age AND (`age`>0 OR `age` IS NOT NULL))
OR (`gender` = @gender AND `gender` IS NOT NULL);

With the above query if all text boxes have value, the result will not be one record (as if you where using logical AND between fields). If you want only that record you will filter it server-side with php from the rest of the results.

You can check the results on your own in this Fiddle

EDIT

In order to solve the above inconvenience (not bringing easily single results when needed) i got a little help from this answer and re-wrote the above query as:

SELECT *, IF(`name`=@name, 10, 0) + IF(`age`=@age, 10, 0) + IF(`gender`=@gender, 10, 0) AS `weight` 
FROM `table` 
WHERE (`name` = @name AND `name` IS NOT NULL) 
OR (`age` = @age AND (`age`>0 OR `age` IS NOT NULL)) 
OR (`gender` = @gender AND `gender` IS NOT NULL) 
HAVING `weight`=30;

enter image description here OR to still get all records with a weight on result

SELECT *, IF(`name`=@name, 10, 0) + IF(`age`=@age, 10, 0) + IF(`gender`=@gender, 10, 0) AS `weight` 
FROM `table` WHERE (`name` = @name AND `name` IS NOT NULL) 
OR (`age` = @age AND (`age`>0 OR `age` IS NOT NULL)) 
OR (`gender` = @gender AND `gender` IS NOT NULL) 
ORDER BY `weight` DESC;

enter image description here

Community
  • 1
  • 1
1

You were pretty close. Let's look at

(FloatNo = @floatNo OR FloatNo = 'None')

So you want the field either to be the given input or 'None'? But there are (supposedly) no records in your table with FloatNo 'None'. What you really want to do is find out whether the input is none (i.e. empty):

(FloatNo = @floatNo OR @floatNo = '')

And for the case the user types in a blank by mistake, you can ignore this, too:

(FloatNo = @floatNo OR TRIM(@floatNo) = '')

The whole thing:

myCommand = New MySqlCommand(
  "Select * from project_record Where
         (FloatNo = @floatNo OR TRIM(@floatNo) = '') AND 
         (DevCompanyName = @devCompanyName OR TRIM(@devCompanyName) = '') AND 
         (DevType = @devType OR TRIM(@devType) = '') AND 
         (LotPt = @lotPt OR TRIM(@lotPt) = '') AND
         (Mukim = @mukim OR TRIM(@mukim) = '') AND
         (Daerah = @daerah OR TRIM(@daerah) = '') AND
         (Negeri = @negeri OR TRIM(@negeri) = '') AND
         (TempReference = @tempRef OR TRIM(@tempRef) = '')", sqlConn)
Thorsten Kettner
  • 89,309
  • 7
  • 49
  • 73
0

What is wrong with your approach?

Just change (FloatNo = @floatNo OR FloatNo = 'None')
to
(FloatNo = @floatNo OR FloatNo = '' or FloatNo IS NULL)

And do that for every criteria.
Your query will respect empty values and NULL values after that.

iLikeMySql
  • 736
  • 3
  • 7