Skip to main content

VillageSQL is a drop-in replacement for MySQL with extensions.

All examples in this guide work on VillageSQL. Install Now →
When a query is slow, your first move is EXPLAIN. It shows you exactly how MySQL plans to execute the query — which indexes it uses, how many rows it expects to scan, and where it’s doing expensive work. Once you can read it, you can fix almost any slow query.

Running EXPLAIN

Put EXPLAIN in front of any SELECT:
MySQL returns one row per table in the query. For joins, you get multiple rows — one per table involved.

The Columns That Matter

A full EXPLAIN row looks like this:
Most columns matter some of the time. These matter almost all of the time:

type — How MySQL scans the table

This is the most important column. It tells you the access method, from fastest to slowest: ALL is the one to fix. It means MySQL is reading every row in the table. On a table with a million rows, that’s a million reads for every query execution.

key — Which index MySQL chose

NULL means no index was used. If possible_keys lists indexes but key is NULL, MySQL decided a full scan was cheaper — usually because the table is small or the query isn’t selective enough.

rows — Estimated rows examined

An estimate, not a guarantee. MySQL uses statistics to produce this number. A query with rows: 1 is fast; rows: 5000000 on a production table is a problem.

Extra — Additional execution details

The Extra column holds important flags: Using filesort and Using temporary together on large tables are red flags.

A Worked Example

Two problems: full table scan (ALL) and a filesort. Fix both with a composite index (see How MySQL Indexes Work for index design principles):
rows dropped from 1,000,000 to 4,200. type is now ref — an equality lookup on status. The filesort is gone because the index already orders rows by created_at within each status value.

EXPLAIN ANALYZE

EXPLAIN ANALYZE actually executes the query and returns real timing alongside the estimates:
cost and rows are the optimizer’s estimates. actual time and actual rows are what really happened. A large gap between estimated and actual rows means stale statistics — run ANALYZE TABLE orders to update them.

Frequently Asked Questions

What’s the relationship between EXPLAIN and covering indexes?

Extra: Using index means MySQL is using a covering index — it can answer the query entirely from the index without reading table rows. That’s one of the most impactful optimizations available on read-heavy tables. See Covering Indexes in MySQL for how to design them.

My query has type: index — is that okay?

Not really. type: index means MySQL scanned the entire index (instead of the table), which is faster than ALL but still a full scan. It appears when a query needs all rows but can satisfy it from the index. If you’re hitting this on large tables, check whether a covering index or a WHERE clause could narrow the scan.

Why does MySQL ignore the index I created?

The optimizer chooses indexes based on statistics. If the table is small, MySQL may decide a full scan is cheaper. If the statistics are stale, it may underestimate how selective your index is. Run ANALYZE TABLE to refresh statistics, then re-check EXPLAIN.

Can I use EXPLAIN on INSERT, UPDATE, and DELETE?

Yes — EXPLAIN works on all DML statements in MySQL 8.0+. The output shows the access method for the rows being modified, which is useful when an UPDATE or DELETE is slow. If you need to identify which queries to EXPLAIN first, the MySQL Slow Query Log captures the slowest queries on your server and ranks them by total time.

Troubleshooting

See also