MySQL 性能調優——SQL 優化

數據庫性能的影響因素很多,包括:

影響因素 說明 解決方案
SQL 查詢速度 存在慢查詢 SQL
大表和大事務 MySQL 單錶行數超過千萬行或單表數據超過10GB就會影響性能。
數據庫存儲引擎
數據庫參數配置 配置不當會直接影響性能。
服務器硬件
網卡流量
磁盤 IO 使用更快的磁盤設備,比如 RAID 卡、SSD、Fusion-IO 等;檢查是否存在其他大量消耗磁盤性能的計劃任務,如果存在,則調整計劃任務,做好磁盤維護。

注意:MySQL 5.7 是單線程的服務,並不支持多 cpu 併發運算,意味着每條 sql 只能用到 1 個 cpu。

如何定位並優化慢查詢 SQL?具體場景具體分析,大致思路如下:

  • 根據慢查詢日誌定位慢查詢 SQL;
  • 使用 explain 等工具分析 SQL;
  • 修改 SQL 或者儘量讓 SQL 走索引。

SQL 查詢優化,索引優化,庫表結構優化需要齊頭並進。本篇主要記錄了一些 SQL 調優的經驗。

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

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

1.SQL調優

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

  • 通過慢查日誌獲取存在性能問題的 SQL;
  • 實時獲取存在性能問題的 SQL;

1.根據慢查詢日誌定位慢查詢 SQL

MySQL 慢查詢日誌是一種性能開銷比較低的解決方案,主要性能開銷在磁盤 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;

開啓慢查詢日誌有兩種方式,第一種是通過配置 /etc/my.cnf 文件開啓,是永久性的,第二種是通過設置全局變量開啓,MySQL 重啓後會失效。

設置全局變量的 SQL 如下:

set global slow_query_log = on;
set global long_query_time = 1;

和二進制日誌不同,慢查詢日誌會記錄所有符合條件的 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.使用explain分析SQL

explain 可以幫助我們分析 select 語句,讓我們知道查詢效率低下的原因。這個關鍵字一般放在 select 語句的前面,用於描述 MySQL 如何執行查詢操作以及 MySQL 成功返回結果集需要執行的行數,執行會輸出一些 explain 的字段。例如:

EXPLAIN SELECT name FROM person_info_large order by name desc;

需要注意的是,執行 explain 並不會真正的執行 SQL,而是對 SQL 做了一些分析,速度非常快。

explain 關鍵字段:

id:表示了 MySQL 的執行順序,id 越大越先執行;
type:表示 MySQL 找到數據行的方式;
key:實際使用的索引;
Extra:額外信息。

type 字段的返回值,性能從最優到最差:

system -> const -> eq_ref -> ref -> fulltext -> ref_or_null -> index_merge -> unique_subquery -> index_subquery -> range -> index -> all

index 和 all 表示本次查詢走的是全表掃描。如果 type 值是這兩個,表明 SQL 是需要優化的。

Extra 中出現了以下兩種意味着 MySQL 根本不能使用索引,效率會受到重大影響,應儘可能對此進行優化:

Extra 項 說明
Using filesort 表示 MySQL 會對結果使用一個外部索引排序,而不是從表裏按索引次序讀到相關內容。可能在內存或者磁盤上進行排序。MySQL 中無法利用索引完成的排序操作稱爲 “文件排序”。
Using temporary 表示 MySQL 在對查詢結果排序時使用的臨時表,常見於排序 order by 和分組查詢 group by。

3.修改 SQL或者儘量讓SQL走索引

例如爲上面 SQL 中的 name 加索引:

ALTER TABLE person_info_large add index idx_name(name);

MySQL的查詢優化器

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())
);
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章