CONCEPT Cited by 1 source
Query stage profiling¶
Query stage profiling is a MySQL diagnostic technique that decomposes a single query execution's wall-clock time into per-stage milliseconds: how much was spent optimising, opening tables, checking permissions, executing, waiting for handler commit, freeing items. It answers the question "my query is slow — slow doing what?"
(Source: sources/2026-04-21-planetscale-identifying-and-profiling-problematic-mysql-queries.)
What it shows vs EXPLAIN ANALYZE¶
EXPLAIN ANALYZEshows per-iterator cost in the execution plan tree — it tells you which operator was expensive.- Stage profiling shows per-execution-stage cost in the server lifecycle — it tells you which phase of the server's work was expensive. Two orthogonal views.
If a query's wall-clock is dominated by stage/sql/executing,
EXPLAIN ANALYZE is the right next step. If it's dominated
by stage/sql/optimizing or lock-wait stages, the fix is
elsewhere (stats, lock contention, schema change ordering).
Enablement (three toggles)¶
PlanetScale's post gives the canonical enablement SQL:
-- 1. Enable instruments (what to measure)
UPDATE performance_schema.setup_instruments
SET ENABLED = 'YES', TIMED = 'YES';
-- 2. Enable consumers (where to store)
UPDATE performance_schema.setup_consumers
SET ENABLED = 'YES', TIMES = 'YES';
-- (Note: MySQL column is actually named TIMED, not TIMES;
-- the post has a typo. The column gates timer collection.)
-- 3. Enable history for the principal (who to profile)
INSERT INTO performance_schema.setup_actors
(HOST, USER, ROLE, ENABLED, HISTORY)
VALUES ('your_host', 'your_user', '%', 'YES', 'YES');
Selective scoping via setup_actors: flip the
default-match row off (WHERE HOST='%' AND USER='%') and
insert only the principal you care about, so the profiling
tax doesn't hit every session.
Selective stages via setup_instruments:
UPDATE performance_schema.setup_instruments
SET ENABLED = 'NO', TIMED = 'NO';
UPDATE performance_schema.setup_instruments
SET ENABLED = 'YES', TIMED = 'YES'
WHERE NAME LIKE '%stage/%';
The four-step workflow¶
From the post, verbatim structure:
- Capture the thread ID of the connection you'll run the query on:
SET @connection_thread = (
SELECT thread_id FROM performance_schema.threads
WHERE PROCESSLIST_ID = CONNECTION_ID() );
-
Execute the query you want to profile.
-
Find the
statement_idby scanning recent history:
SELECT thread_id, statement_id, SUBSTRING(sql_text,1,50)
FROM performance_schema.events_statements_history_long
WHERE thread_id = @connection_thread
ORDER BY event_id DESC LIMIT 20;
- Bracket-query the stage timings using the statement's
event_id/end_event_id:
SET @eid = (SELECT event_id FROM performance_schema.events_statements_history_long WHERE statement_id = @statement_id);
SET @eeid = (SELECT end_event_id FROM performance_schema.events_statements_history_long WHERE statement_id = @statement_id);
SELECT event_name, source, (timer_end-timer_start)/1000000000 AS 'milliseconds'
FROM performance_schema.events_stages_history_long
WHERE event_id BETWEEN @eid AND @eeid;
Canonical output shape¶
event_name | milliseconds
----------------------------------------+-------------
stage/sql/starting | 0.2400
stage/sql/Executing hook on trx begin. | 0.0010
stage/sql/checking permissions | 0.0020
stage/sql/Opening tables | 0.0950
stage/sql/init | 0.0050
stage/sql/System lock | 0.0110
stage/sql/optimizing | 0.0270
stage/sql/statistics | 0.1040
stage/sql/preparing | 0.0590
stage/sql/executing | 735.3020
stage/sql/end | 0.0010
stage/sql/query end | 0.0020
stage/sql/waiting for handler commit | 0.0060
stage/sql/closing tables | 0.0080
stage/sql/freeing items | 0.2810
stage/sql/cleaning up | 0.0010
The post's worked example: 735.3 ms in stage/sql/executing
out of ~736 ms total. This is the common case — most slow
OLTP queries are execution-bound. The value of stage
profiling is that it would flag System lock /
waiting for handler commit / optimizing dominance if
those were the bottleneck.
Caveats¶
- Overhead — history tracking is not free; fleet-wide enablement has a perf cost PlanetScale names as "small" but real.
- In-memory — history tables are bounded rings; old statements age out. Run the profile workflow immediately after the query.
- Thread-ID scoping —
PROCESSLIST_IDvsthread_idare different. The post's translation SQL is the canonical form. - Timer column arithmetic —
(timer_end - timer_start)is in picoseconds; divide by 1e9 for milliseconds, 1e12 for seconds.
Seen in¶
- PlanetScale's field manual for query diagnosis
(2024-03-29). Dicken uses stage profiling as the
drill-down step after digest-level identification
(patterns/digest-based-query-prioritization) and
alongside
EXPLAIN ANALYZE. The four-step workflow (thread-id → execute → statement-id lookup → event-id bracket) is canonicalised as patterns/stage-level-query-profiling. (Source: sources/2026-04-21-planetscale-identifying-and-profiling-problematic-mysql-queries.)