Dare un aggiornamento molto tardivo su questa domanda:
Non ho trovato la causa, ma si scopre che EXPLAIN era diverso in PHP rispetto alla CLI. Non sono sicuro che qualche aspetto della connessione possa indurre MySQL a scegliere di utilizzare un campo diverso per l'indice, perché per quanto ne so queste cose non dovrebbero essere correlate; ma ahimè, EXPLAIN di PHP ha mostrato che l'indice corretto non veniva utilizzato, mentre lo faceva la CLI.
La soluzione in questo caso (sconcertante) è usare index hinting . Vedi la riga "FROM" in questa query modificata dal mio esempio:
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;
Spero che questo aiuti qualcuno!