34

Imagine we have a query:

SELECT * FROM somewhere WHERE `id` IN(1,5,18,25) ORDER BY `name`;

and an array of IDs to fetch: $ids = array(1,5,18,25)

With prepared statements it's adviced to prepare one statement and call it multiple times:

$stmt = $mysqli->prepare('SELECT * FROM somewhere WHERE `id`=?;');
foreach ($ids as $id){
    $stmt->bind_params('i', $id);
    $stmt->exec();
}

But now I'll have to sort the results manually. Do I have any nice alternatives?

Dharman
  • 30,962
  • 25
  • 85
  • 135
kolypto
  • 31,774
  • 17
  • 105
  • 99
  • According to the link you provided I think the reasoning for preparing statements was the need to UPDATE which doesn't support multiple updates in one query. Whereas you are SELECTing and so your first query is sufficient. – Ryan Schumacher Sep 13 '10 at 18:30
  • I was convinced that prepared statements are not good in such cases. The only nice solution is to 'go one query up' and make this array of IDs sorted there, not here. – kolypto Sep 14 '10 at 01:13

8 Answers8

29

you could do it this way:

$ids = array(1,5,18,25);

// creates a string containing ?,?,? 
$clause = implode(',', array_fill(0, count($ids), '?'));


$stmt = $mysqli->prepare('SELECT * FROM somewhere WHERE `id` IN (' . $clause . ') ORDER BY `name`;');

call_user_func_array(array($stmt, 'bind_param'), $ids);
$stmt->execute();

// loop through results

Using this you're calling bind_param for each id and you have sorting done by mysql.

sled
  • 14,525
  • 3
  • 42
  • 70
  • 1
    Yep, but the bad thing is that the query can't be reused so there's no need to prepare it at all :) – kolypto Sep 13 '10 at 18:26
  • 1
    @o_O Tync: The fewer database trips, the better. You're welcome to stick to the PreparedStatement constraints, but it won't scale if you need to run 10/20/50/100/1000+ queries to the same table. – OMG Ponies Sep 13 '10 at 18:30
  • 1
    Wouldn't it be better to just create a view for this statement since it couldn't be reused either way? – Ryan Schumacher Sep 13 '10 at 18:35
  • @OMG Ponies, a good point about scaling! Looks like prepared statements can't handle *everything* in an efficient way. Thanks! – kolypto Sep 13 '10 at 19:29
  • @o_O Tync: Yeah, Prepared Statements are not a silver bullet – OMG Ponies Sep 13 '10 at 19:41
  • 3
    @Ryan Schumacher: Am I missing something? I don't see what benefit a view would be for passing filtration parameters. It's just a stored SQL statement... – OMG Ponies Sep 13 '10 at 19:42
  • @OMG Ponies: Views are just stored SQL statements. I was comparing the use of a prepared statement using static query, but I misread the code. :S Sorry for the confusion. – Ryan Schumacher Sep 14 '10 at 15:00
  • 1
    This is problematic in php 5.3 for this reason: http://ca.php.net/manual/en/mysqli-stmt.bind-param.php#96770 – Levi Jun 30 '14 at 06:04
  • funny how nobody didn't notice input filtering. One of benefits of PDO and this method allows to use it. – OZ_ Jun 10 '15 at 18:52
  • 1
    @kolypto Prepared statements aren't just for reuse! They also provide variable binding, which means translation from PHP datatypes to SQL datatypes is handled by your connector (or even database,) which means less thinking and more security. Pure bonus! – Milosz Jan 17 '17 at 23:28
18

Had the same problem and in addition to the answer of @sled 7 years ago, here is a possibility without making the call_user_func_array(array($stmt, 'bind_param'), $ids); step, but only call bind_params once:

$ids = array(1,5,18,25);

// creates a string containing ?,?,? 
$bindClause = implode(',', array_fill(0, count($ids), '?'));
//create a string for the bind param just containing the right amount of s 
$bindString = str_repeat('s', count($ids));

$stmt = $mysqli->prepare('SELECT * FROM somewhere WHERE `id` IN (' . $bindClause . ') ORDER BY `name`;');

$stmt->bind_param($bindString, ...$ids);
$stmt->execute();
Your Common Sense
  • 156,878
  • 40
  • 214
  • 345
