資料庫效能與維運 ch07 慢查詢的診斷流程:CPU 100%,但慢查詢日誌是空的
下一章→
CH 07 先量再調

慢查詢的診斷流程:CPU 100%,但慢查詢日誌是空的

從症狀分流慢查詢日誌的設定與陷阱digest 才是主力該看的六個指標Threads_running複本延遲會騙人什麼時候根本不是 SQL 的問題

前面六章各自處理一種問題:計畫、優化器、JOIN、排序、鎖、schema。但線上出事的時候,你手上只有一句話:

「系統變慢了。」

沒有人會告訴你「是深分頁把 Buffer Pool 沖掉了」或「有支批次持有鎖不放」。這一章要處理的就是從那句話到根因之間的路徑——也就是這一軌第三條主線「先量再調」的完整版本。

而這一章要對付的最大敵人,是一個很自然的反射動作:

  • 聽到「慢」就去翻慢查詢日誌 → 日誌是空的 → 卡住。
  • 聽到「CPU 高」就去調參數 → 調了沒用,或更糟。
  • 聽到「DB 慢」就去看 DB → 但問題根本不在 DB。

這一章要建立的是一套分流:

  • 第一個問題永遠是「全部變慢,還是某支變慢」——這兩者的排查路徑完全不同。
  • 慢查詢日誌怎麼設:long_query_time 預設 10 秒是什麼都抓不到的。
  • 為什麼 digest 比慢查詢日誌更重要:因為最貴的 SQL 往往一句都不慢。
  • 該看的六個指標:其中 Threads_running 是最被低估的一個。
  • 複本延遲會騙人:Seconds_Behind_Source 顯示 0 不代表沒有延遲。
  • 什麼時候根本不是 SQL 的問題:連線池、GC、網路、複本——四個 DB 以外的方向。
  • 一個真實場景:CPU 打滿、慢查詢日誌完全是空的——因為每一句都只有 8 毫秒。

這一章是前六章的索引:每一種症狀最後都會指向某一章。而最後那個失敗場景會直接把你送進 ch08。

這是整條排查鏈的分岔點,而它決定了接下來要用完全不同的工具。跳過這一步直接去翻慢查詢日誌,是最常見的浪費時間方式。

兩條路完全不同

某一支變慢全部變慢
典型徵狀只有某個 API 的 p99 上升不相干的 API 一起變慢,甚至健康檢查逾時
先看慢查詢日誌/digest → EXPLAIN全域指標:Threads_running、鎖、Buffer Pool、複本
通常是計畫走錯、缺索引、深分頁(ch01~ch04)有東西塞住:鎖、連線池、快取被沖掉(ch04、ch05)
最糟的做法直接加索引試試逐句 EXPLAIN——會看到一堆漂亮的計畫,什麼也找不到

怎麼判斷是哪一種

-- 這一個數字就能分流:目前「正在執行」的執行緒數
SHOW GLOBAL STATUS LIKE 'Threads_running';
  • 健康值:通常在個位數,經驗上不該長期超過 CPU 核心數的兩倍。
  • 突然飆到幾十、上百:代表有東西塞住了——請求進得來、出不去。這是「全部變慢」的特徵。
  • 正常,但某支 API 慢:那就是單句問題,走另一條路。
Threads_running 比 Threads_connected 重要得多。連線數高只代表「連著」(連線池本來就會養著一堆閒置連線),正在跑的數量才代表壓力。很多監控只畫連線數,於是塞車的時候看起來一切正常。

第三種情況:不是變慢,是變錯

還有一種容易被誤判成「慢」的症狀:使用者說「我剛改的資料不見了」。那多半不是效能問題,是讀寫分離的複本延遲(本章後面會講)。這種情況去查慢查詢日誌永遠查不到東西。

排查的總地圖

「系統變慢了」
  │
  ├─ Threads_running 正常 → 某支慢
  │    └→ digest / 慢查詢日誌 → EXPLAIN(ch01)
  │         → 估算失準?(ch02)/JOIN?(ch03)/排序分頁?(ch04)
  │
  ├─ Threads_running 飆高 → 有東西塞住
  │    ├→ 鎖等待?(ch05)
  │    ├→ Buffer Pool 命中率掉?(ch04 深分頁沖快取)
  │    ├→ 磁碟 I/O 打滿?
  │    └→ 都正常但 CPU 高 → 量的問題(本章失敗場景 → ch08)
  │
  └─ DB 指標全正常,但應用端很慢
       └→ 根本不是 DB:連線池、GC、網路、序列化

