1. 程式人生 > >MySQL查詢性能優化

MySQL查詢性能優化

客戶 表示 分組 group 原因 策略 class 語法 lai

本文為《高性能MySQL》讀書筆記

慢查詢基礎:優化數據訪問

查詢性能低下最基本的原因是訪問的數據太多。對於低效的查詢,我們發現通過下面兩個步驟來分析總是很有效的:

  • 確認應用程序是否在檢索大量超過需要的數據。這通常意味著訪問了太多的行,但有時候也可能是訪問了太多的列。
  • 確認MySQL服務器層是否在分析大量超過需要的數據行。

是否向數據庫請求了不需要的數據

有些查詢會請求超過實際需要的數據,然後這些多余的數據會被應用程序丟棄。這會給MySQL服務器帶有額外的負擔,並增加網絡開銷,另外也會消耗應用服務器的CPU和內存資源。

一些典型案例:

技術分享圖片
技術分享圖片
技術分享圖片

MySQL是否在掃描額外的記錄

在確定查詢只返回需要的數據以後,接下來應該看看查詢為了返回數據結果是不是掃描了過多的數據。對於MySQL,最簡單的衡量查詢開銷的三個指標如下:

  • 響應時間
  • 掃描的行數
  • 返回的行數

這三個指標都會記錄到MySQL的慢日誌中,所以檢查慢日誌記錄是找出掃描行數過多的查詢的好辦法。

掃描的行數和返回的行數:

掃描過多的行數 則說明該查詢找到需要的數據的效率不高,其實也就是索引建的不夠優化。

掃描的行數與返回的行數大小相等,是最優的查詢。

掃描的行數和訪問類型:

是不是選擇最優的訪問類型來減少掃描的行數。

在評估查詢開銷的時候,需要考慮一下從表中找到某一行數據的成本。MySQL有好幾種訪問方式可以查找並返回一行結果。有些訪問方式可能需要掃描很多行才能返回一行結果,也有些訪問方式可能無須掃描就能返回結果。

在EXPLAIN語句中的Type列反應了訪問類型。訪問類型有很多種,從全表掃描到索引掃描、範圍掃描、唯一索引查詢、常數引用等。這裏列的這些,速度是從慢到快,掃描的行數也是從小到大。你不需要記住這些訪問類型,但需要明白掃描表、掃描索引、範圍訪問和單值訪問的概念。

建立合適的索引 & 只查詢需要的列。

重構查詢的方式

簡化SQL,拆分大SQL,分解關聯查詢。

MySQL查詢執行過程

技術分享圖片

查詢狀態

查看當前查詢線程狀態:SHOW FULL PROCESSLIST命令。

狀態枚舉值:

  • Sleep:線程正在等待客戶端發送新的請求。
  • Query:線程正在執行查詢或者正在將結果發送給客戶端。
  • Locked:在MySQL服務器層,該線程正在等待表所。在存儲引擎級別實現的鎖,例如InnoDB的行鎖,並不會提現在線程狀態中。
  • Analyzing and statistics:線程正在收集存儲引擎的統計信息,並生成查詢的執行計劃。
  • Copying to tmp table [on disk]:線程正在執行查詢,並將其結果集都復制到一個臨時表中,這種狀態一般要麽是在做GROUP BY操作,要麽是文件排序操作,或者是UNION操作。如果這個狀態後面還有“on disk”標記,那表示MySQL正在將一個內存臨時表存到磁盤上。
  • Sorting result:線程正在對結果集進行排序。
  • Sending data:線程可能在多個狀態之間傳送數據,或者在生成結果集,或者在想客戶端返回數據。

查詢緩存

在解析一個查詢語句之前,如果查詢緩存是打開的,那麽MySQL會優先檢查這個查詢是否命中查詢緩存中的數據。這個檢查是通過一個對大小寫敏感的哈希查找實現的。查詢和緩存中的查詢即使只有一個字節不同,那也不會匹配緩存結果,這種情況下查詢就會進入下一階段的處理。

如果當前的查詢恰好命中了查詢緩存,那麽在返回查詢結果之前MySQL會檢查一次用戶權限。這仍然是無須解析查詢SQL語句的,因為在查詢緩存中已經存放了當前查詢需要訪問的表信息。如果權限沒有問題,MySQL會跳過所有其他階段,直接存緩存中拿到結果並返回給客戶端。這種情況下,查詢不會被解析,不用生成執行計劃,不會被執行。

查詢優化處理

