...

Analyser MySQL's log over langsomme forespørgsler i hosting: Tips til optimering

Log over langsomme forespørgsler i MySQL viser mig i hosting, hvilke forespørgsler der æder tid, hvor ofte de forekommer, og hvorfor de gør tingene langsommere. Jeg viser dig specifikke trin til, hvordan du aktiverer loggen, analyserer den og genopbygger forespørgsler, så siderne indlæses hurtigere, og serverressourcerne arbejder mere effektivt.

Centrale punkter

  • Aktivering og indstil tærskelværdierne fornuftigt
  • Evaluering med pt-query-digest og mysqldumpslow
  • Metrikker fortolke: Query_time, Lock_time, Rows_examined
  • Indstilling gennem indekser, EXPLAIN og omskrivninger
  • Automatisering og overvågning i hosting

Hvad gør den langsomme forespørgselslog i hosting?

Hosting betyder delte ressourcer, så hvert millisekund pr. forespørgsel tæller. Jeg bruger loggen til at finde forespørgsler, der kører længere end en defineret grænse, og ser nøgletal som Query_time, Lock_time, Rows_sent og Rows_examined for hver forespørgsel. Disse tal viser mig, om der ligger et manglende indeks, et ugunstigt join eller en fuld tabelscanning bag. Især på servere med flere sites kan en enkelt dårlig forespørgsel lægge et stort pres på CPU og I/O. Jeg prioriterer derefter de forespørgsler, der har den højeste samlede tid, fordi det er her, den største indflydelse på indlæsningstiden og serverbelastningen ligger.

Aktivering og fornuftige tærskelværdier

StartJeg kan køre runtime eller permanent via my.cnf, afhængigt af adgangen i hostingen. Til hurtige tests slår jeg loggen til midlertidigt og sætter long_query_time til en værdi, der matcher trafikken og hardwaren. Jeg går ofte ned til 0,1 sekunder for meget brugte websteder, men overvåger logstørrelsen, så I/O ikke vokser unødigt. Hvis direkte filadgang er begrænset, bruger jeg performance schema-indstillingerne i MySQL-shell til at generere rapporter. Efter finjusteringen skriver jeg de endelige indstillinger i konfigurationsfilen og genstarter tjenesten.

SET GLOBAL slow_query_log = 'ON';
SET GLOBAL long_query_time = 1;
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow-query.log';
SET GLOBAL log_queries_not_using_indexes = 'ON';

SHOW VARIABLES LIKE 'slow_query%';
SHOW VARIABLES LIKE 'long_query_time';

Permanent Jeg indstiller indstillinger som log_throttle_queries_not_using_indexes og log_slow_admin_statements, så loggen forbliver brugbar og ikke eksploderer. Jeg dokumenterer alle værdier, f.eks. hvorfor long_query_time er 0,5 eller 0,1 sekunder. Det giver mig mulighed for at forfine det senere. I delte miljøer diskuterer jeg ofte aktiveringen med udbyderen eller bruger deres panel. Jeg forbinder hver aktivering med en startdato for at kunne sammenligne effekter i overvågning og metrikker på en ren måde.

[mysqld]
slow_query_log = 1
slow_query_log_file = /var/log/mysql/slow-query.log
lang_forespørgsel_tid = 1
log_queries_not_using_indexes = 1
log_throttle_queries_not_using_indexes = 10
log_slow_admin_statements = 1

Evaluer langsom log effektivt

Rå data er støjende, så jeg opsummerer dem med pt-query-digest og sorterer efter samlet tid over en meningsfuld periode. Det er sådan, jeg genkender mønstre, meget variable forespørgsler og forespørgselsfamilier, der kun varierer efter parameter. Jeg tjekker fordelingen, ikke bare gennemsnittet, fordi afvigelser giver reelle brugerproblemer. For at få et hurtigt overblik hjælper mysqldumpslow mig med at se de ti langsomste grupper. Jeg får mere dybde ved hjælp af tidsvinduer, databasefiltre og en eksport til en tekstanalyse.

pt-query-digest /var/log/mysql/slow-query.log
pt-query-digest --since '24h' /var/log/mysql/slow-query.log
mysqldumpslow -s t -t 10 /var/log/mysql/slow-query.log

