MySQL 優化筆記:Explain、Profiles、Performance_Schema、Trace 優化器追蹤

在這裏插入圖片描述

一、前言

  • 概念:關係型數據庫發展到今天,並且隨着雲計算的普及,對於基礎運維的需求有了顯著的減少,反而對於應用性能的優化變爲重中之重。根據統計 80 % 的響應時間問題都是應用性能差的 SQL 語句造成的。今天讓們一起來學習 MySQL 優化技術,收穫的不止有優化。
  • 優化公式:T = S / V 其中 T 表示查詢所需要的時間 S 表示所需要的資源 V 表示單位時間內資源的使用量。那麼我們可以通過減少 S 增加 V 兩方面入手就可以達到優化目的。其中減少 S 是優化過程中的重頭戲,也就是減少 IO 增加 S 就是提高資源利用率,充分利用軟硬件資源。
  • 目錄介紹:今天我們從設計表原則開始,然後還會聊一些 MySQL 中的極限 和 innodb 啓動過程相當於擴展知識,再着就是優化需要了解的基礎知識,然後就是如何定位到問題,再者就是如何解決問題。

二、表設計規範

以下爲建表規範:

  • Innodb 引擎不使用外鍵約束,使用程序層面來維護數據的一致性。
  • 存儲精確浮點數必須使用 DECIMAL 來代替 FLOAT 和 DOUBLE 定點型字符串格式存儲不會出現精度丟失的問題。
  • 整型定義中無需定義顯示寬度,比如:使用 INT,而不是 INT(4) 前輩經驗。
  • 不建議使用 ENUM 類型,可以使用 TINYINT 代替使用。會插入無效值等一系列問題。
  • 存儲年時使用 YEAR(4),不使用 YEAR(2)。YEAR(2) 已被刪除,會自動轉換爲 YEAR(4)。
  • 建議字段都定義爲 NOT NULL,否則經常會出現奇怪的問題,比如 unqiue 失效(後面有案例)。
  • 儘可能不要使用 TEXTBLOB類型,如果必須使用,建議將過大的字段或不常用的描述型較大字段拆分到其它表中;另外禁止使用數據庫存儲圖片。

以下爲命名規範:

  • 庫、表、字段全部採用小寫。

  • 庫名、表名、字段名、索引名均使用小寫字母,並以 “_” 分割。

  • 庫名、表名、字段名建議不超過 12 個字符。

  • 庫名、表名、字段名需要見名知意,即可不使用註釋。

  • 對象命名規範總結:

    對象中文名 對象英文名 命名規範
    視圖 view view_
    函數 function func_
    存儲過程 procedure proc_
    觸發器 trigger trig_
    普通索引 index idx
    唯一索引 unique index uniq_
    主鍵索引 primary index pk_

以下是索引規範:

  • 複合索引中的字段數建議不要超過 5 個。
  • 單張表的索引數控制在 5 個以內。
  • innodb 表建議有主鍵,主鍵列字段不要太大,會影響輔助索引佔用的空間
  • 創建複合索引時,優先選擇性能高的字段作爲驅動表
  • UPDATE、DELETE 語句需要根據 WHERE 條件創建索引。
  • 不建議使用 % 前綴的模糊查詢,例如 LIKE %fantasy 會導全表掃描。
  • 合理使用索引覆蓋,可以避免回表查詢
  • 避免在字段中使用函數,會導致查詢時索引失效。

以下索引失效場景:

  • 查詢結果是原表中的大部分數據 15~30%以上,優化器就認爲沒有必要走索引了,與預讀能力和一些參數有關。
  • 對於表的內容頻繁更新的情況下,統計信息不準確,可能會出現索引失效,一般是刪除重建
  • 隱式轉換導致索引失效,典型的例子是如果定義的數據類型爲字符串類型,然後查詢時使用數字傳入到 mysql 這時 mysql 可能會進行隱式轉換,索引就會失效。

三、MySQL 中極限及擴展

MySQL中的一些極限值

  • 一張表的字段最多爲 1017 個,多出會報錯。
  • 輔助索引的個數:官方源碼說明 輔助索引最多爲 64 個。
  • 複合索引的字段數:複合索引字段數最多爲 16 個。
  • join 極限值:每個表 join 的最大個數是61個。