慢查詢日誌是最多人知道、也最多人設錯的工具。預設設定下它基本上不會記到任何東西。

SELECT @@slow_query_log, @@long_query_time;
-- 0, 10.000000    ← 沒開,而且門檻是 10 秒
10 秒的門檻意味著:一句跑 3 秒的查詢不會被記錄。而在一個 p99 目標是 200 毫秒的服務裡,3 秒早就是災難了。用預設值等於沒開。

合理的設定

SET GLOBAL slow_query_log = ON;
SET GLOBAL long_query_time = 0.5;        -- 依服務的 SLA 調,通常 0.2~1 秒
SET GLOBAL log_slow_admin_statements = ON;   -- ALTER/ANALYZE 這類也記(ch06 用得到)
SET GLOBAL log_slow_replica_statements = ON; -- 複本上的慢語句也記

兩個要小心的選項

  • log_queries_not_using_indexes:聽起來很有用,但小表的全表掃描是完全正常的(ch02 講過),開了之後日誌會被那些無害的查詢刷爆,真正的問題反而被淹沒。要開的話務必配 min_examined_row_limit 設個門檻。
  • long_query_time = 0(記錄全部):診斷時很有用,但它本身有成本——每一句都要寫檔案,高 QPS 下會拖慢資料庫。只在短時間的診斷視窗開,開完記得關。

分析:不要用眼睛看

原始日誌不適合直接讀。用 pt-query-digest 聚合成報表:

pt-query-digest /var/log/mysql/slow.log | head -50
而看報表時最重要的一件事:依「總耗時」排序,不是依「單次最慢」排序。
一句 5 秒、一天跑 1 次      → 總共 5 秒
一句 10 毫秒、一天跑 200 萬次 → 總共 5.5 小時   ← 該修的是這個
直覺會被那句 5 秒吸引,但它對整體幾乎沒有影響。這個排序方式的差別,就是這一章與下一章的橋樑。

慢查詢日誌的根本限制

它只能記錄「單句超過門檻」的語句。所以下面這些它全部看不到:

  • 一句 8 毫秒但被呼叫 240 萬次(本章的失敗場景)。
  • 等鎖 40 秒的語句——其實會被記錄,但 rows_examined 是 1,用掃描列數排序時完全隱形(ch05 提過)。
  • 連線池排隊——那發生在應用端,資料庫根本沒收到請求。

所以慢查詢日誌是必要但不充分的工具。真正的主力是下一節。

performance_schema 會把所有執行過的語句正規化成「指紋」(digest)後聚合統計——把參數值換成 ?,於是同一種查詢的一百萬次呼叫會被歸成一筆。

SELECT
  LEFT(digest_text, 80) AS query,
  count_star                       AS 次數,
  ROUND(sum_timer_wait/1e12, 1)    AS 總秒數,
  ROUND(avg_timer_wait/1e9, 2)     AS 平均毫秒,
  sum_rows_examined                AS 掃過列數,
  sum_rows_sent                    AS 回傳列數
FROM performance_schema.events_statements_summary_by_digest
ORDER BY sum_timer_wait DESC          -- ← 總耗時,不是平均
LIMIT 10;

為什麼它比慢查詢日誌強

  • 它看得到「不慢但很多」的查詢——依 sum_timer_wait 排序,那句 8 毫秒的 SQL 會自己浮到第一名。
  • 它是即時的,不用等日誌輪替或跑分析工具。
  • 它有效率指標:sum_rows_examined / sum_rows_sent 的比值就是「掃了多少、才回傳多少」。比值越大越可疑——比值 100 代表掃 100 列只回傳 1 列,那是索引或計畫的問題(ch01)。

sys schema:已經寫好的視圖

sys 是 performance_schema 的人類可讀版,直接給答案:

SELECT * FROM sys.statement_analysis LIMIT 10;              -- 綜合分析
SELECT * FROM sys.statements_with_full_table_scans LIMIT 10; -- 全表掃描的(→ ch01)
SELECT * FROM sys.statements_with_sorting LIMIT 10;          -- 有排序的(→ ch04)
SELECT * FROM sys.statements_with_temp_tables LIMIT 10;      -- 用暫存表的(→ ch04)
SELECT * FROM sys.schema_unused_indexes;                     -- 沒被用過的索引(→ ch06 清理)
SELECT * FROM sys.innodb_lock_waits;                         -- 誰擋誰(→ ch05)
這幾個視圖幾乎就是前六章的對照表。先用 digest 找出「最貴的十句」,再用這些視圖判斷它們貴在哪一類,然後翻到對應的那一章。

