【MySQL 原理分析】之 Trace 分析 order by 的索引原理


一、背景

昨天早上,交流群有一位同學提出了一個問題。看下圖:
在這里插入圖片描述在這里插入圖片描述
我不是大佬,而且當時我自己的想法也只是猜測,所以並沒有回復那位同學,只是接下來自己做了一個測試驗證一下。

他只簡單了說了一句話,就是同樣的sql,一個沒加 order by 就全表掃描,一個加了 order by 就走索引了。

我們可以仔細點看一下他提供的圖(主要分析子查詢即可,就是關於表 B 的查詢,因為只有表 B 的查詢前后不一致),我們可以先得出兩個前提:

1、首先可以肯定的是,where 條件中的 mobile 字段是沒有索引的。因為沒有 order by 時,是全表掃描,如果 mobile 字段有索引,查詢優化器必定會使用 mobile 字段的索引。

2、其實重點不但在 order by,更重要的是在於 order by 后面跟着的字段是 表B 的主鍵 id。之所以判斷 id 為主鍵,是因為 explain 執行計划里看到使用了 PRIMARY 索引,即主鍵索引。

二、數據准備和場景重現

創建表 user:

CREATE TABLE `user` (
  `id` int(10) unsigned NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `age` int(11) DEFAULT NULL,
  `phone` varchar(11) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=100007 DEFAULT CHARSET=utf8;

准備數據:

看了一下截圖,數據量應該在10萬左右,我們也准備10萬數據,盡量做到一致。

delimiter ;
CREATE DEFINER=`root`@`localhost` PROCEDURE `iniData`()
begin
  declare i int;
  set i=1;
  while(i<=100000)do
    insert into user(name,age,phone) values('測試', i, 15627230000+i);
    set i=i+1;
  end while;
end;;
delimiter ;

call iniData();

執行 SQL ,查看執行計划:

explain select * from user where phone = '15627231000' limit 1;
explain select * from user where phone = '15627231000' order by id limit 1;

執行結果:

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1	SIMPLE	   user	 (Null)	    ALL	 (Null)	(Null) (Null) (Null) 99927	10	Using where

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1	SIMPLE	 user	 (Null)	    index   (Null)	   PRIMARY	4	1	10	Using where

我們可以看到,執行計划和那位同學的基本一致,都是第一條 SQL 全表掃描,第二條 SQL 是走了主鍵索引。

三、猜想和猜測着總結

只要加 order by 就走索引?

根據上面的執行計划來看,明顯這位同學的表達是不對的,更重要的是因為 order by 后跟着的字段是主鍵 id,所以才走了索引,走了主鍵索引

我們可以試試用 age 字段來排序,這時候肯定是沒有走索引的,因為我們壓根沒有為 age 字段沒有建立索引。

explain select * from user where phone = '15627231000' order by age limit 1;
id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1	SIMPLE	   user	 (Null)	    ALL	 (Null)	(Null) (Null) (Null) 99927	10	Using where; Using filesort

分析:

首先,我們看到 type 是 ALL,就是全表掃描,而且我們還留意到:Extra的值多了 using filesort,表明 MySQL 有文件排序的操作。

我們可以拿 order by age 和 order by id 的執行計划來對比一下。

1、explain 的 tepe 字段:

首先,type 不一樣,一個是 index,表明利用了索引樹;一個是 ALL,表明是全表掃描。

2、explain 的 Extra 字段:

第二,也是最重點的,它其實可以說明為何利用了主鍵索引。就是 Extra 字段。

先說明一下正常的排序,Extra 都會有 Using filesort 來表明使用了文件排序。

而明顯 order by id 是沒有這個,這是因為,索引樹本來就是一個帶有順序的數據結構,大家不了解的可以去看看 B+Tree 的介紹。查詢優化器正是利用了索引的順序性,使得 SQL 的執行計划走主鍵索引樹來去掉原本需要的排序。

之前的大白話 MySQL 學習總結中也提到過查詢優化器。SQL 的執行計划能有很多,並且結果是一樣的,但是為了提高性能,MySQL 的查詢優化器組件會為 SQL 制定一套最優的執行計划。

階段總結:

查詢優化器幫我們制定的最優計划是:充分利用主鍵索引的順序性,避免了全表掃描后還是需要排序操作。

當然了,我們不能自己只是根據現象做判斷,下面將利用 Trace 來查看優化器追蹤的信息,進一步的驗證我們的總結是沒問題的。

四、通過 Trace 分析來驗證

開啟和查看 Trace

-- 開啟優化器跟蹤
set session optimizer_trace='enabled=on';
select * from user where phone = '15627231000' order by id limit 1;
-- 查看優化器追蹤
select * from information_schema.optimizer_trace;

下面我們只看 TRACE 就行了。

{
  "steps": [
    {
      "join_preparation": {
        "select#": 1,
        "steps": [
          {
            "expanded_query": "/* select#1 */ select `user`.`id` AS `id`,`user`.`name` AS `name`,`user`.`age` AS `age`,`user`.`phone` AS `phone` from `user` where (`user`.`phone` = '15627231000') order by `user`.`id` limit 1"
          }
        ]
      }
    },
    {
      "join_optimization": { // 優化工作的主要階段
        "select#": 1,
        "steps": [
          //  .... 省略很多步驟
          {
            "reconsidering_access_paths_for_index_ordering": { // 重新考慮索引排序的訪問路徑
              "clause": "ORDER BY",
              "index_order_summary": {
                "table": "`user`",
                "index_provides_order": true,
                "order_direction": "asc",
                "index": "PRIMARY", // 排序的字段為主鍵 id,有主鍵索引
                "plan_changed": true, // 改變執行計划
                "access_type": "index"
              }
            }
          },
          {
            "refine_plan": [
              {
                "table": "`user`"
              }
            ]
          }
        ]
      }
    },
    {
      "join_explain": {
        "select#": 1,
        "steps": [
        ]
      }
    }
  ]
}

好了,在最后的那里,我們看到了查詢優化器幫我們使用了主鍵索引。

所以,我們上面的猜想是正確的,因為 where 條件后的 phone 字段沒有加上索引,所以到 order by id 時,查詢優化器發現可以利用主鍵索引所以來避免排序,所以最后就使用了主鍵索引。

那么,按照上面的說法,如果 phone 字段加上了索引,那么最后應該就是走 phone 的索引而不是主鍵索引了。而且,SQL 調優有那么一條建議:建議經常在 where 條件后出現的字段加上索引來提高查詢性能。

下面我們來繼續驗證一下我們的猜想。

五、關於 where 條件字段索引和 order by 字段索引的選擇

1、給字段 phone 增加索引:

在這里插入圖片描述

2、執行 SQL :

explain select * from user where phone = '15627231000' order by id limit 1;

3、結果:

我們可以看到,最后查詢優化器判斷 phone索引 比 主鍵索引 更能提高性能,所以使用了 phone 的索引。

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1	SIMPLE	 user	 (Null)	    index index_phone index_phone	36	1	100	Using index condition

4、Trace進一步驗證:

最后,我們可以看到,查詢優化器否定了使用主鍵索引,不改變之前的執行計划。

-- 開啟優化器跟蹤
set session optimizer_trace='enabled=on';
select * from user where phone = '15627231000' order by id limit 1;
-- 查看優化器追蹤
select * from information_schema.optimizer_trace;

Trace 分析:

{
  "steps": [
    {
      "join_preparation": {
        "select#": 1,
        "steps": [
          {
            "expanded_query": "/* select#1 */ select `user`.`id` AS `id`,`user`.`name` AS `name`,`user`.`age` AS `age`,`user`.`phone` AS `phone` from `user` where (`user`.`phone` = '15627231000') order by `user`.`id` limit 1"
          }
        ]
      }
    },
    {
      "join_optimization": {
        "select#": 1,
        "steps": [
          //  .... 省略很多步驟
          {
            "considered_execution_plans": [
              {
                "plan_prefix": [
                ],
                "table": "`user`",
                "best_access_path": {
                  "considered_access_paths": [
                    {
                      "access_type": "ref",
                      "index": "index_phone",
                      "rows": 1,
                      "cost": 1.2,
                      "chosen": true
                    },
                    {
                      "access_type": "range",
                      "range_details": {
                        "used_index": "index_phone" // 使用 phone 的索引
                      },
                      "chosen": false,
                      "cause": "heuristic_index_cheaper"
                    }
                  ]
                },
                "condition_filtering_pct": 100,
                "rows_for_plan": 1,
                "cost_for_plan": 1.2,
                "chosen": true
              }
            ]
          },
          {
            "attaching_conditions_to_tables": {
              "original_condition": "(`user`.`phone` = '15627231000')",
              "attached_conditions_computation": [
              ],
              "attached_conditions_summary": [
                {
                  "table": "`user`",
                  "attached": null
                }
              ]
            }
          },
          {
            "clause_processing": {
              "clause": "ORDER BY",
              "original_clause": "`user`.`id`",
              "items": [
                {
                  "item": "`user`.`id`"
                }
              ],
              "resulting_clause_is_simple": true,
              "resulting_clause": "`user`.`id`"
            }
          },
          {
            "added_back_ref_condition": "((`user`.`phone` <=> '15627231000'))"
          },
          {
            "reconsidering_access_paths_for_index_ordering": { // 重新考慮索引排序的訪問路徑
              "clause": "ORDER BY",
              "index_order_summary": {
                "table": "`user`",
                "index_provides_order": true,
                "order_direction": "asc",
                "index": "index_phone",
                "plan_changed": false  // 不改變執行計划
              }
            }
          },
          {
            "refine_plan": [
              {
                "table": "`user`",
                "pushed_index_condition": "(`user`.`phone` <=> '15627231000')",
                "table_condition_attached": null
              }
            ]
          }
        ]
      }
    },
    {
      "join_explain": {
        "select#": 1,
        "steps": [
        ]
      }
    }
  ]
}

六、最后總結

到這里,分析就結束了,我們可以得出一個結論,當然了,只是基於上面的實驗所得:

1、SQL 帶有 order by :

  • order by 后面的字段有索引:

    • where 條件后面的所有字段都沒索引,則使用 order by 后面的字段的索引。

    • where 條件后面有字段帶有索引,則使用 where 條件對應的字段的索引。

  • order by 后面的字段沒有索引:

    • where 條件后面的所有字段都沒索引,則全表掃描。
    • where 條件后面有字段帶有索引,則使用 where 條件后面的字段的索引。

2、SQL 不帶 order by:

  • where 條件后面的所有字段都沒索引,則全表掃描。

  • where 條件后面只要有字段帶索引,則使用該字段對應的索引。

最后我們也可以得出一個絕對的結論:查詢優化器是真的好使,哈哈哈!

七、題外話

其實上面的實驗需要大家對 MySQL 的索引原理有一定的了解,但是不用特別深。

如果大家感興趣的話,可以關注一下我現在寫的 【大白話系列】MySQL 學習總結 這一系列的文章,我會將自己學習 MySQL 后的學習總結分享在這里。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM