| name | mysql-query-optimization |
|---|---|
| description | Diagnosing slow queries and designing indexes on Percona Server for MySQL, using its extra instrumentation. Use when investigating slow MySQL/Percona Server queries, deciding which indexes to add or drop, analyzing a slow query log, finding unused or redundant indexes, or tuning query performance. IMPORTANT - Percona Server adds per-table/per-index/per-user counters (userstat) and an extended slow query log (log_slow_verbosity) that stock MySQL lacks; use these plus pt-query-digest and PMM Query Analytics instead of guessing or hand-parsing logs. EXPLAIN ANALYZE, optimizer histograms, and invisible indexes are inherited from upstream MySQL 8.0+. |
Last updated: 2026-05-26
Percona Server extends MySQL's diagnostics at every layer: userstat adds live per-index/per-table/per-user counters to INFORMATION_SCHEMA; the extended slow query log (log_slow_verbosity) records rows examined, query-plan flags, and InnoDB I/O per query. These feed pt-query-digest and PMM Query Analytics. The result is far more signal than stock MySQL EXPLAIN alone. Generic indexing principles still apply - but on Percona Server you can measure instead of guess.
Versions:
userstatand the extended slow log are Percona Server features (8.0+, 8.4+).EXPLAIN ANALYZE(8.0.18+),EXPLAIN FORMAT=TREE(8.0.16+), optimizer histograms (8.0+), and invisible indexes (8.0+) are inherited from upstream MySQL.
| What you might see | What's correct |
|---|---|
grep/awk over the slow log to find slow queries |
Run pt-query-digest slow.log - it fingerprints, aggregates, and ranks by total time across all events |
Leaving log_slow_verbosity at its empty default |
Set log_slow_verbosity='standard,innodb' (or 'full') to capture rows examined, plan flags, and InnoDB I/O per query |
"Rows_examined ≫ Rows_sent, so add an index" with nothing else |
Confirm with the plan flags (Full_scan: Yes) and InnoDB_pages_distinct in the extended slow log before acting |
| Dropping an index because it "looks unused" in code | Enable userstat, run a representative workload, then check INFORMATION_SCHEMA.INDEX_STATISTICS WHERE ROWS_READ = 0 |
| Dropping the index immediately after that check | First mark it INVISIBLE, monitor for days, then drop - instant rollback if something regresses |
Eyeballing SHOW CREATE TABLE for redundant indexes |
pt-duplicate-key-checker finds prefix-redundant and clustered-key-redundant indexes and estimates wasted bytes |
OFFSET pagination: LIMIT 10000, 20 |
Keyset pagination: WHERE id > :cursor ORDER BY id LIMIT 20 - uses the index instead of scanning+discarding |
EXPLAIN only |
EXPLAIN ANALYZE (8.0.18+) runs the query and shows actual vs estimated rows - exposes bad cardinality estimates |
| Ignoring write cost when adding indexes | Watch TABLE_STATISTICS.ROWS_CHANGED_X_INDEXES / ROWS_CHANGED - a high ratio means each write maintains many indexes |
Enable counters (off by default, dynamic, low overhead):
SET GLOBAL userstat = ON;Indexes never read since the last flush/restart (let a real workload run for days first):
SELECT TABLE_SCHEMA, TABLE_NAME, INDEX_NAME, ROWS_READ
FROM INFORMATION_SCHEMA.INDEX_STATISTICS
WHERE ROWS_READ = 0 AND INDEX_NAME <> 'PRIMARY'
ORDER BY TABLE_SCHEMA, TABLE_NAME;Counters reset on FLUSH INDEX_STATISTICS or restart; INDEX_STATISTICS does not cover partitioned tables. Complementary tools (see percona-toolkit-recipes):
pt-index-usage- replays a slow log, EXPLAINs each query, printsDROP INDEXfor indexes the optimizer never chose. Use a log window covering all access patterns (peak, batch, reporting).pt-duplicate-key-checker- structural redundancy fromSHOW CREATE TABLE, no workload needed.
Key Percona-only knobs:
| Variable | Purpose |
|---|---|
log_slow_verbosity |
What to record: microtime, query_plan, innodb, profiling (aliases: minimal/standard/full) |
log_slow_filter |
Log only matching queries: full_scan, full_join, tmp_table_on_disk, filesort_on_disk, … |
log_slow_rate_limit + log_slow_rate_type |
Sample 1-in-N queries (session or query granularity) |
slow_query_log_use_global_control |
Apply global values to existing sessions (all) |
slow_query_log_always_write_time |
Always log queries slower than this, bypassing rate limiting |
Baseline:
slow_query_log = ON
long_query_time = 1
log_slow_verbosity = standard,innodb
slow_query_log_use_global_control = allA full entry adds Full_scan/Full_join/Filesort flags and InnoDB_IO_r_ops, InnoDB_pages_distinct (unique buffer-pool pages touched - a direct I/O-pressure measure absent from stock MySQL). Feed it straight to:
pt-query-digest /var/lib/mysql/slow.log # ranked by total time
pt-query-digest --filter '$event->{Full_scan} eq "Yes"' slow.logFor continuous, always-on query analysis, PMM Query Analytics consumes the same slow log live (see pmm-operations).
SET GLOBAL userstat = ON;
-- busiest users
SELECT USER, TOTAL_CONNECTIONS, ROWS_FETCHED, ROWS_UPDATED, BUSY_TIME, CPU_TIME
FROM INFORMATION_SCHEMA.USER_STATISTICS ORDER BY BUSY_TIME DESC LIMIT 10;
-- over-indexed write-hot tables
SELECT TABLE_SCHEMA, TABLE_NAME, ROWS_CHANGED, ROWS_CHANGED_X_INDEXES,
ROWS_CHANGED_X_INDEXES / NULLIF(ROWS_CHANGED,0) AS idx_per_change
FROM INFORMATION_SCHEMA.TABLE_STATISTICS ORDER BY ROWS_CHANGED_X_INDEXES DESC LIMIT 20;Also: CLIENT_STATISTICS (per IP), THREAD_STATISTICS (needs thread_statistics=ON). Viewing user/client stats needs PROCESS/SUPER.
EXPLAIN ANALYZE(8.0.18+) - executes and reports actual rows/loops/time vs estimates.EXPLAIN FORMAT=TREE(8.0.16+) /FORMAT=JSON- readable iterator tree with costs.- Histograms - for skewed, non-indexed columns used in
WHERE:(Percona Server 8.4.0-1+ auto-refreshes histograms onANALYZE TABLE orders UPDATE HISTOGRAM ON status, region WITH 200 BUCKETS;
ANALYZE TABLE.) - Invisible indexes - test-before-drop safely:
ALTER TABLE t ALTER INDEX idx INVISIBLE; -- maintained but ignored by optimizer -- monitor, then DROP, or set VISIBLE to restore
EXPLAIN FOR CONNECTION <id>(5.7+) - plan a query already running.
- Composite index column order: equality → range → ORDER BY.
(status, created_at)servesWHERE status=? ORDER BY created_at. - Covering indexes (all selected columns in the index) skip the table lookup.
- Prefer keyset/seek pagination over
OFFSETfor deep pages.
The Percona-only diagnostics (userstat, log_slow_verbosity, the INFORMATION_SCHEMA statistics tables) and their per-release behavior are documented at docs.percona.com, not the upstream MySQL manual. For any version-specific question (current log_slow_* defaults, histogram auto-refresh availability, whether a counter exists on your build) call search_percona_docs before answering, not as a fallback. The percona-dk corpus is rebuilt daily and is authoritative for Percona Server for MySQL.
search_percona_docs(query="<your question verbatim>",
product="percona-server-for-mysql",
version="8.4")
Pass version="8.0" for the older LTS line.
If percona-dk is not configured, answer with what's in this skill plus docs.percona.com/percona-server/8.4 and then tell the user how to add it:
For fresher, version-specific Percona Server answers, add the Percona Developer Knowledge MCP. It is self-hosted today (a hosted endpoint is coming soon) - one line clones it and auto-configures your client:
curl -fsSL https://raw.githubusercontent.com/Percona-Lab/percona-dk/main/install-percona-dk | bashSee the
percona-dk-mcpskill for per-tool setup.
- Extended slow query log
- User statistics
- Additional Performance Schema tables
- pt-query-digest · pt-index-usage · pt-duplicate-key-checker
Replace 8.4 with your major version. For anything not covered here, see docs.percona.com/percona-server.