digest 的兩個陷阱

  • digest 表有容量上限:performance_schema_digests_size 預設 5000 種指紋,超過之後新的指紋會被丟進一個叫 NULL 的桶子。看到 digest_text 為 NULL 卻佔了很大比重時,代表統計已經失真——通常是因為有人在拼字串 SQL(每次的指紋都不同)。
  • 它不知道是誰呼叫的。digest 只告訴你「這句 SQL 很貴」,不會告訴你它來自哪個 API、哪個排程。解法是在 SQL 裡加註解標籤:
    SELECT /* app=order-api,ep=list */ ... FROM orders WHERE ...
    註解會出現在 digest 裡,於是統計表就能直接對應到程式碼的位置。這件事只要做一次,之後每一場故障都受益。

「全部變慢」那條路上,看這六個就夠了。每個指標異常時,答案都在前面某一章。

① Threads_running — 有沒有塞住

SHOW GLOBAL STATUS LIKE 'Threads_running';

飆高=請求出不去。接著看是被什麼卡住(②③④)。

② 鎖等待 — 是不是有人卡著(→ ch05)

SHOW GLOBAL STATUS LIKE 'Innodb_row_lock%';
-- Innodb_row_lock_waits      等過鎖的次數
-- Innodb_row_lock_time_avg   平均等多久
-- Innodb_row_lock_current_waits  現在有幾個在等   ← 案發時看這個

SELECT * FROM sys.innodb_lock_waits\G   -- 誰擋誰

③ Buffer Pool 命中率 — 快取是不是被沖掉了(→ ch04)

SHOW GLOBAL STATUS LIKE 'Innodb_buffer_pool_read%';
-- 命中率 = 1 - (Innodb_buffer_pool_reads / Innodb_buffer_pool_read_requests)

健康的 OLTP 系統通常在 99% 以上。重點不是絕對值,是「突然掉下來」——那就是有人在大量掃冷資料(深分頁、全表掃描、批次)。

④ 磁碟 I/O — 是不是被打滿

SELECT * FROM sys.io_global_by_file_by_bytes LIMIT 10;
-- 順便看誰在寫:暫存檔很大 = 排序或暫存表落磁碟(→ ch04)

⑤ QPS 與呼叫次數 — 是不是量變了

SHOW GLOBAL STATUS LIKE 'Questions';   -- 兩次取樣相減再除以秒數
SHOW GLOBAL STATUS LIKE 'Com_select';  -- 也可以分讀寫看

QPS 突然翻倍而單句沒變慢——那是量的問題,不是 SQL 的問題(本章失敗場景)。

⑥ 複本延遲 — 而且它會騙人

SHOW REPLICA STATUS\G
-- Seconds_Behind_Source: 0     ← 不要盡信
Seconds_Behind_Source 算的是「套用執行緒落後多久」。如果是接收執行緒斷線或卡住,它可能顯示 0,而實際上資料已經落後好幾分鐘。可靠的做法是自己維護一張心跳表(主庫每秒寫入時間戳,複本讀出來相減),或看 performance_schema.replication_applier_status_by_worker。

複本延遲的症狀特別容易被誤判:使用者說「我剛儲存的資料不見了」——那不是慢,是讀到了舊資料。去查慢查詢日誌永遠查不到。

有四種情況,症狀是「資料庫很慢」,但問題完全不在資料庫。認出它們可以省下好幾個小時。

① 連線池排隊(→ backend ch03)

判斷方式很明確:應用端的延遲很高,但資料庫的 Threads_running 很低。那代表請求根本還沒送到資料庫,它們卡在應用端等連線。

// HikariCP 要看的是「等待取得連線」的時間,不是 SQL 執行時間
hikaricp_connections_pending      // 有幾個在排隊
hikaricp_connections_acquire_seconds // 拿到連線花多久
但要小心:連線池滿了往往又是因為資料庫慢。10 個請求在等鎖 → 10 條連線被佔住 → 連線池耗盡 → 全站拿不到連線(ch05 那條外溢路徑)。所以「連線池滿」是症狀不是根因,要繼續往下追是誰佔住連線。

② GC 停頓(→ backend ch04)

特徵是週期性的 p99 尖刺,而且 DB 端完全看不到對應的慢語句。查 GC 日誌比查資料庫快得多。ch08 會看到一個典型成因:一次載入太多實體。