Nyttig er også et kig på andre logs, når applikations- eller PHP-funktioner kommer i spil. For at gøre dette bruger jeg eksisterende log-workflows og samler resultaterne. Denne guide giver mig ofte en introduktion: Analyser logfiler. Jeg synkroniserer tidsstemplerne, så jeg kan sammenligne trafikspidser med forespørgselsspidser. Det giver mig mulighed for at se, om cache-misses, cron-jobs eller importjobs bruger databasen på samme tid.

Fortolkning af metrikker korrekt

Forespørgsel_tid viser mig den rene køretid; jeg prioriterer forespørgsler på over et sekund først. Lock_time angiver ventetider på grund af låse, som ofte skyldes unødvendigt lange transaktioner eller store batches. Forholdet mellem Rows_examined og Rows_sent fortæller mig, om forespørgsler ser på for mange rækker, og om der mangler indekser. Hvis loggen indeholder mange „No index use“-poster, indstiller jeg throttling og kigger nærmere på de berørte tabeller. Det er stadig vigtigt altid at tackle årsagen i stedet for symptomet: Et indeks på den rigtige kolonne slår enhver hardwareopgradering.

Metrikker Hvad jeg ser Mål
Forespørgsel_tid høj Lang køretid pr. version Tjek EXPLAIN, omskriv forespørgsel, tilføj indeks
Lock_time høj Ventetid på låse Forkorte transaktioner, reducere batchstørrelse, passende isolation
Rækker_undersøgt ≫ Rækker_sendt Scannede for meget, returnerede for lidt Indeksere filterkolonner, skabe sargbarhed
Intet indeks brugt Fuld scanning af bordet Opret indeks, undgå udtryk i WHERE

Grænseværdier Jeg justerer den efter den første uge, så jeg ikke forsvinder i støjen. Jeg sænker long_query_time i etaper, indtil jeg har nok hits til systematiske forbedringer. Jeg dokumenterer hver justering med dato og årsag. Det holder evalueringen fokuseret. Værdifulde hits sparer mig for dobbeltarbejde senere.

Øvelse: Tuning af forespørgsler trin for trin

FORKLAR er min start, før jeg ændrer koden. Jeg kigger efter „type: ALL“, „rows“ med store tal og „Using filesort“ eller „Using temporary“. Funktioner på kolonner i WHERE eller JOIN forhindrer ofte brug af indeks. I stedet formulerer jeg sargbare betingelser og tjekker derefter den nye plan. Hvert trin skal udføre rækkereduktionen tidligt og målrettet.

EXPLAIN SELECT * FROM orders WHERE YEAR(created_at) = 2026;

-- Bedre:
CREATE INDEX idx_orders_created ON orders(created_at);
Vælg * fra ordrer
WHERE created_at >= '2026-01-01' AND created_at < '2027-01-01';

JOIN'er Jeg optimerer ved at tjekke join-rækkefølgen og matchende indekser på join-nøgler. Jeg tjekker, om et sammensat indeks dækker WHERE + ORDER BY for at undgå filesort. Jeg sætter LIMIT, hvor det kun er nødvendigt med en forhåndsvisning. Jeg gemmer resultatcaching på applikationsniveau til gentagne, identiske forespørgsler med en lav ændringsfrekvens. Du kan finde en mere dybdegående introduktion til indekser og låse her: Indekser og låsning.

Indeksstrategier for CMS og butikker

WordPress, WooCommerce- eller shopsystemer skaber typiske mønstre: masser af læsning, selektiv skrivning, ofte med meta- eller produkttabeller. Jeg analyserer de mest almindelige ruter - startside, kategori, søgning, checkout - og placerer indekser specifikt på filter-, sorterings- og join-kolonner. Dækkende indekser (f.eks. (status, created_at, id)) sparer en masse opslag i tabellen. For at søge efter præfikser bruger jeg passende indeksformer eller fuldtekst i stedet for LIKE ‚%wort%‘. Jeg måler hver indeksændring før og efter live-kørslen med de samme belastningsprofiler.

