最新国产好看的视频,伊人天堂AV在线,国产Aaaaaa视频,蜜臀视频在线观看一区,人妻av色图,密臀久久久精品影片,青青视频免费观看毛片,久草在线观看视,国产三级精品色情在线

MYSQL中慢SQL原因與優(yōu)化方法詳解

 更新時(shí)間:2025年07月17日 10:46:18   作者:FixPng  
這篇文章主要為大家詳細(xì)介紹了MYSQL中慢SQL形成的原因以及相關(guān)優(yōu)化方法,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下

一、數(shù)據(jù)庫(kù)故障的關(guān)鍵點(diǎn)

引起數(shù)據(jù)庫(kù)故障的因素有操作系統(tǒng)層面、存儲(chǔ)層面,還有斷電斷網(wǎng)的基礎(chǔ)環(huán)境層面(以下稱為外部因素),以及應(yīng)用程序操作數(shù)據(jù)庫(kù)和人為操作數(shù)據(jù)庫(kù)這兩個(gè)層面(以下稱內(nèi)部因素)。這些故障中外部因素發(fā)生的概率較小,可能幾年都未發(fā)生過(guò)一起。許多 DBA 從入職到離職都可能沒(méi)有遇到過(guò)外部因素導(dǎo)致的故障,但是內(nèi)部因素導(dǎo)致的故障可能每天都在產(chǎn)生。內(nèi)部因素中占據(jù)主導(dǎo)的是應(yīng)用程序?qū)е碌墓收?,可以說(shuō)數(shù)據(jù)庫(kù)故障的主要元兇就是應(yīng)用程序的 SQL 寫(xiě)的不夠好。

SQL 是由開(kāi)發(fā)人員編寫(xiě)的,但是責(zé)任不完全是開(kāi)發(fā)人員的。

  • SQL 的成因:SQL 是為了實(shí)現(xiàn)特定的需求而編寫(xiě)的,那么需求的合理性是第一位的。一般來(lái)說(shuō),在合理的需求下即使有問(wèn)題的 SQL 也是可以挽救的。但是如果需求不合理,那么就為 SQL 問(wèn)題埋下了隱患。
  • SQL 的設(shè)計(jì):這里的設(shè)計(jì)主要是數(shù)據(jù)庫(kù)對(duì)象的設(shè)計(jì)。即使是合理的需求,椰果在數(shù)據(jù)庫(kù)設(shè)計(jì)層面沒(méi)有把控的緩解或者保證,那么很多優(yōu)化就會(huì)大打折扣。
  • SQL 的實(shí)現(xiàn):這部分是開(kāi)發(fā)人員所涉及的工作。但是這已經(jīng)是流程的末端,這個(gè)時(shí)候改善的手段依然有,但是屬于挽救措施。

在筆者多年的工作中,數(shù)據(jù)庫(kù)故障主要來(lái)源于三個(gè)方面:不合理的需求、不合理的設(shè)計(jì)和不合理的實(shí)現(xiàn)。而這些如果從管理和流程上明確規(guī)定由經(jīng)驗(yàn)豐富的 DBA 介入,那么對(duì)數(shù)據(jù)庫(kù)故障的源頭是有很大的控制作用的。

上述摘自薛曉剛老師的 《DBA 實(shí)戰(zhàn)手記》 3.2 節(jié)。

二、慢 SQL 的常見(jiàn)成因

在分析 SQL 語(yǔ)句時(shí),SQL 運(yùn)行緩慢絕對(duì)是最主要的問(wèn)題,沒(méi)有之一。慢 SQL 是數(shù)據(jù)庫(kù)性能瓶頸的主要表現(xiàn),其核心成因可歸納為以下幾類:

  • 索引問(wèn)題:缺失索引導(dǎo)致全表掃描、索引失效(如函數(shù)操作索引列、隱式類型轉(zhuǎn)換)、索引設(shè)計(jì)不合理(單值索引 vs 復(fù)合索引選擇錯(cuò)誤)。
  • 查詢寫(xiě)法缺陷:SELECT * 全字段查詢、復(fù)雜子查詢嵌套、無(wú)過(guò)濾條件的大范圍掃描、低效 JOIN 操作。
  • 數(shù)據(jù)量與結(jié)構(gòu):表數(shù)據(jù)量過(guò)大未分區(qū)、字段類型設(shè)計(jì)不合理(如用 VARCHAR 存儲(chǔ)數(shù)字)、大字段(TEXT/BLOB)頻繁查詢。
  • 執(zhí)行計(jì)劃異常:優(yōu)化器誤判(統(tǒng)計(jì)信息過(guò)時(shí))、JOIN 順序錯(cuò)誤、臨時(shí)表與文件排序?yàn)E用。

下面通過(guò)實(shí)例表和純 SQL 生成的數(shù)據(jù),結(jié)合執(zhí)行計(jì)劃工具詳解優(yōu)化方法。

三、實(shí)驗(yàn)表結(jié)構(gòu)及數(shù)據(jù)

1. 表結(jié)構(gòu)(電商場(chǎng)景)

-- 用戶表
CREATE TABLE users (
  user_id INT AUTO_INCREMENT PRIMARY KEY,
  username VARCHAR(50) NOT NULL,
  email VARCHAR(100) UNIQUE,
  age INT,
  register_time DATETIME,
  INDEX idx_age (age),
  INDEX idx_register_time (register_time)
) ENGINE=InnoDB;

-- 商品表
CREATE TABLE products (
  product_id INT AUTO_INCREMENT PRIMARY KEY,
  product_name VARCHAR(100) NOT NULL,
  price DECIMAL(10,2),
  category_id INT,
  stock INT,
  INDEX idx_category (category_id),
  INDEX idx_name_price (product_name, price)
) ENGINE=InnoDB;

-- 訂單表
CREATE TABLE orders (
  order_id INT AUTO_INCREMENT PRIMARY KEY,
  user_id INT NOT NULL,
  product_id INT NOT NULL,
  order_time DATETIME,
  amount DECIMAL(10,2),
  status TINYINT, -- 1:待支付 2:已支付 3:已取消
  INDEX idx_user_time (user_id, order_time),
  INDEX idx_product_id (product_id)
) ENGINE=InnoDB;

2. 生成測(cè)試數(shù)據(jù)(存儲(chǔ)過(guò)程)

生成用戶數(shù)據(jù)(10 萬(wàn)條)

DELIMITER //
CREATE PROCEDURE prod_generate_users()
BEGIN
  DECLARE i INT DEFAULT 1;
  DECLARE batch_size INT DEFAULT 1000; -- 每批處理1000條記錄
  DECLARE total INT DEFAULT 100000;   -- 總記錄數(shù)

  WHILE i <= total DO
    START TRANSACTION;

    -- 插入當(dāng)前批次的記錄
    WHILE i <= total AND i <= (batch_size * FLOOR((i-1)/batch_size) + batch_size) DO
      INSERT INTO users (username, email, age, register_time)
      VALUES (
        CONCAT('user_', i),
        CONCAT('user_', i, '@example.com'),
        FLOOR(RAND() * 40) + 18,
        DATE_SUB(NOW(), INTERVAL FLOOR(RAND() * 3650) DAY)
      );
      SET i = i + 1;
    END WHILE;

    COMMIT;
  END WHILE;
END //
DELIMITER ;

-- 執(zhí)行存儲(chǔ)過(guò)程
CALL prod_generate_users();

生成商品數(shù)據(jù) 1 萬(wàn)條)

DELIMITER //
CREATE PROCEDURE prod_generate_products()
BEGIN
  DECLARE i INT DEFAULT 1;
  DECLARE batch_size INT DEFAULT 1000; -- 每批處理1000條記錄
  DECLARE total INT DEFAULT 10000;     -- 總記錄數(shù)

  WHILE i <= total DO
    START TRANSACTION;

    -- 插入當(dāng)前批次的記錄
    WHILE i <= total AND i <= (batch_size * FLOOR((i-1)/batch_size) + batch_size) DO
      INSERT INTO products (product_name, price, category_id, stock)
      VALUES (
        CONCAT('product_', i),
        ROUND(RAND() * 999 + 1, 2),      -- 1-1000元
        FLOOR(RAND() * 20) + 1,          -- 1-20類分類
        FLOOR(RAND() * 1000) + 10        -- 10-1009庫(kù)存
      );
      SET i = i + 1;
    END WHILE;

    COMMIT;
  END WHILE;