③ 結果集太大:網路與序列化

資料庫端 5 毫秒、應用端 2 秒——時間花在傳輸幾 MB 的結果、以及 ORM 把它映射成物件。digest 的 sum_rows_sent 很大就是這個訊號(回扣 ch04 的 SELECT *)。

④ 複本延遲:不是慢,是讀到舊資料

使用者的描述會是「資料不見了」「剛改的沒生效」。判斷方式是去主庫查同一筆——如果主庫是新的、複本是舊的,就是延遲。常見成因:大交易、批次寫入、以及 ch06 那個在複本上也要跑一次的 DDL。

一句話的分流表

症狀DB 端的樣子去哪裡查
某支 API 慢digest 裡找得到那句ch01~ch04
全部慢、Threads_running 高鎖等待或命中率掉ch05、ch04
全部慢、Threads_running 低DB 指標都正常連線池/GC(backend ch03、ch04)
CPU 高、慢日誌空digest 的 count_star 極高量的問題 → ch08
「資料不見了」主庫有、複本沒有複本延遲,不是效能問題

徵狀

週一上午,資料庫 CPU 從平常的 30% 升到 95% 並維持不下。

  • 慢查詢日誌是空的(long_query_time = 0.5,設定沒問題)。
  • Threads_running 在 40~60 之間(平常 5 以下)。
  • 鎖等待正常、Buffer Pool 命中率 99.4% 正常、磁碟 I/O 也不高。
  • QPS 從 3,000 升到 21,000——而前端流量只成長了 15%。

最後那一條是關鍵:流量沒怎麼變,但送到資料庫的查詢變成七倍。

診斷

慢查詢日誌是空的,代表沒有任何單句超過 0.5 秒。所以問題不在「單價」,在「數量」——換 digest,依總耗時排序:

SELECT LEFT(digest_text, 60) AS q, count_star,
       ROUND(sum_timer_wait/1e12, 0) AS 總秒數,
       ROUND(avg_timer_wait/1e9, 2)  AS 平均毫秒,
       sum_rows_examined, sum_rows_sent
FROM performance_schema.events_statements_summary_by_digest
ORDER BY sum_timer_wait DESC LIMIT 3;

-- q                                          count_star  總秒數  平均毫秒
-- SELECT ... FROM products WHERE id = ?        8,214,003   65,712    8.00
-- SELECT ... FROM members  WHERE id = ?        2,104,881   14,734    7.00
-- SELECT ... FROM orders   WHERE user_id = ?      92,441    1,201   13.00
第一句:平均 8 毫秒、rows_examined = 1、走主鍵、計畫完美——它永遠不會進慢查詢日誌。但它一小時被呼叫了 820 萬次,總共吃掉 18 小時的 CPU 時間。整場故障的元兇,是一句「一點都不慢」的查詢。

接著要回答「誰在呼叫它」。digest 不記錄呼叫端,所以用之前埋好的 SQL 註解標籤:

SELECT digest_text FROM performance_schema.events_statements_summary_by_digest
WHERE digest_text LIKE '%products%' LIMIT 1;
-- SELECT /* app=web,ep=order-list */ ... FROM products WHERE id = ?

ep=order-list——訂單列表頁。一個列表頁為什麼要查商品表八百萬次?

根因

1
上週上線的改版,在訂單列表加了「顯示商品縮圖」。
2
ORM 的關聯是 lazy 的,而 DTO 轉換時逐一存取了 item.getProduct()。
3
一頁 20 筆訂單 × 每筆平均 10 個品項 = 200 次額外查詢,全部是主鍵查詢、全部很快。
4
每秒 100 次列表請求 × 200 = 20,000 QPS,正好對上觀測到的數字。

這就是 N+1——而它在資料庫端的樣子,就是「一堆完美的查詢」。

為什麼所有 DB 端的工具都指不出它

  • EXPLAIN:type=const、rows=1,漂亮得不得了(ch01 早就說過這個盲點)。
  • 慢查詢日誌:一句都不超過門檻。
  • 索引:全都用對了,沒有任何東西可以加。
N+1 不是資料庫問題,是應用程式問題。資料庫忠實地執行了 820 萬次它被要求做的事,每一次都很有效率。能看見它的唯一位置,是「一個請求發出幾句 SQL」——而那個數字只有應用端量得到。

修法

