MySQL 性能調優——SQL 查詢優化


如何設計最優的數據庫表結構,如何建立最好的索引,以及如何擴展數據庫的查詢,這些對於高性能來說都是必不可少的。但是只有這些還不夠,要獲得良好的數據庫性能,我們還要設計合理的數據庫查詢,如果查詢設計的很糟糕,即使增加再多的只讀從庫,表結構設計的再合理,索引再合適,只要查詢不能使用到這些東西,也無法實現高性能的查詢。所以說查詢優化,索引優化,庫表結構優化需要齊頭並進。

在進行庫表結構設計時,我們要考慮到以后的查詢要如何的使用這些表,同樣,編寫 SQL 語句的時候也要考慮到如何使用到目前已經存在的索引,或是如何增加新的索引才能提高查詢的性能。

想要對存在性能問題的查詢進行優化,需要能夠找到這些查詢,下面先看下如何獲取有性能問題的 SQL。

1.獲取有性能問題的SQL

獲取有性能問題的 SQL 的三種方法:

  • 通過用戶反饋獲取存在性能問題的 SQL;
  • 通過慢查日志獲取存在性能問題的 SQL;
  • 實時獲取存在性能問題的 SQL;

1.慢查詢日志獲取性能問題SQL

MySQL 慢查詢日志是一種性能開銷比較低的獲取存在性能問題 SQL 的解決方案,其主要的性能開銷在磁盤 IO 和存儲日志所需要的磁盤空間。對於磁盤 IO 來說,由於寫日志是順序存儲,開銷基本上忽略不計,所以主要需要關注的還是磁盤空間。

MySQL 提供了以下參數用於控制慢查詢日志:

slow_query_log:是否啟動慢查詢日志,默認不啟動,on 啟動; slow_query_log_file:指定慢查詢日志的存儲路徑及文件,默認情況下保存在 MySQL 的數據目錄中; long_query_time:指定記錄慢查詢日志 SQL 執行時間的閾值,單位秒,默認 10 秒,通常對於一個繁忙的系統來說,改為0.001秒比較合適; log_queries_not_using_indexes:是否記錄未使用索引的 SQL; 

和二進制日志不同,慢查詢日志會記錄所有符合條件的 SQL,包括查詢語句、數據修改語句、已經回滾的 SQL。

慢查詢日志中記錄的內容:

# Query_time: 0.000220 //執行時間,可以精確到毫秒,220毫秒 # Lock_time: 0.000120 //所使用鎖的時間,可以精確到毫秒 # Rows_sent: 1 //返回的數據行數 # Rows_examined: 1 //掃描的數據行數 SET timestamp=1538323200; //執行sql的時間戳 SELECT c FROM test1 WHERE id =100; //sql 

通常情況下,在一個繁忙的系統中,短時間內就可以產生幾個 G 的慢查詢日志,人工檢查幾乎是不可能的,為了快速分析慢查詢日志,必須借助相關的工具。

常用的慢查詢日志工具:

1、mysqldumpslow:一個常用的,MySQL 官方提供的慢查詢日志分析工具,隨着 MySQL 服務器的安裝而被安裝。可以匯總除查詢條件外其他完全相同的 SQL,並將分析結果按照參數中所指定的順序輸出。

2、pt-query-digest:用於分析 MySQL 慢查詢的一個工具。

2.實時獲取性能問題SQL

為了更加及時的發現當前的性能問題,我們還可以通過實時的方法來獲取有性能問題的 SQL。最方便的一種方法就是利用 MySQL information_schema 數據庫下的 PROCESSLIST 表來實現實時的發現性能問題 SQL。例如下面這條 SQL 表示查詢出當前服務器中執行時間超過 1 秒的 SQL:

SELECT id,user,host,db,command,time,state,info FROM information_schema.PROCESSLIST WHERE TIME>=1 

然后我們可以通過腳本周期性的來執行這條 SQL,實時的發現哪些 SQL 執行的是比較慢的。

