PHP search many inputs [duplicate] - php

This question already has answers here:
Search Form with One or More (Multiple) Parameters
(2 answers)
Closed 4 years ago.
I have search field in which user can specify a lot of values to search like price, surface, year, garden, balcony etc.
In my search there is not even one field required every one is optional so user can provide 0 inputs filled or all.
Basically all this info's are saved in my database but I don't really know how to structure my code.
At the moment I have PHP file which I call from front and in this file I'm checking which field was filled and I'm executing method from class which do select to db and return data. This is working fine for every input separately but when I combain for example 2 different fields like price and surface then none of methods will be executed.
Im basically asking about an idea for architecture of search where user can fullfill many different fields. Im not using any PHP framework.
I could do something like:
if(a & b & c & d & e & f) then execute method a
if(a & b & c & d & e) then execute method b
if(a & b & c & d) then execute method c
and so on.. where this letters(a, b, c etc...) are $_POST['something'] but I would have a lots of if's to check which POST (which inputs) user fullfill and sent. Later on I would need to create a lot of methods in class with different SELECTs to db basing on which POST we have... I don't think that's best solution because I would basically repeat my code.

Something like this
$sql = 'SELECT * FROM sometable';
$where = [];
$params = [];
if($a){
$where[] = 'a = :a';
$params[':a'] = $a;
}
if($b){
$where[] = 'b = :b';
$params[':b'] = $b;
}
if(!empty($where)){
$sql .= ' WHERE '.implode(' AND ', $where);
}
$stmt = $PDO->prepare($sql);
$res = $stmt->execute($params);
And so On.
It almost always preferable to use and array and implode for things like this instead of concatenation. Often concatenation will leave you with a hanging "separator" in this case " AND ". For example if we tried this with concatenation:
//if we put WHERE here and then nothing passes our conditions we wind up with:
//"SELECT * FROM sometable WHERE" which wont work
$sql = 'SELECT * FROM sometable ';
//we still need something like an array if we want to prepare our query.
//which is something we should always do
$params = [];
if($a){
//if we put WHERE here, then what if this condition doesn't pass
//do we put it in the next condition? How do we tell. .
$sql .= 'WHERE a = :a AND ';
$params[':a'] = $a;
}
if($b){
//again if the first condition didn't pass how do we know to put "WHERE" here.
//"SELECT * FROM sometable b = :b AND" which wont work
$sql .= 'b = :b AND ';
$params[':b'] = $b;
}
if($c){
//lets say the first 2 conditions passes but this last one failed
//"SELECT * FROM sometable WHERE a = :a AND b = :b AND" which wont work
$sql .= 'c = :c';
$params[':c'] = $c;
}
//we would need to do something like this to trim the last "AND" off
$sql = preg_replace('/\sAND\s$/', '', $sql);
//--------------------
//now if we were prepending "AND" instead of appending it, we're no better off.
//--------------------
//we can fix the where issue by using a string variable (and testing it latter)
$where = '';
if($a){
$where .= 'a = :a';
$params[':a'] = $a;
}
if($b){
//However lets say the first condition failed, we get this:
//"SELECT * FROM sometable WHERE AND b = :b" which wont work
$where .= ' AND b = :b';
$params[':b'] = $b;
//--------------------------
//so in every condition following we would have to test $where
//and if its not empty then we can prepend "AND"
if(!empty($where)) $where .= ' AND ';
$where .= 'b = :b';
$params[':b'] = $b;
}
if($c){
if(!empty($where)) $where .= ' AND ';
$where .= 'c = :c';
$params[':c'] = $c;
}
//finally to fix the "WHERE" issue we need to do something like this:
if(empty($where)) $sql .= ' WHERE '.$where;
//we could also try something like this in every condition:
if($d){
if(empty($where)) $where .= ' WHERE ';
//However, this breaks our fix for prepending "AND", because
//$where will never be empty when we test it.
//if(!empty($where)) $where .= ' AND ';
$where .= 'd = :d';
$params[':d'] = $d;
}
Hopefully that all makes sense. It's just so much easier to use an array and implode it later.
I just wanted to show that to help visualize the issues with concatenation. We wind writing more code, using the same number of variables and double the conditional logic. Or we can get into complicated things like Regex to trim the hanging AND off etc.
Hope that helps!
BECAUSE I mentioned it in the comments.
If you are using "OR" you can of course do the same thing with that, but typically "OR" will cause a full scan of the DB. It's just the way OR works. When we use "AND" the DB (basically) takes the return set and applies the next condition to that, because both have to pass. However, with "OR" rows that failed the first condition could still pass if the second condition passes. So the DB must scan the full record set for each or, as well as keep track of all the rows that passed in the previous conditions. It's just the way the logic works for "OR".
Now for improved "OR" performance we can use a sub-query that is a union. Like this:
$sql = 'SELECT * FROM sometable AS t';
$union = [];
$params = [];
if($a){
$union[] = 'SELECT id FROM sometable WHERE a = a:';
$params[':a'] = $a;
}
if($b){
$union[] = 'SELECT id FROM sometable WHERE b = b:';
$params[':b'] = $b;
}
if(!empty($union)){
$sql .= '
JOIN( '.
implode(' UNION ', $union).
' ) AS u ON t.id = u.id
}
What we wind up with is something like this query:
SELECT
*
FROM
sometable AS t
JOIN (
SELECT id FROM sometable WHERE a = a:
UNION
SELECT id FROM sometable WHERE b = b:
) AS u ON t.id = u.id
When we use "OR" as our dataset grows the DB must store these results in temp table as well as search the entire dataset. Because we are pulling all the columns in the table, this dataset will quickly grow. Once it hits a certian size it will get swapped to Disc and our performance will take a big hit for that.
With the Union query, we also create a temp table. But because we are only concerned with pulling out the ids this temp table will be very small. Union unlike Union ALL will also automatically remove duplicate records further reducing our dataset. So we want to use Union and not Union ALL.
Then we join this back on the table in the outer query and use that to pull the all the columns from just the rows that we need.
Basically we are accepting the fact that we need a temp table and minimizing the impact of that.
This might not seem like it would be much faster, and in some cases it might not be (when no swapping happens). But for me, using a query like you describe where users can search on multiple fields, I was able to reduce the time it took from about 15 seconds to under 1 second. My query had several joins in it such as if a user put in a state, I had to join on participant then participants_addresses (junction table) and then addresses and then finally on states. But if they put in a phone I had to join participant > participants_phones > phone etc.
I can't guarantee this will work in every case and you should use Explain and SQL_NO_CACHE when benchmarking your queries. For example EXPLAIN SELECT SQL_NO_CACHE * FROM .... Explain will tell you how the indexes are working and No Cache prevents the DB from caching the query if you run it multiple times. Caching will make it look like it is fast when it's really not.
You can do something similar when sorting, which also kills performance.
SELECT
*
FROM
sometable AS t
JOIN (
SELECT id FROM sometable WHERE a = a: ORDER BY date DESC
) AS u ON t.id = u.id
This has a similar effect of only sorting the id's in the temp table (instead of the whole dataset), and then when we join it, it actually keeps the order the ids are in. I forget if the order of the subquery vs the outer query matter.
For fun you can even combine the two with 2 nested sub-queries, with the Union as the deepest query (it's something like this).
SELECT
*
FROM
sometable AS t
JOIN (
SELECT id FROM sometable AS t0 JOIN (
SELECT id FROM sometable WHERE a = a:
UNION
SELECT id FROM sometable WHERE b = b:
) AS u ON t0.id = u.id
ORDER BY t0.date DESC
) AS t1 ON t.id = t1.id
It can get pretty complicated though ... lol.
Anyway, I was bored and maybe, just maybe, it will work for someone like it did for me. (this is what happens when I don't get sleep) :)
UPDATE
IF you have problems with the parameters you can output the SQL with the values filled in by doing this:
echo str_replace(array_keys($params), $params, $sql)."\n";
But use this only for Debugging, not for putting the data into the query because that would defeat the purpose of using prepared statements and open you up to SQLInjection attacks. That said, it can make it easier to see if you are missing anything or have any spelling errors. I also use this when I just want to test the query in PHPMyAdmin, but am to lazy to cut an paste the data into it. Then I just copy the output put it in PHPMyAdmin and then I can rule out any issues with PHP or tweak the query if need be.
You can also have issues if you have to many elements in the array, AKA extra placeholders that are not in the query.
For that you can do
//count the number of : in the query
$num_placeholders = substr_count(':', $sql);
//count the elements in the array
$num_params = count($params);
if($num_placeholders > $num_params ) echo "to many placeholders\n";
else if($num_placeholders < $num_params ) echo "to many params\n";
One last thing to be mindful of when mixing "AND" and "OR" is stuff like this
SELECT * FROM foo WHERE arg1 = :arg1 OR arg2 = :arg2 AND arg3 = :arg3
The way it executes this is like this
SELECT * FROM foo WHERE arg1 = :arg1 OR (arg2 = :arg2 AND arg3 = :arg3)
This will return all rows that match arg1 regardless of the rest of the query.
Most of the time this would not be what you want. You would actually want it to do it this way:
SELECT * FROM foo WHERE (arg1 = :arg1 OR arg2 = :arg2) AND arg3 = :arg3
Which is called an "Exclusive OR". This will return all rows that match arg1 OR arg2 AND arg3
Hope that helps.

You could also create an wanted list of nesseccary items and Check If each Item is Set by the PHP function isset().

Related

PHP: Search mysql database using multiple select dropdown lists?

I'm trying to search MYSQL database using multiple Dropdown lists on my page.
However, there is a small twist in this search function.
Basically, I need to make sure All the criteria (all the multiple select dropdonw values) match the items and if they do match then show the result!
At the moment, my code shows the results even if one of the dropdown values match the items which is not what i am trying to do.
This is my code:
$searchList = "";
$clause = " WHERE ";//Initial clause
$sql="SELECT *
FROM `product_details`
INNER JOIN `ATTRIBUTES` ON product_details.id=ATTRIBUTES.id";//Query stub
if(isset($_POST['keyword']) && !empty($_POST['keyword'])){
foreach($_POST['keyword'] as $c){
if(!empty($c)){
$currentproduct = $_POST['product'];
$cat = $_POST['cat'];
##NOPE##$sql .= $clause."`".$c."` LIKE '%{$c}%'";
$sql .= $clause . " (ATTRIBUTES.attr LIKE BINARY '$c') AND ATTRIBUTES.sub_cat_name='$currentproduct'";
$clause = " OR ";//Change to OR after 1st WHERE
}
}
$sql .= " GROUP BY product_details.id";
//print "SQL Query: $sql<br />"; //<-- Debug SQl syntax.
// Run query outside of foreach loop so it only runs one time.
$query = mysqli_query($db_conx, $sql);
I even tried to remove the isset and did if(!empty($_POST['keyword'])){ but i still get results even if one of the dropdown lists values match the items credentials.
I'm not sure what I am doing wrong here as I thought using if(!empty($_POST['keyword'])){ should solve this issue but it hasn't.
Could someone please advise on this issue?
any help would be appreciated.
EDIT: I changed the CODE to the following and it doesn't display anything:
$clause = " WHERE ";//Initial clause
$sql="SELECT *
FROM `product_details`
INNER JOIN `ATTRIBUTES` ON product_details.id=ATTRIBUTES.id";//Query stub
$currentproduct = $_POST['product'];
$cat = $_POST['cat'];
if(!empty($_POST['keyword'])){
foreach($_POST['keyword'] as $c){
if(!empty($c)){
##NOPE##$sql .= $clause."`".$c."` LIKE '%{$c}%'";
$sql .= $clause . " (ATTRIBUTES.attr LIKE BINARY '$c') AND ATTRIBUTES.sub_cat_name='$currentproduct'";
$clause = " AND ";//Change to OR after 1st WHERE
}
}
$sql .= " GROUP BY product_details.id";
//print "SQL Query: $sql<br />"; //<-- Debug SQl syntax.
// Run query outside of foreach loop so it only runs one time.
$query = mysqli_query($db_conx, $sql);
//var_dump($query); //<-- Debug query results.
// Check that the query ran fine.
if (!$query) {
print "ERROR: " . mysqli_error($db_conx);
}
$clause = " OR ";//Change to OR after 1st WHERE
The above OR operator will cause your where criteria to select a record even if 1 keyword matches the attr field. Change it to " AND " to expect all keywords to apply.
Furthermore, ... AND ATTRIBUTES.sub_cat_name='$currentproduct'" criterion seems to apply to all keywords, so this criterion should be added once, not at every iteration of the loop. $currentproduct = $_POST['product']; row should also be moved in fron of the loop.
EDIT: to reflect to changing the opreator to AND and not having any rows returned.
...ATTRIBUTES.attr LIKE BINARY '$c'...
If there are no wildcards in $c, then the above criterion will require the word to match the attr field as if = operator had been used, which is unlikely to happen. Wildcards must be included in the search: '%$c%'
Plus some protection from sql injection would also be nice.
EDIT2:
If each attribue is stored in its own record, then it complicates things a little bit, since the where criteria is evaluated against a single record, not a collection of them.
I'll give you a sample select command, but you will have to incorporate it into your php code.
select product_details.* FROM product_details INNER JOIN
(select product_details.id, count(ATTRIBUTES.id) as total
FROM `product_details`
INNER JOIN `ATTRIBUTES` ON product_details.id=ATTRIBUTES.id
WHERE ATTRIBUTES.attr in (...)
GROUP BY product_details.id
HAVING total=...) as t
on t.id=product_details.id
The subquery counts how many attributes were matched for a product and eliminates those, where the count does not equal to the number of parameters submitted via the form. The outer query gets the product details for those, where the count matched.
For the ... in the in() clause you need to provide a comma separated, ' enclosed list of the keywords, like: "'computer', 'apple'". Use implode() function in php and sztring concatenation to get the results.
For the ... in the having clause substitute the number of keywords in the $_POST['keyword'] array (you should check in the code if it's an array or just a single value, though).
Still, you should consider the impact of sql injection on your code.

Query Takes 30 seconds long

We have a registration system Database and basically what this query does is check the students that are in the class so that they can be selected to be marked as absent if they are absent. For some reason, it takes 30 seconds. Does anybody know why?
FROM Stdts
LEFT JOIN StdtReg ON StdtReg.StdtID = Stdts.ID
LEFT JOIN usrs ON StdtReg.userID = usrs.ID
WHERE (SELECT ID FROM ClssInstncEnrol cie WHERE cie.status = 0 AND classInstanceID={$_GET['ci']} AND StdtID = Stdts.ID LIMIT 1) IS NOT NULL
OR (SELECT ID FROM DropIns di WHERE di.type <> -1 AND classInstanceID= {$_GET['ci']} AND StdtID = Stdts.ID LIMIT 1) IS NOT NULL
AND (CONCAT(Stdts.firstName, ' ', Stdts.lastName) OR CONCAT(usrs.firstName,' ', usrs.lastName))
ORDER BY firstName, lastName
Run the query with "EXPLAIN " before it and it will tell you how each table is being joined and where you might be missing an index.
Also, you have an SQL injection waiting to happen with queries of this form with HTTP params interpolated directly in the query.
Finally, you've left off some of the query and the schema, but this strikes me as something that could be done with joins rather than subselects, or even as separate queries to generate the list of student ids more efficiently before you even run the main query.
Try to check the execution plan of your query to see what could be wrong (if you have huge tables and do not use appropriate index it can be long)
Maybe this:
AND (CONCAT(Stdts.firstName, ' ', Stdts.lastName) OR CONCAT(usrs.firstName,' ', usrs.lastName))
should be:
AND (CONCAT(Stdts.firstName, ' ', Stdts.lastName) = CONCAT(usrs.firstName,' ', usrs.lastName))

Update multiple rows with one query

How can I update hundreds of rows at once?
Like:
UPDATE table SET a = ? WHERE b = ? AND c = 1
but for many rows. The ? parameters are arrays...
I read this answer but it uses CASE and I don't think I can do that...
Right now I have something like this:
foreach($values as $key => $value)
$res = $pdo->prepare('UPDATE table SET a = ? WHERE b = ? AND c = 1');
$res->execute(array($value, $key));
}
To do it in a single run of a query, you'd need to use a CASE and assemble the parameters programmatically. SQL doesn't support variadic prepared statements, and only simple values can be parameterized.
Alternatively, define a statement to only take data for one row at a time and run the query in a loop. Repeated execution is how prepared statements are designed to be used for cases like this.
try {
$query = $db->prepare('UPDATE table SET a = ? WHERE b = ? AND c = 1');
foreach ($as as $i => $a) {
$query->execute(array($a, $bs[$i]));
}
} catch (PDOException $e) {
...
}
Use the CASE method as described in the link you provided, but build the query dynamically with the values you want.
Likely, this will be built with a for loop similar to how you're already doing it, but you will end up with a single query rather than querying your database every iteration.
Another way would be to insert your key value pairs (all at once) into a temporary table then do something like this:
UPDATE table t
SET t.a = (SELECT p.a FROM tmp p WHERE p.b = t.b)
WHERE t.b IN (SELECT p.b FROM tmp p) AND t.c = 1

SQL Query Optimization help

my foundation on SQL is pretty weak so I hope you could bear with me. I have three tables: contents, categories, and categorization. The setup was chosen since some content will belong to one or more categories.
I want to fetch contents and its corresponding categories.
This is an overly-simplified version of the current script, without error-checking routines:
$q = "SELECT * FROM contents WHERE contents.foo = 'bar'"
$resource = mysql_query($q);
$categoryFilter = array();
$q2 = "SELECT * FROM categorization WHERE ";
while($content = mysql_fetch_assoc($resource))
{
$categoryFilter[] = "content_id='" . $content["id"] . "'";
}
if(count($categoryFilter))
{
$q2 .= implode(" OR ", $categoryFilter);
mysql_query($q2);
}
That's the gist of it. I hope you get what I am trying to do. I don't know if I can actually use JOINS the content_id may be present in multiple rows in categorization. So what I did was to simply append multiple OR's, trying to fetch items one by one. I really would not like to use multiple queries in this scenario. I hope anyone could suggest an approach
Thanks for your time
One query should be enough to fetch data from all three tables:
SELECT categories.category_id #, other fields
FROM contents
INNER JOIN categorization ON contents.content_id = categorization.content_id
INNER JOIN categories ON categorization.category_id = categories.category_id
WHERE contents.content_id = 1 # AND other filters
Tweak the columns in the SELECT clause and/or conditions in WHERE clause according to your needs.
This should do the same thing as in your example:
$q = "
SELECT *
FROM
contents c
categorization ctg ON ctg.content_id = c.id
WHERE c.foo = 'bar'
";
$result = mysql_query($q);
If I understand it correctly, you can do this in one sql statement
SELECT *
FROM contents t1
JOIN categorization t2
WHERE t1.content_id = t2.content_id AND t1.foo = 'bar'
Also ensure that content_id is indexed both in 'content' and 'categorization'. You may find it worthwhile indexing 'foo' aswell, but it depends on how you are actually searching.

Trying to create a dynamically generated mySQL select statement using PHP

I have 5 variables which can either be true or false and I have to generate different sql SELECT statements for each different possible outcome. Right now I have a ton of if else statements but I'm wondering if theres a smarter way to do this?
so for example, i have
if (x=true AND y=false AND z=false AND a=false AND b =false) {
$sql= "SELECT...."
} else if(x=true AND y=true AND z=false AND a=false AND b=false) {
$sql= "SELECT...."
}
The first select statement is if the user entered text and didnt select anything else:
$sql="SELECT CompanyName, Keywords, Product, Industry, Link, region, hot FROM searchtest_tbl WHERE Keywords LIKE '%$formSearch%' OR CompanyName LIKE '%$formSearch%' OR Product LIKE '%$formSearch%' OR Industry LIKE '%$formSearch%' ORDER BY hot DESC, CompanyName";
this statement is if they only selected from the Industry dropdown:
$sql="SELECT CompanyName, Product, Industry, Link, hot, region FROM searchtest_tbl WHERE Industry='$formIndustry' ORDER BY hot DESC, CompanyName";
and here is one if they entered text, selected an industry, but didnt select anythign else:
$sql="SELECT CompanyName, Product, Industry, Link, hot, region FROM searchtest_tbl WHERE Industry='$formIndustry' AND (Keywords LIKE '%$formSearch%' OR CompanyName LIKE '%$formSearch%' OR Product LIKE '%$formSearch%') ORDER BY hot DESC, CompanyName";
The rest are basically like this, but if the other fields are selected it will say WHERE Product='$formProduct' AND.... etc
Perhaps you can break down your logic into smaller components. For example, if x=true corresponds to a specific part of your select statement, and y=false corresponds to a different part, build the select statement piece by piece:
$sql_statement = "SELECT ";
$sql_statement .= ($x) ? "`column_a` " : "`column_a`, `column_b` ";
$sql_statement .= "FROM ";
$sql_statement .= ($y) ? "`table_a` " : "`table_b` ";
If this approach is not suitable for you, can you post additional code so we can make a better judgement?
I'm going to go right out and assume that your true and false statements don't relate to your SQL, because if they did you could simply replace them with variables that could be placed directly into your SQL. For Example
Instead of having a true $x variable mean ASC order and a false variable mean DESC order. You could create a new variable (we'll call it a) and put the value ASC or DESC in it. That way instead of having
if($x){
//sql
}
else{
//sql
}
You could have someting to the effect of
SELECT * FROM search $a;
Now if for some reason these variables are completely random and unrelated.
And you have to use ifs and else ifs, I would suggest changing your code for comprehensibility and logic. But I'll be able to help you mor if you post your original code, or at least the variables.
You might want to consider using binary numbers and bitwise operators.
FLAG_1 = bindec('0001') = 1
FLAG_2 = bindec('0010') = 2
FLAG_3 = bindec('0100') = 4
FLAG_4 = bindec('1000') = 8
then you can combine all of the flags into one int using the bitwise operator "|"
so if flag 2 and 4 are on
$combined_flags = (FLAG_2 | FLAG_4) = bindec('1010') = 10;
you could then use the combined flag int to determine which SQL statement to generate
you can even test for individual flags using the bitwise & operator
if ($combined_flags & FLAG_2) // true
if ($combined_flags & FLAG_1) // false

Categories