1
止血:先回滾那個改版,或暫時關掉縮圖。CPU 立刻回到 30%。
2
治本:用 @BatchSize 或 EntityGraph 把 200 次變成 1~2 次;更好的是列表頁直接用 DTO 投影,一句 SQL 取齊所有欄位——這是 ch08 的主題。
3
防再犯:把「單一請求發出幾句 SQL」納入監控與測試。Spring 可以用 datasource-proxy 計數,在整合測試裡直接斷言「這個端點不得超過 5 句 SQL」——N+1 是少數可以被自動化測試擋住的效能問題。

教訓

「慢查詢日誌是空的」不代表資料庫沒問題,它只代表「沒有單句超過門檻」。當總量是問題時,唯一看得見的角度是依總耗時排序的 digest——而不是任何一種「找最慢那句」的工具。

這一節把前面六章接成一條可以直接照著跑的流程。照順序走,不要跳。

1
分流:看 Threads_running。正常=某支慢;飆高=有東西塞住;DB 全正常但應用慢=根本不在 DB。
2
某支慢:digest 或慢查詢日誌找出那句 → EXPLAIN(ch01)→ 估算與實測差很多就修統計資訊(ch02)→ 有 JOIN 看驅動表與被驅動表索引(ch03)→ 有排序或大 OFFSET 看 Extra(ch04)。
3
塞住了:先看鎖(sys.innodb_lock_waits,ch05)→ 再看 Buffer Pool 命中率有沒有掉(誰在掃冷資料,ch04)→ 再看有沒有 DDL 卡在 MDL(ch06)。
4
都正常但 CPU 高:digest 依總耗時排序,看 count_star。量的問題 → ch08。
5
DB 指標全正常:去應用端看連線池等待、GC、結果集大小。

平常就要準備好的六件事

鎖與 N+1 這類問題必須抓現場,事後查不到。所以這些要在沒事的時候先做:

  • slow_query_log = ON,long_query_time 設成符合 SLA 的值(不是預設的 10 秒)。
  • innodb_print_all_deadlocks = ON(ch05)。
  • SQL 註解標籤(/* app=…,ep=… */)——讓 digest 對得回程式碼。
  • 長交易告警:innodb_trx 裡超過 N 秒的交易。
  • 單請求 SQL 次數的監控與測試斷言(本章失敗場景)。
  • 心跳表量複本延遲,不要只信 Seconds_Behind_Source。

三個要避免的反射動作

  • 「慢就加索引」:先看它為什麼不用現有的索引(ch01、ch02)。索引不是免費的(ch06)。
  • 「先調參數」:innodb_buffer_pool_size 以外的參數,在沒有量測之前調整多半是換一種問題。而且參數是全域的,為了一句 SQL 動全域是最糟的交易。
  • 「重啟看看」:它會清掉所有現場證據(SHOW ENGINE INNODB STATUS、digest 統計、正在跑的交易),下次還會再發生一次,而你少了一份證據。
這一章的核心只有一句:症狀決定工具。用錯工具比不查更浪費時間——拿慢查詢日誌去查 N+1、拿 EXPLAIN 去查鎖等待、拿資料庫指標去查連線池問題,都會得到「一切正常」這個最誤導人的結論。

這一章推薦了一堆要開的東西。它們都不是免費的,這一節把帳算清楚。

五筆帳

  • long_query_time = 0(記錄全部) → 每句都寫檔案,高 QPS 下明顯拖慢資料庫。只在診斷視窗開,而且要設鬧鐘記得關。
  • log_queries_not_using_indexes → 小表全表掃描是正常的,日誌會被無害查詢刷爆,真問題反而被淹沒。
  • performance_schema → 預設開啟,佔幾百 MB 記憶體,而那是從 Buffer Pool 分走的。要調的是收集哪些 instrument,而不是整個關掉——關掉之後你就沒有 digest 了。
  • digest 表容量 → performance_schema_digests_size 預設 5000,超過的指紋全被丟進 NULL 桶。拼字串 SQL 的專案很容易撐爆它,統計就此失真。
  • EXPLAIN ANALYZE 在正式環境 → 它真的執行(ch01),對重查詢等於再壓一次。配 max_execution_time。

指標本身的盲點

  • 平均值會騙人。平均 8 毫秒可能是「九成 2 毫秒、一成 60 毫秒」。要看 p95/p99,而 digest 只給平均與最大值——這是它比應用端 APM 弱的地方。
  • digest 不知道呼叫端(除非埋標籤),也不知道一個請求發了幾句。這兩件事只有應用端量得到。
  • Seconds_Behind_Source 可能顯示 0 而實際落後很多(本章講過)。
  • 抽樣視窗會蓋掉尖峰。五分鐘取樣一次的監控,看不到持續 40 秒的鎖風暴——而那 40 秒就是使用者實際遇到的故障。
