Below is my working query that gets into action after a form submit. My query works only if all textboxes are filled, so currently everything is required.
WORKING QUERY
SELECT behaviour.hash, Sum(behaviour.timespent) AS timeSpent, new_table.percentile_rank, Count(*) AS total FROM behaviour, audience, new_table WHERE ( $url ) AND behaviour.timestamp >= Date_sub(Curdate(), INTERVAL $last_visit day) AND behaviour.timestamp < Date_add(Curdate(), INTERVAL 1 day) AND behaviour.hash = audience.hash AND behaviour.hash = new_table.hash AND audience.country = '$from_country' GROUP BY behaviour.hash HAVING Count(*) >= $more_than AND timespent >= $time_spent AND new_table.percentile_rank >= $lead_scoring
What I want to achieve is not to require the user to fill all the textboxes in order to submit, but only those he prefers. So I built the following, but it has some errors.
My problem is that my query has a having
clause so not every condition
is connected with AND
as I have for now (look below my code). So, the first of the $more_than
or $time_spent
or $lead_scoring
textboxes that submitted, it must have HAVING
instead of AND
.
How to edit my code in order to achieve this "special conditions" ?
MY CODE
$url= 'url="'.implode('" OR url="', $vals).'"'; $conditions = array(); if (!empty($last_visit)) $conditions[] = "behaviour.TIMESTAMP >= DATE_SUB( CURDATE( ) , INTERVAL '".$last_visit."' DAY) AND behaviour.TIMESTAMP < DATE_ADD( CURDATE( ) , INTERVAL 1 DAY ) "; if (!empty($from_country)) $conditions[] = "audience.country = '".$from_country."'"; if (!empty($more_than)) $conditions[] = "COUNT( * ) >= '".$more_than."'"; if (!empty($time_spent)) $conditions[] = "timeSpent >= '".$time_spent."'"; if (!empty($lead_scoring)) $conditions[] = "new_table.percentile_rank >= '".$lead_scoring."'"; $conditionString = implode(' AND ', $conditions); $sql = "SELECT behaviour.hash, Sum(behaviour.timespent) AS timeSpent, new_table.percentile_rank, Count( * ) AS total FROM behaviour, audience, new_table WHERE ($url) AND ".$conditionString;
CURRENT OUTPUT
In the example below, all textboxes except more_than
have been filled. The thing is that instead AND timespent >= '20'
should be HAVING timespent >= '20'
SELECT behaviour.hash, SUM(behaviour.timespent) AS timeSpent, new_table.percentile_rank, Count(*) AS total FROM behaviour, audience, new_table WHERE ( url = "/10369" ) AND behaviour.timestamp >= Date_sub(Curdate(), interval '3' day) AND behaviour.timestamp < Date_add(Curdate(), interval 1 day) [missing] AND behaviour.hash = audience.hash [missing] AND behaviour.hash = new_table.hash AND audience.country = 'it' [missing] GROUP BY behaviour.hash [wrong] AND timespent >= '20' ////// it should be HAVING ///// AND new_table.percentile_rank >= '30'
6 Answers
Answers 1
First, you must ensure SQL injection is not possible. To do that, lets use PDO.
Next, to solve your actual problem, you simple need to create two lists with conditions. One with the conditions you want to have in the WHERE
part of the query, and one with the conditions that need to go in the HAVING
part of the query.
$pdo = new PDO(/* See http://php.net/manual/en/pdo.construct.php */); $whereConditions = []; $havingConditions = []; $parameters = []; if (!empty($last_visit)) { $whereConditions[] = "behaviour.TIMESTAMP >= DATE_SUB( CURDATE( ) , INTERVAL :last_visit DAY) AND behaviour.TIMESTAMP < DATE_ADD( CURDATE( ) , INTERVAL 1 DAY ) "; $parameters['last_visit'] = $last_visit; } if (!empty($from_country)) { $whereConditions[] = "audience.country = :from_country"; $parameters['from_country'] = $from_country; } if (!empty($more_than)) { $havingConditions[] = "COUNT( * ) >= :more_than"; $parameters['more_than'] = $more_than; } if (!empty($time_spent)) { $havingConditions[] = "timeSpent >= :time_spent"; $parameters['time_spent'] = $time_spent; } if (!empty($lead_scoring)) { $havingConditions[] = "new_table.percentile_rank >= :lead_scoring"; $parameters['lead_scoring'] = $lead_scoring; } if (count($vals)) { $escapedUrlList = implode(', ', array_map(function ($url) use ($pdo) { return $pdo->quote($url); }, $vals)); $whereConditions[] = "url IN($escapedUrlList)"; } $whereClause = count($whereConditions) ? ' AND ' . implode(' AND ', $whereConditions) : ''; $havingClause = count($havingConditions) ? ' HAVING ' . implode(' AND ', $havingConditions) : ''; $statement = $pdo->prepare(" SELECT behaviour.hash, Sum(behaviour.timespent) AS timeSpent, new_table.percentile_rank, Count(*) AS total FROM behaviour, audience, new_table WHERE behaviour.hash = audience.hash AND behaviour.hash = new_table.hash {$whereClause} GROUP BY behaviour.hash {$havingClause} "); $result = $statement->execute($parameters);
Answers 2
Here is a less complicated way using string concatenation instead of implode
. The "trick" is to start the conditions with 1=1
. So every following condition can begin with AND
.
$andWhere = ''; $andHaving = ''; $params = []; if (!empty($last_visit)) { $andWhere .= " AND behaviour.TIMESTAMP >= CURDATE() - INTERVAL :last_visit DAY AND behaviour.TIMESTAMP < CURDATE() + INTERVAL 1 DAY"; $params['last_visit'] = $last_visit; } if (!empty($from_country)) { $andWhere .= " AND audience.country = :from_country"; $params['from_country'] = $from_country; } if (!empty($more_than)) { $andHaving .= " AND COUNT( * ) >= :more_than"; $params['more_than'] = $more_than; } if (!empty($time_spent)) { $andHaving .= " AND timeSpent >= :time_spent"; $params['time_spent'] = $time_spent; } if (!empty($lead_scoring)) { $andHaving .= " AND new_table.percentile_rank >= :lead_scoring"; $params['lead_scoring'] = $lead_scoring; } $urlPlaceholders = []; foreach ($vals as $key => $val) { $urlPlaceholders[] = ":url_$key"; $params["url_$key"] = $val; } if (count($vals) > 0) { $inUrl = implode(',', $urlPlaceholders); $andWhere .= " AND url IN ($inUrl)"; } $sql = " SELECT behaviour.hash, Sum(behaviour.timespent) AS timeSpent, new_table.percentile_rank, Count(*) AS total FROM behaviour JOIN audience ON behaviour.hash = audience.hash JOIN new_table ON behaviour.hash = new_table.hash WHERE 1=1 {$andWhere} GROUP BY behaviour.hash HAVING 1=1 {$andHaving} "; #var_export($sql); #var_export($params); $sth = $dbh->prepare($sql); $sth->execute($params); $data = $sth->fetchAll(PDO::FETCH_ASSOC); #var_export($data);
Having sample data like
$last_visit = ''; $from_country = 'UK'; $more_than = '5'; $time_spent = '3'; $lead_scoring = ''; $vals = ['u1', 'u2'];
You would get the following query:
SELECT behaviour.hash, Sum(behaviour.timespent) AS timeSpent, new_table.percentile_rank, Count(*) AS total FROM behaviour JOIN audience ON behaviour.hash = audience.hash JOIN new_table ON behaviour.hash = new_table.hash WHERE 1=1 AND audience.country = :from_country AND url IN (:url_0,:url_1) GROUP BY behaviour.hash HAVING 1=1 AND COUNT(*) >= :more_than AND timeSpent >= :time_spent
with these bindings:
array ( 'from_country' => 'UK', 'more_than' => '5', 'time_spent' => '3', 'url_0' => 'u1', 'url_1' => 'u2', )
Answers 3
Here's a bit "tricky" approach (looks clean though) that is using prepared statements. I've added some general purpose "features" in case of future changes. Read the comments with explanations (will be more convenient this way I think):
//assume established PDO connection - example: try { $pdo = new PDO("mysql:dbname={$database_name};host=localhost", $user, $password); } catch (PDOException $e) { echo 'Connection failed: ' . $e->getMessage(); } //static: conditional strings without parameters check (no keys required) //conditional: assoc - keys should match both query placeholders and variable names $static_where = []; $optional_where = [ 'last_visit' => 'behaviour.TIMESTAMP >= DATE_SUB(CURDATE(), INTERVAL :last_visit DAY) AND behaviour.TIMESTAMP < DATE_ADD(CURDATE(), INTERVAL 1 DAY)', 'from_country' => 'audience.country = :from_country' ]; $static_having = []; $optional_having = [ 'more_than' => 'COUNT(*) >= :more_than', 'time_spent' => 'timeSpent >= :time_spent', 'lead_scoring' => 'new_table.percentile_rank >= :lead_scoring' ]; //params: query parameters array - assigned manually + resolved from optional variables $params = []; //resolve condition from $urls array if (count($urls) == 1) { $static_where[] = 'url = :url'; $params['url'] = reset($urls); } else if (!empty($urls)) { foreach ($urls as $idx => $url) { $params['url' . $idx] = $url; } $static_where[] = 'url IN(:' . implode(', :', array_keys($params)) . ')'; } //filtering existing params used in query //empty() is not a good idea for general purpose though, //because some valid values might be recognised as empty (int 0, string '0') $params += array_filter( compact(array_keys($optional_where), array_keys($optional_having)), function ($value) { return !empty($value); } ); //concatenating conditional strings //with corresponding params that weren't filtered out //or these without params (static) $where_clause = implode(' AND ', $static_where + array_intersect_key($optional_where, $params)); $having_clause = implode(' AND ', $static_having + array_intersect_key($optional_having, $params)); //don't need clauses without conditions - same as if (!empty($where)) {...} empty($where_clause) or $where_clause = 'WHERE ' . $where_clause; empty($having_clause) or $having_clause = 'HAVING ' . $having_clause; $sql = "SELECT behaviour.hash, Sum(behaviour.timespent) AS timeSpent, new_table.percentile_rank, Count( * ) AS total FROM behaviour, INNER JOIN audience ON behaviour.hash = audience.hash, INNER JOIN new_table ON behaviour.hash = new_table.hash {$where_clause} GROUP BY behaviour.hash {$having_clause}"; //PDO part $query = $pdo->prepare($sql); $result = $query->execute($params); //...
Answers 4
If having is the only problem why not splitting it into different block like this:
$conditions = array(); if (!empty($last_visit)) $conditions[] = "behaviour.TIMESTAMP >= DATE_SUB( CURDATE( ) , INTERVAL '".$last_visit."' DAY) AND behaviour.TIMESTAMP < DATE_ADD( CURDATE( ) , INTERVAL 1 DAY ) "; if (!empty($from_country)) $conditions[] = "audience.country = '".$from_country."'"; $conditionString = implode(' AND ', $conditions); $conditions_having = array(); if (!empty($more_than)) $conditions_having[] = "COUNT( * ) >= '".$more_than."'"; if (!empty($time_spent)) $conditions_having[] = "timeSpent >= '".$time_spent."'"; if (!empty($lead_scoring)) $conditions_having[] = "new_table.percentile_rank >= '".$lead_scoring."'"; $conditionString .= " GROUP BY behaviour.hash" if(count($conditions_having)) $conditionString .= " HAVING ".implode(' AND ', $conditions_having); $sql = "SELECT behaviour.hash, Sum(behaviour.timespent) AS timeSpent, new_table.percentile_rank, Count( * ) AS total FROM behaviour, audience, new_table WHERE ($url) AND ".$conditionString;
Answers 5
You can change only your implode function to this code
$conditionString = implode(' ', array_map(function($item) { if ((strpos($item, 'timeSpent') !== false)) return 'HAVING '.$item; return 'AND '.$item; }, $conditions));
Be aware that your code is vulnerable.
for more information see this: SQL Injection In CAPEC
Answers 6
$url= 'url="'.implode('" OR url="', $vals).'"'; $conditions = array(); $havings = array(); if (!empty($last_visit)) $conditions[] = "behaviour.TIMESTAMP >= DATE_SUB( CURDATE( ) , INTERVAL '".$last_visit."' DAY) AND behaviour.TIMESTAMP < DATE_ADD( CURDATE( ) , INTERVAL 1 DAY ) "; if (!empty($from_country)) $conditions[] = "audience.country = '".$from_country."'"; if (!empty($more_than)) $havings[] = "COUNT( * ) >= '".$more_than."'"; if (!empty($time_spent)) $havings[] = "timeSpent >= '".$time_spent."'"; if (!empty($lead_scoring)) $havings[] = "new_table.percentile_rank >= '".$lead_scoring."'"; $conditionString = implode(' AND ', $conditions); $havingString = ''; if(count($havings)>0) { $havingString = ' having '.implode(', ', $havings); } $sql = "SELECT behaviour.hash, Sum(behaviour.timespent) AS timeSpent, new_table.percentile_rank, Count( * ) AS total FROM behaviour, audience, new_table WHERE ($url) AND ".$conditionString.$havingString;
This comment has been removed by the author.
ReplyDeleteThis comment has been removed by the author.
ReplyDeleteAwesome post! Thanks for sharing.
ReplyDeleteOracle Training in Chennai
Oracle Training institute in chennai
Oracle course in Chennai
Embedded Training
LINUX Course in Chennai
Tally Institute in Chennai
Manual Testing Course
the article is good.the information is well and useful for all.i want this type of article.thanks.
ReplyDeleteAWS Training in Chennai
Data Science Course in Chennai
Data Science Training in Chennai
DevOps certification in Chennai
DevOps course in Chennai
AWS Training in Velachery
AWS Training in Tambaram
english to kannada typing
ReplyDeleteimo download for pc
ReplyDeleteimo download for pc
ReplyDeleteawesome post really a great one
ReplyDeleteoracle training in chennai