查詢的生命周期的下一步是將一個SQL轉換為一個執行計劃,MySQL再按照這個執行計劃和存儲引擎進行交互。這個階段可以分為幾個子階段:解析SQL、預處理、優化SQL執行計劃。

語法解析器和預處理:

首先,MySQL通過關鍵字將SQL語句進行解析,並生成一棵對應的“解析樹”。MySQL解析器將使用MySQL語法規則校驗和解析查詢。例如,它將校驗是否使用錯誤的關鍵字,或者使用關鍵字的順序是否正確等,再或者它還會驗證引號是否能前後正確匹配。

預處理器則根據一些MySQL規則進一步檢查解析樹是否合法,例如,這裏講檢查數據表和規則列是否存在,還會解析名字和別名,看看它們是否有歧義。

查詢優化器:

一條查詢可以有很多種執行方式,最後都返回相同的結果。優化器的作用就是找到這其中最好的執行計劃。

mysql的查詢優化器是一個非常復雜的部件,它使用了很多優化策略來生成一個最優的執行計劃。優化策略可以簡單的分為兩種,一種是靜態優化,一種是動態優化。靜態優化可以直接對解析樹進行分析,並完成優化。例如,優化器可以通過一些簡單的代數變換將where條件轉換成另一種等價形勢。靜態優化不依賴於特別的數值,如where條件中帶入的一些常熟等等。靜態優化在第一次完成後就一直有效,幾遍使用不同的參數重復執行查詢也不會發生變化。可以認為這是一種“編譯時優化”。

相反動態優化則和查詢上下文有關,也可能和很多其他因素有關,例如,where條件中的取值、索引中條目對應的數據行等等。著需要在每次查詢的時候重新評估,可以認為這是“運行時的優化”。

下面是一些mysql能夠處理的優化類型:

  • 重新定義關聯表的順序;
  • 將外鏈接轉化成內連接;
  • 使用等價變換規則(如 5=5 AND a>5將被改寫成a>5);
  • 優化count()、min()和max();
  • 預估並轉化為常數定義式;
  • 覆蓋索引掃描;
  • 子查詢優化;
  • 提前終止查詢;
  • 等值傳播;
  • 對象IN()的比較;

MySQL如何執行關聯查詢

mysql中的“關聯”一次所包涵的意義比一般意義上理解的要更廣泛。總的來說,mysql認為任何一個查詢都是一個“關聯” —— 並不僅僅是一個查詢需要到兩個表匹配才叫關聯,所以在mysql中,每一個查詢,每一個片段(包括子查詢,甚至基於單表的select)都有可能是關聯。

我們根據union查詢的例子來理解關聯查詢。對於union查詢,mysql先將一系列的單個查詢結果放到一個臨時表中,然後再重新讀出臨時表來完成union查詢。在mysql的概念中,每個查詢都是一次關聯,所以讀取結果臨時表也是一次關聯。

當前mysql關聯執行得策略很簡單:mysql對任何關聯都執行嵌套循環關聯操作,即mysql先在一個表中循環取出單條數據,然後再嵌套循環到下一個表尋找匹配的行,一次下去,知道找到所有表中匹配的行為止。然後根據各個表匹配的行,返回查詢結果匯總需要的各個列。mysql會嘗試在最後一個關聯表中找到所有匹配的行,如果最後一個關聯表無法找到更更多行以後,mysql返回到上一層次的關聯表,看是否能夠找到更多的匹配記錄,一次類推叠代執行。

按照這樣的方式查找第一個表的記錄,再嵌套查詢下一個關聯表,然後回溯到上一個表,在mysql中時通過嵌套循環的方式實現的。 請看下面的例子中的簡單查詢:

SELECT tbl1.col1 , tbl2.col2 FROM tbl1 INNER JOIN tbl2 USING(col3) where tbl1.col1 in (5,6)

假設MySQL按照查詢中的表順序進行關聯操作,我們則可以使用下面的偽代碼表示MySQL將如何完成這個查詢:

技術分享圖片

技術分享圖片
技術分享圖片

特定類型的查詢優化

優化COUNT()查詢

COUNT()可以統計某個列值的數量,也可以統計行數。在統計列值時要求列值是非空的(不統計NULL)。如果在COUNT()的括號中指定了列或者列的表達式,則統計的就是這個表達式有值的結果數(而非NULL)。

