Using fewer foreach loops to bind values to parameters in PDO
The PDOStatement class (which is what PDO::prepare returns, allows you to pass an array of values to the execute method. If you use that, you don't have to loop over any arrays, calling bindValue over and over:

$vals = array(
    $_SESSION['user_id']
);
$vals = array_merge(
    $vals,
    $_POST['tid']
);
$in = array_fill(
    0, count($vals),
    '?'
);//construct array containing ?
$stmt = $pdo-&gt;prepare(
    'SELECT b.name,a.users
     FROM `table1` a
       INNER JOIN `table2` b
         ON a.id = b.id 
     WHERE a.id IN ('.implode(',',$in).')
     GROUP BY a.id
     ORDER BY FIELD(a.id, '.implode(',',$in).')'
);
$stmt-&gt;execute($vals);


The added benefit of this approach is that you're able to use named placeholders, with just a few minor adjustments:

$vals = array(
    ':sessionUid' =&gt; $_SESSION['user_id']
);
foreach ($_POST['tid'] as $idx =&gt; $val) {
    $vals[':uid'.$idx] =&gt; $val;
}
$in = implode(',', array_keys($vals));
$stmt = $pdo-&gt;prepare(
    'SELECT b.name,a.users
     FROM `table1` a
       INNER JOIN `table2` b
         ON a.id = b.id 
     WHERE a.id IN ('.$in.')
     GROUP BY a.id
     ORDER BY FIELD(a.id, '.$in.')'
);
$stmt-&gt;execute($vals);


Of course, there are a few tradeoffs. Nothing too bad, but still:


PDOStatement::execute(array $bind) binds all of the values as PDO::PARAM_STR (ie string values). This implies you can't use array binding if you are using placeholders in, for example, LIMIT clauses. In those cases, a loop + $stmt-&gt;bindValue(':offset', $value, PDO::PARAM_INT); is required. Writing a wrapper that does this for you is not uncommon, and in some cases desirable.
re-use of placeholders is only supported if you're using PDO's emulated prepared statements:


Because you seem to be re-using the same placeholders here, you cannot disable emulated prepares. MySQL does not support the re-use of named placeholders. If you do disable emulated prepares, you will need to add a loop:

//create vals as before
$in = array_keys($vals);
$query = 'SELECT b.name,a.users
     FROM `table1` a
       INNER JOIN `table2` b
         ON a.id = b.id 
     WHERE a.id IN ('.implode(','$in).')
     GROUP BY a.id
     ORDER BY FIELD(a.id, ';
foreach ($in as $idx =&gt; $key) {
    $vals[$key.$idx] = $vals[$key];//add value a second time, using a different key
    $in[$idx] .= $idx;//change values in $in
}
$query .= implode(',', $in).')';//add new placeholders to query
$stmt = $pdo-&gt;prepare($query);
$stmt-&gt;execute($vals);//should work now


Apart from that, I'd suggest you set PDO's error mode so that it actually throws exceptions. As it stands, PDO will throw an exception if the connection fails, but not if any of the other calls fail (prepare, execute,...).
To fix this, either call PDO::setAttribute, or pass an array to the constructor:

$pdo = new PDO(
    $dsn,
    $user,
    $pass,
    array(
        PDO::ATTR_ERRMODE =&gt; PDO::ERRMODE_EXCEPTION
    )
);
//or
$pdo-&gt;setAttribute(PDO::ATTR_ERRMODE, PDO::ERRMODE_EXCEPTION);


All in all, loops like you have do very little harm, avoiding them can improve readability, but their impact on performance is so minute that getting rid of them, for the sake of optimization is not a great idea. Remember "Premature optimization is the root of all evil".