Vækst Jeg bruger kardinalitet og histogrammer til at tjekke datasæt, så jeg ikke indekserer på sjældne værdier. Jeg holder antallet af indekser lavt for at holde skrivebelastningen og hukommelseskravene under kontrol. Konsoliderede sammensatte indekser erstatter flere individuelle indekser. Jeg regulerer autovacuum-lignende opgaver i MySQL ved at analysere dem regelmæssigt og kun genopbygge dem, når det er nødvendigt. Dette holder optimeringen pålidelig.

Serverindstillinger, caching og hukommelse

InnoDB Jeg bestemmer størrelsen på bufferpuljen ud fra aktive dataposter og indeksstørrelser, ikke ud fra generelle værdier. Jeg øger den, indtil arbejdssættets størrelse stort set er i hukommelsen, og page miss rate falder. Jeg indstiller tmp_table_size og max_heap_table_size, så færre midlertidige tabeller ender på disken. Af hensyn til skrivesikkerhed og latenstid afbalancerer jeg innodb_flush_log_at_trx_commit på passende vis i forhold til applikationen. På applikationsniveau cacher jeg hyppige resultater og bruger HTTP-caching, så databasen ser færre anmodninger.

Hardware og netværkseffekter er inkluderet i diagnosen: Langsom storage-I/O eller en overbelastet CPU genkendes straks af forespørgsler. Jeg måler derfor IO-wait parallelt med databasemålingerne. Hvis du har brug for flere reserver, skal du planlægge vertikal eller horisontal skalering med et målbart mål. Denne guide giver dig et kompakt overblik over flaskehalse, tuning og ressourcer: Hardware og cache. På den måde er jeg sikker på, at jeg ikke blindt drejer på den forkerte knap.

Samtidighed og låsning i hosting

Lås_tid vokser, når lange transaktioner berører mange linjer, eller når oprydningsjobs kører i den bedste tid. Jeg forkorter skriveoperationer, opdeler store opdateringer i mindre batches og reducerer dermed holdetiden for låse. Passende isolationsniveauer reducerer konflikter uden at bringe datakonsistensen i fare. Jeg aflaster hotspots med sekundære indekser og passende WHERE-betingelser, så færre rækker påvirkes. Jeg planlægger baggrundsjobs i tidsvinduer med lav trafik, så brugerhandlinger prioriteres.

Dødvande Jeg analyserer dem ved hjælp af tilbagevendende mønstre: samme tabeller, skiftende rækkefølge, identiske linjer. Jeg standardiserer adgangssekvensen i kode og stored procedures. Retry-logik med jitter løser midlertidige kollisioner. Hvor det er muligt, isolerer jeg de dyreste operationer i jobkøer. Det reducerer variansen mærkbart og øger den oplevede performance.

Automatiserede alarmer og arbejdsgange

Rutine beats actionism: Jeg analyserer loggen dagligt eller ugentligt, afhængigt af trafik og udgivelsesfrekvens. Et lille script tæller nye hits inden for de sidste par minutter og sender mig en e-mail, hvis tærskelværdien stiger. Jeg genererer også regelmæssige pt-query-digest-rapporter og holder altid øje med top 10. Jeg overvåger udgivelsesdage mere nøje. Det giver mig mulighed for at opdage fejl, før brugerne opdager dem.

#!/bin/bash
LOG_FILE="/var/log/mysql/slow-query.log"
THRESHOLD=100
RECENT_COUNT=$(awk -v cutoff="$(date -d '5 minutes ago' '+%Y-%m-%dT%H:%M')" '/^# Time:/ { if ($3 >= cutoff) count++ } END { print count+0 }' "$LOG_FILE")
if [ "$RECENT_COUNT" -gt "$THRESHOLD" ]; then
    echo "ALERT: $RECENT_COUNT langsomme forespørgsler" | mail -s "MySQL Alert" [email protected]
fi

Gennemsigtighed Jeg skaber klare ansvarsområder: Hvem reagerer på spidsbelastninger, hvem justerer indekser, hvem tester udgivelser. Jeg opsummerer resultaterne i korte changelogs. På den måde forstår alle teammedlemmer, hvorfor en ændring blev foretaget, og hvilken effekt den havde. En struktureret proces sparer tid og forhindrer falske alarmer.