END //
DELIMITER ;

CALL prod_generate_products();

生成訂單數(shù)據(jù)(100 萬(wàn)條)

DELIMITER //
CREATE PROCEDURE prod_generate_orders()
BEGIN
  DECLARE i INT DEFAULT 1;
  DECLARE batch_size INT DEFAULT 500;  -- 每批處理500條記錄(訂單數(shù)據(jù)量大,批次更小)
  DECLARE total INT DEFAULT 1000000;    -- 總記錄數(shù)
  DECLARE max_user INT;
  DECLARE max_product INT;
  DECLARE rand_product_id INT;
  DECLARE product_price DECIMAL(10,2);

  -- 獲取用戶和商品的最大ID
  SELECT MAX(user_id) INTO max_user FROM users;
  SELECT MAX(product_id) INTO max_product FROM products;

  WHILE i <= total DO
    START TRANSACTION;

    -- 插入當(dāng)前批次的記錄
    WHILE i <= total AND i <= (batch_size * FLOOR((i-1)/batch_size) + batch_size) DO
      -- 優(yōu)化:預(yù)先計(jì)算隨機(jī)商品ID和價(jià)格,避免子查詢
      SET rand_product_id = FLOOR(RAND() * max_product) + 1;
      SELECT price INTO product_price FROM products WHERE product_id = rand_product_id LIMIT 1;

      INSERT INTO orders (user_id, product_id, order_time, amount, status)
      VALUES (
        FLOOR(RAND() * max_user) + 1,    -- 隨機(jī)用戶
        rand_product_id,                 -- 隨機(jī)商品
        DATE_SUB(NOW(), INTERVAL FLOOR(RAND() * 365) DAY), -- 近1年訂單
        product_price * (FLOOR(RAND() * 5) + 1),            -- 1-5件數(shù)量
        FLOOR(RAND() * 3) + 1             -- 隨機(jī)狀態(tài)
      );
      SET i = i + 1;
    END WHILE;

    COMMIT;
  END WHILE;
END //
DELIMITER ;

CALL prod_generate_orders();

查看數(shù)據(jù)

select count(1) from users
union all
select count(1) from products
union all
select count(1) from orders;

+----------+
| count(1) |
+----------+
|   100000 |
|    10000 |
|  1000000 |
+----------+

四、執(zhí)行計(jì)劃詳解:從分析到優(yōu)化完整指南

三類工具的選擇指南

工具核心價(jià)值適用場(chǎng)景
EXPLAIN快速預(yù)判執(zhí)行計(jì)劃(預(yù)估)日常開(kāi)發(fā)、索引設(shè)計(jì)驗(yàn)證、排查明顯低效操作
optimizer_trace深入優(yōu)化器決策過(guò)程(分析“為什么這么做”)復(fù)雜查詢的索引選擇問(wèn)題、JOIN 順序優(yōu)化
EXPLAIN ANALYZE量化實(shí)際執(zhí)行性能(精確耗時(shí)、行數(shù))性能瓶頸量化、優(yōu)化效果對(duì)比、分頁(yè)/排序分析

通過(guò)這三類工具的組合使用,可從“預(yù)判”到“分析”再到“量化”,全面掌握 MySQL 查詢的執(zhí)行邏輯,精準(zhǔn)定位并解決性能問(wèn)題。

1. 基礎(chǔ)分析工具:EXPLAIN——預(yù)判查詢執(zhí)行邏輯

EXPLAIN是 MySQL 中最常用的執(zhí)行計(jì)劃分析工具,無(wú)需實(shí)際執(zhí)行查詢,即可返回優(yōu)化器對(duì)查詢的執(zhí)行方案(如索引選擇、掃描方式等),幫助提前發(fā)現(xiàn)性能隱患。

核心語(yǔ)法與使用場(chǎng)景

-- 對(duì)任意SELECT查詢執(zhí)行分析
EXPLAIN SELECT 列名 FROM 表名 WHERE 條件;
-- 支持復(fù)雜查詢(JOIN、子查詢等)
EXPLAIN SELECT u.username, o.order_id FROM users u JOIN orders o ON u.user_id = o.user_id WHERE u.age > 30;
+----+-------------+-------+------------+------+----------------------+----------------------+---------+------------------+-------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys        | key                  | key_len | ref              | rows  | filtered | Extra       |
+----+-------------+-------+------------+------+----------------------+----------------------+---------+------------------+-------+----------+-------------+
|  1 | SIMPLE      | u     | NULL       | ALL  | PRIMARY,idx_age      | NULL                 | NULL    | NULL             | 99864 |    50.00 | Using where |
|  1 | SIMPLE      | o     | NULL       | ref  | idx_user_time_amount | idx_user_time_amount | 4       | testdb.u.user_id |     9 |   100.00 | Using index |
+----+-------------+-------+------------+------+----------------------+----------------------+---------+------------------+-------+----------+-------------+
2 rows in set, 1 warning (0.14 sec)

適用場(chǎng)景

  • 快速判斷查詢是否使用索引、是否存在全表掃描;
  • 分析 JOIN 語(yǔ)句的表連接順序和連接方式;
  • 定位Using filesort(文件排序)、Using temporary(臨時(shí)表)等低效操作。

字段深度解讀

在 MySQL 的 EXPLAIN 執(zhí)行計(jì)劃中,除了 typekey、rows、Extra 這幾個(gè)核心字段外,其他字段也承載著查詢執(zhí)行邏輯的關(guān)鍵信息。以下是 EXPLAIN 所有字段的完整含義:

字段名核心含義補(bǔ)充說(shuō)明
id查詢中每個(gè)操作的唯一標(biāo)識(shí)(多表/子查詢時(shí)用于區(qū)分執(zhí)行順序)。- 若 id 相同:表示操作在同一層級(jí),按表的順序(從左到右)執(zhí)行(如 JOIN 時(shí)的驅(qū)動(dòng)表和被驅(qū)動(dòng)表)。
- 若 id 不同:id 越大優(yōu)先級(jí)越高,先執(zhí)行(如子查詢會(huì)嵌套在主查詢內(nèi)部,id 更大)。
select_type查詢的類型(區(qū)分簡(jiǎn)單查詢、子查詢、聯(lián)合查詢等)。常見(jiàn)值:
- SIMPLE:簡(jiǎn)單查詢(無(wú)子查詢、JOIN 等復(fù)雜結(jié)構(gòu))。
- PRIMARY:主查詢(包含子查詢時(shí),最外層的查詢)。
- SUBQUERY:子查詢(SELECT 中的子查詢,不依賴外部結(jié)果)。
- DERIVED:衍生表(FROM 中的子查詢,會(huì)生成臨時(shí)表)。
- UNION:UNION 語(yǔ)句中第二個(gè)及以后的查詢。
- UNION RESULT:UNION 結(jié)果集的合并操作。
table當(dāng)前操作涉及的表名(或臨時(shí)表別名,如 derived2 表示衍生表)。若為 NULL:可能是 UNION RESULT(合并結(jié)果集時(shí)無(wú)具體表),或子查詢的中間結(jié)果。
partitions查詢匹配的分區(qū)(僅對(duì)分區(qū)表有效)。非分區(qū)表顯示 NULL;分區(qū)表會(huì)顯示匹配的分區(qū)名稱(如 p2023 表示命中 p2023 分區(qū))。
type訪問(wèn)類型(索引使用效率等級(jí),最關(guān)鍵的性能指標(biāo))。詳見(jiàn)前文“type 字段優(yōu)先級(jí)與解讀”,從優(yōu)到差反映索引利用效率(如 const > ref > ALL)。
possible_keys優(yōu)化器認(rèn)為可能使用的索引(候選索引列表)。該字段僅表示“可能有效”的索引,不代表實(shí)際使用;若為 NULL,說(shuō)明沒(méi)有可用索引。
key實(shí)際使用的索引(NULL 表示未使用任何索引)。若 possible_keys 有值但 key 為 NULL,可能是索引選擇性差(如字段值重復(fù)率高)或優(yōu)化器判斷全表掃描更快。
key_len實(shí)際使用的索引長(zhǎng)度(字節(jié)數(shù))。用于判斷復(fù)合索引的使用情況:
- 若 key_len 等于復(fù)合索引總長(zhǎng)度,說(shuō)明整個(gè)索引被使用;
- 若較短,說(shuō)明僅使用了復(fù)合索引的前綴部分(需檢查是否因類型不匹配導(dǎo)致索引截?cái)啵缱址粗付ㄩL(zhǎng)度)。
ref表示哪些字段或常量被用來(lái)匹配索引列。- 若為常量(如 const):表示用固定值匹配索引(如 WHERE id=1)。
- 若為表名.字段(如 u.user_id):表示用其他表的字段關(guān)聯(lián)當(dāng)前表的索引(如 JOIN 時(shí)的關(guān)聯(lián)條件)。
rows優(yōu)化器預(yù)估的掃描行數(shù)(基于表統(tǒng)計(jì)信息估算)。數(shù)值越小越好,反映查詢的“工作量”;若遠(yuǎn)大于實(shí)際數(shù)據(jù)量,可能是統(tǒng)計(jì)信息過(guò)時(shí),需執(zhí)行 ANALYZE TABLE 表名 更新。
filtered經(jīng)過(guò)過(guò)濾條件后,剩余記錄占預(yù)估掃描行數(shù)的比例(百分比)。如 filtered=50 表示掃描的 rows 中,有 50% 滿足過(guò)濾條件;值越高,說(shuō)明過(guò)濾效率越好(減少后續(xù)處理的數(shù)據(jù)量)。
Extra額外的執(zhí)行細(xì)節(jié)(補(bǔ)充說(shuō)明索引使用、排序、臨時(shí)表等特殊行為)。包含大量關(guān)鍵信息,如 Using filesort(文件排序)、Using temporary(臨時(shí)表)等,是優(yōu)化的核心線索(詳見(jiàn)前文)。