2.SQL的解析預處理及生成執行計划

找到了那些查詢存在性能問題的 SQL,那么下面我們就看下,為什么這些 SQL 會存在性能問題?

為了搞清楚這個問題,我們先來看下 MySQL 服務器處理一條 SQL 請求所需要經歷的步驟都有哪些:

1.客戶端通過 MySQL 的接口發送 SQL 請求給服務器,這一步通常不會影響查詢性能;
2.MySQL 服務器檢查是否可以在查詢緩存中命中該 SQL,如果命中,則立即返回存儲在緩存中的結果,否則進入下一階段;
3.MySQL 服務器進行 SQL 解析,預處理,再由 SQL 優化器生成對應的執行計划;
4.根據執行計划,調用存儲引擎 API 來查詢數據;
5.將結果返回給客戶端。

這就是 MySQL 服務器處理查詢請求的整個過程。在第二到第五步,都有可能對查詢的響應速度造成影響,下面來分別看下這些過程可能對查詢的響應速度有影響的因素都有些什么:

在解析查詢語句前,如果查詢緩存是打開的,那么 MySQL 優先檢查這個查詢是否命中查詢緩存中的數據,這個檢查是通過一個對大小寫敏感的 Hash 查找實現的。由於 Hash 查找只能進行全值匹配,所以請求的查詢和緩存中的查詢就算只有一個字節的不同,那么也不會匹配到緩存中的結果,這種情況下,查詢就會進入到下一階段處理。如果正好命中查詢緩存,在返回查詢結果之前,MySQL 就會檢查用戶權限,也是無需解析 SQL 語句的,因為在查詢緩存中,已經存放了當前查詢所需要訪問的表的信息,如果權限沒有問題,MySQL 會跳過所有的其他階段,直接從緩存中拿到結果,並返回給客戶端,這種情況下查詢是不會被解析的,也不會生成查詢計划,不會被執行。

可以發現,從查詢緩存中直接返回結果並不容易。

查詢緩存對 SQL 性能的影響:

  • 如果查詢緩存,一旦數據更新,都要對緩存中數據進行刷新,影響性能;
  • 每次在查詢緩存中檢查 SQL 是否被命中,都要對緩存加鎖,影響性能;

對於一個讀寫頻繁的系統來說,查詢緩存很可能會降低查詢處理的效率。所以在這種情況下建議大家不要使用查詢緩存。

對查詢緩存影響的一些系統參數:

query_cache_type: 設置查詢緩存是否可用,可以設置為ON、OFF、DEMAND,DEMAND表示只有在查詢語句中使用 SQL_CACHE 和 SQL_NO_CACHE 來控制是否需要緩存。
query_cache_size: 設置查詢緩存的內存大小,必須是1024字節的整數倍。 
query_cache_limit: 設置查詢緩存可用存儲的最大值,如果知道很大不會被緩存,可以在查詢上加上 SQL_NO_CACHE 提高效率。
query_cache_wlock_invalidate: 設置數據表被鎖后是否返回緩存中的數據,默認關閉。
query_cache_min_res_unit: 設置查詢緩存分配的內存塊最小單位。

對於一個讀寫頻繁的系統來說,可以把 query_cache_type 設置為 OFF,並且把 query_cache_size 設置為 0。

當查詢緩存未啟用或者未命中則會進入下一階段,也就是需要將一個 SQL 轉換成一個執行計划,MySQL 再依據這個執行計划和存儲引擎進行交互,這個階段包括了多個子過程:解析 SQL,預處理,優化 SQL 執行計划。在這個過程中,出現任何錯誤,比如語法錯誤等,都有可能中止查詢的過程。

在語法解析階段,主要是通過關鍵字對 MySQL 語句進行解析,並生成一棵對應的 “解析樹”。這一階段,MySQL 解析器將使用 MySQL 語法規則驗證和解析查詢,包括檢查語法是否使用了正確的關鍵字、關鍵字的順序是否正確等。