以下屬於擴展知識 innodb 的啓動過程(擴展知識可忽略):

  • innobase_initMySQL在啓動過程中會進行引擎初始化,innodb 引擎初始化的入口就是 innobase_init 函數。
  1. Innobase_start_or_create_for_mysql主要完成 Innodb 的啓動過程,會初始化一些系統模塊,如下:
    1. srv_general_init:初始化同步控制系統,內存管理系統,日誌恢復變量等。
    2. srv_inif:初始化後臺線程同步控制系統。
  2. buf_pool_initinnodb buffer pool 初始化,會根據日誌文件 innodb_buffer_pool_size 和 innodb_buffer_pool_instances 中的數據爲依據。
  3. log_init初始化日誌系統,初始化 innodb 所有的日誌系統。
  4. Io_handler_thread創建 IO 異步線程,作用是等待 buffer pool 發出的讀寫指令,讀寫相關的數據頁面。
  5. recv_sys_init初始化日誌恢復系統,當數據庫異常宕機時,會根據 redo、undo 日誌進行解析內容和恢復。
  6. open_or_create_data_files創建或者打開系統數據文件 ibdata 如果文件存在則打開並讀取相關信息,如果不存在則會創建一個新的 ibdata 文件,此時相當於初始化一個新的數據庫實例。
  7. srv_undo_tablespaces_init初始化 undo 文件並加載到文件系統中,默認存儲在 Ibdata 文件中,5.6 版本以後可以 innodb_undo_tablespaces 來設置 undo 與 Ibdata 文件分開存儲。
  8. New or Open Database:
    1. New Database
      1. fsp_header_init:初始化文件,在系統文件 ibdata 中分配空間,便於存儲一些系統模塊、回滾段和數據字典等信息。
      2. trx_sys_create_sys_pages:上面函數已經開闢了存儲空間,接下來直接進行事務系統存儲初始化等操作。
      3. dict_create:記錄存儲數據字典 ROWID、表ID、索引ID等,並將系統表加載到內存中。
    2. Open Database
      1. recv_recovery_from_checkpoint_start:掃描日誌文件,分析日誌的完整性按照頁面號歸類並作 REDO 操作。
      2. dict_boot:將所有的系統表加載到內存中。
      3. trx_sys_init_at_db_start:初始化事務系統,將所有回滾段中需要處理的事務加載到內存中,爲“回滾”操作做準備。
      4. recovery_finish:將 trx_sys_init_at_db_start 加載需要回滾的事務進行回滾。
  9. buf_dblwr_create創建兩次寫緩存 double_write
  10. srv_master_thread創建 master 線程,主要功能是每隔一秒進行一次後臺循環,所做的事情主要是後臺刪除廢棄表、檢測日誌空間是否足夠、日誌刷盤、做檢測點。
  11. srv_purge_coordinator_thread與下一個函數 srv_worker_thread 配合實現 PURGE 操作。
  12. srv_worker_thread與上一個函數 srv_purge_coordinator_thread 配合實現 PURGE 操作
  13. buf_flush_page_cleaner_thread在後臺每隔一秒鐘,試圖刷一次 Buffer 頁面

四、優化基礎

以下是索引基礎知識:

  • 聚簇索引:建表時要指定主鍵列,InnoDB 會將主鍵作爲聚簇索引列,如果沒有指定主鍵,引擎會選擇唯一值的列作爲聚簇索引。如果以上都沒有,系統生成一個內部的 rowid 做爲聚簇索引。有了聚簇索引後,插入的數據行,在同一個區內,都會按照 ID 值的順序,有序的在磁盤中存儲數據。
  • 輔助索引:如果有經常需要查詢的字段並且該字段爲非聚簇索引,那麼我們可以人爲創建索引。使用輔助索引檢索時,會通過葉節點找到聚簇索引的鍵,然後通過聚簇索引找到完整的數據記錄,過程稱爲回表查詢。如果輔助索引的樹高度爲 M,而聚簇索引的高度爲 N,那麼最終會進行 M+N 次 IO 才能定位到最終的數據。如果輔助索引可以完全覆蓋查詢那麼就不會進行回表查詢

以下是兩個需要注意的問題:

  • 主鍵隱患:剛纔我們在表設計規範中學習到,每張表都要創建主鍵,爲什麼呢?除了規範,從存儲方式來講,在 innodb 引擎中,表都是按照主鍵的順序存放的,這就是聚簇索引也叫索引組織表 IOT 而且 innodb 引擎也對主鍵有自己的維護機制,剛纔也談到了二級索引(輔助索引) 從存儲角度來講,二級索引默認包含主鍵列,如果主鍵太長,會使得二級索引很佔空間。

  • 唯一性索引產生的重複數據:
    創建一張測試表,只有兩個字段,此時沒有任何約束,給 id 字段添加 unique 唯一索引,驗證不可以存儲重複 id 然後刪除 unique 約束,再 id 和 name 創建複合唯一索引。

    在這裏插入圖片描述
    目前兩個字段已經有了複合唯一性索引,

    insert into test_unique values (1, null),(1, null),(1, null);
    
    id name
    1 NULL
    1 NULL
    1 NULL
    1 fantasy

    我們發現唯一約束失效了,如果多字段情況下很有可能會產生類似的重複數據,而這一切的罪魁禍首就是 null

關於 Null 的一些問題:

  • Null 和空字符串是兩個完全獨立的對象,儘管表現方式都一樣,都是沒有數據。
  • count(*) 和 count(id) 兩個輸出的結果不同,區別就是前者會統計 null 後者不會,但是兩者的性能相同。

SQL 的解析過程:

  • 第一步,對 SQL 文法檢查,查看是否有文法錯誤,比如 from、select 拼寫錯誤;
  • 第二步,對象檢測,在數據字典中校驗 SQL 語句涉及的對象是否存在;
  • 第三步,將對象名稱進行轉換,將同義詞轉換爲對象;
  • 第四步,檢查語句的用戶是否具有訪問對象的權限;
  • 第五步,生成執行計劃。
    在這裏插入圖片描述

SQL語句執行順序

  • 關於執行順序我們可以使用非常簡單的方式來驗證,就是寫一個 SQL 語句可以在關鍵字部分設置錯誤語法根據報錯信息來推理。感興趣可以試一試,在這裏不多描述。
  • 經過測試 SQL語句執行順序是這樣的:
    1. FROM
    2. WHERE
    3. GROUP BY
    4. HAVING
    5. ORDER BY
    6. SELECT
    7. LIMIT
  • 我們可以看到解析過程和執行順序差距還是蠻大的,歸根結底,兩者做的事情不一樣,解析是在 SQL 文本的解析,而執行順序則是在解析的基礎上做數據的提取

