26

How would you write a prepared MySQL statement in PHP that takes a differing number of arguments each time? An example such query is:

SELECT `age`, `name` FROM `people` WHERE id IN (12, 45, 65, 33)

The IN clause will have a different number of ids each time it is run.

I have two possible solutions in my mind but want to see if there is a better way.

Possible Solution 1 Make the statement accept 100 variables and fill the rest with dummy values guaranteed not to be in the table; make multiple calls for more than 100 values.

Possible Solution 2 Don't use a prepared statement; build and run the query checking stringently for possible injection attacks.

Michael
  • 8,362
  • 6
  • 61
  • 88
smarthall
  • 288
  • 1
  • 3
  • 7
  • I suspect I would avoid both of the solutions you suggest. Prepared statements make securing your code far easier. And your first solution seems pretty inefficient and wasteful if you have a small list. – Zoredache Nov 29 '08 at 09:40
  • I agree I thought those ideas might stimulate others minds. :) – smarthall Nov 29 '08 at 12:17
  • For SQL Server, see [Parameterizing an SQL IN clause?](http://stackoverflow.com/q/337704/90527) – outis Apr 04 '12 at 12:02
  • For modern implementations of mysqli prepared statements with variadic bound parameters see: [Use an array in a mysqli prepared statement: `WHERE .. IN(..)` query](https://stackoverflow.com/a/71718174/2943403) and [mysqli bind_param for array of strings](https://stackoverflow.com/a/58355651/2943403) – mickmackusa Apr 03 '22 at 20:09

7 Answers7

25

I can think of a couple solutions.

One solution might be to create a temporary table. Do an insert into the table for each parameter that you would have in the in clause. Then do a simple join against your temporary table.

Another method might be to do something like this.

$dbh=new PDO($dbConnect, $dbUser, $dbPass);
$parms=array(12, 45, 65, 33);
$parmcount=count($parms);   // = 4
$inclause=implode(',',array_fill(0,$parmcount,'?')); // = ?,?,?,?
$sql='SELECT age, name FROM people WHERE id IN (%s)';
$preparesql=sprintf($sql,$inclause);  // = example statement used in the question
$st=$dbh->prepare($preparesql);
$st->execute($parms);

I suspect, but have no proof, that the first solution might be better for larger lists, and the later would work for smaller lists.


To make @orrd happy here is a terse version.

$dbh=new PDO($dbConnect, $dbUser, $dbPass);
$parms=array(12, 45, 65, 33);
$st=$dbh->prepare(sprintf('SELECT age, name FROM people WHERE id IN (%s)',
                          implode(',',array_fill(0,count($parms),'?'))));
$st->execute($parms);

Ludo
  • 743
  • 1
  • 10
  • 25
Zoredache
  • 37,543
  • 7
  • 45
  • 61
  • 2
    i like your second suggestion. do it and forget about it until performance is an issue. at that point it might be worth investigating the first option. – benlumley Nov 29 '08 at 09:46
  • If only I'd have thought of that! Your first solution sounds like the exact thing I was looking for. – smarthall Nov 29 '08 at 12:16
  • I've used pattern #2 frequently. Perl's DBI has a prepare_cached() function, so if you queries with similar numbers of placeholders, it will reuse statement handles. Not sure about PHP though.. – Gary Richardson Nov 29 '08 at 23:33
  • Creating a temporary table would be overkill unless the data set is huge. The sample code is overly complicated (half of those variables can be eliminated and the code can be greatly simplified by doing more of the work in-line). But the basic idea is a good one, using implode(',',array_fill(0,count($params),'?')) to generate the "?"s, then just pass the $params as the data to bind. – orrd Jan 14 '14 at 23:49
  • 9
    @orrd, meh... When I composed this answer I was trying to make verbose so it would be easy to understand. I agree, that it can be simplified, but I don't think the simplification you suggested would make the answer easier to follow, or have any significant impact on performance. In any case, I'll update my answer and add a compact version for you. – Zoredache Jan 15 '14 at 08:12
  • After so many years, there still is no better way than to hack around it? Why does it not accept an array... As far as I have seen, `call_user_func_array` is still the most 'less hacky' way to do it... – Kerwin Sneijders Sep 08 '18 at 22:31
10

There is also the FIND_IN_SET function whose second parameter is a string of comma separated values:

SELECT age, name FROM people WHERE FIND_IN_SET(id, '12,45,65,33')
Gumbo
  • 643,351
  • 109
  • 780
  • 844
  • 6
    Problem with this is: it won't use the primary index, defaulting to a full table scan while executing FIND_IN_SET for every row. – Hugo Maxwell Jan 26 '15 at 20:56
3

decent sql wrappers support binding to array values. i.e.

$sql = "... WHERE id IN (?)";
$values = array(1, 2, 3, 4);
$result = $dbw -> prepare ($sql, $values) -> execute ();
Eimantas
  • 48,927
  • 17
  • 132
  • 168
  • I actually do not know about any native PHP database access library for MySQL (neither mysql, mysqli nor PDO) that allows for binding parameters of the array type. – Stefan Gehrig Nov 29 '08 at 10:09
  • back when i was developing in php few years ago, adodb did a really good job for me. i think you should check it out. – Eimantas Nov 29 '08 at 10:14
  • 2
    Any framework that does this is doing it by expanding the list and interpolating it into the SQL query before the prepare(). It's not the same as bound parameters. – Bill Karwin Nov 29 '08 at 17:53
2

Please take #2 off the table. Prepared statements are the only way you should consider protecting yourself against SQL injection.

What you can do, however, is generate a dynamic set of binding variables. i.e. don't make 100 if you need 7 (or 103).

Dustin
  • 89,080
  • 21
  • 111
  • 133
  • what? that doesn't make sense. He is using prepared statements, but he's dynamically setting the number of placeholders. – Gary Richardson Nov 29 '08 at 23:35
  • In scenario #1, he was statically defining the query to take 100 parameters, in #2, he was not using a prepared statement. My suggestion was to dynamically build the query with bindings, which is the same thing you're saying. – Dustin Nov 29 '08 at 23:46
  • 1
    whoops. I was reading #2 from http://stackoverflow.com/questions/327274/mysql-prepared-statements-with-a-variable-size-variable-list#327384. Sorry! – Gary Richardson Dec 02 '08 at 06:59
2

I got my answer from: http://bugs.php.net/bug.php?id=43568.
This is my working mysqli solution to my problem. Now I can dynamically use as many parameters as I want. They will be the same number as I have in an array or as in this case I am passing the ids from the last query ( which found all the ids where email = 'johndoe@gmail.com') to the dynamic query to get all the info about each of these id no matter how many I end up needing.

<?php $NumofIds = 2; //this is the number of ids I got from the last query
    $parameters=implode(',',array_fill(0,$NumofIds,'?')); 
    // = ?,? the same number of ?'s as ids we are looking for<br />
    $paramtype=implode('',array_fill(0,$NumofIds,'i')); // = ii<br/>
    //make the array to build the bind_param function<br/>
    $idAr[] = $paramtype; //'ii' or how ever many ?'s we have<br/>
    while($statement->fetch()){ //this is my last query i am getting the id out of<br/>
        $idAr[] = $id;  
    }

    //now this array looks like this array:<br/>
    //$idAr = array('ii', 128, 237);

    $query = "SELECT id,studentid,book_title,date FROM contracts WHERE studentid IN ($parameters)";
    $statement = $db->prepare($query);
    //build the bind_param function
    call_user_func_array (array($statement, "bind_param"), $idAr);
    //here is what we used to do before making it dynamic
    //statement->bind_param($paramtype,$v1,$v2);
    $statement->execute();
?>
mickmackusa
  • 43,625
  • 12
  • 83
  • 136
  • If this snippet is building a new query based on the result set of an earlier query, this is generally a symptom of a missed opportunity to use a query with a JOIN and reduce total trips to the database. – mickmackusa Apr 03 '22 at 20:15
1

If you're only using integer values in your IN clause, there's nothing that argues against constructing your query dynamically without the use of SQL parameters.

function convertToInt(&$value, $key)
{
    $value = intval($value);
}

$ids = array('12', '45', '65', '33');
array_walk($ids, 'convertToInt');
$sql = 'SELECT age, name FROM people WHERE id IN (' . implode(', ', $ids) . ')';
// $sql will contain  SELECT age, name FROM people WHERE id IN (12, 45, 65, 33)

But without doubt the solution here is the more general approach to this problem.

Community
  • 1
  • 1
Stefan Gehrig
  • 82,642
  • 24
  • 155
  • 189
  • 2
    Why is the solution to "how do X with a prepared statement?" to dynamically build a different query? If you are using prepared statements to re-use cached query plans, then you have undermined that. If you are doing it to prevent SQL injection, well, that is different. – Brandon Jul 10 '14 at 23:57
0

I had a similiar problem today and I found this topic. Looking at the answers and searching around the google I found a pretty solution.

Although, my problem is a little bit more complicated. Because I have fixed binding values and dynamic too.

This is the mysqli solution.

$params = array()
$all_ids = $this->get_all_ids();

for($i = 0; $i <= sizeof($all_ids) - 1; $i++){
    array_push($params, $all_ids[$i]['id']);
}

$clause = implode(',', array_fill(0, count($params), '?')); // output ?, ?, ?
$total_i = implode('', array_fill(0, count($params), 'i')); // output iiii

$types = "ss" . $total_i; // will reproduce : ssiiii ..etc

// %% it's necessary because of sprintf function
$query = $db->prepare(sprintf("SELECT * 
                                FROM clients    
                                WHERE name LIKE CONCAT('%%', ?, '%%') 
                                AND IFNULL(description, '') LIKE CONCAT('%%', ?, '%%')
                                AND id IN (%s)", $clause));

$thearray = array($name, $description);
$merge    = array_merge($thearray, $params); // output: "John", "Cool guy!", 1, 2, 3, 4

// We need  to pass variables instead of values by reference
// So we need a function to that
call_user_func_array('mysqli_stmt_bind_param', array_merge (array($query, $types), $this->makeValuesReferenced($merge))); 

And the function makeValuesreferenced:

public function makeValuesReferenced($arr){
    $refs = array();
    foreach($arr as $key => $value)
        $refs[$key] = &$arr[$key];
    return $refs;
}

Links for getting this 'know-how': https://bugs.php.net/bug.php?id=49946, PHP append one array to another (not array_push or +), [PHP]: Error -> Too few arguments in sprintf();, http://no2.php.net/manual/en/mysqli-stmt.bind-param.php#89171, Pass by reference problem with PHP 5.3.1

mickmackusa
  • 43,625
  • 12
  • 83
  • 136
user3065191
  • 145
  • 3
  • 17