type字段優(yōu)先級(jí)與解讀(從優(yōu)到差)

type值含義性能影響
system表中只有 1 行數(shù)據(jù)(如系統(tǒng)表),無(wú)需掃描理想狀態(tài),僅特殊場(chǎng)景出現(xiàn)。
const通過(guò)主鍵/唯一索引匹配 1 行數(shù)據(jù)(如WHERE id=1)高效,索引精確匹配,推薦。
eq_ref多表 JOIN 時(shí),被驅(qū)動(dòng)表通過(guò)主鍵/唯一索引匹配,每行只返回 1 條數(shù)據(jù)高效,適合關(guān)聯(lián)查詢(如orders.user_id關(guān)聯(lián)users.user_id主鍵)。
ref非唯一索引匹配,可能返回多行(如WHERE age=30,age為普通索引)較好,索引部分匹配,需關(guān)注返回行數(shù)。
range索引范圍掃描(如BETWEEN、IN、>等)中等,比全表掃描高效,適合范圍查詢(需確保索引覆蓋條件)。
index掃描整個(gè)索引樹(shù)(未命中索引過(guò)濾條件,僅用索引排序/覆蓋)低效,相當(dāng)于“索引全表掃描”(如SELECT id FROM users,id為主鍵但無(wú)過(guò)濾)。
ALL全表掃描(未使用任何索引)極差,大表中會(huì)導(dǎo)致查詢超時(shí),必須優(yōu)化。

Extra字段關(guān)鍵值解讀

Extra值含義優(yōu)化方向
Using where使用WHERE條件過(guò)濾,但未使用索引(全表掃描后過(guò)濾)為過(guò)濾字段創(chuàng)建索引。
Using index索引覆蓋掃描(查詢字段均在索引中,無(wú)需回表查數(shù)據(jù))理想狀態(tài),說(shuō)明索引設(shè)計(jì)合理(如SELECT user_id FROM orders使用user_id索引)。
Using where; Using index既用索引過(guò)濾,又用索引覆蓋最優(yōu)狀態(tài),索引同時(shí)滿足過(guò)濾和查詢需求。
Using filesort無(wú)法通過(guò)索引排序,需在內(nèi)存/磁盤中排序(大結(jié)果集極慢)優(yōu)化排序字段,創(chuàng)建“過(guò)濾+排序”復(fù)合索引(如WHERE status=1 ORDER BY time需(status, time)索引)。
Using temporary需要?jiǎng)?chuàng)建臨時(shí)表存儲(chǔ)中間結(jié)果(如GROUP BY非索引字段)避免在大表上使用GROUP BY非索引字段,或創(chuàng)建包含分組字段的復(fù)合索引。
Using join buffer多表 JOIN 未使用索引,通過(guò)連接緩沖區(qū)匹配為 JOIN 條件字段創(chuàng)建索引(如ON u.user_id = o.user_id,需o.user_id索引)。

案例:從EXPLAIN結(jié)果到優(yōu)化

需求:查詢年齡 30-40 歲的用戶用戶名和郵箱。

原始查詢

EXPLAIN SELECT username, email FROM users WHERE age BETWEEN 30 AND 40;

執(zhí)行計(jì)劃結(jié)果(問(wèn)題版)

+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows  | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------------+
|  1 | SIMPLE      | users | NULL       | ALL  | idx_age       | NULL | NULL    | NULL | 99776 |    47.03 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+-------+----------+-------------+

問(wèn)題分析

  • type=ALL:全表掃描,未使用索引;
  • possible_keys=idx_agekey=NULL:索引存在但未被使用(可能因統(tǒng)計(jì)信息過(guò)時(shí)或索引選擇性差)。

優(yōu)化步驟

  1. 確認(rèn)索引是否有效:SHOW INDEX FROM users WHERE Key_name='idx_age';(若不存在則創(chuàng)建);
  2. 更新表統(tǒng)計(jì)信息:ANALYZE TABLE users;(讓優(yōu)化器獲取準(zhǔn)確數(shù)據(jù)分布);
  3. 優(yōu)化后預(yù)期結(jié)果:type=rangekey=idx_age,Extra=Using where; Using index(若usernameemail不在索引中,至少實(shí)現(xiàn)range掃描)。

2. 深入優(yōu)化工具:optimizer_trace——揭秘優(yōu)化器決策過(guò)程

EXPLAIN只能展示執(zhí)行計(jì)劃的“結(jié)果”,而optimizer_trace可以展示優(yōu)化器生成計(jì)劃的“過(guò)程”(如索引選擇的權(quán)衡、成本計(jì)算、JOIN 順序決策等),適合分析復(fù)雜查詢的深層性能問(wèn)題。

核心作用與適用場(chǎng)景

  • 分析“明明有索引卻不用”的原因(優(yōu)化器認(rèn)為全表掃描成本更低?);
  • 對(duì)比不同索引的成本差異,指導(dǎo)索引設(shè)計(jì);
  • 解讀 JOIN 語(yǔ)句中表連接順序的決策邏輯(為什么 A 表驅(qū)動(dòng) B 表而不是相反?)。

使用步驟與注意事項(xiàng)

-- 默認(rèn)是關(guān)閉的
show global variables like 'optimizer_trace';
+-----------------+--------------------------+
| Variable_name   | Value                    |
+-----------------+--------------------------+
| optimizer_trace | enabled=off,one_line=off |
+-----------------+--------------------------+

-- 開(kāi)啟跟蹤(修改當(dāng)前會(huì)話)
SET optimizer_trace = "enabled=on";

-- 僅影響當(dāng)前會(huì)話(看global全局還是off的)
show session variables like 'optimizer_trace';
+-----------------+-------------------------+
| Variable_name   | Value                   |
+-----------------+-------------------------+
| optimizer_trace | enabled=on,one_line=off |
+-----------------+-------------------------+

-- 執(zhí)行目標(biāo)查詢
SELECT * FROM orders WHERE user_id = 100 AND order_time > '2024-01-01';