SQL 執行計劃:

  • 下面介紹的是 MySQL 中的 explain 可以輸出 SQL 的執行計劃。

  • 爲什麼要使用 SQL 執行計劃?
    答:分析優化器內置 cost 計算模型評估計算,最終選擇後的執行 SQL 語句的順序狀態

  • 查詢SQL語句執行計劃方式 explain + SQL 語句

    在這裏插入圖片描述

  • 我們可以看到輸出的執行計劃有很多字段,下面就幾個關鍵字段來解讀:

    字段 功能
    id 執行計劃中查詢的序列號
    select_type 語句所使用的查詢類型
    table 查詢時使用的表
    type 查詢類型(全表/索引)
    possible_keys 預測會使用的索引
    key 實際使用的索引
    key_len 索引覆蓋長度
    rows 本次查詢掃描行數
    Extra 額外信息
  • id 執行順序解讀
    在這裏插入圖片描述

    id select_type table partitions type
    1 SIMPLE t NULL ALL
    1 SIMPLE tc NULL ALL
    1 SIMPLE cu NULL ALL

    上面是一條多表連接查詢語句,可以看到 id 都爲 1 按照 MySQL 官網介紹 id 值相同時自上而下順序執行

    在這裏插入圖片描述

    id select_type table partitions type
    1 PRIMARY tc NULL ALL
    2 SUBQUERY t NULL ALL
    3 SUBQUERY c NULL ALL

    上圖是一條包含子查詢的 SQL 語句此時 id 都不相同,根據官網介紹 id 值不同時id 值越大越先執行

    那麼結論來了:

    1. id相同時,執行順序由上至下;

    2. 如果是子查詢,id的序號會遞增,id值越大優先級越高,越先被執行;

    3. id如果相同,可以認爲是一組,從上往下順序執行;在所有組中,id值越大,優先級越高,越先執行;

  • select_type SQL 語句都查詢類型,歸結以下幾種:

    1. SIMPLE:簡單的SELECT,不使用UNION或子查詢等;

    2. PRIMARY:子查詢中最外層查詢,查詢中若包含任何複雜的子部分,最外層的select被標記爲PRIMARY;

    3. UNION:UNION中的第二個或後面的SELECT語句;

    4. DEPENDENT UNION:UNION中的第二個或後面的SELECT語句,取決於外面的查詢;

    5. UNION RESULT:UNION的結果,union 語句中第二個select開始後面所有 select;

    6. SUBQUERY:子查詢中的非最外層;

    7. DEPENDENT SUBQUERY:子查詢中的第一個SELECT,依賴於外部查詢;

    8. DERIVED:派生表的 SELECT, FROM 子句的子查詢;

    9. UNCACHEABLE SUBQUERY:一個子查詢的結果不能被緩存,必須重新評估外鏈接的第一行。

    10. MATERIALIZED :物化子查詢,在SQL執行過程中,第一次需要子查詢結果時執行子查詢並將子查詢的結果保存爲臨時表 ,後續對子查詢結果集的訪問將直接通過臨時表獲得。

  • 接下來介紹的是 type 字段屬於 SQL 執行的幾種級別,分別爲 const(system) > eq_ref > ref > range > index > all 可以看到性能屬於 const 聚簇索引等值查詢性能最好,最差的爲 ALL 全表掃描,解下來我們一起了解在什麼情況下可以達到什麼級別

    index:全索引掃描 將所有索引掃描一遍

    -- tno 字段有索引
    explain select tno from teacher;
    
    id select_type table partitions type
    1 SIMPLE teacher NULL index

    range:索引範圍查詢(>, <, >=,<=,like, in, or, between, and)

    -- tno 字段有索引
    explain select * from teacher where tno >= 2;
    explain select * from teacher where tno  in (1,2);
    
    id select_type table partitions type
    1 SIMPLE teacher NULL range

    ref:輔助索引的等值查詢

    explain select * from course where cname = "DBA";
    
    id select_type table partitions type
    1 SIMPLE course NULL ref

    eq_ref:多連接中,非驅動表連接條件是主鍵或唯一鍵

    -- course 爲驅動表,teacher 爲非驅動表,此時 tno 爲 Teacher 的 主鍵
    explain select course.tno from course join teacher on course.tno=teacher.tno;
    
    id select_type table partitions type
    1 SIMPLE course NULL index
    1 SIMPLE teacher NULL eq_ref

    const(system):聚簇索引等值查詢

    explain select * from course where cno = 1001;
    
    id select_type table partitions type
    1 SIMPLE course NULL const

    想必此時你已經對 SQL 執行計劃有了瞭解,我們優化 SQL 語句的底線就是全表掃描了,太耗費資源。後面我們還會介紹一種比 explain 更詳細的方法優化器追蹤 trace

  • key_len 字段解讀:可以使用它來判斷是否全部使用聯合索引如何計算呢?我回顧一下數據類型默認字符集爲:UTF8

    數據類型 not null 約束 無 not null 約束
    Tinyint 1 1+1
    int 4 4+1
    bigint 8 8+1
    char(10) 3*10 30+1
    varchar(10) 3*10+2 30+2+1

    上表是一個示例,也就是 ken_len 的計算方法,我們使用的是 utf8 字符集允許 1 個字符佔 3 個字節,那麼我們使用 char(10) 它的計算方法就爲 3 x 10 = 30 如果無 not null 約束的話,那 null 還需要一個字節的空間來存儲就爲 3 x 10 + 1 = 31 varchar(10) 的話也很好計算 3 x 10 + 2 爲什麼要加 2 ?因爲 varchar 類型會用兩個字節的空間來存儲長度,現在想必你應該對 key_len 的計算已經瞭解,那麼我們做個小案例吧。

    create table key_test(
    a int not null,
    b int ,
    c char(10) not null ,
    d varchar(10)
    ) charset=utf8mb4; -- 默認字符集爲 utf8mb4 最多存儲4個字節
    
    -- 添加一個複合索引
    alter table key_test add index idx_s7(a,b,c,d);
    
    explain
    select * from key_test
    where a = 3 and b = 6 and c = 'EDG' and d = 'ch';
    
    id select_type table partitions type possible_keys key key_len
    1 SIMPLE key_test NULL ref idx idx 92

    可以看到 key_len 爲 92 我們計算一下

    字段 數據類型 計算過程
    a Int not noll 4
    b Int 4+1
    c char(10) not null 4*10
    d varchar(10) 4*10+2+1

    結論:explain 執行結果一致都爲92,則說明查詢過程中使用了全部索引 如果現在依然感覺很模糊,那麼接着看吧,在表設計規範裏已經提到過隱式轉換會導致索引失效,d 字段爲字符串類型,我們使用數字來查會觸發隱式轉換,索引會失效,請看下面 SQL 語句。

    desc
    select * from key_test
    where a = 3 and b = 6 and c = 'EDG' and d = 777;
    
    id select_type table partitions type possible_keys key key_len
    1 SIMPLE key_test NULL ref idx_s7 idx_s7 49

    可以看到 key_len 現在爲 49 按照 92 - 43 = 49 剛好是 d 字段索引失效的 key_len 值,我們就可以使用這種方法來判斷複合索引是否都有效果,畢竟創建索引是有代價的,我們要保證每個索引都有效準確