Fejlbilleder og hurtige rettelser

Fuldstændig Tabelscanninger udløser en uforholdsmæssig høj belastning. Jeg tjekker først, om der mangler et passende indeks i filterkolonnen, eller om et udtryk blokerer indekset. Jeg eliminerer høj lock_time ved at forkorte transaktioner og udligne konkurrerende operationer. Jeg afværger overfyldte logfiler med log_throttle_queries_not_using_indexes og en realistisk long_query_time. Jeg måler straks hver korrektion i forhold til de oprindelige tal, så succeserne forbliver synlige.

Opbevaring-Jeg genkender flaskehalse ved at øge IO-wait og high disc latency under forespørgselspeaks. Derefter reducerer jeg unødvendige skriveoperationer, f.eks. ved at opdatere uforanderlige felter mindre hyppigt. Når tabellerne vokser, planlægger jeg arkiverings- eller partitioneringsstrategier, så varme data forbliver i hukommelsen. For admin-statements i spidsbelastningsperioder slår jeg log_slow_admin_statements til for at identificere tavse omkostningsdrivere. Små, målrettede rettelser betaler sig hurtigere her end store omorganiseringer.

Særlige funktioner i administrerede miljøer og cloud-miljøer

Administreret hosting eller cloud-tjenester begrænser ofte filadgangen. I sådanne tilfælde sætter jeg log_output til TABLE og evaluerer den langsomme log direkte fra databasen. I MySQL 8.0 bruger jeg også SET PERSIST til at indstille indstillinger permanent uden direkte adgang til my.cnf. I cloud-parametergrupper (f.eks. for managed services) indtaster jeg de samme variabler og planlægger et vedligeholdelsesvindue for genstart.

-- Hvis tilladt: Vedvarende indstillinger uden genstart
SET PERSIST slow_query_log = ON;
SET PERSIST long_query_time = 0.5;
SET PERSIST log_output = 'TABLE'; -- Alternativ til FILE for begrænset filadgang

-- Evaluering med log_output=TABLE
SELECT start_time, user_host, query_time, lock_time, rows_sent, rows_examined, sql_text
FROM mysql.slow_log
ORDER BY query_time DESC
LIMIT 50;

Hint: Med tung trafik kan log_output=FILE være mere effektiv, da tabel-logning genererer ekstra overhead. I restriktive miljøer er TABLE dog ofte den eneste vej at gå. Så sætter jeg strammere grænser (f.eks. min_examined_row_limit) for at holde mængden under kontrol.

Rotation, opbevaring og databeskyttelse

Rotation forhindrer logfiler i at fylde disken op. Jeg roterer dagligt eller efter størrelse, komprimerer gamle filer og opretholder en klar opbevaringspolitik (f.eks. 14 dage). Efter rotation udløser jeg en logflush, så MySQL skriver rent til den nye fil. Det holder analysen og driften stabil.

# /etc/logrotate.d/mysql-slow
/var/log/mysql/slow-query.log {
  dagligt
  roter 14
  størrelse 100M
  komprimering
  missingok
  notifempty
  opret 640 mysql adm
  postrotate
    test -x /usr/bin/mysqladmin || exit 0
    /usr/bin/mysqladmin flush-logs
  endscript
}

Databeskyttelse er obligatorisk: langsomme logs kan indeholde parameterværdier. Jeg begrænser adgangen strengt (filtilladelser, grupper) og kontrollerer, om der logges følsomme data. Hvis det er nødvendigt, arbejder jeg med parameterbinding i applikationen, så der ikke vises personlig klartekst i loggen. Til teamdeling foretrækker jeg at dele aggregerede rapporter frem for rå logfiler.

Brug performance schema og sys schema

Performance-ordning giver metrikker selv uden en aktiveret langsom log. Jeg aktiverer de relevante forbrugere for statements og analyserer derefter sys-viewene. Fordel: Jeg kan se topdigest og latency-distribution næsten i realtid, grupperet på tværs af lignende forespørgsler.

