What id does is tell you the byte value for a character. Ord treats each byte as one character, while utf-8 is encoded using 1 to 3. Or if it was 1 to 4. See wikipedia if you need the definitive specs.
Now, let's say you have $str = "Some ? strange? ?? chars"
$out = "";
for ($i = 0, $len = strlen($str); $i < $len; ++$i) {
$out .= "$i: " . ord($str[$i]) . "\t" . $str[$i] . PHP_EOL;
}
file_put_contents("out.txt", $out);
0: 83 S
1: 111 o
2: 109 m
3: 101 e
4: 32
5: 63 ?
6: 32
etc...
ord might not work perfectly since you're dealing with utf-8, but it should still give you an idea. What you are looking for is byte sequences that needs to be replaced. One ? could actually be encoded with several bytes.
Still, once you see what characters are supposed to be shown and have their byte sequences, write code to retrieve all data from the DB, loop over all rows and reconstruct the string while replacing the screwed up byte sequences with whatever is appropriate.
But if you don't have mixed working and non-working content in a single row, it might be enough to detect the problems, then simply doing an encoding conversion on the problematic rows and updating them.