最後一個盲點是這一整軌的收尾:資料庫端的所有工具,都只能回答「資料庫做了什麼」。它們永遠回答不了「為什麼有人要求它做這些」。而那個答案在應用程式裡——下一章就去那裡。
症狀分流表:先判斷再選工具
症狀關鍵判斷根因方向去哪一章
某支 API 慢Threads_running 正常計畫、索引、JOIN、排序ch01~ch04
全部慢 + Threads_running 高Innodb_row_lock_current_waits 大於 0鎖等待ch05
全部慢 + I/O 打滿Buffer Pool 命中率突然掉有人在掃冷資料(深分頁/批次)ch04
整張表突然不能用大量 Waiting for table metadata lockDDL 卡在 MDL 佇列ch06、ch05
CPU 高但慢日誌空digest 的 count_star 極高量的問題(N+1、缺快取)ch08
全部慢但 DB 指標正常Threads_running 很低連線池排隊/GC/序列化backend ch03、ch04
「資料不見了」主庫有、複本沒有複本延遲——不是效能問題本章第六個指標
三種工具的能力邊界
工具回答什麼看不到什麼設定要點
慢查詢日誌哪些單句超過門檻不慢但很多的查詢;鎖等待在裡面也很隱形long_query_time 設成符合 SLA 的值,不要用預設 10 秒
digest(events_statements_summary_by_digest)哪些 SQL 總共吃掉最多時間誰呼叫的、一個請求發幾句、p99依 sum_timer_wait 排序;SQL 加註解標籤
sys schema 視圖誰全表掃描/誰在排序/誰擋誰同 digest直接查,已經寫好了
應用端 APM / datasource-proxy一個請求發幾句 SQL、連線等待時間、p99資料庫內部的鎖與計畫N+1 只有這裡看得見
六個指標與它們的異常含意
指標怎麼看異常代表對應章節
Threads_running正在執行的執行緒數飆高=有東西塞住(比連線數更重要)分流起點
Innodb_row_lock_current_waits現在有幾個在等鎖大於 0 就去看 sys.innodb_lock_waitsch05
Buffer Pool 命中率1 - reads / read_requests突然掉=有人在掃冷資料ch04
暫存檔 I/Osys.io_global_by_file_by_bytes很大=排序或暫存表落磁碟ch04
QPS(Questions)兩次取樣相減翻倍但流量沒變=N+1 或缺快取ch08
複本延遲心跳表,不要只信 Seconds_Behind_Source讀到舊資料——是正確性問題不是效能問題本章

練習題 點選選項查看解析

