performance_schema carries a reputation it earned in 5.5 and has not deserved since: too heavy, too cryptic, turn it off if you care about throughput. I still find production servers with performance_schema disabled by someone who read a blog post a decade ago, running blind through incidents that the default configuration would have explained. These are field notes on the setup I actually run on MySQL 8.x: what stays on, the little I add, what it costs, and how to get answers out of it without memorizing eighty table names.
The thesis: the 8.0 defaults are close to right, the marginal additions are cheap, and the statement digest tables alone justify the whole subsystem.
The defaults are better than the folklore
On MySQL 8.0 and 8.4, performance_schema is on by default, and the default instrumentation already covers the highest-value ground: statement instruments are enabled, the statements_digest consumer aggregates every statement family into a summary table, memory instrumentation is on, and transaction and metadata-lock visibility are available. That means a stock server is already answering "what are my worst query families?" and "what is eating memory?" with no configuration at all.
What the defaults deliberately leave off is the expensive stuff: most fine-grained wait and stage instruments, and the historical event consumers that feed them. That is the correct trade. The folklore about overhead comes from enabling everything, wait instruments on hot mutexes fire at enormous frequency, and measuring at that frequency is never free. Do not start by turning things on; start by learning what is already there.
A concrete way to build that trust: on a stock 8.0 server, without touching a single setting, you can already answer which statement families dominate total latency, which sessions hold metadata locks, which memory consumers grew since startup, and what each connection is executing right now with its current wait state. Walk through those four questions on a quiet server once, so that the first time you run them on a loud one is not also the first time you are reading the output format.
Instruments and consumers: the two-switch mental model
Everything in performance_schema hangs off two setup tables, and confusion about them causes most wasted effort. Instruments (setup_instruments) decide what gets measured, wait/io/file, statement/sql/select, memory/innodb, and so on. Consumers (setup_consumers) decide where measurements go, current-event tables, history rings, digest aggregation. An instrument without a consumer is a sensor wired to nothing; a consumer without instruments is an empty table. When a performance_schema table you expected to be useful is empty, check both switches.
SELECT 'instruments_on' AS what, COUNT(*) AS n
FROM performance_schema.setup_instruments
WHERE enabled = 'YES'
UNION ALL
SELECT 'consumers_on', COUNT(*)
FROM performance_schema.setup_consumers
WHERE enabled = 'YES';
SELECT name, enabled
FROM performance_schema.setup_consumers
ORDER BY name;
One operational trap: UPDATE statements against the setup tables take effect immediately but do not survive a restart. For anything you decide to keep, write the matching performance-schema-instrument and performance-schema-consumer lines into the server configuration, or your carefully tuned instrumentation quietly reverts at the next maintenance window and nobody notices until the next incident.
The short list I enable beyond defaults
My additions are modest. First, the events_statements_history consumer, so that each session keeps a small ring of its recent statements; when a connection is misbehaving right now, reading its last ten statements beats guessing. Second, if we are actively chasing I/O attribution, the wait/io/file and wait/io/table instruments with the matching waits consumers, enabled for the duration of the investigation and often turned back off afterwards. Instrumentation you can toggle at runtime without a restart is a diagnostic tool, not a permanent tax.
UPDATE performance_schema.setup_consumers
SET enabled = 'YES'
WHERE name = 'events_statements_history';
UPDATE performance_schema.setup_instruments
SET enabled = 'YES', timed = 'YES'
WHERE name LIKE 'wait/io/table/%';
What I do not enable fleet-wide: synchronization waits (mutexes, rwlocks), stage instruments, and the history_long consumers. Each has a real diagnostic niche, and each is exactly the kind of always-on cost that gave this subsystem its bad name. Enable them for a question, get the answer, turn them off.
The statement history ring earns its keep in a specific, recurring scene: an application team reports that one worker pod is "stuck on the database," and the connection in question shows an innocuous-looking statement in the current-events table. The history ring shows the ten statements before it, which is usually enough to spot the pattern, a transaction opened three statements ago and never committed, a loop issuing the same UPDATE with escalating lock waits, a migration statement nobody mentioned. Ten statements of context routinely converts "the database is slow" into a specific application behavior with an owner.
Measure the memory cost instead of arguing about it
performance_schema allocates memory for its tables and buffers, and in 8.0 most sizing parameters autoscale with max_connections and table counts, with many buffers allocated incrementally as they are first used. I will not quote you an overhead percentage, because it depends on connection count, table count, and which instruments you enabled; the honest move is that the subsystem measures itself, so ask it.
SELECT event_name,
current_number_of_bytes_used / 1024 / 1024 AS mb_used
FROM performance_schema.memory_summary_global_by_event_name
WHERE event_name LIKE 'memory/performance_schema/%'
ORDER BY current_number_of_bytes_used DESC
LIMIT 15;
SHOW ENGINE PERFORMANCE_SCHEMA STATUS gives the same story with a total at the bottom. Check it on a representative server before and after any instrumentation change, write the number into the change ticket, and the overhead debate becomes a measurement instead of a religious dispute.
sys schema: the entry points worth memorizing
Raw performance_schema tables are wide, timer units are picoseconds, and nobody remembers the join paths under pressure. The bundled sys schema exists to fix that, and four views cover most of my incident usage: sys.statement_analysis for worst statement families with latency, rows, and temp-table behavior already formatted; sys.innodb_lock_waits for who is blocking whom, with the killable blocker identified; sys.schema_unused_indexes for index cleanup candidates; and sys.statements_with_full_table_scans for the queries your indexes are failing. Each view also has an x$ twin with raw numbers for when you are feeding a script instead of a human.
If you are coming from the slow query log world, or from PostgreSQL, the conceptual mapping matters more than the syntax: sys.statement_analysis plays the role pg_stat_statements plays on the other side, a claim I unpacked properly in the slow query log vs pg_stat_statements comparison.
Digests are your regression detector
The single highest-value table in the subsystem is events_statements_summary_by_digest: every statement normalized to a fingerprint, aggregated with counts, total and max latency, rows examined, temp tables, and sort behavior. It is cumulative since the last truncate or restart, which makes it a baseline machine: snapshot it before a deploy, snapshot after, and the diff names the exact query family that regressed.
SELECT schema_name,
LEFT(digest_text, 80) AS query_family,
count_star AS calls,
ROUND(sum_timer_wait / 1e12, 1) AS total_s,
ROUND(sum_timer_wait / count_star / 1e9, 2) AS avg_ms,
sum_rows_examined,
sum_created_tmp_disk_tables
FROM performance_schema.events_statements_summary_by_digest
WHERE schema_name IS NOT NULL
ORDER BY sum_timer_wait DESC
LIMIT 20;
Two sizing caveats keep this table honest. The table holds a bounded number of digests (performance_schema_digests_size); once full, new statement families collapse into a catch-all row with a NULL digest, so a large NULL row means you are losing resolution and should raise the limit. And digest text is truncated at max_digest_length bytes, which matters for ORM-generated queries with enormous IN lists. Check both before trusting the data on a busy multi-tenant server.
To turn the table into an actual regression detector, add a cadence. A periodic job that snapshots the digest table into a plain schema of your own, every few minutes on busy systems, hourly on quiet ones, gives you rates instead of lifetime totals, and rates are what make a deploy comparison meaningful. Handle the two resets in your differencing logic: a server restart zeroes everything, and any explicit TRUNCATE of the summary table does the same. Snapshot deltas that go negative mean a reset happened, not that a query un-executed itself. The metrics this table feeds, and how they line up with what a PostgreSQL operator would expect from their side, are mapped in the MySQL to PostgreSQL monitoring translation notes.
Where MonPG fits today
Positioning, stated plainly: MonPG is a PostgreSQL monitoring platform, and it does not monitor MySQL today. MySQL monitoring (coming soon) is under active development, and the digest workflow above is its backbone, continuous snapshots of events_statements_summary_by_digest so that regressions come with before-and-after evidence attached instead of depending on whoever remembered to baseline. If you also operate PostgreSQL, that half of the story is shipped and you can start there now. Either way, turn performance_schema back on if someone disabled it in 2015; the modern defaults are close to free, and flying blind is not.