預處理階段則是根據 MySQL 規則進一步檢查解析樹是否合法,比如檢查查詢中所涉及的表和數據列是否存在、檢查名字或別名是否存在歧義等。

如果語法檢查全部都通過了,查詢優化器就可以生成查詢計划了。

會造成 MySQL 生成錯誤的執行計划的原因:

  • 統計信息不准確;
  • 執行計划中的成本估算不等同於實際的執行計划的成本;
  • MySQL 查詢優化器所認為的最優可能與你所認為的最優不一樣;
  • MySQL 從不考慮其他並發的查詢,這可能會影響當前查詢的速度;
  • MySQL 有時候也會基於一些固定的規則來生成執行計划;
  • MySQL 不會考慮不受其控制的成本,例如存儲過程、用戶自定義的函數等。

MySQL 的查詢優化器可以優化的 SQL 類型:

  • 重新定義表的關聯順序,優化器會根據統計信息來決定表的關聯順序;
  • 將外連接轉化為內連接,比如 where 條件和庫表結構都可能讓一個外連接等價於內連接;
  • 使用等價變換規則,比如 (5=5 and a>5) 將被改寫為 a>5;
  • 利用索引和列是否為空來優化 count()、min() 和 max() 等聚合函數;
  • 將一個表達式轉換為常數表達式;
  • 使用等價變換規則,比如覆蓋索引,當 MySQL 查詢優化器發現索引中的列包含所有查詢中所需要的信息的時候,MySQL 就能使用索引返回需要的數據;
  • 子查詢優化,比如把子查詢轉換為關聯查詢,減少表的查詢次數;
  • 提前終止查詢;
  • 對 in() 條件進行優化。

以上這些就是 MySQL 查詢優化器可以自動對查詢所做的一些優化。經過查詢優化器改寫后的 SQL,查詢優化器會對其生成一個 SQL 執行計划,然后 MySQL 服務器就可以根據執行計划調用存儲引擎的 API,通過存儲引擎獲取數據了。

3.確定查詢處理各個階段的耗時

SQL 查詢優化的主要目的就是減少查詢所消耗的時間,加快查詢的響應速度。下面來介紹如何度量查詢處理各個階段所消耗的時間。

對於一個存在性能問題的 SQL 來說,必須知道在查詢的哪一階段消耗的時間最多,然后才能有針對性的進行優化。度量查詢處理各個階段所消耗的時間,常用的方法有兩種:

  • 使用 profile;
  • 使用 performance_schema;

4.特定SQL的查詢優化

前面介紹的方法,已經可以獲取一個存在性能問題的 SQL 和獲取一個 SQL 在執行的各個階段所消耗的時間了。得到這些信息后,我們就可以針對性的對 SQL 進行優化了,下面舉幾個對特定 SQL 優化的案例:

1.大表的更新和刪除

對於大表的數據修改最好要分批處理,比如我們要在一個 1000 萬行記錄的表中刪除/更新 100 萬行記錄,那么我們最好分多個批次進行刪除/更新,一次只刪除/更新 5000 行記錄,避免長時間的阻塞,並且為了減少對主從復制帶來的壓力,每次刪除/修改數據后需要暫停幾秒。這里提供一個可以完成這樣工作的 MySQL 存儲過程的實例:

DELIMITER $$
USE 'db_name'$$ DROP PROCEDURE IF EXISTS 'p_delete_rows'$$ CREATE DEFINER='mysql'@'127.0.0.1' PROCEDURE 'p_delete_rows'() BEGIN DECLARE v_rows INT; SET v_rows = 1; WHERE v_rows > 0 DO DELETE FROM table_name WHERE id >= 9000 AND id <= 290000 LIMIT 5000; SELECT ROW_COUNT() INTO v_rows; SELECT SLEEP(5); END WHERE; END$$ DELIMITER; 

大家可以根據自己的情況來修改這個存儲過程,或者使用自己熟悉的開發語言實現這個處理過程,使用這個存儲過程只需要修改 DELETE FROM table_name WHERE id >= 9000 AND id <= 290000 LIMIT 5000; 部分的內容即可。