-- 查看跟蹤結(jié)果
SELECT * FROM information_schema.optimizer_trace\G
*************************** 1. row ***************************
                            QUERY: SELECT * FROM orders WHERE user_id = 100 AND order_time > '2024-01-01'
                            TRACE: {
  "steps": [
    {
      "join_preparation": {
        "select#": 1,
        "steps": [
          {
            "expanded_query": "/* select#1 */ select `orders`.`order_id` AS `order_id`,`orders`.`user_id` AS `user_id`,`orders`.`product_id` AS `product_id`,`orders`.`order_time` AS `order_time`,`orders`.`amount` AS `amount`,`orders`.`status` AS `status` from `orders` where ((`orders`.`user_id` = 100) and (`orders`.`order_time` > '2024-01-01'))"
          }
        ]
      }
    },
    {
      "join_optimization": {
        "select#": 1,
        "steps": [
          {
            "condition_processing": {
              "condition": "WHERE",
              "original_condition": "((`orders`.`user_id` = 100) and (`orders`.`order_time` > '2024-01-01'))",
              "steps": [
                {
                  "transformation": "equality_propagation",
                  "resulting_condition": "((`orders`.`order_time` > '2024-01-01') and multiple equal(100, `orders`.`user_id`))"
                },
                {
                  "transformation": "constant_propagation",
                  "resulting_condition": "((`orders`.`order_time` > '2024-01-01') and multiple equal(100, `orders`.`user_id`))"
                },
                {
                  "transformation": "trivial_condition_removal",
                  "resulting_condition": "((`orders`.`order_time` > TIMESTAMP'2024-01-01 00:00:00') and multiple equal(100, `orders`.`user_id`))"
                }
              ]
            }
          },
          {
            "substitute_generated_columns": {
            }
          },
          {
            "table_dependencies": [
              {
                "table": "`orders`",
                "row_may_be_null": false,
                "map_bit": 0,
                "depends_on_map_bits": [
                ]
              }
            ]
          },
          {
            "ref_optimizer_key_uses": [
              {
                "table": "`orders`",
                "field": "user_id",
                "equals": "100",
                "null_rejecting": true
              }
            ]
          },
          {
            "rows_estimation": [
              {
                "table": "`orders`",
                "range_analysis": {
                  "table_scan": {
                    "rows": 925560,
                    "cost": 93207.1
                  },
                  "potential_range_indexes": [
                    {
                      "index": "PRIMARY",
                      "usable": false,
                      "cause": "not_applicable"
                    },
                    {
                      "index": "idx_user_time",
                      "usable": true,
                      "key_parts": [
                        "user_id",
                        "order_time",
                        "order_id"
                      ]
                    },
                    {
                      "index": "idx_product_id",
                      "usable": false,
                      "cause": "not_applicable"
                    }
                  ],
                  "setup_range_conditions": [
                  ],
                  "group_index_range": {
                    "chosen": false,
                    "cause": "not_group_by_or_distinct"
                  },
                  "skip_scan_range": {
                    "potential_skip_scan_indexes": [
                      {
                        "index": "idx_user_time",
                        "usable": false,
                        "cause": "query_references_nonkey_column"
                      }
                    ]
                  },
                  "analyzing_range_alternatives": {
                    "range_scan_alternatives": [
                      {
                        "index": "idx_user_time",
                        "ranges": [
                          "user_id = 100 AND '2024-01-01 00:00:00' < order_time"
                        ],
                        "index_dives_for_eq_ranges": true,
                        "rowid_ordered": false,
                        "using_mrr": false,
                        "index_only": false,
                        "in_memory": 1,
                        "rows": 10,
                        "cost": 3.76,
                        "chosen": true
                      }
                    ],
                    "analyzing_roworder_intersect": {
                      "usable": false,
                      "cause": "too_few_roworder_scans"
                    }
                  },
                  "chosen_range_access_summary": {
                    "range_access_plan": {
                      "type": "range_scan",
                      "index": "idx_user_time",
                      "rows": 10,
                      "ranges": [
                        "user_id = 100 AND '2024-01-01 00:00:00' < order_time"
                      ]
                    },
                    "rows_for_plan": 10,
                    "cost_for_plan": 3.76,
                    "chosen": true
                  }
                }
              }
            ]
          },
          {
            "considered_execution_plans": [
              {
                "plan_prefix": [
                ],
                "table": "`orders`",
                "best_access_path": {
                  "considered_access_paths": [
                    {
                      "access_type": "ref",
                      "index": "idx_user_time",
                      "chosen": false,
                      "cause": "range_uses_more_keyparts"
                    },
                    {
                      "rows_to_scan": 10,
                      "access_type": "range",
                      "range_details": {
                        "used_index": "idx_user_time"
                      },
                      "resulting_rows": 10,
                      "cost": 4.76,
                      "chosen": true
                    }
                  ]
                },
                "condition_filtering_pct": 100,
                "rows_for_plan": 10,
                "cost_for_plan": 4.76,
                "chosen": true
              }
            ]
          },
          {
            "attaching_conditions_to_tables": {
              "original_condition": "((`orders`.`user_id` = 100) and (`orders`.`order_time` > TIMESTAMP'2024-01-01 00:00:00'))",
              "attached_conditions_computation": [
              ],
              "attached_conditions_summary": [
                {
                  "table": "`orders`",
                  "attached": "((`orders`.`user_id` = 100) and (`orders`.`order_time` > TIMESTAMP'2024-01-01 00:00:00'))"
                }
              ]
            }
          },
          {
            "finalizing_table_conditions": [
              {
                "table": "`orders`",
                "original_table_condition": "((`orders`.`user_id` = 100) and (`orders`.`order_time` > TIMESTAMP'2024-01-01 00:00:00'))",
                "final_table_condition   ": "((`orders`.`user_id` = 100) and (`orders`.`order_time` > TIMESTAMP'2024-01-01 00:00:00'))"
              }
            ]
          },
          {
            "refine_plan": [
              {
                "table": "`orders`",
                "pushed_index_condition": "((`orders`.`user_id` = 100) and (`orders`.`order_time` > TIMESTAMP'2024-01-01 00:00:00'))",
                "table_condition_attached": null
              }
            ]
          }
        ]
      }
    },
    {
      "join_execution": {
        "select#": 1,
        "steps": [
        ]
      }
    }
  ]
}
MISSING_BYTES_BEYOND_MAX_MEM_SIZE: 0
          INSUFFICIENT_PRIVILEGES: 0


-- 關(guān)閉跟蹤(避免性能消耗)
SET optimizer_trace = "enabled=off";

注意事項(xiàng)

  • 僅在分析復(fù)雜查詢時(shí)使用,開(kāi)啟后會(huì)增加 CPU 和內(nèi)存消耗;
  • 結(jié)果中MISSING_BYTES_BEYOND_MAX_MEM_SIZE>0表示內(nèi)容被截?cái)?,需調(diào)大optimizer_trace_max_mem_size(默認(rèn) 1MB);
  • PROCESS權(quán)限才能查看information_schema.optimizer_trace。

關(guān)鍵結(jié)果解讀

從 JSON 結(jié)果中重點(diǎn)關(guān)注以下部分:

  • rows_estimation:優(yōu)化器對(duì)各表行數(shù)的估算(若與實(shí)際偏差大,需更新統(tǒng)計(jì)信息);
  • potential_range_indexes:優(yōu)化器考慮的所有候選索引(包括未被選中的);
  • analyzing_range_alternatives:各索引的成本對(duì)比(cost字段,值越小越優(yōu));
  • chosen_range_access_summary:最終選擇的索引及原因(如cost=3.76的索引被選中)。

示例解讀

potential_range_indexes中包含idx_user_time,但chosen_range_access_summary未選中,需查看cost是否高于全表掃描(可能因索引選擇性差,優(yōu)化器認(rèn)為全表掃描更快),此時(shí)需優(yōu)化索引(如增加區(qū)分度更高的前綴字段)。

3. 精準(zhǔn)量化工具:EXPLAIN ANALYZE(MySQL 8.0+)——實(shí)測(cè)執(zhí)行性能

