Skip to content

← All posts

With performance_schema off, MySQL metrics are absent not zero

Cache hit ratio, queries per second and buffer-pool usage are omitted rather than defaulted, and two more figures mean less than their names suggest.

A cache hit ratio of 0 and a cache hit ratio that nobody measured are not the same fact. The first is a server in trouble. The second is a server that was never asked, or was asked and declined to answer. A MySQL server with performance_schema off does not raise an error when you query its performance figures - it answers a row of NULLs. Anything that coerces those NULLs to numbers has just written down three measurements that were never taken.

The rule this provider follows is short. A metric the server never published is left out of the reading rather than defaulted into it.

Where each panel reads from

Monitoring on MySQL is not one query. getMonitoringData() fans out several reads in parallel, each against a different reporting interface, and they fail independently:

Panel Source
Overview VERSION(), SHOW STATUS / SHOW VARIABLES, information_schema
Performance performance_schema.global_status and SHOW STATUS
Queries performance_schema.events_statements_summary_by_digest
Sessions information_schema.PROCESSLIST
Tables information_schema.TABLES, with bloat estimated from DATA_FREE
Indexes information_schema.STATISTICS plus mysql.innodb_index_stats
Storage information_schema.TABLES and SHOW BINARY LOGS

Reading that table top to bottom tells you which panels are conditional. Only two rows depend on performance_schema. Schema introspection, sessions, sizes, row counts and EXPLAIN FORMAT=JSON are on the other interfaces and are unaffected by its state.

The reads are gathered with Promise.allSettled, so a refused panel costs only itself. A rejected read is recorded under errors.<panel> and rendered as an unavailable panel carrying the server’s own sentence - ERROR 1142, say, when the connected user has no grant on the digest table. One panel going dark does not empty the six beside it; the reading throws only when all four core reads reject.

MySQL monitoring with performance_schema disabled

With performance_schema off, the tables still exist and are still selectable. The metric queries therefore succeed and return NULL. Every reading in getPerformanceMetrics() is taken through a measuredNumber() helper, and a field with nothing behind it is left out of the returned object entirely rather than set to zero. Cache hit ratio, queries per second (Queries divided by Uptime) and buffer-pool usage are absent. The panels show them as unmeasured.

The digest table behaves the same way. Off, it is readable and answers zero rows, so the Queries panel is empty rather than broken. Measured 2026-08-27 on MySQL 26.7.0 started with --performance-schema=OFF, the slow-query list came back as [] on both the health line and the panel. That is why an exception cannot be used as the signal for “the capability is off” - the off state never raises one.

What does raise is the source being unreadable: no performance_schema database at all (ERROR 1049), or a grant denied on it (ERROR 1142). Where the database is absent outright, getPerformanceMetrics() returns {} - nothing measured, rather than the cacheHitRatio: 99 an earlier version defaulted to.

If you want the figures, start the server with the instrumentation on:

services:
  mysql:
    image: mysql:latest
    command: ['--performance-schema=ON']
    environment:
      MYSQL_ROOT_PASSWORD: root
      MYSQL_DATABASE: app
    ports:
      - '3306:3306'

This is worth checking before you conclude anything from an empty Performance tab. MariaDB reaches this same provider - there is no separate type id - and it ships performance_schema off by default.

Index scans are a cardinality estimate, not a usage counter

The Indexes panel has a scans column, and on MySQL that column is information_schema.STATISTICS.CARDINALITY. Cardinality is the optimiser’s estimate of how many distinct values an index holds. It is not a count of how many times the index was used, and there is no MySQL equivalent of the per-index scan counter other engines expose.

The practical consequence is that you cannot find an unused index this way. A never-touched index on a high-cardinality column reports a large number; a constantly-used index on a boolean column reports two. Sorting the panel by that column ranks columns by their distribution, not by anything about your workload.

The same panel is careful in the other direction about size. The per-index byte figure comes from mysql.innodb_index_stats, which needs SELECT on the mysql schema and has no row at all for a MyISAM table. In both cases indexSizeBytes is omitted and the cell reads N/A, rather than a 0 B the server never reported. Row counts on the Tables panel carry the same caveat from the other direction: TABLE_ROWS is an InnoDB estimate, and bloat is inferred from DATA_FREE.

The deadlock counter that is the inverse of expectation

Deadlocks are the one performance figure that survives performance_schema being off, because it comes from SHOW STATUS, which answers either way. A 0 there is a real measurement and is reported as one, where the server publishes the variable.

It is also the one figure absent on the engine this provider is named for. The metric reads the status variable Innodb_deadlocks, which MariaDB publishes and MySQL does not - measured as an empty SHOW STATUS result on both 8.0.46 and 26.7.0:

SHOW STATUS LIKE 'Innodb_deadlocks';
-- MySQL: Empty set

So the expectation inverts. Turn instrumentation on and you get cache hit ratio, queries per second and buffer-pool usage but still no deadlock count. Point the same code at a default MariaDB and you get the deadlock count and none of the other three. Neither server is broken; each publishes a different set, and the reading reports the intersection of what was asked for and what came back.

Reading a dashboard full of honest gaps

With performance_schema off every performance field is omitted rather than defaulted, index scans are CARDINALITY rather than a usage counter, and the deadlocks metric is absent on MySQL because it reads a status variable MySQL does not publish. Those three are the difference between a panel you can act on and a panel you can be misled by.

A gap on these panels means the server did not publish the number. It never means the number is zero, and it never means the value is small. When absence has its own rendering, a 0 you do see is load-bearing.

It is also why the empty state carries no cause. The Queries panel shows one fixed sentence for every empty list, whatever produced it, because the one cause a reader would guess - instrumentation off - is the cause that never reaches the error path. Guessing there would be the same defect one level up.

The monitoring surface is bounded by what each engine’s own reporting interface publishes, and that boundary differs from engine to engine. What each one does and does not answer is published on the engine pages next to its transport and default port, so you can check before you build a runbook on a figure that turns out to be a cardinality estimate.