2.如何修改大表的表結構

對於 InnoDB 存儲引擎來說,對表中的列的字段類型進行修改或者改變字段的寬度時還是會鎖表,同時也無法解決主從數據庫延遲的問題。

解決方案:

在主服務器上建立新表,新表的結構就是修改之后的結構,再把老表的數據導入到新表中,並且在老表上建立一系列的觸發器,把老表數據的修改同步更新到新表中,當老表和新表的數據同步后,再對老表加一個排它鎖,然后重新命名新表為老表的名字,最好刪除重命名的老表,這樣就完成了大表表結構修改的工作。這樣處理的好處是可以盡量減少主從延遲,以及在重命名之前不需要加任何的鎖,只需要在重命名的時候加一個短暫的鎖,這對應用通常是無影響的,缺點就是操作比較復雜。好在有工具可以幫我們實行這個過程,這個工具同樣是 percona 公司 MySQL 工具集中的一個,叫做 pt-online-schema-change:

pt-online-schema-change \
--alter="MODIFY c VARCHAR(150) NOT NULL DEFAULT ''" \ --user=root --password=password D=db_name,t=table_name \ --charset=utf8 --execute 

這個命令就是把 db_name 數據庫下的 table_name 表中 c 列的寬度改為 VARCHAR(150)。

3.如何優化not in和<>查詢

MySQL 查詢優化器可以自動的把一些子查詢優化為關聯查詢,但是對於存在not in和<>這樣的子查詢語句來說,就無法進行自動優化了,這就造成了會循環多次來查找子表來確認是否滿足過濾條件,如果子查詢恰好是一個很大的表的話,這樣做的效率會非常低,所以我們在進行 SQL 開發時,最好把這類查詢自行改寫成關聯查詢。

改寫前:

SELECT id,name,email FROM customer WHERE id NOT IN(SELECT id FROM payment) 

優化改寫后:

SELECT a.id,a.name,a.email 
FROM customer a 
LEFT JOIN payment b ON a.id=b.id 
WHERE b.id IS NULL 

使用 LEFT JOIN 關聯替代了 NOT IN 過濾,這樣避免了對 payment 表進行多次查詢,這是一種非常常用的對 NOT IN 的優化方式。

4.使用匯總表優化查詢

最常見的就是商品的評論數,如果我們在用戶訪問頁面時,實時的訪問商品的評論數,通常來說,查詢的 SQL 會類似於下面這個樣子:

SELECT COUNT(*) FROM product_comment WHERE product_id = 10001; 

這個 SQL 就是統計出所有 product_id = 10001 的評論,假設評論表中有上億條記錄,那么這個 SQL 執行起來是非常的慢的,如果有大量的並發訪問,則會對數據庫帶來很大的壓力。對於這么情況,我們通常使用匯總表的方式進行優化。所謂的匯總表就是提前把要統計的數據進行匯總並記錄到表中已備后續的查詢使用。針對這個查詢,我們可以使用下面的方式進行優化:

CREATE TABLE product_comment_cnt(product_id INT, cnt INT); //建立匯總表 //查詢評論數 SELECT SUM(cnt) FROM( SELECT cnt FROM product_comment_cnt WHERE product_id = 10001 UNION ALL SELECT COUNT(*) FROM product_comment WHERE product_id = 10001 AND timestr > DATE(NOW()) ); 

覺得有什么問題還不明白或者還弄不明白,切不知道怎么學習的,大家可以加群:833145934,純屬程序員交流的圈子,里面可以討論技術問題(Spring,MyBatis,Netty源碼分析,高並發、高性能、分布式、微服務架構的原理,JVM性能優化、分布式架構等),學習方向,還有面試資料分享。進群如果你看到有人找你私聊或者打培訓類的廣告,請告知群主,群主會第一時間提除。本群只講干貨,不搞虛的。

 


免責聲明!

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



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