Donner une mise à jour très tardive sur cette question :
Je n'ai pas trouvé la cause, mais il s'avère que l'EXPLAIN était différent en PHP par rapport à la CLI. Je ne sais pas si un aspect de la connexion amènerait MySQL à choisir d'utiliser un champ différent pour l'index, car pour autant que je sache, ces choses ne devraient pas être liées; mais hélas, EXPLAIN de PHP a montré que l'index approprié n'était pas utilisé, contrairement à la CLI.
La solution dans ce cas (déconcertant) est d'utiliser index hinting . Voir la ligne 'FROM' dans cette requête modifiée de mon exemple :
SELECT HEX(al.uuid) hexUUID, al.created_on,
IFNULL(al.state, 'ON') actionType, pp.publishers_id publisher_id,
pp.products_id product_id, al.action_id, al.last_updated
FROM ActionAPI.actionLists al USE INDEX (created_on)
LEFT JOIN ActionAPI.publishers_products pp
ON al.publisher_product_id = pp.id
WHERE (al.test IS NULL OR al.test = 0)
AND (al.created_on >= :since OR al.last_updated >= :since)
ORDER BY created_on ASC
LIMIT :skip, 100;
J'espère que cela aidera quelqu'un !