On a managed hosting server you almost never own the application. You can't read its source, you can't refactor its ORM, and the developer who wrote the query left two years ago. What you can do is find the specific query that's hurting the server, prove it, and either fix it at the database layer or hand the developer something actionable.
This is that workflow.
Step 1: confirm it's actually the database
Before blaming MySQL, check that MySQL is the bottleneck rather than a victim of one. A server that's swapping makes every query slow — that's a memory problem wearing a database costume.
# What's running right now
mysqladmin processlist | head -30
# Queries running longer than 5 seconds — the ones that matter
mysql -e "SELECT id, user, db, time, state, LEFT(info, 120) AS query
FROM information_schema.processlist
WHERE command != 'Sleep' AND time > 5
ORDER BY time DESC;"
A processlist full of Sending data or Copying to tmp table entries with high
time values is a genuine query problem. A processlist that's mostly idle while load
is high means look elsewhere.
Step 2: turn on the slow query log
The processlist is a snapshot. The slow query log is the record — and it's the single most useful diagnostic MySQL offers.
SET GLOBAL slow_query_log = 'ON';
SET GLOBAL slow_query_log_file = '/var/log/mysql/slow.log';
SET GLOBAL long_query_time = 1;
SET GLOBAL log_queries_not_using_indexes = 'ON';
Set long_query_time to 1, not 10. The default of 10 seconds only catches disasters. Most real damage comes from a query taking 1.5 seconds that runs four hundred times a minute — invisible at the default threshold, and by far the biggest contributor to load.
Turn log_queries_not_using_indexes back off when you're done. On a busy server it can fill a disk in hours. Set a reminder, and make sure logrotate covers the file.
Step 3: analyse it properly
Don't read the raw log. Aggregate it — you want the query that costs the most in total, which is often not the slowest single query.
# Bundled with MySQL — sort by total time, top 10
mysqldumpslow -s t -t 10 /var/log/mysql/slow.log
# Percona Toolkit — better output, worth installing
pt-query-digest /var/log/mysql/slow.log | head -60
pt-query-digest gives you a ranked profile that looks roughly like this:
# Profile
# Rank Query ID Response time Calls R/Call
# ==== ================== =============== ===== ======
# 1 0x3A2E9F1C... 842.1131 61.2% 4021 0.2094
# 2 0x9B4D2E88... 201.7742 14.7% 3 67.2581
# 3 0x1F7C0A55... 98.4410 7.1% 8802 0.0112
Read that carefully. Query #2 is the slowest per call at 67 seconds — genuinely alarming. But query #1 consumes 61% of all database time because it runs four thousand times. Fix #1 first. This is exactly the judgement the aggregate view gives you and a raw log doesn't.
Step 4: EXPLAIN the offender
EXPLAIN SELECT * FROM wp_postmeta
WHERE meta_key = '_order_total' AND meta_value > 100;
id select_type table type possible_keys key rows Extra
1 SIMPLE wp_postmeta ALL NULL NULL 2841003 Using where
Three fields tell the story:
type: ALL— a full table scan. Every row read to answer the query.key: NULL— no index used at all.rows: 2841003— the estimated number of rows examined. Compare that with how many the query actually returns; a huge gap is the problem in one number.
The type column, best to worst:
| type | Meaning | Verdict |
|---|---|---|
const / eq_ref | One row via primary or unique key | Optimal |
ref | Index lookup returning several rows | Good |
range | Indexed range scan | Usually fine |
index | Full scan of the index | Suspicious |
ALL | Full table scan | Fix this |
And in Extra, three phrases that mean real work is happening on disk:
Using filesort— results sorted outside an index. Expensive on large sets.Using temporary— a temp table was built; if it exceedstmp_table_sizeit lands on disk.Using join buffer— a join running without a usable index on the joined column.
Step 5: add the index — carefully
-- What already exists?
SHOW INDEX FROM wp_postmeta;
-- Table size, so you know what the operation will cost
SELECT table_name,
ROUND(data_length/1024/1024) AS data_mb,
ROUND(index_length/1024/1024) AS index_mb,
table_rows
FROM information_schema.tables
WHERE table_schema = DATABASE()
ORDER BY data_length DESC LIMIT 10;
-- Create it (online DDL avoids locking on modern InnoDB)
ALTER TABLE wp_postmeta
ADD INDEX idx_meta_key_value (meta_key, meta_value(20)),
ALGORITHM=INPLACE, LOCK=NONE;
Adding an index to a large production table is a real operation. Do it in a maintenance window, take a backup first, and verify ALGORITHM=INPLACE, LOCK=NONE is actually accepted — if MySQL rejects it, the operation will lock the table for the duration. On a multi-gigabyte table that's an outage, not a tune-up.
Index principles worth holding on to:
- Order matters in composite indexes. An index on
(a, b)serves queries filtering ona, or ona AND b— but notbalone. - Most selective column first, generally — the one that eliminates the most rows.
- Indexes cost writes. Every INSERT and UPDATE maintains them. Don't index everything.
- Prefix-index long text columns (
meta_value(20)) rather than indexing the whole column.
Step 6: check the obvious config mistakes
Before deep tuning, verify the two settings that are wrong most often:
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
SHOW VARIABLES LIKE 'max_connections';
-- Buffer pool efficiency: reads served from memory vs. from disk
SHOW STATUS LIKE 'Innodb_buffer_pool_read%';
innodb_buffer_pool_size is the single most impactful setting. On a dedicated
database server, 60–70% of RAM. On a shared hosting box also running Apache and PHP, much
less — but the default of 128 MB is almost always far too small, and I've found it
unchanged on servers with 64 GB of RAM.
Compare Innodb_buffer_pool_reads (served from disk) against
Innodb_buffer_pool_read_requests (served from memory). If the disk figure is more
than a percent or so of the total, your working set doesn't fit in the pool.
max_connections set very high is a common cargo-cult fix. It doesn't add
capacity — it just lets more clients queue up simultaneously, converting a fast failure
into a slow, memory-hungry one. If you're hitting the limit, the fix is upstream: fewer
PHP-FPM children, or connection pooling.
Step 7: when you can't fix it yourself
Sometimes the query is genuinely bad and no index saves it — a SELECT * with three
joins and no LIMIT against a table that grows forever. That needs an application
change, and your job becomes making the case unarguable.
Give the developer:
- The exact query text, normalised, from the digest.
- Its
EXPLAINoutput showing the scan. - Call count and total database time — "this is 61% of all query time on the server".
- Rows examined versus rows returned, which usually makes the problem self-evident.
- A specific suggestion: the index you'd add, or the missing
LIMIT.
That's a great deal more persuasive than "the database is slow", and in my experience it's the difference between a fix this sprint and a ticket that ages for six months.
The method in one line: confirm it's the database, log the slow queries, aggregate by total time not per-call time, EXPLAIN the top offender, and fix what that reveals. Most of the time it's a missing index and you can fix it yourself in a window. The rest of the time, you now have the evidence to get someone else to.