MySQL — Sorts Spill to Disk: Creating tmp table to disk
Fix slow ORDER BY and GROUP BY queries whose sorts and temp tables spill to disk, size sort_buffer and tmp_table appropriately, and index the sort order.
Symptoms
- Slow query log entries flagged with Creating sort index or Copying to tmp table on disk for seconds
- EXPLAIN shows Using filesort or Using temporary on queries that were fast on smaller data
- Disk IO spikes on the tmpdir filesystem whenever report queries run
Possible causes
- ORDER BY columns have no covering index, so every execution sorts the full result set
- sort_buffer_size is below the row width times row count, forcing a merge sort through disk files
- GROUP BY with temp tables exceeds tmp_table_size / max_heap_table_size and converts to on-disk tables
- LIMIT with high offsets forces sorting the entire prefix anyway
Diagnosis steps
Read the execution plan
mysql -e "EXPLAIN FORMAT=JSON SELECT ... ORDER BY created_at DESC LIMIT 100\G" | grep -E "using_filesort|using_temporary_table|sorts"Interpretation
The JSON plan shows using_filesort, estimated rows to sort, and whether the sort fits memory — the full spill story in one output.
Check the session spill status
mysql -e "SHOW GLOBAL STATUS LIKE 'Sort_merge_passes'; SHOW GLOBAL STATUS LIKE 'Created_tmp_disk_tables';"Interpretation
Sort_merge_passes above zero means memory sorts are merging through disk; growing Created_tmp_disk_tables tracks temp-table conversions.
Compare row width versus buffers
mysql -e "SELECT AVG(LENGTH(CONCAT_WS(',', col1, col2))) FROM t;"Interpretation
Average row width times rows to sort versus sort_buffer_size predicts the spill arithmetically.
Root cause
When the rows a query must sort exceed the per-connection sort buffer, MySQL writes runs to disk and merge-sorts them through the tmpdir. Similarly, intermediate GROUP BY tables beyond the memory tmp limits convert to on-disk tables. The query then pays disk IO per execution, and latency scales with data size.
Fix
Add a covering index matching the ORDER BY (and WHERE prefix) so no sort happens at all, project only needed columns to shrink row width, raise sort_buffer_size and tmp_table_size to fit measured working sets, and replace high-offset pagination with keyset pagination.
Risk note
sort_buffer_size is per-connection: a global raise multiplies by max_connections and can exhaust memory. Size from measured sorts, keep sessions bounded.
Verify the fix
After indexing and sizing, EXPLAIN shows no filesort, Sort_merge_passes stays at zero under load, and the slow query log no longer flags the statement.
Symptom walkthrough
The slow log annotates the mechanism directly:
# Query_time: 4.9 Lock_time: 0.000 Rows_sent: 100 Rows_examined: 2,140,882
# Creating sort index
SELECT id, title, body FROM articles ORDER BY created_at DESC LIMIT 100;
Two million rows examined to send one hundred: the sort ran over the entire table because no index carries the order. Confirm with the status counters — Sort_merge_passes is the disk-merge receipt, and Created_tmp_disk_tables tracks GROUP BY conversions:
SHOW GLOBAL STATUS LIKE 'Sort_merge_passes';
SHOW GLOBAL STATUS LIKE 'Created_tmp_disk_tables';
Fix in practice
Index the order so no sort exists:
CREATE INDEX articles_created ON articles (created_at DESC, id);
-- WHERE + ORDER BY sharing the index prefix:
-- SELECT ... WHERE status = 'published' ORDER BY created_at DESC
CREATE INDEX articles_status_created ON articles (status, created_at DESC, id);
Shrink the row width — sort cost scales with bytes per row:
-- deferred join: sort only ids, then join back:
SELECT a.* FROM articles a
JOIN (SELECT id FROM articles ORDER BY created_at DESC LIMIT 100) t
ON a.id = t.id;
Size memory from arithmetic, not folklore:
sort_buffer_size = 8M # row_width x rows for your measured sorts
tmp_table_size = 256M # with max_heap_table_size matched
And replace OFFSET pagination — keyset form sorts nothing extra:
WHERE (created_at, id) < (last_seen_created, last_seen_id)
ORDER BY created_at DESC, id DESC LIMIT 100;
Verification
Run EXPLAIN FORMAT=JSON on the fixed statement and confirm using_filesort is gone from the sort node; the plan should read the index in order. Snapshot the global counters before and after a load run: Sort_merge_passes stays at zero and Created_tmp_disk_tables stops growing during the report workload. Watch the tmpdir filesystem IO during the same run — the disk spikes that correlated with report queries disappear. Re-check the slow query log: the statement no longer carries Creating sort index annotations, and query time drops to single-digit milliseconds. For keyset pagination, verify page two returns instantly with Rows_examined near the LIMIT instead of millions.