Fabian N.
  • 1,221
  • 10
  • 18
  • Very smart code. What does the three dots before $ids in your code in the bind_params function represent? – Hmerman6006 Feb 01 '20 at 12:00
  • 2
    Its called argument unpacking or splat operator. The `bind_params` function takes a variable number of arguments. `...$ids` means, that the array is unpacked in the n-th parameter of the function call. – Fabian N. Feb 02 '20 at 20:34
  • can u suggest for a situation with more than one "where in" inside single query? – Pankaj Nov 14 '21 at 22:26
  • @Pankaj it's the same, just use array_merge to put all arrays in one – Your Common Sense May 15 '22 at 06:08
  • @Pankaj Here is [an example of how to set up the placeholders, the query, and the bindings](https://phpize.online/sql/mysql80/0ae0d8b4f91ad0df0ae116f30a50ba9b/php/php8/24604e03f73c6a43ca2b3e1d54580fbc/). – mickmackusa Jun 06 '22 at 20:47
6

I believe this is the simplest possible answer :

$ids = [1,2,3,4,5];
$pdos = $pdo->prepare("SELECT * FROM somwhere WHERE id IN (:"
        . implode(',:', array_keys($ids)) . ") ORDER BY id");

foreach ($ids as $k => $id) {
    $pdos->bindValue(":". $k, $id);
}

$pdos->execute();
$results = $pdos->fetchAll();

So long your array of Ids does not contain keys or keys with illegal characters, it wil work.

ByteHamster
  • 4,884
  • 9
  • 38
  • 53
Martin J.
  • 364
  • 3
  • 7
5

For the task of executing a secure mysqli query with a dynamic number of incoming values to be fed into the sql string, a prepared statement is the professional technique to implement.

Let's assume that the incoming data payload is user-supplied data -- this means that we cannot guarantee the integrity of the data nor can we guarantee the volume of data. In fact, the expected array of data might be empty. The below snippet will demonstrate how to pass an array of ids to the IN () condition in the WHERE clause of a prepared statement. If there are no values in the array, then a prepared statement provides no benefit and should not be used.

MySQLi result set objects can be immediately iterated by a foreach() loop. Therefore, it is not necessary to make iterated fetch calls; just access the rows' data using array syntax.

The array of ids means that the sql will expect integer values. When calling bind_param(), the first parameter will be a single string of repeated i characters. For general use, if the data will be strings or you might have a mix of data types (e.g. integers, floats/doubles, or strings), then is simpler to just use repeated s characters instead of i characters.

Code: (PHPize.online Demo with SQL)

$ids = [1, 5, 18, 25];  // this could be, for example: $_GET['ids']
$count = count($ids);

$sql = 'SELECT name FROM somewhere';
$orderBy = 'ORDER BY name';
if ($count) {
    $placeholders = implode(',', array_fill(0, $count, '?'));
    $stmt = $mysqli->prepare("$sql WHERE id IN ($placeholders) $orderBy");
    $stmt->bind_param(str_repeat('i', $count), ...$ids);
    $stmt->execute();
    $result = $stmt->get_result();
} else {
    $result = $mysqli->query("$sql $orderBy"); // a prepared statement is unnecessary
}
foreach ($result as $row) {
    echo "<div>{$row['name']}</div>\n";
}

Output from my PHPize demo:

<div>Alan</div>
<div>Bill</div>
<div>Chad</div>
<div>Dave</div>

If you don't need to iterate the result set for any reason, then you can fetch_all(). This is commonly used when immediately echoing or returning a json-encoded string (say, as the response to an ajax call). In this case, you replace the foreach() block with: (PHPize.online Demo with SQL)

echo json_encode($result->fetch_all(MYSQLI_ASSOC));

or simply dump the multidimensional array:

var_export($result->fetch_all(MYSQLI_ASSOC));

Output from my PHPize demo:

[{"name":"Alan"},{"name":"Bill"},{"name":"Chad"},{"name":"Dave"}]

From PHP8.1 and higher, it is no longer necessary to call bind_param() because the execute() method can receive the payload of parameters as an array (like PDO).

This means that...

$stmt->bind_param(str_repeat('i', $count), ...$ids);
$stmt->execute();

can be replaced with...

$stmt->execute($ids);

Here's a complete, basic example: (PHPize.online Demo)

$ids = [1, 2, 3, 4, 5];
$stmt = $mysqli->prepare("SELECT * FROM somewhere WHERE id IN (" . rtrim(str_repeat('?,', count($ids)), ',') . ") ORDER BY id");
$stmt->execute($ids);
var_export($stmt->get_result()->fetch_all(MYSQLI_ASSOC));

Topical Resources:

mickmackusa
  • 43,625
  • 12
  • 83
  • 136
2

I'll add a slow & ugly solution which nevertheless uses prepared statements for ANY number of array items :) 3 statements are universal for any case and can be reused everywhere.

  1. CREATE TEMPORARY TABLE `ids`( `id` INT );
  2. INSERT INTO `ids` VALUES(?); this will insert your IDs
  3. SELECT `id` FROM `ids` LEFT JOIN .... ; use data from other tables to sort the ids list
  4. SELECT `id` FROM `ids`; select everything back

Otherwise you'll have to use IN (?,?,?,.... or sort the rows manually. The best idea is to use simple MySQL-queries, or, try to get the list of IDs already sorted in the way you like.

kolypto
  • 31,774
  • 17
  • 105
  • 99
0

Have you considered rewriting you original query using a JOIN and WHERE clause to get the IDS you need to avoid the need for a WHERE IN clause? I came here with the same question and after reviewing the possible solutions I realized an INNER JOIN was my solution.

zeros-and-ones
  • 4,227
  • 6
  • 35
  • 54
  • 1
    That was the internal logic: the app needed to fetch N users by id, provided externally. Glad that your case turned out to be not so specific :) – kolypto Jan 07 '14 at 04:22
0

Copied from my answer here How to use PDO prepared statements with IN clause?

using named place holders

$values = array(":val1"=>"value1", ":val2"=>"value2", ":val2"=>"$value3");
$statement = 'SELECT * FROM <table> WHERE `column` in(:'.implode(', :',array_keys($values)).') ORDER BY `column`';

using ??

$values = array("value1", "value2", "$value3");
$statement = 'SELECT * FROM <table> WHERE `column` in('.trim(str_repeat(', ?', count($values)), ', ').')  ORDER BY `column`';
Clint
  • 973
  • 7
  • 18
-1

An alternative would be to use PHP usort function on the result object, but this is "manual."

See this: Sort Object in PHP

Community
  • 1
  • 1
Ryan Schumacher
  • 1,816
  • 2
  • 21
  • 33