Med `mysql explain` analyserer jeg, hvordan MySQL 8 udarbejder en plan udfører og hvilke trin der tager en målbar tid. På den måde kan jeg ud fra de faktiske køretider, antallet af linjer og løkker se, hvor jeg skal justere en plan og Ydelse målrettet øge antallet af mine søgninger.
Centrale punkter
For at du straks kan komme til sagens kerne, vil jeg kort opsummere de vigtigste læringsmål og angive de relevante Prioriteringer. Hver linje i planen fortæller en historie, og jeg viser dig, hvad du virkelig respekterede. Læs punkterne, gennemgå dine forespørgsler og omsæt indsigterne direkte til optimeringstiltag.
- Faktiske løbetider: EXPLAIN ANALYZE udfører forespørgslen og måler tiden for hvert trin.
- Skøn vs. virkelighed: Store afvigelser tyder på fejl i statistikkerne eller manglende indekser.
- TREE-format: Planen som et træ gør iteratorer, filtre og sammenføjninger synlige.
- Hotspots: Lange „time to last row“-værdier og mange loop markerer tuningmålene.
- Indeksstrategi: Passende (også sammensatte) indekser sænker omkostningerne markant.
Listen giver dig et klart retning, men det er først, når du rent praktisk læser planen, at du kan udnytte denne viden til din fordel. Umiddelbart derefter viser jeg, hvordan jeg vurderer hvert nøgletal, og hvilke næste Trin som jeg udleder heraf.
EXPLAIN vs. EXPLAIN ANALYZE: Hvad jeg egentlig måler
Med klassisk EXPLAIN kan jeg se optimeringsprogrammets planlagte forløb, altså en Udkast med anslåede omkostninger og antal linjer. Denne plan afslører rækkefølgen af tabellerne, de anvendte indekser og sammenkædningsstrategien, dog uden egentlig Målte værdier. EXPLAIN ANALYZE fortsætter og udfører faktisk forespørgslen, måler tiden indtil den første og sidste række samt antallet af løkker. På den måde kan jeg straks se, hvilken node i træet der tager mest tid, og hvor jeg skal sætte ind. Således erstatter jeg gisninger med målte Data og træffe velunderbyggede beslutninger om optimering.
Syntaks og typiske anvendelsestilfælde
Jeg starter analysen med en simpel kommando: EXPLAIN ANALYZE SELECT ..., fordi jeg dermed umiddelbart Løbetider pr. node. Udskriften i TREE-format viser iteratorer som scanninger, sammenføjninger, sorteringer og filtre med estimerede og faktiske Linjer. Jeg bruger det især til gentagne problemforespørgsler, UPDATE/DELETE på flere tabeller og til sætninger med ORDER BY eller GROUP BY. Som et ekstra hjælpemiddel bruger jeg FORMAT=JSON, hvis jeg vil dykke dybt ned i omkostningsmodellen, men til finjustering i hverdagen er træet som regel nok. Den, der ønsker at gå mere i dybden med optimeringsspørgsmål, kan finde gode ideer i Optimizer-detaljer, som jeg bruger i praksis.
Sådan læser jeg TREE-planen
Jeg betragter hver knude som et selvstændigt trin, der genererer data eller filtrerer. Scanninger leverer rækker fra tabeller eller indekser, sammenkoblinger forbinder datastrømme, filtre reducerer antallet af rækker, og sorteringer ordner eller grupperer de Resultater. Felterne „rows (actual/estimated)“, „time to first row“, „time to last row“ og „loops“ er mine vigtigste pejlemærker. Hvis det faktiske antal rækker afviger markant fra estimatet, korrigerer jeg statistikker eller indekser. Hvis „time to last row“ trækker ekstremt ud, tjekker jeg sene sorteringer, store sammenføjninger eller uhensigtsmæssige Filtre.
At forstå nøgletal: Fra skøn til virkelighed
Jeg har samlet de vigtigste nøgletal i en overskuelig tabel, så du hurtigt kan se de typiske signaler genkende. Hver linje viser dig, hvad en måling betyder, hvilket advarselstegn jeg observerer, og hvilken foranstaltning der oftest Hjælper.
| Nøgletal | Betydning | advarselssignal | Tuning-tilgang |
|---|---|---|---|
| rækker (est./akt.) | Planlagt vs. faktisk Linjer | Stor afvigelse (f.eks. 10 mod 100.000) | Opdatere statistikker, manglende Indekser Tjek |
| tid til første række | Tid indtil den første Udgave | Langsomt på trods af et lille antal resultater | Kontroller startknudepunktet, tidlige filtre styrke |
| tid til sidste række | Samlet varighed af Knudepunkter | Betydeligt højere end „first row“ | Sortering, sammenkoblingsstrategi, strømme reducere |
| loops | Hyppigheden af Gentagelse | Mange iterationer | Omstrukturering af JOIN'er, underforespørgsler omforme |
Korrekt fortolkning af operatorer: scanninger, sammenkoblinger, sorteringer
Jeg lægger mærke til, hvilken Iterator faktisk udfører arbejdet:
- Indeksinterval/unik scanning: Ideel ved selektive WHERE-betingelser og passende præfikser; „time to first row“ er kort, „time to last row“ afhænger af resultatsættet.
- Tabelgennemgang: Advarselssignal ved store tabeller; i så fald leder jeg efter egnede filtre, sammensatte indekser eller en omformulering af forespørgslen.
- Nested loop-sammenføjning: Standardstrategi; mange „loops“ tyder på en uegnet driver eller manglende indeks på den indre tabel.
- Hash-sammenføjning (MySQL 8): Velegnet til store, jævnt fordelte Equi-Joins. „Time to first row“ kan være længere (i opbygningsfasen), men „time to last row“ forbedres, hvis prøvestrømmen er stor.
- Sorter/Gruppe: Synlig som en separat node i TREE. Lange køretider tyder ofte på manglende understøttelse via indekser.
- Filtre: Senere filtre indikerer forpassede muligheder for indeksbetinget pushdown eller tidligere selektion.
Hvis en Sort-node dominerer „time to last row“, undersøger jeg, om den ønskede rækkefølge kan opnås via et indeks, f.eks. ved hjælp af Covering-Indekser med passende sorteringsrækkefølge. Hvis ORDER BY-klausulen stemmer overens med indeksdefinitionen (retning, præfiks), kan sorteringstrinnet ofte helt udelades.
Målemetode: Sådan sammenligner jeg på en retfærdig måde
Jeg måler ikke kun én gang. Caching-effekter kan forvride indtrykket, derfor:
- Jeg kører EXPLAIN ANALYZE flere gange og vurderer medianen og spændvidden i stedet for en enkelt værdi.
- Jeg skelner mellem „kold“ og „varm“ cache: Varme målinger viser, hvad brugerne oplever efter den første kørsel.
- Jeg varierer repræsentative parametre, så planen ikke kun ser god ud i et trivielt eksempel.
- Jeg dokumenterer skemaet og datastatus, så jeg senere kan spore resultaterne.
Ved DML-sætninger (UPDATE/DELETE) bruger jeg en transaktion: START TRANSACTION; EXPLAIN ANALYZE UPDATE ...; ROLLBACK;. På den måde får jeg reelle måleværdier uden varige ændringer. Vigtigt: EXPLAIN ANALYZE fører – derfor bruger jeg det med omtanke på produktionssystemerne.
Statistik og datadistribution: Afhjælpning af skønfejl
Store forskelle mellem linjerne „estimated“ og „actual“ skyldes ofte skæve datafordelinger. I sådanne tilfælde anvender jeg en tostrenget tilgang:
- Opdater statistikker: Jeg sørger for, at optimeringsværktøjet har aktuelle oplysninger. Opdaterede statistikker forbedrer valget af sammenkædninger og indekser.
- Brug af histogrammer: Ved kolonner med stor skævhed hjælper histogrammer med at estimere selektiviteterne mere realistisk. I EXPLAIN ANALYZE mindskes forskellen mellem estimatet og virkeligheden så mærkbart.
Hvis estimaterne fortsat er forkerte efter opdateringen, gennemgår jeg sammensatte indekser i rækkefølge efter de mest selektive prædikater og ser på korrelationer mellem kolonnerne. Målet er, at så få, godt forfiltrerede rækker som muligt så tidligt som muligt indgår i de dyre operatorer.
Semi-join-strategier og underforespørgsler
MySQL 8 omdanner ofte IN/EXISTS-prædikater til semi-join-planer. I TREE ser jeg det som »Materialization«, »FirstMatch« eller »Loose Index Scan«. Jeg er opmærksom på:
- Materialisering: En delmængde oprettes én gang og genbruges flere gange – velegnet til moderat størrelse.
- FirstMatch: Stop tidligt ved det første hit – det sparer loops, hvis der kun forventes få hits pr. ydre række.
- Løs indeksscanning: Meget effektiv ved DISTINCT-lignende mønstre via indekser.
Underforespørgsler, der kører for hver række i den ydre tabel, forlænger „loops“. Jeg omdanner dem til JOIN’er eller materialiserer dem bevidst (CTE/Derived), så kørselsplanen udfører det ressourcekrævende arbejde én gang og derefter henviser til resultatet på en mere effektiv måde.
Målrettet SQL-optimering: trin for trin
Jeg starter med indeksstrategien og sikrer hyppige WHERE- og JOIN-betingelser med Indekser . Hvis jeg har brug for flere kolonner i filtrering eller sortering, opretter jeg sammensatte indekser og tilpasser kolonnernes rækkefølge efter de hyppigst forekommende Prædikater. Derefter optimerer jeg underforespørgsler, der kører i løkker, ved at omformulere dem eller omdanne dem til sammenkædninger. Jeg erstatter SELECT * med konkrete kolonner, så der flyttes færre data, og planen aflastes. Derefter holder jeg statistikkerne opdaterede, da unøjagtige skøn leder optimeringsværktøjet i den forkerte retning Afvigelser.
Indeks-praksis: Dækning, rækkefølge, eksperimenter
Jeg bruger tre enkle parametre, som straks bliver synlige i EXPLAIN ANALYZE:
- Dækkende indekser: Hvis indekset indeholder alle de nødvendige kolonner (filter, sammenkobling, projektion), sparer planen for tabelopslag. „time to last row“ falder ofte markant.
- Rækkefølgen af kolonnerne: Jeg sorterer efter selektivitet og brugstype (filter før sortering). Til ORDER BY/GROUP BY bruger jeg den korrekte retning og det passende præfiks.
- Indeksforsøg: Med midlertidige, usynlige Jeg tester indekser for at se, om optimeringsværktøjet ville vælge dem uden at forstyrre eksisterende planer. Hvis planen bliver bedre, aktiverer jeg indekset permanent.
Hvis der findes flere kandidatindekser, sammenligner jeg planerne med EXPLAIN ANALYZE og måler konsekvent „time to last row“. I tvivlstilfælde vælges den plan, der har den mest stabile køretid på tværs af forskellige parameterværdier.
Praktisk eksempel: Læse planen, oprette indeks, måle succesen
Jeg tager et typisk spørgsmål: EXPLAIN ANALYZE SELECT o.id, o.date, c.name FROM orders o JOIN customers c ON c.id = o.customer_id WHERE o.date >= '2025-01-01' ORDER BY o.date DESC; og tjek først knuden for tabellen ordrer. Hvis planen viser et stort antal faktiske rækker og en fuld tabelscanning, opretter jeg en passende indeks, f.eks. på orders(dato, kunde-id). Derefter sammenligner jeg „time to last row“ før og efter ændringen, fordi dette tal meget tydeligt viser den samlede effekt viser. Hvis ORDER BY stemmer overens med indeksrækkefølgen, sparer jeg mig for en sortering og reducerer den samlede varighed betydeligt. På den måde dokumenterer jeg fremskridt med målte værdier i stedet for med vage Indtryk.
Sikker analyse af DML-sætninger
Når det gælder UPDATE/DELETE-kommandoer, der ændrer datamængden, går jeg struktureret til værks:
- Jeg indkapsler målingen i en transaktion og ruller den tilbage, hvis jeg blot vil foretage en måling.
- Jeg undersøger, om triggere/begrænsninger medfører ekstra omkostninger – EXPLAIN ANALYZE viser længere køretider i de berørte noder.
- Jeg holder øje med forholdet mellem „affected rows“ og „rows actual“ – et dårligt forhold tyder på, at filtreringen sker for sent, eller at der mangler indekser.
Ved UPDATE-sæt, der involverer flere tabeller, er rækkefølgen af sammenkoblinger og indeksdækning afgørende. Lange „time to last row“-tider ved sorterings-/sammenkoblingsknudepunkter tyder på, at der er potentiale for indeksforbedringer eller en omformulering til to målrettede sætninger med mellemopbevaring.
Hostingens indflydelse på forespørgselens ydeevne
Jeg betragter ikke databasen isoleret, for hukommelse, I/O og CPU præger hver Runtime. Hurtige SSD’er forkorter ventetiden ved læsning, tilstrækkelig RAM udvider bufferpuljen, og en solid CPU-stack fremskynder sortering, aggregering og Tilslutter sig. I produktive miljøer foretrækker jeg hosting-opsætninger, der klarer datakrævende arbejdsbelastninger godt. Jeg får også nyttig baggrundsviden om emner vedrørende optimering fra Optimizer internt, som jeg bruger som et supplerende perspektiv. Når jeg kombinerer en velgennemtænkt plan med et stærkt miljø, opnår jeg mærkbare gevinster ved Svartider.
Ressourcer og operatorer i sammenhæng
Når jeg læser planen, lægger jeg især mærke til hukommelseskrævende knudepunkter. Store sorteringer eller hash-joins kræver arbejdshukommelse; hvis de er for store, udføres de i midlertidige tabeller. I TREE kan jeg se dette på sene, langsomme knudepunkter og en tydelig forskel mellem „time to first row“ og „time to last row“. Jeg reagerer ved at:
- Reduktion af indgangsdatamængden (tidligere filtre, bedre join-drivere).
- Forbedret indeksstøtte til den ønskede rækkefølge for at undgå sorter.
- Kontroller, om sammenkoblingstypen (Nested Loop eller Hash) passer til datamængden.
Især ved rapportkørsler kører jeg EXPLAIN ANALYZE på repræsentative data, ikke på mini-snapshots. Først da afspejler måleværdierne den reelle belastning.
Gode råd til hverdagen
Først analyserer jeg de forespørgsler, der springer i øjnene i logfilerne, eller som brugerne regelmæssigt oplever som langsomme anmelde. Derefter foretager jeg målinger med EXPLAIN ANALYZE, dokumenterer de vigtigste tal og sammenligner skøn med de faktiske resultater. På dette grundlag ændrer jeg målrettet indekser og formuleringer og noterer resultaterne før og efter, så fremskridtene kan spores lave. Jeg indplanlægger disse analyser tidligt i udviklingsforløbet i stedet for at vente på, at der opstår produktionsproblemer. Gennem gentagne gennemgange kan jeg hurtigere genkende mønstre og træffe mere sikre beslutninger om Indstilling-tiltag.
Pragmatisk tjekliste til hurtigere planer
- Anslåede og faktiske stemmer rækker Stemmer det groft set overens? Hvis ikke: Tjek statistikker/histogrammer.
- Er der en node, der dominerer „time to last row“? Første kandidat til optimering (indeks, valg af sammenføjning, undgåelse af sortering).
- Er „loops“ meget høje? Forbedr join-driveren/indekset på den indre tabel, eller brug semi-join.
- Er der sene sorteringer/grupperinger? Tilpas indeksrækkefølgen og -retningen efter ORDER BY/GROUP BY.
- Er det virkelig nødvendigt at hente alle kolonner i forespørgslen? Arbejd hen imod et covering-indeks, og stram SELECT-listen op.
- En underforespørgsel pr. linje? Omskriv til JOIN eller materialiser.
- Stabil over parametrene? Mål med flere realistiske værdier.
Almindelige fejlfortolkninger og hvordan jeg undgår dem
Jeg stoler ikke blindt på anslåede tal Omkostninger, hvis det faktiske antal linjer afviger markant. Ligeledes drager jeg ingen forhastede konklusioner ud fra „time to first row“, hvis „time to last row“ udgør hovedparten bærer. En hurtig start nytter ikke meget, hvis sortering eller sammenkobling dominerer i sidste ende. Desuden gennemgår jeg sløjfer grundigt, da de ofte skjuler en ineffektiv sammenkobling eller en underforespørgsel, der kører for hver linje. Først når kørselsplanen, måleværdierne og datafordelingen stemmer overens, ændrer jeg Ting.
Særlige tilfælde: CTE’er, afledte tabeller, partitioner
Common Table Expressions (CTE’er) og afledte tabeller kan materialiseres eller sammenføjes. I TREE opfatter jeg materialisering som et separat opbygningsskridt. Det er en fordel, hvis delstrømmen bruges flere gange eller er dyr at beregne. Hvis CTE'er kun bruges én gang og er selektive, er en sammenlægning ofte mere effektiv, fordi der ikke kræves yderligere lagringsarbejde. Jeg holder øje med, om „time to first row“ stiger markant – i så fald er materialiseringen muligvis overdimensioneret.
Partitionerede tabeller er en hjælp ved store datamængder, hvis prædikatet klart afgrænser partitionerne. Jeg tjekker i udførelsesplanen, om pruning finder anvendelse (kun få partitioner bliver scannet). Mangler dette, fordeles omkostningerne på alle partitioner – et tegn på, at partitionsnøglerne bør tilpasses de hyppigst anvendte filtre, eller at forespørgslen bør formuleres således, at pruning bliver mulig.
Kort opsummeret
Med EXPLAIN ANALYZE gør jeg MySQL-planer målbare og afdækker hotspots, som jeg med Indekser, omformulering af forespørgsler og aktuelle statistikker. Jeg fokuserer på afvigelser mellem det anslåede og det faktiske antal rækker, tiden indtil den første og sidste række samt Loops. Ud fra dette udleder jeg nogle få, effektive trin og kontrollerer hver enkelt effekt igen med EXPLAIN ANALYZE. Med tiden genkender jeg mønstre med det samme og iværksætter passende tiltag hurtigere. På den måde øger jeg Ydelse pålidelig og sikrer, at forespørgsler forbliver stabile på lang sigt.


