1. 程式人生 > 程式設計 >【重溫msql】2、一條 sql 在 MySQL 中是如何執行的?

【重溫msql】2、一條 sql 在 MySQL 中是如何執行的?

我們的程式通過連線池向 MySQL 傳送了一條sql語句,MySQL 就按照要求給我們返回了正確的結果,有時我們不免好奇,這個過程中究竟發生了什麼?它是如何工作的?有什麼方法可以加速我們的查詢?需要解答這些疑問,首先我們需要對 MySQL 的架構體繫有所瞭解。 下圖為 MySQL Server 的體系結構:

MySQL 的層次

MySQL Server 在邏輯上一共分為三層:連線層解析層儲存引擎層。各個層次負責不同的分工:

  • 連線層,主要包含連線池,負責與程式的連線、授權認證、快取等等;
  • 解析層,主要包括了 Sql Interface、解析器、優化器以及快取,主要負責解析使用者傳遞的sql,並對 sql 的檢索過程進行優化形成執行方案。
  • 儲存引擎層,主要包括 MySQL 的各種引擎以及所產生的各種資料、Schema、索引以及日誌檔案儲存,儲存引擎層提供了相關的資料查詢檢索介面與上層進行互動,依據上層生成的執行方案查詢、儲存資料。

Sql 執行過程

那麼 sql 從外部聯結器傳送給 MySQL Server 後,具體的執行過程是怎麼樣的呢?

  • 1、連線池鑑權,判定當前使用者是否有當前操作的許可權
  • 2、如若為查詢操作則看快取是否有資料,快取命中則直接返回
  • 3、傳遞給解析器進行sql詞法分析
  • 4、優化器依據詞法分析結果,決定後續處理方式,生成執行計劃
  • 5、依據上面生成的執行計劃,呼叫儲存引擎 api
  • 6、如若為刪改資料或該表等操作,如若開啟 binlog 則在事務最終提交前寫入 binlog 日誌
  • 7、返回相關處理結果

Query Cache

從上圖我們可以看到,在真正執行 sql 解析前,會有一次查詢快取的過程,目的是為了加速資料查詢的速度,這對於讀多寫少的場景看起來是在合適不過了,但實際上這個快取真的有效嗎?《Query Cache,看上去很美》這篇文章中提到了一個生產資料:

一個更新頻繁的BBS系統。下面是一個實際執行的論壇資料庫的狀態引數:
QCache_hit 5280438
QCache_insert 8008948
Qcache_not_cache 95372
Com select 8104159
可以看到,資料庫一共往QC中寫入了約800W次快取,但是實際命中的只有約500W次。也就是說,每一個快取的使用率約為0.66次。

從快取命中率上來看0.66的快取命中率實在是太低了,那為何會有這個問題呢?我想應該有如下幾個方面的原因:

  • 1、Query Cache 是按照 sql 進行快取的,需要 sql 完全一致才能命中
  • 2、Query Cache 使用場景有限在有些場景如子查詢、函式、schema、臨時表等等下將會失效
  • 3、MySQL中一個表中的任意一條資料更新都會引發快取失效

由上面的分析可以看出,query cache的開啟對效能提升有限,因此很多DBA 都建議生產環境中禁用該功能,MySQL 本身也在5.7.20將查詢快取標記為廢棄,並在 MySQL 8.0 以上版本中移除了該功能。如下是MySQL官方檔案中所提到的:

NOTE
The query cache is deprecated as of MySQL 5.7.20,and is removed in MySQL 8.0.

快取具體機制可以檢視這篇文章《Mysql 快取機制》,作者寫的很詳細。

優化器與執行計劃

在具體執行獲取資料前,mysql會對sql的執行方案進行優化,通過優化器得到最終的執行計劃。所謂的執行計劃指的就是MySQL將如何執行一條Sql語句,包括Sql查詢的先後順序、使用索引、索引長度、排序等資訊。有個時候我們的程式可能載入資料非常慢,也有可能我們明明建立了索引,卻並沒有什麼效果,通過執行方案我們可以知道mysql到底進行那些操作,這對於我們的 sql 優化有著重要意義。那麼我們要如何檢視我們的sql最終的執行計劃是什麼呢?

檢視執行計劃語法

explain 你的sql
複製程式碼

通過上述方式即可獲得檢視你的sql的執行計劃。你會獲得例如這樣的結果:

執行計劃包含資訊

列名 作用
id 表示執行計劃中的各條sql記錄的查詢順序
select_type sql查詢型別
table 執行計劃中的該條查詢使用了哪個表
partions 分割槽,在使用分割槽表的時候會用到
type 查詢型別,通過該欄位可以瞭解查詢使用索引的型別
possible_keys 可能可以用到的索引
key 實際使用的索引
key_len 索引長度
ref 表示上述表的連線匹配條件,即哪些列或常量被用於查詢索引列上的值
rows 大致反映所需要讀取的行數,基於統計實現,非真實查詢資料量
filtered 大致反映所需過濾的資料,基於統計實現,非真實查詢資料量
extra 其他資訊,如使用了索引、使用了檔案排序等等