EXPLAIN ANALYZE是 MySQL 8.0 引入的增強(qiáng)功能,會(huì)實(shí)際執(zhí)行查詢,并返回精確的執(zhí)行時(shí)間、掃描行數(shù)等 metrics,適合量化性能瓶頸(如排序耗時(shí)、掃描行數(shù)與預(yù)期的偏差)。

核心優(yōu)勢(shì)與使用場(chǎng)景

  • 對(duì)比EXPLAINEXPLAIN返回“預(yù)估”值,EXPLAIN ANALYZE返回“實(shí)際”值(如actual time、actual rows);
  • 適合分析分頁(yè)查詢(LIMIT offset, size)、排序、JOIN 等操作的真實(shí)耗時(shí);
  • 量化索引優(yōu)化效果(如優(yōu)化前后的執(zhí)行時(shí)間對(duì)比)。

警告

  • 對(duì)大表執(zhí)行EXPLAIN ANALYZE會(huì)消耗實(shí)際資源(如全表掃描 1000 萬(wàn)行),生產(chǎn)環(huán)境謹(jǐn)慎使用
  • SELECT語(yǔ)句(如UPDATE、DELETE)禁用,避免誤操作數(shù)據(jù)。
  • 會(huì)實(shí)際執(zhí)行語(yǔ)句,因此非 DQL 語(yǔ)句謹(jǐn)慎執(zhí)行?。?!

案例:分析分頁(yè)查詢性能

需求:分析“查詢狀態(tài)為 2 的訂單,按時(shí)間排序后取第 10001-10020 條”的性能瓶頸。

查詢

EXPLAIN ANALYZE
SELECT * FROM orders
WHERE status = 2
ORDER BY order_time
LIMIT 10000, 20\G

未優(yōu)化結(jié)果

EXPLAIN: -> Limit/Offset: 20/10000 row(s)  (cost=93205 rows=20) (actual time=744..744 rows=20 loops=1)
    -> Sort: orders.order_time, limit input to 10020 row(s) per chunk  (cost=93205 rows=925560) (actual time=742..743 rows=10020 loops=1)
        -> Filter: (orders.`status` = 2)  (cost=93205 rows=925560) (actual time=3.26..591 rows=333807 loops=1)
            -> Table scan on orders  (cost=93205 rows=925560) (actual time=3.25..505 rows=1e+6 loops=1)

關(guān)鍵信息解讀
總執(zhí)行時(shí)間:744ms(根節(jié)點(diǎn)的actual time

各階段實(shí)際耗時(shí):

  1. 表掃描(Table scan on orders):(505ms - 3.25ms)*1 ≈ 502ms
  2. 過(guò)濾(Filter):(591ms - 3.26ms)*1 ≈ 588ms(包含表掃描時(shí)間)
  3. 排序(Sort):(743ms - 3.26ms)*1 ≈ 740ms(包含表掃描和過(guò)濾時(shí)間)
字段含義
cost優(yōu)化器預(yù)估的執(zhí)行成本(數(shù)值越小越好,基于 CPU 消耗、IO 操作等計(jì)算)
rows(預(yù)估)優(yōu)化器預(yù)估的需要處理的行數(shù)(反映查詢的“工作量”,數(shù)值越小效率越高)
actual time實(shí)際執(zhí)行時(shí)間(格式為 開(kāi)始時(shí)間..結(jié)束時(shí)間,單位為毫秒)
actual rows實(shí)際處理的行數(shù)(反映真實(shí)數(shù)據(jù)量,與預(yù)估 rows 差異過(guò)大需關(guān)注)
loops操作執(zhí)行的循環(huán)次數(shù)(非join或帶子查詢的sql通常為 1)

優(yōu)化方案:創(chuàng)建覆蓋“過(guò)濾+排序”的復(fù)合索引:

CREATE INDEX idx_status_time ON orders(status, order_time);

優(yōu)化后結(jié)果

EXPLAIN: -> Limit/Offset: 20/10000 row(s)  (cost=48225 rows=20) (actual time=48.4..48.4 rows=20 loops=1)
    -> Index lookup on orders using idx_status_time (status=2)  (cost=48225 rows=462780) (actual time=25.3..47.9 rows=10020 loops=1)

優(yōu)化效果

  • 總時(shí)間從 744ms 降至 48ms(提升 93%);
  • 消除全表掃描和文件排序,直接通過(guò)索引定位數(shù)據(jù)(Index lookup)。

五、案例 - 索引問(wèn)題與表結(jié)構(gòu)

1. 索引失效 - 函數(shù)操作索引列

案例 SQL

EXPLAIN SELECT COUNT(1) FROM users WHERE YEAR(register_time) = 2025;
+----+-------------+-------+------------+-------+---------------+-------------------+---------+------+-------+----------+--------------------------+
| id | select_type | table | partitions | type  | possible_keys | key               | key_len | ref  | rows  | filtered | Extra                    |
+----+-------------+-------+------------+-------+---------------+-------------------+---------+------+-------+----------+--------------------------+
|  1 | SIMPLE      | users | NULL       | index | NULL          | idx_register_time | 6       | NULL | 99776 |   100.00 | Using where; Using index |
+----+-------------+-------+------------+-------+---------------+-------------------+---------+------+-------+----------+--------------------------+
1 row in set, 1 warning (0.00 sec)

EXPLAIN SELECT COUNT(1) FROM users WHERE register_time >= '2025-01-01' AND register_time < DATE_ADD('2025-01-01', INTERVAL 1 YEAR);
+----+-------------+-------+------------+-------+-------------------+-------------------+---------+------+------+----------+--------------------------+
| id | select_type | table | partitions | type  | possible_keys     | key               | key_len | ref  | rows | filtered | Extra                    |
+----+-------------+-------+------------+-------+-------------------+-------------------+---------+------+------+----------+--------------------------+
|  1 | SIMPLE      | users | NULL       | range | idx_register_time | idx_register_time | 6       | NULL | 5300 |   100.00 | Using where; Using index |
+----+-------------+-------+------------+-------+-------------------+-------------------+---------+------+------+----------+--------------------------+
1 row in set, 1 warning (0.00 sec)

執(zhí)行計(jì)劃問(wèn)題:對(duì)索引列register_time使用了函數(shù),導(dǎo)致索引失效。

優(yōu)化手段:改為范圍查詢,不要對(duì)索引列使用函數(shù)。

2. 索引失效 - 隱式類型轉(zhuǎn)換

案例 SQL

EXPLAIN select count(1) from products where product_name=12345;
+----+-------------+----------+------------+-------+----------------+----------------+---------+------+------+----------+--------------------------+
| id | select_type | table    | partitions | type  | possible_keys  | key            | key_len | ref  | rows | filtered | Extra                    |
+----+-------------+----------+------------+-------+----------------+----------------+---------+------+------+----------+--------------------------+
|  1 | SIMPLE      | products | NULL       | index | idx_name_price | idx_name_price | 408     | NULL | 9642 |    10.00 | Using where; Using index |
+----+-------------+----------+------------+-------+----------------+----------------+---------+------+------+----------+--------------------------+
1 row in set, 3 warnings (0.00 sec)

EXPLAIN select count(1) from products where product_name='12345';
+----+-------------+----------+------------+------+----------------+----------------+---------+-------+------+----------+-------------+
| id | select_type | table    | partitions | type | possible_keys  | key            | key_len | ref   | rows | filtered | Extra       |
+----+-------------+----------+------------+------+----------------+----------------+---------+-------+------+----------+-------------+
|  1 | SIMPLE      | products | NULL       | ref  | idx_name_price | idx_name_price | 402     | const |    1 |   100.00 | Using index |
+----+-------------+----------+------------+------+----------------+----------------+---------+-------+------+----------+-------------+
1 row in set, 1 warning (0.00 sec)

執(zhí)行計(jì)劃問(wèn)題product_name是 varchar 類型,但查詢條件使用了數(shù)字類型,發(fā)生隱式類型轉(zhuǎn)換導(dǎo)致索引失效。參考:select count(1) from products where CAST(product_name AS SIGNED)=12345;

優(yōu)化手段:查詢條件類型與索引列字段一致。

可能有小伙伴會(huì)問(wèn),明明查出來(lái)是 0 行,怎么執(zhí)行計(jì)劃 rows=1 呢,實(shí)際在掃索引時(shí) mysql 也已經(jīng)知道了沒(méi)有匹配的結(jié)果,理論返回 0 行就行,但 mysql 代碼里寫(xiě)死了這種情況返回 1,他們也沒(méi)有解釋那就這樣吧。

3. 索引設(shè)計(jì)不合理 - 單值索引 vs 復(fù)合索引選擇錯(cuò)誤

案例 SQL

-- 刪掉原本的 idx_category 方便驗(yàn)證該案例
alter table products drop index idx_category,add index idx_price_category(price,category_id);

EXPLAIN SELECT * FROM products WHERE category_id = 5 AND price > 900;
+----+-------------+----------+------------+-------+--------------------+--------------------+---------+------+------+----------+-----------------------+
| id | select_type | table    | partitions | type  | possible_keys      | key                | key_len | ref  | rows | filtered | Extra                 |
+----+-------------+----------+------------+-------+--------------------+--------------------+---------+------+------+----------+-----------------------+
|  1 | SIMPLE      | products | NULL       | range | idx_price_category | idx_price_category | 6       | NULL | 1003 |    10.00 | Using index condition |
+----+-------------+----------+------------+-------+--------------------+--------------------+---------+------+------+----------+-----------------------+
1 row in set, 1 warning (0.00 sec)

-- 優(yōu)化索引順序
alter table products drop index idx_price_category,add index idx_category_price(category_id,price);

EXPLAIN SELECT * FROM products WHERE category_id = 5 AND price > 900;
+----+-------------+----------+------------+-------+--------------------+--------------------+---------+------+------+----------+-----------------------+
| id | select_type | table    | partitions | type  | possible_keys      | key                | key_len | ref  | rows | filtered | Extra                 |
+----+-------------+----------+------------+-------+--------------------+--------------------+---------+------+------+----------+-----------------------+
|  1 | SIMPLE      | products | NULL       | range | idx_category_price | idx_category_price | 11      | NULL |   45 |   100.00 | Using index condition |
+----+-------------+----------+------------+-------+--------------------+--------------------+---------+------+------+----------+-----------------------+
1 row in set, 1 warning (0.00 sec)

-- 改回去
alter table products drop index idx_category_price,add index idx_category(category_id);

執(zhí)行計(jì)劃問(wèn)題:雖然有復(fù)合索引idx_price_category(price, category_id),但范圍查詢price > 900導(dǎo)致索引截?cái)唷?/p>

