Knowing that a table is being scanned tells you almost nothing useful. The number that matters is which statement is doing it, and MySQL records that separately, in a place most people never open.

This is a worked example of reading it. A previous measurement counted how much of our production read traffic used no index, named three tables that looked wrong, and stopped there — saying plainly that it had not read the statement digests and had closed none of the cases it opened. This is that follow-up, and all three close the same way.

First, check the instrument is telling you everything

The digest table keeps one row per normalised statement shape, up to a configured limit. Past that limit MySQL stops adding rows and dumps the overflow into a single anonymous bucket — at which point every share you compute from it is wrong, quietly.

limit            10,000
stored              702
overflow bucket       0
digests lost          0

Complete. Worth thirty seconds before trusting anything downstream, because a truncated digest table looks exactly like an untruncated one until you ask.

The three cases

Asking the digests which statements touched those tables, ordered by rows examined:

rows examined   execs   statement
   16,006,827      28   SELECT SQL_NO_CACHE `id`,`page_type`,… FROM analytics_hits
    3,463,631       6   SELECT SQL_NO_CACHE * FROM analytics_hits
    1,404,884      28   SELECT SQL_NO_CACHE `id`,… FROM ai_directory_clicks
    1,191,913      28   SELECT SQL_NO_CACHE `id`,… FROM analytics_engagement

Every one of them is SQL_NO_CACHE, a full column list, and no WHERE. That is the shape mysqldump issues against each table in turn: read every row, do not pollute the cache on the way out.

The scans were the backups. Not a missing index, not a bad query — the nightly job reading every row, which is the only way a backup can work.

How much of everything that accounts for

all rows examined            34,014,591
dump-shaped rows examined    31,072,723   (91.4%)
non-dump, no index used       2,182,669

Nine tenths of the examined-row volume on this server is backups. The application's entire unindexed read load, across a day, is 2.2 million rows — an order of magnitude below what the table-level view suggested, and unremarkable for a database this size.

The two instruments do not agree, and both are right

The earlier measurement reported 150 million reads; this one totals 34 million examined rows. That is a four-fold difference, on the same server, over the same window.

Neither is wrong. table_io_waits_summary_by_index_usage counts storage-engine row I/O, which includes index traversals and internal access; events_statements_summary_by_digest counts rows examined as the optimiser accounts for them per statement. They measure different layers of the same work.

So the 91.4% above is computed entirely within the digest instrument, and cannot be applied to the 150 million figure. Dividing one instrument's numerator by another's denominator produces a meaningless number that looks completely reasonable, which is a trap worth pointing out rather than assuming nobody would fall into.

The rule that falls out of this is short. Pick an instrument, compute the whole ratio inside it, and name which one you used when you publish the number.

What this does to the earlier conclusion

The earlier guide's per-table numbers stand — those tables are heavily read without an index. What changes is the interpretation. "Sixty percent of reads use no index" is true and describes a server whose largest single reader is its own backup.

It also means the shortlist was not a list of problems. All three tables named as suspicious were explained by a scheduled job, and the correct action for all three is none.

No fix is needed, which is the ordinary result of following an alarming number through to its owner.

Some of those backups were ours

5,320 dump executions across 312 statement shapes is more than one nightly job. This session took several manual database backups before deploying schema changes, each of which reads every table, and each of which lands in exactly these counters.

The measurement includes our own housekeeping. That is not a flaw — taking a backup before a schema change is correct practice — but it does mean the nightly job is not solely responsible for the 91.4%. Stating it here is cheaper than letting somebody else discover it.

How to run this on your own

SELECT DIGEST_TEXT, COUNT_STAR, SUM_ROWS_EXAMINED
FROM performance_schema.events_statements_summary_by_digest
WHERE DIGEST_TEXT LIKE '%your_table%'
ORDER BY SUM_ROWS_EXAMINED DESC
LIMIT 10;

Check the overflow bucket first. Then look for SQL_NO_CACHE before you look for anything else, because if your heaviest reader is a dump you can stop, and the alternative is spending an afternoon adding an index that a backup will never use.

How this was measured, and what it does not settle

One production MySQL 8.0.46 server, the same 23-hour window as the measurement it follows up, read from performance_schema.events_statements_summary_by_digest. Nothing was changed.

The signature is strong, not conclusive. SQL_NO_CACHE with a full column list and no filter is what mysqldump emits, and nothing else on this server has reason to. But the digests record statement shapes, not clients — confirming it would mean catching the process list mid-dump, and this did not.

Normalised text hides the parameters. Every literal in a digest is a ?, which is what makes shapes comparable and what makes it impossible to tell two callers of the same shape apart. Attribution above is by shape and timing.

One window, one workload. A server whose backups run weekly rather than nightly, or whose application scans harder, would produce a different split entirely. The method transfers; the 91.4% does not.

Removing the backups leaves a much smaller pile of scanning, and the largest thing left in it was us.