Essa é uma consulta muito interessante. Durante sua otimização, você pode descobrir e entender muitas informações novas sobre como o MySQL funciona. Não tenho certeza de que terei tempo para escrever tudo em detalhes de uma só vez, mas posso atualizar gradualmente.
Por que é lento
Existem basicamente dois cenários:um rápido e um lento .
Em um rápido cenário você está andando em alguma ordem predefinida sobre uma tabela e provavelmente ao mesmo tempo busca rapidamente alguns dados por id para cada linha de outras tabelas. Nesse caso, você para de andar assim que tiver linhas suficientes especificadas pela sua cláusula LIMIT. De onde vem a ordem? De um índice b-tree que você tem na tabela ou na ordem de um conjunto de resultados em uma subconsulta.
Em um lento cenário você não tem essa ordem predefinida, e o MySQL tem que colocar implicitamente todos os dados em uma tabela temporária, ordenar a tabela em algum campo e retornar o n linhas da sua cláusula LIMIT. Se algum dos campos que você colocar nessa tabela temporária for do tipo TEXT (não VARCHAR), o MySQL nem tentará manter essa tabela na RAM e a liberará e a classificará no disco (daí processamento de IO adicional).
Primeira coisa a corrigir
Existem muitas situações em que você não pode construir um índice que permita seguir sua ordem (quando você ORDER BY colunas de tabelas diferentes, por exemplo), então a regra geral em tais situações é minimizar os dados que o MySQL colocará na tabela temporária. Como você pode fazer isso? Você seleciona apenas os identificadores das linhas em uma subconsulta e, após ter os ids, une os ids à própria tabela e a outras tabelas para buscar o conteúdo. Ou seja, você faz uma pequena mesa com um pedido e depois usa o cenário rápido. (Isso contradiz um pouco o SQL em geral, mas cada sabor do SQL tem seus próprios meios para otimizar as consultas dessa maneira).
Coincidentemente, seu
SELECT -- everything is ok here
parece engraçado, já que é o primeiro lugar onde não está ok. SELECT p.*
, u.name user_name, u.status user_status
, c.name city_name, t.name town_name, d.name dist_name
, pm.meta_name, pm.meta_email, pm.meta_phone
, (SELECT concat("{",
'"id":"', pc.id, '",',
'"content":"', replace(pc.content, '"', '\\"'), '",',
'"date":"', pc.date, '",',
'"user_id":"', pcu.id, '",',
'"user_name":"', pcu.name, '"}"') last_comment_json
FROM post_comments pc
LEFT JOIN users pcu ON (pcu.id = pc.user_id)
WHERE pc.post_id = p.id
ORDER BY pc.id DESC LIMIT 1) AS last_comment
FROM (
SELECT id
FROM posts p
WHERE p.status = 'published'
ORDER BY
(CASE WHEN p.created_at >= unix_timestamp(now() - INTERVAL p.reputation DAY)
THEN +p.reputation ELSE NULL END) DESC,
p.id DESC
LIMIT 0,10
) ids
JOIN posts p ON ids.id = p.id -- mind the join for the p data
LEFT JOIN users u ON (u.id = p.user_id)
LEFT JOIN citys c ON (c.id = p.city_id)
LEFT JOIN towns t ON (t.id = p.town_id)
LEFT JOIN dists d ON (d.id = p.dist_id)
LEFT JOIN post_metas pm ON (pm.post_id = p.id)
;
Esse é o primeiro passo, mas mesmo agora você pode ver que não precisa fazer essas serializações LEFT JOINS e json inúteis para as linhas que você não precisa. (Eu pulei
GROUP BY p.id
, porque não vejo qual LEFT JOIN pode resultar em várias linhas, você não faz nenhuma agregação). ainda escrever sobre:
- índices
- reformule a cláusula CASE (use UNION ALL)
- provavelmente forçando um índice