五、定位性能問題

  • 介紹:我們在前面已經瞭解關於優化的基礎內容索引SQL 解析過程SQL 執行順序以及執行計劃,相信你已經對索引有了一定程度的認知,那麼我們現在一起來學習,如果定位到性能問題,如何去發現問題。

  • MySQL Profile 定位到性能瓶頸:Profile 是用來分析執行計劃的開銷,可以查看內存、CPU 等使用情況。可以幫助我們來評估 SQL 語句的性能。下面是 MySQL 官網對 profile 的介紹,總結一下就可以定位到當前會話的資源消耗情況。

    The SHOW PROFILE and SHOW PROFILES statements display profiling information that indicates resource usage for statements executed during the course of the current session.

    下面我們來學習如何使用 profiling:

    -- 查詢 profile 的狀態是否開啓 0 表示未開啓
    select @@profiling;
    
    @@profiling
    0
    set profiling=1;
    

    在這裏插入圖片描述
    開啓 profile 後 MySQL 提醒一個警告,查看後發現:

    @@profiling’ is deprecated and will be removed in a future release.
    @@profiling’已被棄用,並將在未來的版本中被刪除。

    現在我使用的環境是 5.7.28 相信 8.0 應該已經被刪除,我們可以瞭解一下(用起來也挺香的),還有第二種方法那就是 performance_schema。我們先學習 profile 如何使用,再循序漸進學習 performance_schema 相關知識。

    select @@profiling;
    
    @@profiling
    1

    現在已經開啓 profiling 現在我們運行 SQL 語句

    select count(*) from information_schema.COLUMNS;
    
    count(*)
    3817

    然後運行 show profiles; 即可得到剛剛運行的 SQL 語句

    show profiles;
    
    Query_ID Duration Query
    71 0.000104 SHOW WARNINGS
    72 0.000145 /* ApplicationName=DataGrip 2019.3.4 */ select count\(\*\) from information\_schema.COLUMNS

    在這裏插入圖片描述
    開啓 profiling 後會記錄運行的 SQL 語句,使用 show profiles; 可以查詢到記錄的 SQL 語句,然後使用 show profile cpu for query Query_ID 即可查詢到 SQL 語句資源使用情況,其中 cpu 是可以換做其它指標的比如 ALL 將會輸出 SQL 語句使用的各種資源情況不過太多,眼睛看不過來,所以我們一般都會使用查看某個指標而不是全部。

    選項 解釋
    ALL 顯示所有開銷信息
    BLOCK IO 查看塊操作數的相關開銷信息
    CONTEXT SWITCHS 上下文切換相關的開銷信息
    CPU 顯示CPU相關開銷信息
    IPC 顯示發送和接收相關開銷信息
    MEMORY 顯示內存相關的開銷信息
    PAGE FAULTS 顯示頁面錯誤相關的開銷信息
    SOURCE 顯示 Source_function、Source_file、Source_line 相關開銷信息
    SWAPS 顯示交換次數相關的開銷信息
  • performance_schema 定位性能瓶頸:屬於視圖表,而且統計數據過程中需要佔用系統資源,所以不建議在線上業務上使用它做監控之類的應用或者就不要在業務線上使用,會降低系統性能,經過測試大約會降低 10% 的系統性能,所以說在業務線上謹慎使用。以下是開啓配置方法及 MySQL 官網相關描述。下面請跟我一起完成配置吧!

    update performance_schema.setup_instruments
    set ENABLED = 'YES',
    	TIMED   = 'YES'
    where NAME LIKE '%statement/%';
    
    update performance_schema.setup_instruments
    set ENABLED = 'YES',
        TIMED   = 'YES'
    where NAME LIKE '%stage/%';
    
    update performance_schema.setup_consumers
    set ENABLED = 'YES'
    WHERE NAME LIKE '%events_statements_%';
    
    update performance_schema.setup_consumers
    set ENABLED = 'YES'
    WHERE NAME LIKE '%events_stages_%';
    

    採集功能已經開啓,所有 SQL 語句查都會記錄可以試着運行幾條DQL語句

    SELECT EVENT_ID, TRUNCATE(TIMER_WAIT / 1000000000000, 6) as Duration, SQL_TEXT
    FROM performance_schema.events_statements_history_long
    WHERE SQL_TEXT LIKE '%where%';
    
    EVENT_ID Duration SQL_TEXT
    2090 0.000264 /* ApplicationName=DataGrip 2019.3.4 */ update performance_schema.setup_consumers set ENABLED = 'YES’
    WHERE NAME LIKE ‘%events_statements_%’
    2209 0.000250 /* ApplicationName=DataGrip 2019.3.4 */ update performance_schema.setup_consumers set ENABLED = 'YES’
    WHERE NAME LIKE ‘%events_stages_%’
    2347 0.001330 /* ApplicationName=DataGrip 2019.3.4 */ select * from performance_schema.setup_instruments where NAME LIKE ‘%stage/%’
    2472 0.000315 /* ApplicationName=DataGrip 2019.3.4 */ select \* from school where id = 2
    2722 0.000796 /* ApplicationName=DataGrip 2019.3.4 */ select END\_EVENT\_ID, SQL\_TEXT<br/>from performance\_schema.events\_statements\_history\_long<br/>where SQL\_TEXT like '%select%'
    2847 0.000767 /* ApplicationName=DataGrip 2019.3.4 */ select END_EVENT_ID,

    可以根據 WHERE 中的條件快速查找到需要優化的 SQL 語句然後記住 EVENT_ID 比如我們現在需要查看 EVENT_ID2722 的 SQL 語句的消耗資源情況。

    SELECT event_name AS Stage, TRUNCATE(TIMER_WAIT / 1000000000000, 6) AS Duration
    FROM performance_schema.events_stages_history_long
    WHERE NESTING_EVENT_ID = 2722;
    
    Stage Duration
    stage/sql/starting 0.000106
    stage/sql/checking permissions 0.000007
    stage/sql/Opening tables 0.000023
    stage/sql/init 0.000030
    stage/sql/System lock 0.000005
    stage/sql/optimizing 0.000006
    stage/sql/statistics 0.000013
    stage/sql/preparing 0.000009
    stage/sql/executing 0.000000
    stage/sql/Sending data 0.000493
    stage/sql/end 0.000001
    stage/sql/query end 0.000004
    stage/sql/closing tables 0.000005
    stage/sql/freeing items 0.000086
    stage/sql/cleaning up 0.000000

    到此爲止,相當於 performance_schema 性能監測環境是配置完成,日常使用中我們主要查看的是 stage/sql/Sending data 關於 I/O 相關階段,SQL 語句慢此選項都會比較大,其它字段見名知意。另外,performance_schema 的數據不會持久化存儲在磁盤中,而是存儲在內存中 MySQL 重啓後也會自動刷新,我們配置的性能監測環境也會失效。可以利用這點來關閉監測環境。

  • 到此爲止我們已經介紹兩種定位到性能的方法 profilesperformance_schema 在使用過程中兩者可互補使用。這也是性能檢測和 explain 的區別,我們可以通過性能檢測得到每個階段所有的時間和消耗的資源,而 explain 只是優化器方面信息,得到的是 SQL 執行計劃。現在我們再介紹一個與之相似的但結果更加準確豐富優化器檢測手段:優化器追蹤 trace

    trace 是 5.6 版本新增的功能,可以幫助我們理解優化器選擇 A 執行計劃而不是選擇 B 執行計劃,trace 會給我們一個比 explain 更加詳細的信息。下面是配置方法:

    在這裏插入圖片描述

    -- 查看優化器狀態
    show variables like 'optimizer_trace';
    -- 開啓會話級別的 trace 僅在本會話有效
    set session optimizer_trace="enabled=on",end_markers_in_json=on;
    -- 設置優化器追蹤的內存大小
    set OPTIMIZER_TRACE_MAX_MEM_SIZE=1000000;
    

    以上就是優化器 trace 會話級別的開啓方法,關閉會話後使用的資源會釋放。接下來執行需要追蹤優化器的 SQL 語句系統就會追蹤到優化器的數據。再通過查詢元數據表即可得到答案。因爲開啓的是會話級別所以關閉 DBMS 的連接即可關閉,也可以手動關閉。

    -- 關閉本會話的 trace
    set session optimizer_trace="enabled=off"
    
    -- 查詢 information_schema.optimizer_trace 得到結果
    SELECT trace FROM information_schema.OPTIMIZER_TRACE;
    

    在這裏插入圖片描述
    上面就是優化器追蹤的結果,通常我們是直接導出文件然後再分析,運行如下 SQL 語句。

    -- 將查詢結果導入到 /data/trace.trace
    SELECT TRACE INTO DUMPFILE "/data/trace.json" FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE;
    

    通常會報錯:大概意思是導出文件必須要按照 secure_file_priv 參數指定的路徑
    ERROR 1290 (HY000): The MySQL server is running with the --secure-file-priv option so it cannot execute this statement

    -- 查詢 secure_file_priv 參數指定的目錄
    show global variables like '%secure_file_priv%';
    
    Variable_name Value
    secure_file_priv /var/lib/mysql-files/
    -- 修改 SQL 語句中的路徑即可導出文件
    SELECT TRACE INTO DUMPFILE "/var/lib/mysql-files/trace.json" FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE;
    

    經過以上過程相信大家已經掌握定位優化追蹤 teace 會話級配置、執行 SQL、導出文件、分析文件中的數據即可。下圖是得到 trace 的 Json 數據信息可分爲三部分。

    在這裏插入圖片描述

    • 準備階段:對應文本中的 join_preparation
    • 優化階段:對應文本中的 join_optimization
    • 執行階段:對應文本中的 join_execution

    注:其中我們重點關注的是 rows_estimationconsidered_execution_plans 是關於優化器計算代價選擇的執行分案。由於篇幅原因我後面還會再寫一篇博客將 trace 單獨拎出來介紹,直接使用 Python 分析結果省的我們花時間分析,目前在籌備中....

    總結:至此我們已經學習四種定位 SQL 問題的方法 explaintrace(優化器追蹤) 主要分析 SQL 執行過程,profilesperformance_schema 主要是關於 SQL 語句的性能考究。沒有絕對的解決方案,適合的場景下使用適合的方法。

  • 補充:通過 performance_schema 相信大家對 MySQL 元數據有一定的瞭解,performance_schema 主要包含數據庫關於性能的數據,information_schema 包含數據庫中的維護信息,兩者都屬於虛擬表沒有存儲數據,只是存儲生成數據的方法(概念有點像 Python 中的生成器) 但是從 performance_schema 中獲取想要的數據比較複雜,到 5.7 版本已經有 80 多張表,每張表都是對統計信息的羅列,而且這些表和 information_schema 也有關聯使用起來非常不方便,所以在 5.7 版本出現了 Sys Schemainformation_schemaperformance_schema 中的數據以更加容易的方式歸納提煉出來。下面介紹幾個常用的應用場景。


    查看數據庫中每張表的訪問量,通常數據庫由少數人來維護,突然某個實例的 QPS 上升,我們可以通過 schema_table_statistics 快速定位到訪問量的增長情況。

    select table_schema, table_name, (io_read_requests + io_write_requests) as io_num
    from sys.schema_table_statistics;
    
    table_schema table_name io_num
    new_data new 704
    new_data new_correlation 427
    new_data tb_users 12
    new_data django_migrations 11
    booktest auth_group 10
    booktest auth_group_permissions 10

    冗餘的索引和未使用的索引檢查,創建索引的代價蠻高的,我們要幫助每個索引都準確有效,通過 schema_redundant_indexes 快速幫助我們定位到索引的使用情況。

    select * from sys.schema_redundant_indexes;
    

    表自增 ID 監控,隨着數據增長,可能會出現某長表的自增快要超過閾值了,繼而導致業務問題。通過 schema_auto_increment_columns 精確查詢到每張表的自增 ID 信息。

    select * from sys.schema_auto_increment_columns;
    
    table_schema table_name column_name data_type column_type is_signed is_unsigned max_value auto_increment auto_increment_ratio
    new_data new_correlation id int int(11) 1 0 2147483647 1119611 0.0005
    meiduo_mall_prepare tb_areas id int int(11) 1 0 2147483647 820001 0.0004
    BookTest auth_group id int int(11) 1 0 2147483647 1 0.0000
    BookTest auth_group_permissions id int int(11) 1 0 2147483647 1 0.0000
    BookTest auth_permission id int int(11) 1 0 2147483647 25 0.0000
    BookTest auth_user id int int(11) 1 0 2147483647 1 0.0000
    BookTest auth_user_groups id int int(11) 1 0 2147483647 1 0.0000
    BookTest auth_user_user_permissions id int int(11) 1 0 2147483647 1 0.0000

    查詢數據庫中走全表掃描的 SQL 語句,有些語句因爲某種問題走全表掃描,這些 SQL 會導致數據庫的性能極具下降,併發量大的情況下甚至可以使服務器響應變慢,直到夯住。使用 statements_with_full_table_scans 幫助我們找出走全表掃描的 SQL 語句。

    select * from sys.statements_with_full_table_scans where db = 'new_data';
    
    query db exec_count total_latency no_index_used_count no_good_index_used_count no_index_used_pct rows_sent rows_examined rows_sent_avg rows_examined_avg first_seen last_seen digest
    SELECT `new_correlation` . `id … w_correlation` . `new_id` = ? new_data 2 78.59 ms 2 0 100 0 316920 0 158460 2020-06-17 08:39:19 2020-06-17 08:44:31 dca696ca75eb6e6cfaa9120f4ee82a96
    SELECT `django_migrations` . ` … ame` FROM `django_migrations` new_data 1 283.00 us 1 0 100 33 33 33 33 2020-06-17 08:38:47 2020-06-17 08:38:47 3ce5fc5bdb2cd93d45573041efa28fdb
    SELECT `new` . `id` , `new` . … ` . `new_seenum` DESC LIMIT ? new_data 3 106.59 ms 3 0 100 24 9081 8 3027 2020-06-17 08:39:01 2020-06-17 08:44:31 6986c7aa0ec5d45ab155e3ede0b0ee9a

    查看實例消耗的磁盤 I/O 某天數據庫響應變慢了,這時我們需要關心數據庫到底慢在哪裏?通過 io_global_by_file_by_bytes 可以快速定位到具體文件消耗的 I/O 從而幫助我們排查問題。

    select file, avg_write + avg_read as avg_io
    from io_global_by_file_by_bytes
    order by avg_io desc
    limit 10;
    
    file avg_io
    @@basedir/data/ib_logfile0 1331
    @@basedir/data/sys/schema_tables_with_full_table_scans.frm 1023
    @@basedir/data/sys/schema_unused_indexes.frm 1006
    @@basedir/data/sys/x@0024schema_tables_with_full_table_scans.frm 994
    @@basedir/data/help/helplist.frm 955
    @@basedir/data/mysql/tables_priv.MYD 947
    @@basedir/data/sys/x@0024ps_digest_95th_percentile_by_avg_us.frm 906
    @@basedir/data/performance_schema/table_lock_waits_summary_by_table.frm 897
    @@basedir/data/mysql/proxies_priv.MYD 837
    @@basedir/data/mysql/user.MYD 748

    注意:不要在線上業務大量使用元數據表,來做監控或者巡檢,因爲查詢信息時 MySQL 會消耗大量資源去收集相關信息,有讓業務請求堵塞的風險。在使用前務必瞭解清楚。 MySQL 官網 Sys-Schema

六、SQL 查詢優化

  1. 感言:首先 MySQL 優化是門比較複雜的技術,爲什麼這樣講呢?涉及到的東西特別多,想想一名 DBA 要看多少書。本篇博客只是爲大家引出 MySQL 優化,當你需要做優化的時候不至於沒有一點頭緒,可以順者我的博客找找思路。後續我還會繼續更新博文,找點比較酷的事情做。感謝關注,祝進步!

  2. 派生表(Derived Table):當 from 後面的對象是子查詢返回的結果時,此時就會出現派生表。請看示例。

    在這裏插入圖片描述
    可以看到 from 後面直接跟了一個子查詢,此時子查詢就是一個派生表。

  3. 深入派生表:使用 explain 查看派生表執行計劃。

    explain
    select *
    from (select * from new where id < 500) news
    where new_cate_id = 2;
    

    id select_type table partitions type possible_keys key key_len ref rows filtered Extra
    1 SIMPLE new NULL index_merge PRIMARY,new_new_cate_id_3e5fac50_fk_cate_id new_new_cate_id_3e5fac50_fk_cate_id,PRIMARY 8,4 NULL 21 100 Using intersect(new_new_cate_id_3e5fac50_fk_cate_id,PRIMARY); Using where

    發現 select_type 依然是 SIMPLE 沒有出現派生表,原因是 5.7 版本後 MySQL 會對派生表進行優化,那麼現學現用使用 trace 查看優化器的解析過程。
    在這裏插入圖片描述
    看來是優化器對 SQL 語句進行優化,沒有走派生表,可以關閉優化器相關派生表的功能。

    -- 查詢優化器相關功能開啓狀態
    select @@optimizer_switch;
    
    @@optimizer_switch
    index_merge=on,index_merge_union=on,index_merge_sort_union=on,index_merge_intersection=on,engine_condition_pushdown=on,index_condition_pushdown=on,mrr=on,mrr_cost_based=on,block_nested_loop=on,batched_key_access=off,materialization=on,semijoin=on,loosescan=on,firstmatch=on,duplicateweedout=on,subquery_materialization_cost_based=on,use_index_extensions=on,condition_fanout_filter=on,deriv ed\_merge=on

    可以看到 derived_merge 開啓狀態,我們關閉即可。

    set optimizer_switch = 'derived_merge=off';
    

    再次 explain 剛剛的 SQL 查詢語句 select_type = DERIVED 此時 SQL 觸發派生表 結果如下:

    id select_type table partitions type possible_keys key key_len ref rows filtered Extra
    1 PRIMARY <derived2> NULL ref <auto_key0> <auto_key0> 4 const 10 100 NULL
    2 DERIVED new NULL range PRIMARY PRIMARY 4 NULL 950 100 Using where

    爲什麼要學習派生表呢?因爲它可能會帶來性能隱患。如果服務剩餘資源比較緊的情況下還可能會導致查詢失敗。我們可以從 explain 中得到派生表的執行過程:首先執行 from 後的子查詢語句,然後將查詢結果寫入臨時表,再回讀,按照條件查詢。 可以瞭解到派生表會生成臨時文件不會應用外部的過濾條件,生成的文件可能會很大,總結一句:派生表有潛在的性能隱患,儘量不要使用。

    -- 剛纔將優化器關閉的同學可以打開了!!!
    set optimizer_switch = 'derived_merge=on';
    
  4. MySQL 中的半連接(semi join):半連接聽起來好像很高大上,其實就是我們比較普遍的查詢方式。

    -- 類似這種子查詢語句 將 test1 和 test2 連接起來
    select * from test1 where id in (select * from test2 where id < 100);
    

    接下來我們來使用一個比較常用的例子來介紹半連接,首先需要搭建環境:

    -- 創建測試表
    create table users(
    user_id int(11) unsigned not null primary key ,
    user_name varchar(64) default null
    ) engine=innodb default charset=UTF8;
    
    -- 創建存儲過程
    delimiter $$
    drop procedure if exists insert_df$$
    create procedure insert_df()
    begin
        declare
            init_data integer default 1;
        while init_data < 20000
            do
                insert into users values (init_data, concat('user', init_data));
                set init_data = init_data + 1;
            end while;
    end $$;
    
    -- 運行存儲過程
    call insert_df();
    
    select count(*) from users;
    
    count(*)
    19999

    環境搭建完成,接下來我們執行一條半連接 SQL 語句,我們發現竟然足足使用 3.5 秒

    -- 代號:Q1
    select count(u.user_id)
    from users u where u.user_name in
    (select u2.user_name from users u2 where u2.user_id < 2000);
    

    在這裏插入圖片描述
    做同樣的事情我們稍微修改一下 SQL 語句,僅僅用時 0.03 秒,保守估計性能提升 50 倍。

    -- 代號-Q2
    select *
    from users u
    where (u.user_name in (select t.user_name from users t where t.user_id < 2000) or
           (u.user_name in (select t.user_name from users t where t.user_id < -1)));
    

    在這裏插入圖片描述
    從結果中我們可以看到只是添加 or 條件,性能就提升 50 多倍爲什麼呢?接下來先介紹 MySQL 執行監控計數器,來對比兩條 SQL 語句的執效率對比情況。

    -- 重置計數器 保證計數器每次從新的開始
    flush status
    

    重置計數器後直接運行需要查詢的 SQL 語句,然後使用 Handler_read 可以得到結果。

    -- 查看計數器
    show status like 'Handler_read%';
    
    Variable_name Value
    Handler_read_first 2
    Handler_read_key 2
    Handler_read_last 0
    Handler_read_next 1999
    Handler_read_prev 0
    Handler_read_rnd 0
    Handler_read_rnd_next 22000

    介紹 Handler_read 中的重要參數含義:

    1. Handler_read_key:通過 index 獲取數據的次數。如果較高,說明查詢和表索引使用正確。
    2. Handler_read_next:通過索引讀取下一條數據的次數。如果用範圍約束或者執行索引掃描來查詢索引列,該值會增加。
    3. Handler_read_rnd_next:從數據節點讀取下一條數據的次數。如果正在進行大量的表掃描,該值會比較高。通常說明表索引使用不正確或寫入的查詢沒有利用索引。
    4. Handler_read_first:索引中第一個條目被讀取的次數。如果這個值很高,說明服務器正在進行大量的全索引掃描。

    瞭解完 Handler_read 計數器後我們來對比剛纔兩條 SQL 語句的性能

    Variable_name Value1-Q1 Value2-Q2
    Handler_read_first 2 2
    Handler_read_key 2 20001
    Handler_read_last 0 0
    Handler_read_next 1999 1999
    Handler_read_prev 0 0
    Handler_read_rnd 0 0
    Handler_read_rnd_next 22000 20000

    value1 是第一條 SQL 語句(3.5s)的計數器結果 Q1,value2 是第二條 SQL 語句 (0.03s)的計數結果 Q2,忘記的可以翻到前面熟悉一下。

    結論:現在我們發現 Q1Handler_read_key 遠遠小於 Q2 根據上面的參數說明,瞭解到 Q2 查詢時索引使用正確。下面我們簡單分析一下,爲什麼 Q2 會有更多的索引讀?

    -- 查看 Q1 執行計劃
    explain
    select count(u.user_id)
    from users u
    where u.user_name in
          (select u2.user_name from users u2 where u2.user_id < 2000);
    
    id select_type table partitions type possible_keys key key_len ref rows filtered Extra
    1 SIMPLE <subquery2> NULL ALL NULL NULL NULL NULL NULL 100 NULL
    1 SIMPLE u NULL ALL NULL NULL NULL NULL 19762 10 Using where; Using join buffer (Block Nested Loop)
    2 MATERIALIZED u2 NULL range PRIMARY PRIMARY 4 NULL 1999 100 Using where
    -- 查看 Q2 執行計劃
    explain
    select count(u.user_id)
    from users u
    where (u.user_name in (select t.user_name from users t where t.user_id < 2000) or
           (u.user_name in (select t.user_name from users t where t.user_id < -1)));
    
    id select_type table partitions type possible_keys key key_len ref rows filtered Extra
    1 PRIMARY u NULL ALL NULL NULL NULL NULL 19762 100 Using where
    3 SUBQUERY NULL NULL NULL NULL NULL NULL NULL NULL NULL no matching row in const table
    2 SUBQUERY t NULL range PRIMARY PRIMARY 4 NULL 1999 100 Using where

    發現 Q1 查詢第一步進行物化(MATERIALIZED) 使用臨時表後面的查詢全都走全表掃描是物化導致索引失效嗎?那麼使用我們前面學習到的 trace 優化器追蹤來對 Q1 一探究竟。

    在這裏插入圖片描述
    直接上結果,感興趣的朋友可以自己試試,前面已經演示過 trace 從結果來看優化器在物化後又進行了 semi join 半連接優化,那麼問題原因就是 Q1 物化使用臨時表(臨時表會自己創建索引),又進行半連接優化,後面的查詢就走了全表掃描。那麼如果關閉半連接優化效果會不會好些呢?

    -- 關閉半連接優化
    set optimizer_switch='semijoin=off';
    

    在這裏插入圖片描述
    關閉半連接優化器後,原本需要 3.5s 的查詢現在只需要 0.02s 執行時間大大降低,那麼初步印象已經形成:半連接存在性能隱患,可以選擇關閉優化器的半連接優化。

    -- 關閉半連接優化
    set optimizer_switch='semijoin=off';
    

  5. 反連接 (antijoin):相信瞭解半連接,反連接也很好理解就是 not in 或者 not exists 子句形式,就用到了反連接。


  6. 行值表達式 (Row Value Expressions):聽起來好像有點抽象,行值表達式也叫行值構造器,通常我們所操作的SQL表達式都只能針對一行中的單一字段進行操作比較,而行值表達式可以針對一行中的多個字段進行操作比較。

    說了比較抽象那麼看一個例子吧!比如有三名學生分別爲:張三(大數據) 王五(軟件工程) 李四(信息對抗) 因爲不能保證其它專業沒有相同的名字所以我們要查詢三位同學的 SQL 應該如下:where course in (‘大數據’, ‘軟件工程’, ‘信息對抗’) and username in ('張三‘, ‘李四’, ‘王五’) 此時如果使用行值表達式 此時如果使用行值表達式 where (course, username) in ((‘大數據’, ‘張三’), (‘軟件工程’, ‘李四’), (‘信息對抗’, ‘王五’)) 也就是說此時查詢條件是多維的 可以使用行值表達式。

    因爲行列表達式在 MySQL 不同版本中有比較大的差異,我們用剛纔半連接創建的 users 表再次做一個小測試:

    -- 5.7 版本
    explain
    select *
    from users
    where (user_id, user_name) in ((1, 'user1'), (2, 'user2'), (3, 'user4'));
    
    id select_type table partitions type possible_keys key key_len ref rows filtered Extra
    1 SIMPLE users NULL range PRIMARY,idx_users idx_users 199 NULL 3 100 Using where; Using index
    -- charset = utf8
    desc users;
    
    Field Type Null Key Default Extra
    user_id int(11) unsigned NO PRI NULL
    user_name varchar(64) YES NULL

    還記得 key_len 的計算方法嗎? user_id 類型爲 int 佔 4 個字節,varchar(64) 64 * 3 = 192 再加上 varchar 類型需要額外兩個字節空間來存儲長度,並且可以爲 null 還需要一個字節空間那麼最後 user_name 的 key_len 爲 64*3+2+1 = 195 結果說明在 5.7 版本使用行值表達式索引能夠被充分利用。

    id select_type table partitions type possible_keys key key_len ref rows filtered Extra
    1 SIMPLE users NULL range PRIMARY,idx_users idx_users 195 NULL 3 100 Using where; Using index

    表格爲 5.6 版本相同環境下運行的結果 key_len 爲 195 則說明 user_id 索引失效,5.7 版本優化器做了改動。爲什麼優化需要了解這些呢?其實呢,任何數據庫的優化器都不是萬能的。 瞭解優化器的特性後並規避其短處,才能寫出最優SQL語句。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章