0 / 10
01 / 10
線上「系統變慢了」,排查的第一步應該是什麼?
A 翻慢查詢日誌找最慢的那句
B 先判斷是「全部變慢」還是「某一支變慢」——兩者的排查路徑與工具完全不同
C 重啟資料庫釋放資源
D 調大 innodb_buffer_pool_size
解析
跳過分流直接翻慢查詢日誌是最常見的浪費時間方式。全部變慢時逐句 EXPLAIN 會看到一堆漂亮的計畫、什麼也找不到;某支變慢時去看全域指標也一樣白費。Threads_running 就是最快的分流依據。
02 / 10
為什麼 Threads_running 比 Threads_connected 更值得看?
A 因為它的數值比較大
B 因為連線數高只代表「連著」(連線池本來就養著閒置連線),正在執行的數量才代表壓力
C 因為 Threads_connected 不準確
D 因為 Threads_running 包含了複本執行緒
解析
Threads_running 飆高代表請求進得來、出不去,這是「有東西塞住」的特徵。很多監控只畫連線數,於是塞車時看起來一切正常。經驗值是它不該長期超過 CPU 核心數的兩倍。
03 / 10
慢查詢日誌的預設設定有什麼問題?
A 沒有問題,開箱即用
B 預設沒開,而且 long_query_time 是 10 秒——在 p99 目標 200 毫秒的服務裡等於什麼都抓不到
C 預設會記錄所有查詢,太吵
D 預設只記錄 UPDATE 與 DELETE
解析
10 秒門檻代表一句跑 3 秒的查詢不會被記錄。合理做法是依 SLA 設成 0.2~1 秒。另外 log_queries_not_using_indexes 要小心——小表全表掃描是正常的,開了會被無害查詢刷爆日誌。
04 / 10
分析慢查詢報表時,最重要的排序方式是什麼?
A 依單次執行最慢排序
B 依總耗時(次數 × 平均)排序——一句 10 毫秒跑 200 萬次,比一句 5 秒跑一次值得修得多
C 依掃描列數排序
D 依語句長度排序
解析
直覺會被那句 5 秒吸引,但它對整體幾乎沒有影響(總共 5 秒);而 10 毫秒 × 200 萬次是 5.5 小時的資料庫時間。這個排序方式的差別正是本章失敗場景的核心,也是通往 ch08 的橋樑。
05 / 10
digest(events_statements_summary_by_digest)比慢查詢日誌強在哪?
A 它記錄的資訊更詳細,包含每次執行的參數值
B 它把同類查詢聚合統計,因此看得到「不慢但很多」的查詢;而且是即時的、還有掃描/回傳列數比值
C 它會自動修復慢查詢
D 它可以看到呼叫端的程式碼位置
解析
digest 把參數正規化成 ? 後聚合,依 sum_timer_wait 排序時,那句「8 毫秒但 820 萬次」的查詢會自己浮到第一名。至於呼叫端它並不知道——除非你在 SQL 裡埋註解標籤,那件事只要做一次就長期受益。
06 / 10
digest 統計裡出現大量 digest_text 為 NULL 的項目,代表什麼?
A 那些查詢執行失敗了
B 指紋種類超過 performance_schema_digests_size(預設 5000),新的都被丟進 NULL 桶,統計已失真
C 那些是系統內部查詢
D performance_schema 沒有啟用
解析
常見成因是有人在拼字串 SQL(每次指紋都不同),很快撐爆上限。看到 NULL 佔比很大時,digest 的排序結果就不能盡信了,要先處理指紋爆炸的來源。
07 / 10
應用端延遲很高,但資料庫的 Threads_running 很低。最可能是什麼?
A 資料庫的索引失效了
B 請求還沒送到資料庫——卡在應用端等連線池,或是 GC 停頓
C 網路頻寬不足
D 慢查詢日誌設定錯誤
解析
Threads_running 低代表資料庫其實很閒。但要注意:連線池滿了往往又是因為有人佔住連線(例如等鎖),所以「連線池滿」是症狀不是根因,要繼續往下追是誰佔住連線。
08 / 10
關於複本延遲的 Seconds_Behind_Source,下列何者正確?
A 它是最可靠的延遲指標
B 它算的是套用執行緒落後多久;接收執行緒斷線或卡住時可能顯示 0,實際已落後很久
C 它只在 MySQL 5.7 可用
D 它包含網路傳輸時間
解析
可靠做法是自建心跳表(主庫每秒寫時間戳、複本讀出來相減)。另外複本延遲的症狀特別容易被誤判——使用者說「剛儲存的資料不見了」,那不是慢而是讀到舊資料,去查慢查詢日誌永遠查不到。
09 / 10
CPU 打滿、慢查詢日誌完全是空的、QPS 從 3,000 升到 21,000 但前端流量只增加 15%。最可能是什麼?
A 有人在跑全表掃描
B 應用程式的 N+1:一個請求發出大量「單句很快」的查詢,總量壓垮資料庫
C 統計資訊過期導致計畫走錯
D 複本延遲
解析
「流量沒變但查詢量變七倍」就是 N+1 的簽名。而它在資料庫端的樣子是一堆完美的查詢:type=const、rows=1、每句 8 毫秒,永遠不會進慢查詢日誌。唯一能發現它的角度是依總耗時排序的 digest。
10 / 10
為什麼說 N+1 問題「資料庫端的所有工具都指不出它」?
A 因為 performance_schema 不會記錄這類查詢
B 因為每一句在資料庫看來都是完美的(走主鍵、rows=1、很快),問題在於「一個請求發了幾句」——而那個數字只有應用端量得到
C 因為 N+1 只發生在 ORM 專案
D 因為那些查詢都在複本上執行
解析
資料庫忠實地執行了它被要求做的事,每一次都很有效率。這也是為什麼防禦手段要放在應用端:用 datasource-proxy 之類的工具計數,並在整合測試裡斷言「這個端點不得超過 N 句 SQL」——N+1 是少數可以被自動化測試擋住的效能問題。

點擊卡片翻面查看答案,共 13 張。