id 列詳解

id 表明執行計劃的執行順序。共有如下三種可能性:

  • 1、id相同,則從上到下依次執行
  • 2、id不同,id較大的語句先執行,一般發生在子查詢的情況下
  • 3、上述兩種情況同時存在,id較大的語句先執行,id相同的從上到下依次執行

select_type 詳解

select_type select的型別。

型別 解析
SIMPLE 簡單查詢(不使用 union 或子查詢的查詢)
PRIMARY 主查詢的意思,複雜查詢中最外層的 select
UNION union查詢中的第二個及第二個以後的查詢標記為union
DEPENDENT UNION union查詢中的第二個及第二個以後的查詢標記為union,需要依賴外層查詢
UNION RESULT union查詢結果集
SUBQUERY 子查詢中的第一個查詢標記為subquery
DEPENDENT SUBQUERY 子查詢中的第一個查詢標記為subquery,需要依賴外層查詢
DERIVED 衍表,在From後where前的子查詢
MATERIALIZED 物化子查詢,如果子查詢執行一次即可以得到結果,即子查詢的結果是穩定的,則這樣的子查詢可以被快取起來,多次使用。快取到記憶體中,如果記憶體中放不下,則會寫外存。在MySQL中,這個快取對應的是臨時表
UNCACHEABLE SUBQUERY 無法快取的子查詢,對於第一行必需重新執行
UNCACHEABLE UNION 不可快取的子查詢裡 UNION 中第二個及之後的 SELECT

table 詳解

table 表明查詢的是哪張表,有如下三種情況。

型別 解析
unionM,N 引用id為M和N UNION後的結果集
derivedN 引用id為N的結果派生表的結果集,派生表可以是一個結果集,例如派生自FROM中子查詢的結果
subqueryN 引用id為N的物化子查詢結果集

partions 詳解

表明本次查詢匹配結果所在分割槽標的那個分割槽。

type 詳解

type 為查詢的連線型別。執行效率依次為system,const,eq_ref,ref,fulltext,ref_or_null,index_merge,unique_subquery,index_subquery,range,index,ALL

型別 解析
system 一個表裡面只有一條記錄的情況
const 常量,表裡面至多隻有一條資料匹配的情況,如按照主鍵或唯一索引查詢
eq_ref 多表join時,對於來自前面表的每一行,在當前表中只能找到一行。當主鍵或唯一非NULL索引的所有欄位都被用作join聯接時會使用此型別
ref 對於來自前面表的每一行,在此表的索引中可以匹配到多行。若聯接只用到索引的最左字首或索引不是主鍵或唯一索引時,使用ref型別
fulltext 全文索引,優先順序很高,若全文索引和普通索引同時存在時,mysql不管代價,優先選擇使用全文索引
ref_or_null 同ref,增加了null判斷
index_merge 表明查詢用到了兩個以上的索引,最後取交集或者並集,常見and ,or的條件使用了不同的索引,官方排序這個在ref_or_null之後,但是實際上由於要讀取多個索引,效能可能大部分時間都不如range
unique_subquery 用於where中的主鍵in形式子查詢,子查詢返回不重複值唯一值,可以完全替換子查詢,效率更高。
index_subquery 類似於unique_subquery。適用於非唯一索引,可以返回重複值。
range 索引範圍查詢,如 =,<>,>,>=,<,<=,IS NULL,<=>,BETWEEN,LIKE,or IN()操作符
index 同all全表查詢差不多,唯一區別是用到了索引樹掃描
ALL 全表掃描

possible_keys 詳解

possible_keys表示可能可以用到的索引。

keys 詳解

表明實際使用的索引。

key_len 索引長度

key_len顯示了MySQL使用索引的長度,對於聯合索引,通過這個欄位可以看出MySQL具體使用了聯合索引中那些部分的索引。 那麼索引長度是如何計算的呢?

普通欄位索引長度 = 索引列長度 + 欄位允許為空需要加1
字元欄位索引長度 = 單字元所佔位元組*索引列長度 + 欄位允許為空需要加1 + 動態列還需再加2
複製程式碼
  • 1.MySQL 如果索引列可以為null,則需要加1個位元組,經過測試在MySQL5.7上如果資料集中沒有改欄位沒有為null的資料,則MySQL 會進行優化,不會加上1。
  • 2、對於普通非字元欄位,即本身的長度
  • 對於字元欄位,則與字符集有關,長度為單個字元所佔位元組*長度,可變長字串還需增加2個位元組
    舉個例子,例如有這麼一個表,對idNo與name聯合建立了一個索引:
CREATE TABLE `t1` (
  `id` int(11) NOT NULL AUTO_INCREMENT,`name` varchar(50) DEFAULT NULL,`idNo` char(18) DEFAULT NULL,PRIMARY KEY (`id`),KEY `INX_IDNO_NAME` (`name`,`idNo`)
) ENGINE=InnoDB AUTO_INCREMENT=5 DEFAULT CHARSET=utf8mb4;
複製程式碼

那麼 INX_IDNO_NAME 索引的長度為:18×4 + 1 + 50×4 + 1 + 2 = 276

ref 詳解

連線匹配條件,即哪些列或常量被用於查詢索引列上的值。

rows 詳解

大致反映所需要掃描的行數,基於統計實現,非真實查詢資料量。這個值非常直觀顯示 SQL 的效率好壞,原則上 rows 越少越好。

filtered 詳解

大致反映所需過濾後的資料,基於統計實現,非真實查詢資料量。

extra 詳解

extra 顯示了MySQL處理查詢的附加資訊,通常有如下的資訊:

型別 解析
distinct 使用了distinct關鍵字
Using filesort 當 Extra 中有 Using filesort 時,表示 MySQL 需額外的排序操作,不能通過索引順序達到排序效果
Using index 使用了覆蓋索引
Using where 使用了嚴格條件匹配,除了特定需要獲取或掃描全表資料的情況,一般這種情況都可能是查詢問題
Using temporary 查詢有使用臨時表,一般出現於排序,分組和多表 join 的情況,查詢效率不高

索引

索引的實質為資料結構,MySQL的索引與具體的引擎實現有關,MySQL 中索引從資料結構可以分為:

  • B-Tree 索引,使用b+ tree
  • Hash 索引,只在Memory 引擎中實現
  • 全文索引,底層也是基於b+ tree,基於倒排索引實現
  • R-Tree 索引,空間索引

從索引組織上來劃分:

  • 聚集索引
  • 非聚集索引

從邏輯角度上來劃分:

  • 主鍵索引
  • 唯一索引
  • 單列索引
  • 複合索引
  • 空間索引

SQL優化

一般來說,常見引發索引失效的情況有如下幾種:

  • 索引列上做了操作如(計算、函式、自動/手動型別轉換 如字串沒有加''號)
  • 索引列使用了 != 或 <> 判斷
  • 索引欄位使用了 is null / is not null 的非空判斷
  • 索引列使用了 like 以萬用字元開頭如'%關鍵字'
  • 索引列使用了or

那麼我們要如何對SQL進行優化呢?原則是什麼?

整體上將,MySQL 的優化原則應該遵循:

  • 儘可能的最大化利用索引
  • 減少資料掃描量
  • 減少回表查詢次數

從表結構方面:

  • 欄位型別選擇遵循小而簡單的原則,在滿足功能的前提下對於索引欄位列我們需要優先選擇佔用空間較小的型別,可以使用數值的就不用字串,整體上來講,字串比較要比數值的比較開銷大。
  • 對需要頻繁進行連表查詢的欄位在變化情況不大並且在顯示上沒有大的影響的情況下考慮建立冗餘。
  • 只建立合適的索引,對於長期不用的索引考慮刪除。
  • 對於大表的ALTER TABLE非常耗時,可用新的結構建立一個張空表,從舊錶中查出所有的資料插入新表,然後再刪除舊錶的方式處理。
  • 對於索引來說,索引的長度是有限的。對於MyISAM來說索引最大長度為1000,而對於InnoDB來說則只有767。對於單列索引的建立原則上應該儘可能的短。而對於聯合索引來說,我們可以把使用頻繁的索引放在最左邊,使用時遵循最左原則,雖然MySQL優化器能夠一定程度上識別並使用正確的索引。
  • 對於資料比較重複的欄位如性別這種情況索引效率太低就沒有必要建立索引

從SQL優化方面:

  • 使用覆蓋索引,儘量減少查詢欄位數量
  • 使用聯合索引,減少索引數量,提高索引複用
  • 避免多個範圍條件,MySQL無法同時使用多個索引來優化範圍條件
  • 使用索引順序來優化排序,因為排序與索引順序不一致的話會使用file sort 帶來額外開銷
  • 使用 union 或子查詢來優化 or
  • 優化limit,大資料量下的limit 如 limit 1000000,20 這種情況下 MySQL 需要掃描前1000020條記錄,並將前1000000 條記錄丟棄,只返回20條記錄。這種情況可以通過子查詢來延遲掃描,儘可能的減少掃描的資料。 比如:
select id,name,age from user inner join ( 
    select id from user  limit 10000,10
) b using (id)
複製程式碼

感謝