COUNT()的另一個作用是統計結果集的行數,當MySQL確認括號內的表達式值不可能為空時,實際上就是在統計行數。最簡單的就是當我們使用COUNT()的時候,這種情況下通配符 並不會像我們猜想的那樣擴展成所有的列,實際上,它會忽略所有的列而直接統計所有的行數。

一種常見的錯誤就是,在括號內指定了一個列卻希望統計結果集的行數。如果希望知道的是結果集的行數,最好使用count(*),這樣寫意義清晰,性能也會很好。

MYISAM只有當沒有任何WHERE條件的COUNT(*)才非常快,因為此時無須實際地計算表的行數。MySQL可以利用存儲引擎的特性直接獲得這個值。如果MySQL知道某列col不可能為NULL值,那麽MySQL內部會將COUNT(col)表達式優化為COUNT(*)。當統計待WHERE子句的結果集行數時,MyISAM與其他數據庫引擎沒有太大區別。

優化關聯查詢

  • 確保ON或者USING子句中的列上有索引。在創建索引的時候就要考慮到關聯的順序。當表A和表B用列C關聯的時候,如果優化器的關聯順序是B、A,那麽就不需要在B表的對應列上建上索引。沒有用到的索引只會帶來額外的負擔。一般來說,除非有其他理由,否則只需要在關聯順序中的第二個表的相應列上創建索引。
  • 確保任何的GROUP BY和ORDER BY 中的表達式只涉及到一個表中的列,這樣MySQL才有可能使用索引來優化這個過程。

優化子查詢

盡可能使用關聯查詢代替子查詢。

優化GROUP BY優化

針對以GROUP BY的優化 主要分為有索引和無索引兩種情況。

當無法使用索引的時候,GROUP BY使用兩種策略來完成分組工作:使用臨時表或者文件排序來做分組,其實就是進行一次全表掃描篩選數據形成一個臨時表,然後按照GROUP BY 指定的列進行排序。在這個臨時表裏面,對於每一個group的數據行來說是連續在一起的。完成排序之後,就可以發現所有的GROUPS,並可以執行聚合函數。所以,我們常常在explain後看到“Using temporary; Using filesort”。

如果沒有通過 ORDER BY子句顯示地指定排序列,當查詢使用GROUP BY子句的時候,結果集會自動按照分組的字段進行排序。如果不關心結果集的順序,而這種默認排序又導致了需要文件排序,則可以使用ORDER BY NULL,讓MySQL不再進行文件排序。也可以在GROUP BY子句中直接使用DESC或者ASC關鍵字,使分組的結果集按需要的方向進行排序。

優化LIMIT分頁查詢

假設有如下的分頁SQL語句:

SELECT * FROM table LIMIT offset , rows ;

這是一條典型的LIMIT語句,常見的使用場景是,某些查詢返回的內容特別多,而客戶端處理能力有限,希望每次只取一部分結果進行處理。

上述SQL語句的實現機制是:

  1. 從“table”表中讀取offset+rows行記錄。
  2. 拋棄前面的offset行記錄,返回後面的rows行記錄作為最終的結果。

這種實現機制存在一個弊端:雖然只需要返回rows行記錄,但卻必須先訪問offset行不會用到的記錄。對一張數據量很大的表進行查詢時,offset值可能非常大,此時limit語句的效率就非常低了。

使用覆蓋索引來優化:

盡可能使用索引覆蓋掃描,確定需要返回行的主鍵等,然後再根據需要做一次關聯操作再返回所需的列。通常此種優化都是使用子查詢來實現。

比如我們有如下的SQL語句:

select * from student where score > 90 limit 1000,10 

我們就可以先利用覆蓋索引查詢速度快的優點先查詢出對應分頁段內的學號,然後再根據學號去做關聯查詢,第二步是直接使用主鍵做關聯,也是非常快的。優化後的SQL如下:

select * from student as stu INNER JOIN (select id from student limit 1000,10) as tmp on stu.id = tmp.id;

確定分頁起始值,減少掃描行數

我們可以記住上次取數據的位置,然後下次就可以直接從該位置開始掃描數據,然後取指定的長度。假設上次獲取到的最後一個學生學號是:20180131200,則我們可以改寫成如下SQL:

select * from student stu where stu.id > 20180131200 order by id limit 100 ;

上面的SQL首先使用主鍵進行排序,因為聚簇索引的特性,所以主鍵ID在索引樹中本身已經有序存儲了,所以此處的order by 非常快。然後再使用主鍵進行篩選 也是非常快的。

優秀網文:mysql大數據量之limit優化

MySQL查詢性能優化