Dette er en meget interessant forespørgsel. Under dens optimering kan du opdage og forstå en masse ny information om, hvordan MySQL fungerer. Jeg er ikke sikker på, at jeg får tid til at skrive alt i detaljer på én gang, men jeg kan gradvist opdatere.
Hvorfor er det langsomt
Der er grundlæggende to scenarier:et hurtigt og en langsom .
hurtigt scenarie du går i en foruddefineret rækkefølge over en tabel og sandsynligvis samtidig hurtigt hente nogle data efter id for hver række fra andre tabeller. I dette tilfælde stopper du med at gå, så snart du har nok rækker specificeret af din LIMIT-klausul. Hvor kommer ordren fra? Fra et b-træindeks, som du har på bordet eller rækkefølgen af et resultatsæt i en underforespørgsel.
I en langsom scenario har du ikke den foruddefinerede rækkefølge, og MySQL skal implicit lægge alle data i en midlertidig tabel, sortere tabellen på et felt og returnere n rækker fra din LIMIT-klausul. Hvis nogen af de felter, du indsætter i den midlertidige tabel, er af typen TEXT (ikke VARCHAR), forsøger MySQL ikke engang at beholde den tabel i RAM og tømmer og sorterer den på disken (derfor yderligere IO-behandling).
Første ting at rette
Der er mange situationer, hvor du ikke kan bygge et indeks, der giver dig mulighed for at følge dets rækkefølge (når du f.eks. BESTILLER EFTER kolonner fra forskellige tabeller), så tommelfingerreglen i sådanne situationer er at minimere de data, som MySQL vil lægge i den midlertidige tabel. Hvordan kan du gøre det? Du vælger kun identifikatorer for rækkerne i en underforespørgsel, og når du har id'erne, forbinder du id'erne til selve tabellen og andre tabeller for at hente indholdet. Det vil sige, at du laver et lille bord med en ordre og derefter bruger det hurtige scenarie. (Dette er lidt i modstrid med SQL generelt, men hver variant af SQL har sit eget middel til at optimere forespørgsler på den måde).
Tilfældigvis er din SELECT -- everything is ok here
ser sjovt ud, da det er det første sted, hvor det ikke er 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)
;
Det er det første skridt, men allerede nu kan du se, at du ikke behøver at lave disse ubrugelige LEFT JOINS og json-serialiseringer for de rækker, du ikke har brug for. (Jeg sprang GROUP BY p.id
over , fordi jeg ikke kan se, hvilken LEFT JOIN der kan resultere i flere rækker, foretager du ikke nogen aggregering).
endnu at skrive om:
- indekser
- omformuler CASE-klausulen (brug UNION ALL)
- sandsynligvis fremtvinger et indeks