QUESTION
線上「系統變慢了」,排查的第一個問題是什麼?
點擊翻面
ANSWER
「全部變慢,還是某一支變慢?」兩者用完全不同的工具:某支慢走 digest/慢日誌 → EXPLAIN;全部慢走全域指標(鎖、Buffer Pool、複本)。最快的分流依據是 Threads_running。
點擊翻回
QUESTION
為什麼 Threads_running 比 Threads_connected 重要?
點擊翻面
ANSWER
連線數高只代表「連著」(連線池本來就養著閒置連線),正在執行的數量才代表壓力。它飆高=請求進得來出不去,是「有東西塞住」的特徵。經驗值:不該長期超過 CPU 核心數的兩倍。
點擊翻回
QUESTION
慢查詢日誌的預設設定為什麼等於沒開?
點擊翻面
ANSWER
預設 slow_query_log=OFF,而且 long_query_time=10 秒——在 p99 目標 200 毫秒的服務裡,一句 3 秒的查詢都不會被記錄。要依 SLA 設成 0.2~1 秒。
點擊翻回
QUESTION
分析慢查詢報表時該用什麼排序?為什麼?
點擊翻面
ANSWER
依總耗時(次數 × 平均),不是單次最慢。一句 5 秒跑一次=5 秒;一句 10 毫秒跑 200 萬次=5.5 小時。直覺會被最慢的那句吸引,但它對整體幾乎沒影響。
點擊翻回
QUESTION
digest 比慢查詢日誌強在哪三點?
點擊翻面
ANSWER
①看得到「不慢但很多」的查詢(依 sum_timer_wait 排序)②即時,不用等日誌分析 ③有 rows_examined/rows_sent 比值當效率指標,比值大=掃很多才回傳一點。
點擊翻回
QUESTION
digest 的兩個陷阱是什麼?
點擊翻面
ANSWER
①容量上限 performance_schema_digests_size 預設 5000,超過的指紋丟進 NULL 桶(拼字串 SQL 很容易撐爆)②它不知道是誰呼叫的——解法是在 SQL 埋 /* app=…,ep=… */ 註解標籤。
點擊翻回
QUESTION
「全部慢但 DB 指標都正常、Threads_running 很低」代表什麼?
點擊翻面
ANSWER
請求還沒送到資料庫——卡在應用端的連線池等待或 GC 停頓。但注意連線池滿往往又是因為有人佔住連線(等鎖),所以它是症狀不是根因。
點擊翻回
QUESTION
Seconds_Behind_Source 為什麼不可盡信?
點擊翻面
ANSWER
它算的是套用執行緒落後多久;接收執行緒斷線或卡住時可能顯示 0,實際已落後好幾分鐘。可靠做法是自建心跳表(主庫每秒寫時間戳、複本讀出相減)。
點擊翻回
QUESTION
使用者說「我剛儲存的資料不見了」,該往哪個方向查?
點擊翻面
ANSWER
複本延遲——去主庫查同一筆,主庫有而複本沒有就確認了。這不是效能問題是正確性問題,查慢查詢日誌永遠查不到。常見成因:大交易、批次寫入、複本上也要跑一次的 DDL。
點擊翻回
QUESTION
「CPU 打滿但慢查詢日誌是空的」怎麼查?
點擊翻面
ANSWER
換 digest 依總耗時排序看 count_star。慢日誌空只代表沒有單句超過門檻——當總量是問題時(N+1、缺快取、QPS 暴增),只有依總耗時排序的 digest 看得見。
點擊翻回
QUESTION
為什麼資料庫端的工具都指不出 N+1?
點擊翻面
ANSWER
每一句在資料庫看來都完美:走主鍵、rows=1、8 毫秒,永遠不進慢日誌。問題在「一個請求發了幾句」,而那個數字只有應用端量得到——所以 N+1 是應用程式問題,不是資料庫問題。
點擊翻回
QUESTION
平常(沒事的時候)就該準備好的六件事?
點擊翻面
ANSWER
①慢日誌開好且門檻符合 SLA ②innodb_print_all_deadlocks=ON ③SQL 註解標籤讓 digest 對得回程式碼 ④長交易告警 ⑤單請求 SQL 次數監控與測試斷言 ⑥心跳表量複本延遲。
點擊翻回
QUESTION
排查時要避免的三個反射動作?
點擊翻面
ANSWER
①「慢就加索引」——先看它為什麼不用現有索引 ②「先調參數」——參數是全域的,為一句 SQL 動全域最糟 ③「重啟看看」——會清掉所有現場證據,下次還會再發生而你少了一份證據。
點擊翻回