優(yōu)化手段:調(diào)整索引順序?yàn)?code>(category_id, price)

六、案例 - 執(zhí)行計(jì)劃異常與查詢寫(xiě)法缺陷

1. 優(yōu)化器誤判(統(tǒng)計(jì)信息過(guò)時(shí),以 products 表為例)

案例 SQL

-- 查詢特定分類的商品,products表有idx_category索引
EXPLAIN SELECT * FROM products WHERE category_id = 15;

ANALYZE TABLE products; -- 更新統(tǒng)計(jì)信息后,執(zhí)行計(jì)劃會(huì)選擇idx_category索引

執(zhí)行計(jì)劃問(wèn)題:優(yōu)化器誤判(因統(tǒng)計(jì)信息過(guò)時(shí)),認(rèn)為全表掃描比走idx_category快(實(shí)際category_id=15的數(shù)據(jù)量很小)。

統(tǒng)計(jì)信息過(guò)時(shí)的原因:

  • 數(shù)據(jù)量劇變:批量插入 / 刪除 / 更新超過(guò)表總量的 10%。
  • 分布劇變:字段值的重復(fù)度、基數(shù)發(fā)生顯著變化(如從低重復(fù)到高重復(fù))。
  • 配置限制:關(guān)閉自動(dòng)更新、采樣精度不足。
  • 結(jié)構(gòu)變更:新增索引、修改字段類型后未同步更新統(tǒng)計(jì)信息。

統(tǒng)計(jì)信息過(guò)時(shí)會(huì)直接導(dǎo)致優(yōu)化器誤判執(zhí)行計(jì)劃(如全表掃描 vs 索引掃描、JOIN 順序錯(cuò)誤),因此需在上述場(chǎng)景中定期執(zhí)行 ANALYZE TABLE 或開(kāi)啟自動(dòng)更新(非極致寫(xiě)入場(chǎng)景)。

優(yōu)化手段:更新表統(tǒng)計(jì)信息,幫助優(yōu)化器正確判斷。

2. SELECT * 全字段查詢

案例 SQL

-- 查詢用戶的訂單記錄,使用SELECT * 導(dǎo)致讀取不必要字段
SELECT * FROM users u
JOIN orders o ON u.user_id = o.user_id
WHERE u.user_id = 100;


SELECT u.username, o.order_id, o.order_time, o.amount
FROM users u
JOIN orders o ON u.user_id = o.user_id
WHERE u.user_id = 100;

問(wèn)題users表的email、register_timeorders表的status等非必要字段被讀取,增加 IO 和內(nèi)存開(kāi)銷(尤其orders表數(shù)據(jù)量達(dá) 100 萬(wàn)條)。多余字段占用大量 IO 帶寬,拖慢查詢。

優(yōu)化手段:只查詢業(yè)務(wù)需要的字段

3. 復(fù)雜子查詢嵌套

案例 SQL

-- 統(tǒng)計(jì)2024 年每個(gè)用戶的訂單總金額(包含所有用戶,即使無(wú)訂單也顯示 0)
EXPLAIN ANALYZE SELECT
  u.user_id,
  u.username,
  -- 子查詢:統(tǒng)計(jì)該用戶2024年的訂單總金額(無(wú)訂單則返回NULL,用IFNULL轉(zhuǎn)為0)
  IFNULL((SELECT SUM(o.amount)
          FROM orders o
          WHERE o.user_id = u.user_id
            AND o.order_time >= '2024-01-01'
            AND o.order_time < '2025-01-01'), 0) AS total_2024_amount
FROM users u \G;
*************************** 1. row ***************************
EXPLAIN: -> Table scan on u  (cost=10098 rows=99776) (actual time=0.147..44.7 rows=100000 loops=1)
-> Select #2 (subquery in projection; dependent)
    -> Aggregate: sum(o.amount)  (cost=3.26 rows=1) (actual time=0.0155..0.0155 rows=1 loops=100000)
        -> Index lookup on o using idx_user_time (user_id=u.user_id), with index condition: ((o.order_time >= TIMESTAMP'2024-01-01 00:00:00') and (o.order_time < TIMESTAMP'2025-01-01 00:00:00'))  (cost=2.36 rows=9.02) (actual time=0.0133..0.0143 rows=4.74 loops=100000)

1 row in set, 1 warning (1.87 sec)


-- 覆蓋索引:包含所有查詢需要的字段
alter table orders drop index idx_user_time,add index idx_user_time_amount(user_id, order_time, amount);

EXPLAIN ANALYZE SELECT
  u.user_id,
  u.username,
  COALESCE(s.total_amount, 0) AS total_2024_amount
FROM users u
LEFT JOIN (
  SELECT
    user_id,
    SUM(amount) AS total_amount
  FROM orders
  WHERE order_time >= '2024-01-01'
    AND order_time < '2025-01-01'
  GROUP BY user_id  -- 預(yù)聚合訂單數(shù)據(jù)
) s ON u.user_id = s.user_id \G;
*************************** 1. row ***************************
EXPLAIN: -> Nested loop left join  (cost=1.01e+9 rows=10e+9) (actual time=706..892 rows=100000 loops=1)
    -> Table scan on u  (cost=10098 rows=99776) (actual time=0.175..39.2 rows=100000 loops=1)
    -> Index lookup on s using <auto_key0> (user_id=u.user_id)  (cost=113560..113562 rows=10) (actual time=0.00809..0.00831 rows=0.988 loops=100000)
        -> Materialize  (cost=113559..113559 rows=100725) (actual time=706..706 rows=98795 loops=1)
            -> Group aggregate: sum(orders.amount)  (cost=103487 rows=100725) (actual time=1.24..571 rows=98795 loops=1)
                -> Filter: ((orders.order_time >= TIMESTAMP'2024-01-01 00:00:00') and (orders.order_time < TIMESTAMP'2025-01-01 00:00:00'))  (cost=93205 rows=102819) (actual time=1.23..487 rows=473886 loops=1)
                    -> Covering index scan on orders using idx_user_time_amount  (cost=93205 rows=925560) (actual time=1.23..340 rows=1e+6 loops=1)