-- Aktiver consumer for statement history (så vidt muligt på runtime)
UPDATE performance_schema.setup_consumers
SÆT AKTIVERET = 'JA'
WHERE NAME IN ('events_statements_history', 'events_statements_history_long');

-- Hurtigt overblik over dyre forespørgselsgrupper
SELECT schema_name, digest_text, count_star,
       ROUND(sum_timer_wait/1e12, 3) AS total_s,
       ROUND(avg_timer_wait/1e9, 3) AS avg_ms,
       ROUND(max_timer_wait/1e9, 3) AS pmax_ms
FROM sys.statement_analysis
ORDER BY sum_timer_wait DESC
LIMIT 10;

Kombination fra slow log (slow outliers) og performance scheme (width, frequency) viser mig både individuelle tilfælde og systematiske omkostningsdrivere. Jeg sammenligner begge visninger med trafikmønstre for at skabe prioriterede to-dos.

EXPLAIN ANALYZE og Optimiser Trace

FORKLAR ANALYSE (fra og med MySQL 8.0.18) supplerer estimater med målte tider. Jeg sammenligner rækkeestimater med faktiske værdier og afdækker fejlvurderinger fra optimeringsprogrammet. I tilfælde af modstridende planer analyserer jeg optimeringssporet for at se, hvorfor et indeks ikke blev valgt.

-- Planlæg med målte værdier
FORKLAR ANALYSE
SELECT o.id, o.created_at
FRA ordrer o
JOIN customers c ON c.id = o.customer_id
WHERE c.country = 'DE' AND o.status = 'paid'
ORDER BY o.created_at DESC
LIMIT 50;

-- Spor optimeringsbeslutninger
SET optimizer_trace="enabled=on";
SELECT ...; -- Forespørgsel, der skal analyseres
SELECT TRACE FROM information_schema.OPTIMIZER_TRACE\G
SET optimiser_trace="enabled=off";

ResultatHvis estimaterne er helt ved siden af, opdaterer jeg statistikker (ANALYZE TABLE), tilføjer histogrammer eller omformer indekser/forespørgsler, så selektiviteten træder i kraft tidligt.

Omskriv mønstre, der næsten altid virker

ELLER til UNION ALLFlere OR-betingelser på forskellige kolonner forhindrer ofte brug af indeks. Jeg adskiller dem i to selektive forespørgsler og fletter resultaterne sammen, hvis dubletter kan udelukkes.

-- Før:
SELECT * FROM t WHERE a = ? OR b = ?

-- Bedre:
(SELECT * FROM t WHERE a = ?)
UNION ALL
(SELECT * FROM t WHERE b = ? AND a  ?);

PagineringOFFSET/LIMIT bliver dyrt med stigende OFFSET. Jeg skifter til keyset-paginering og bruger en passende sorteringsnøgle (ideelt set indekseret og monoton).

-- Dyrt:
SELECT id, title FROM posts ORDER BY created_at DESC LIMIT 50 OFFSET 5000;

-- Bedre (nøglesæt):
Udvælg id, titel
FRA indlæg
WHERE created_at < :cursor
ORDER BY created_at DESC
LIMIT 50;

Sammensatte indekser: Orden tæller. Jeg sorterer kolonner i indekset i henhold til selektivitet og forespørgselsmønster (WHERE-filter først, derefter sortering af kolonner). Målet er et dækkende indeks, der undgår filsortering og tabelopslag.

Funktionelle og genererede indekser i MySQL 8

Udtryk i WHERE/JOIN blokerer ofte indekser. I MySQL 8.0 indekserer jeg specifikt udtryk eller arbejder med genererede kolonner for at skabe sargability. Dette er især nyttigt for CASTs for numeriske metaværdier eller JSON-felter.

-- Eksempel: numerisk sortering på tekstfelt
ALTER TABLE product ADD COLUMN price_num DECIMAL(10,2)
  GENERATED ALWAYS AS (CAST(price AS DECIMAL(10,2))) GEMT;
CREATE INDEX idx_product_price_num ON product(price_num);