1 row in set (0.92 sec)

執(zhí)行計(jì)劃問(wèn)題:10 萬(wàn)次子查詢重復(fù)執(zhí)行 sum(),累積開(kāi)銷大。

優(yōu)化手段

  • 用覆蓋索引避免回表 IO:子查詢和主查詢均通過(guò) idx_user_time_amount 獲取所需字段(user_id、order_time、amount),無(wú)需回表。
  • 減少中間結(jié)果集大小(如預(yù)聚合):將 SUM(amount)的計(jì)算放在子查詢中,避免主查詢處理大量中間結(jié)果。

七、案例 - 分頁(yè)查詢

傳統(tǒng)分頁(yè)問(wèn)題(LIMIT 大偏移量)

案例 SQL:

-- 查詢第100001-100020條訂單(偏移量10萬(wàn))
EXPLAIN ANALYZE
SELECT * FROM orders
ORDER BY order_id  -- 按主鍵排序(默認(rèn)也可能按此排序)
LIMIT 100000, 20 \G;
*************************** 1. row ***************************
EXPLAIN: -> Limit/Offset: 20/100000 row(s)  (cost=1074 rows=20) (actual time=50.3..50.3 rows=20 loops=1)
    -> Index scan on orders using PRIMARY  (cost=1074 rows=100020) (actual time=2.84..46.1 rows=100020 loops=1)

1 row in set (0.05 sec)

核心問(wèn)題:

  • LIMIT 100000, 20 需要掃描前 100020 行數(shù)據(jù),然后丟棄前 100000 行,僅返回 20 行,99.98%的掃描是無(wú)效的。
  • 即使有排序,大偏移量仍會(huì)導(dǎo)致全表掃描+排序,IO 和 CPU 開(kāi)銷極高。

分頁(yè)優(yōu)化核心思路

優(yōu)化方案核心思路適用場(chǎng)景性能提升幅度
主鍵偏移量分頁(yè)用WHERE id > 偏移值替代LIMIT 偏移量按連續(xù)主鍵排序,有上一頁(yè) ID50-100 倍
條件+邊界值分頁(yè)用WHERE 條件 AND 字段 > 上一頁(yè)值按非主鍵排序,有過(guò)濾條件30-50 倍
延遲關(guān)聯(lián)+小范圍 LIMIT先查 ID 再回表,減少大偏移量數(shù)據(jù)量必須用大偏移量,無(wú)邊界值2-3 倍

優(yōu)化方案 1:主鍵偏移量分頁(yè)(利用連續(xù)主鍵)

通過(guò)已知的最后一條記錄的主鍵(如第 100000 行的order_id)作為條件,直接定位到起始位置,避免掃描偏移量?jī)?nèi)的所有行。

優(yōu)化后 SQL:

-- 假設(shè)第100000行的order_id為100000(可從上次查詢獲?。?
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE order_id > 100000  -- 直接定位到偏移量位置
ORDER BY order_id
LIMIT 20 \G;  -- 僅取20行
*************************** 1. row ***************************
EXPLAIN: -> Limit: 20 row(s)  (cost=99909 rows=20) (actual time=2.08..2.09 rows=20 loops=1)
    -> Filter: (orders.order_id > 100000)  (cost=99909 rows=498729) (actual time=2.08..2.08 rows=20 loops=1)
        -> Index range scan on orders using PRIMARY over (100000 < order_id)  (cost=99909 rows=498729) (actual time=2.07..2.08 rows=20 loops=1)

1 row in set (0.01 sec)

寫(xiě)法優(yōu)勢(shì):

  • 無(wú)需掃描偏移量?jī)?nèi)數(shù)據(jù):通過(guò)WHERE order_id > 100000直接定位到起始點(diǎn),掃描行數(shù)從 100020 降至 20 行。
  • 利用現(xiàn)有主鍵索引PRIMARY KEY (order_id)天然存在,無(wú)需額外索引,通過(guò)范圍查詢(range)快速定位。

適用場(chǎng)景:

  • 分頁(yè)按連續(xù)自增主鍵排序(如order_id)。
  • 前端分頁(yè)可記錄上一頁(yè)最后一條記錄的order_id(如“下一頁(yè)”按鈕傳遞該值)。

優(yōu)化方案 2:基于條件過(guò)濾的分段分頁(yè)(非主鍵排序)

當(dāng)分頁(yè)需要按非主鍵字段排序(如order_time),且有固定過(guò)濾條件(如status=2,已支付訂單),傳統(tǒng)LIMIT大偏移量同樣低效。

傳統(tǒng)寫(xiě)法問(wèn)題:需掃描前 1020 條符合status=2的記錄,丟棄前 10000 條,無(wú)效掃描多。

-- 按訂單時(shí)間排序,查詢第1001-1020條已支付訂單(偏移量1000)
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE status = 2
ORDER BY order_time
LIMIT 10000, 20 \G;
*************************** 1. row ***************************
EXPLAIN: -> Limit/Offset: 20/10000 row(s)  (cost=52012 rows=20) (actual time=23.9..23.9 rows=20 loops=1)
    -> Index lookup on orders using idx_status_time (status=2)  (cost=52012 rows=498729) (actual time=11.6..23.6 rows=10020 loops=1)

1 row in set (0.02 sec)

優(yōu)化寫(xiě)法(利用上一頁(yè)邊界值):

-- 假設(shè)上一頁(yè)最后一條記錄的order_time為'2025-05-01 10:00:00',order_id為50000
EXPLAIN ANALYZE
SELECT * FROM orders
WHERE status = 2
  AND order_time >= '2025-05-01 10:00:00'  -- 用上一頁(yè)時(shí)間作為起點(diǎn)
  AND NOT (order_time = '2025-05-01 10:00:00' AND order_id <= 50000)  -- 排除同時(shí)間的前序記錄
ORDER BY order_time, order_id  -- 時(shí)間+ID聯(lián)合排序,避免重復(fù)/遺漏
LIMIT 20 \G;
*************************** 1. row ***************************
EXPLAIN: -> Limit: 20 row(s)  (cost=58539 rows=20) (actual time=12.3..12.3 rows=20 loops=1)
    -> Index range scan on orders using idx_status_time over (status = 2 AND '2025-05-01 10:00:00' <= order_time), with index condition: ((orders.`status` = 2) and (orders.order_time >= TIMESTAMP'2025-05-01 10:00:00') and ((orders.order_time <> TIMESTAMP'2025-05-01 10:00:00') or (orders.order_id > 50000)))  (cost=58539 rows=130086) (actual time=12.3..12.3 rows=20 loops=1)

1 row in set (0.02 sec)

寫(xiě)法優(yōu)勢(shì):

  • 通過(guò)條件過(guò)濾替代偏移量:利用上一頁(yè)最后一條記錄的order_timeorder_id作為邊界,直接定位到下一頁(yè)起始位置,避免掃描前 10000 條記錄。
  • 聯(lián)合排序去重ORDER BY order_time, order_id確保排序唯一,避免同時(shí)間訂單重復(fù)或遺漏。
  • 復(fù)用現(xiàn)有索引idx_user_time (user_id, order_time)雖以user_id開(kāi)頭,但order_time作為第二列可輔助范圍查詢(配合status=2過(guò)濾)。

適用場(chǎng)景:

  • 非主鍵字段排序(如order_time)。
  • 有固定過(guò)濾條件(如status=2),可通過(guò)條件+邊界值快速定位。
  • 前端需記錄上一頁(yè)最后一條記錄的order_timeorder_id(如傳遞給“下一頁(yè)”接口)。

優(yōu)化方案 3:延遲關(guān)聯(lián)+小范圍 LIMIT(無(wú)邊界值時(shí))

當(dāng)無(wú)法獲取上一頁(yè)邊界值(如“跳轉(zhuǎn)至第 500 頁(yè)”),且必須使用大偏移量,可通過(guò)“先查 ID,再回表”減少無(wú)效數(shù)據(jù)傳輸。

優(yōu)化寫(xiě)法:

-- 子查詢先獲取目標(biāo)頁(yè)的order_id,再關(guān)聯(lián)回表
EXPLAIN ANALYZE
SELECT o.*
FROM orders o
JOIN (
  -- 子查詢僅查ID,數(shù)據(jù)量小,排序/偏移高效
  SELECT order_id
  FROM orders
  WHERE status = 2
  ORDER BY order_time
  LIMIT 10000, 20  -- 大偏移量?jī)H處理ID,而非全字段
) tmp ON o.order_id = tmp.order_id \G;
*************************** 1. row ***************************
EXPLAIN: -> Nested loop inner join  (cost=52567 rows=20) (actual time=7.07..7.15 rows=20 loops=1)
    -> Table scan on tmp  (cost=50058..50060 rows=20) (actual time=7.04..7.04 rows=20 loops=1)
        -> Materialize  (cost=50058..50058 rows=20) (actual time=7.04..7.04 rows=20 loops=1)
            -> Limit/Offset: 20/10000 row(s)  (cost=50056 rows=20) (actual time=7.01..7.01 rows=20 loops=1)
                -> Covering index lookup on orders using idx_status_time (status=2)  (cost=50056 rows=498729) (actual time=2.36..6.37 rows=10020 loops=1)
    -> Single-row index lookup on o using PRIMARY (order_id=tmp.order_id)  (cost=0.25 rows=1) (actual time=0.00447..0.00453 rows=1 loops=20)

1 row in set (0.01 sec)

寫(xiě)法優(yōu)勢(shì):

  • 減少排序/偏移的數(shù)據(jù)量:子查詢僅處理order_id(4 字節(jié)),比全字段(*包含多個(gè)字段,約 50 字節(jié))更輕量,排序和偏移效率更高。
  • 回表數(shù)據(jù)量小:僅對(duì) 20 條order_id回表查詢?nèi)侄?,避?10000 條無(wú)效記錄的全字段傳輸。

適用場(chǎng)景:

  • 必須使用大偏移量(如“跳轉(zhuǎn)至第 N 頁(yè)”)。
  • 表字段較多(*包含大量數(shù)據(jù)),通過(guò)先查 ID 減少中間數(shù)據(jù)傳輸。

以上就是MYSQL中慢SQL原因與優(yōu)化方法詳解的詳細(xì)內(nèi)容,更多關(guān)于MYSQL慢SQL的資料請(qǐng)關(guān)注腳本之家其它相關(guān)文章!

相關(guān)文章

  • 徹底刪除MySQL步驟介紹

    徹底刪除MySQL步驟介紹

    大家好,本篇文章主要講的是徹底刪除MySQL步驟介紹,感興趣的趕緊來(lái)看看吧,對(duì)你有幫助的話記得收藏一下,方便下次瀏覽
    2021-12-12
  • MySQL使用binlog日志進(jìn)行數(shù)據(jù)庫(kù)遷移和數(shù)據(jù)恢復(fù)

    MySQL使用binlog日志進(jìn)行數(shù)據(jù)庫(kù)遷移和數(shù)據(jù)恢復(fù)

    MySQL的二進(jìn)制日志是MySQL數(shù)據(jù)庫(kù)中非常關(guān)鍵的一個(gè)組件,主要用于記錄所有數(shù)據(jù)庫(kù)表結(jié)構(gòu)或表數(shù)據(jù)改變的操作語(yǔ)句,binlog是MySQL數(shù)據(jù)復(fù)制的基礎(chǔ),并且常常被用于數(shù)據(jù)恢復(fù),本文給大家介紹了MySQL使用binlog日志進(jìn)行數(shù)據(jù)庫(kù)遷移和數(shù)據(jù)恢復(fù),需要的朋友可以參考下
    2024-04-04
  • MySQL Range Columns分區(qū)的使用

    MySQL Range Columns分區(qū)的使用

    Range Columns分區(qū)是一種靈活的分區(qū)策略,允許基于列值的范圍將數(shù)據(jù)分到不同的分區(qū),本文主要介紹了MySQL Range Columns分區(qū)的使用,具有一定的參考價(jià)值,感興趣的可以了解一下
    2024-07-07
  • MySQL觸發(fā)器使用過(guò)程詳解

    MySQL觸發(fā)器使用過(guò)程詳解

    觸發(fā)器,就是一種特殊的存儲(chǔ)過(guò)程。觸發(fā)器和存儲(chǔ)過(guò)程一樣是一個(gè)能夠完成特定功能、存儲(chǔ)在數(shù)據(jù)庫(kù)服務(wù)器上的SQL片段。本文將通過(guò)簡(jiǎn)單的實(shí)力介紹一下觸發(fā)器的操作,需要的可以參考一下
    2023-03-03
  • Windows下安裝MySQL5.5.19圖文教程

    Windows下安裝MySQL5.5.19圖文教程

    這篇文章主要介紹了Windows下安裝MySQL5.5.19圖文教程,非常詳細(xì),對(duì)每一步都做了說(shuō)明,需要的朋友可以參考下
    2014-07-07
  • MySQL索引使用全程分析

    MySQL索引使用全程分析

    本文將介紹MySQL索引詳細(xì)使方法;需要的朋友可以參考下
    2012-11-11
  • mysql 無(wú)法聯(lián)接常見(jiàn)故障及原因分析

    mysql 無(wú)法聯(lián)接常見(jiàn)故障及原因分析

    這篇文章主要介紹了mysql 無(wú)法聯(lián)接常見(jiàn)故障及原因分析,本文是小編日常收集整理的,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下
    2017-11-11
  • mysql 5.6.23 winx64.zip安裝詳細(xì)教程

    mysql 5.6.23 winx64.zip安裝詳細(xì)教程

    這篇文章主要介紹了mysql 5.6.23 winx64.zip安裝詳細(xì)教程,非常不錯(cuò),具有參考借鑒價(jià)值,需要的朋友可以參考下
    2017-02-02
  • MySQL中sleep函數(shù)的特殊現(xiàn)象示例詳解

    MySQL中sleep函數(shù)的特殊現(xiàn)象示例詳解

    這篇文章主要給大家介紹了關(guān)于MySQL中sleep函數(shù)特殊現(xiàn)象的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者使用MySQL具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧
    2019-10-10
  • MySQL運(yùn)維神器pt-kill清理垃圾SQL的完整指南

    MySQL運(yùn)維神器pt-kill清理垃圾SQL的完整指南

    在日常開(kāi)發(fā)過(guò)程中,誰(shuí)沒(méi)遇到過(guò)這種窘境:業(yè)務(wù)高峰期突然 CPU 狂飆,大量連接堆積,排查發(fā)現(xiàn)是幾條失控的慢查詢?cè)诏偪裾加觅Y源,Percona Toolkit 中的 pt-kill 工具,正是解決這類問(wèn)題的手術(shù)刀,今天就帶大家從基礎(chǔ)用法到生產(chǎn)實(shí)戰(zhàn),徹底掌握這個(gè)運(yùn)維必備工具
    2026-03-03

最新評(píng)論

连云港市| 探索| 额尔古纳市| 桃源县| 四会市| 祁连县| 永昌县| 鄂托克前旗| 陆丰市| 杂多县| 九龙县| 拜城县| 吉安县| 桐柏县| 普兰县| 阳新县| 双桥区| 体育| 桂阳县| 怀安县| 扶风县| 和硕县| 广州市| 连云港市| 尼勒克县| 德令哈市| 日土县| 济源市| 红河县| 会东县| 抚远县| 绥宁县| 嘉义市| 常宁市| 通渭县| 方山县| 辽宁省| 富源县| SHOW| 怀远县| 新宾|