-- Forespørgsel uden CAST og med indeks
Vælg * fra produkt
WHERE price_num BETINGENDE 10 OG 50
ORDER BY price_num;

ØvelseJeg tester, om det nye indeks virkelig virker (EXPLAIN), og måler effekten i den langsomme log. Genererede kolonner hjælper også med at filtrere præfikser eller normaliserede varianter (LOWER(email)) effektivt.

En mere målrettet tilgang til CMS/butiksmønstre

Meta-tabeller (f.eks. wp_postmeta) drage fordel af kombinerede indekser på (post_id, meta_key) eller (meta_key, meta_value). Til hyppige filtre på meta_value_numeric bruger jeg genererede kolonner som ovenfor i stedet for CASTing i hver forespørgsel. Jeg gør søgesiderne hurtigere ved at fjerne overflødigheder (denormalisering light) og gøre læseadgangen indeksvenlig.

-- Typisk for WordPress: hurtig adgang til metadata for et indlæg
CREATE INDEX idx_postmeta_postid_metakey ON wp_postmeta(post_id, meta_key);
CREATE INDEX idx_postmeta_metakey_metavalue ON wp_postmeta(meta_key, meta_value(100));

KasseJeg optimerer stierne til minimale blokeringstider: korte transaktioner, kun de nødvendige rækker og indekser, der passer nøjagtigt til de anvendte WHERE-betingelser. For rapporter planlægger jeg asynkron aggregering (mellemliggende tabeller), så brugerflowet ikke bliver bremset.

Grænser for den langsomme log og supplerende metrikker

Mange små, hurtige forespørgsler er ikke synlige i den langsomme log, men bidrager til belastningen. Derfor sporer jeg også throughput (forespørgsler/sek.), 95./99. percentil og andelen af forespørgsler uden indeks. I Performance Schema eller APM-værktøjer genkender jeg N+1-mønstre, som jeg så løser specifikt ved hjælp af joins, batch loading-processer eller caching.

Prøveudtagning er nyttig, når loggen bliver for stor. Jeg øger long_query_time en smule eller sætter min_examined_row_limit til kun at omfatte relevante forespørgsler. Vigtigt: Notér altid ændringer, så tidsserier forbliver sammenlignelige.

Arbejdsmetode: Fra resultater til bæredygtig forbedring

Baseline Først: Jeg gemmer en før-rapport (tidsvindue, trafik, konfiguration). Derefter optimerer jeg den ene forespørgselsfamilie efter den anden og sammenligner identiske tidsvinduer. Hver rettelse dokumenteres i repository'et (Hvad? Hvorfor? Målt værdi før/efter?). På den måde forbliver succeserne sporbare og regressionssikre.

# Grov procedure (eksempel)
1) pt-query-digest --since '7d' slow-query.log > baseline.txt
2) Vælg top 3 query digests (efter samlet tid)
3) EXPLAIN/EXPLAIN ANALYZE, udarbejd indeks- og omskrivningsforslag
4) Generer testdata, simulér belastningsprofil
5) Udrulning med overvågning (strammere grænser i 48 timer)
6) Sammenligningsrapport: pt-query-digest --since '48h' > after.txt
7) Dokumenter resultatet, planlæg næste del

Planlæg stabilitet Jeg overvåger optimeringen over tid: Hvis planerne ændres (nye versioner, ændrede statistikker), tjekker jeg histogrammer, ANALYZE TABLE og indekslandskabet. Jeg indstiller kun hints selektivt og på en dokumenteret måde for ikke at binde optimeringen permanent.

Opsummering i klare trin

Start betyder: aktiver loggen, indstil fornuftige grænseværdier, indsaml den første uges data. Derefter opsummerer jeg med pt-query-digest, prioriterer efter samlet tid og varians og vælger de bedste drivere. Jeg optimerer forespørgsler med EXPLAIN, sargable betingelser og passende indekser og kontrollerer låsning med kortere transaktioner. På serversiden indstiller jeg buffere, midlertidige tabeller og flush-strategier på passende vis. Endelig automatiserer jeg alarmer og gentager cyklussen regelmæssigt - det holder databasen hurtig, selv når trafikken og datamængderne vokser.

Aktuelle artikler