MySQL性能優化學習——(四)優化總結篇

一、優化思路

性能優化的思路應該是什麼樣的?

說到性能調優,大部分時候想要實現的目標是讓我們的查詢更快。
一個查詢的動作又是由很多個環節組成的,每個環節都會消耗時間,在第一篇 關於SQL 語句的
執行流程中已經分析過了。
第一篇中說了一條SQL的執行過程,想要減少查詢所消耗的時間,就要從過程中每一個環節入手。
 
 

二、連接——配置優化

第一個環節是客戶端連接到服務端,連接這一塊有可能會出現什麼樣的性能問題?

 
有可能是服務端連接數不夠導致應用程序獲取不到連接。比如報了一個 Mysql: error
1040: Too many connections 的錯誤。
可以從兩個方面來解決連接數不夠的問題:
1、從服務端來說,我們可以增加服務端的可用連接數。
如果有很多請求同時訪問數據庫,連接數不夠的時候,我們可以:
(1)修改配置參數,增加可用連接數,修改max_connections的大小:
show variables like 'max_connections'; -- 修改最大連接數,當有多個應用連接的時候
 (2)或者即使釋放不活動的連接。交互式和非交互式的客戶端的默認超時時間都是28800秒,
8小時,我們可以把這個值調小。
show global variables like 'wait_timeout'; --及時釋放不活動的連接,注意不要釋放連接池還在使用的連接
 
2、從客戶端來說,可以減少從服務端獲取的連接數。
如果我們想要不是每次執行SQL都創建一個新的連接,應該怎麼做?
我們可以引入連接池,實現連接的重用。
 
我們可以在哪些層面使用連接池?
ORM層面:MyBatis自帶了一個連接池;
或者使用專業也連接池工具:阿里的Druid、Spring Boot 2.x版本默認的連接池Hikari、DBCP、C3P0;
 
當客戶端改成從連接池獲取連接之後,連接池的大小應該怎麼設置呢?
常有一個誤解,覺得連接池的最大連接數越大越好,這樣在高併發時客戶端可以獲取更多的連接數,不需要排隊。
這是錯誤的,連接池並不是越大越好,只要維護一定數量帶下的連接池,其他客戶端排隊等待連接就可以了。
有點時候連接池越大,效率反而越低。
 
Druid的默認最大連接池大小是8,Hikari的默認最大連接池大小是10,爲什麼默認值都這麼小?
在Hiari的github文檔中,給出了一個PostgreSQL數據庫建議的設置連接池大小的公式:

connections = ((core_count * 2) + effective_spindle_count)

它的建議是機器核數乘以 2 加 1。也就是說,4 核的機器,連接池維護 9 個連接就夠了。
這個公式從一定程度上來說對其他數據庫也是適用的。
 
爲什麼有的情況下,減少連接數反而會提升吞吐量呢?
爲什麼建議設置的連接池大小要跟 CPU 的核數相關呢?
每一個連接,服務端都需要創建一個線程去處理它。連接數越多,服務端創建的線程數就越多。
CPU的核數是有限的,執行多個線程,頻繁切換(線程)上下文會造成比較大的性能開銷。

 

不管是數據庫本身的配置,還是按照這個數據庫服務的操作系統的配置,

對配置進行優化,最終的目的都是使硬件本身的性能更好發揮,包括CPU、內存、磁盤、網絡。

在之前的內容中也接觸了很多的MySQL和InnoDB的配置參數,包括各種開關和數值的配置,

大多數參數都提供了一個默認值,比如默認的buffer_pool_size,默認的頁大小,InnoDB的併發線程數等等。

這些默認配置可以瞞住大部分情況的需求,除非有特殊的需求,在清楚參數的含義時再去修改它。

修改配置的工作一般由專業的DBA完成。

這是官網系統的參數列表,需要時再做參考:

https://dev.mysql.com/doc/refman/5.7/en/server-system-variables.html

 

除了合理設置服務端連接數和客戶端連接池大小外,還有哪些減少客戶端和數據庫服務端的連接數的方案呢?

可以引入緩存。

 

三、緩存——架構優化

3.1 緩存

在應用的併發數非常大的情況下,如果沒有緩存,會造成兩個問題:

一是會給數據庫帶來很大壓力,二是對於應用來說操作數據的速度也會受影響。

可以採用第三方緩存來解決這個問題,比如 Redis。

運行獨立的緩存服務,屬於架構層面的優化

爲了減少單臺數據庫服務器的讀寫壓力,在架構層面還可以做哪些其他優化措施?

還可以採取主從複製、分庫分表等方案。

 

四、優化器——SQL語句分析優化

優化器就是對我們的 SQL 語句進行分析,生成執行計劃。
 

4.1 慢查詢日誌(slow query log)

我們可以通過慢查詢日誌知道服務中哪些SQL語句比較慢。
 
因爲開啓慢查詢日誌是有代價的(和bin log、optimizer-trace一樣),所以默認關閉。
show variables like '%slow_query%';
 
除了這個開關,還有一個參數,控制執行多久的SQL才被記錄到慢日誌,默認是10秒:
show variables like '%long_query%';
可以直接動態修改參數(重啓後失效):
set @@global.slow_query_log=1; -- 慢查詢日誌開關  1 開啓,0 關閉,重啓後失效
set @@global.long_query_time=3; -- mysql 默認的慢查詢時間是 10 秒,另開一個窗口後纔會查到最新值
 
或者修改配置文件 my.cnf,讓配置永久生效。
以下配置定義了慢查詢日誌的開關、慢查詢的時間、慢日誌文件的存放路徑。
slow_query_log = ON
long_query_time=2
slow_query_log_file =/var/lib/mysql/localhost-slow.log
 
show global status like 'slow_queries'; -- 查看有多少慢查詢
show variables like '%slow_query%'; -- 獲取慢日誌目錄
 
雖然有了慢日誌,但是慢日誌記錄了所有超過設定值的慢查詢,如何統計分析呢?總不能一條一條數。
MySQL提供了mysqldumpslow的工具,在MySQL的bin目錄下。
例如:查詢用時最多的20條慢SQL:
mysqldumpslow -s t -t 20 -g 'select' /var/lib/mysql/localhost-slow.log
 
Count:代表這條SQL被執行了多少次;
Time:代表執行的時間,括號內是累計時間;
Lock:代表鎖定的時間,括號是累計鎖定時間;
Rows:代表返回的記錄數,括號是累計;
 
 

4.2 show profile

除了慢查詢日誌,還有show profile工具可以使用
https://dev.mysql.com/doc/refman/5.7/en/show-profile.html
 
 
show profile可以查看SQL語句執行的時使用的資源,比如CPU、IO的消耗情況。
 
查看是否開啓:
select @@profiling;
若未開啓則手動開啓:
set @@profiling=1;
 
查看 profile 統計
show profiles;(命令最後帶一個 s)
 
查看最後一個 SQL 的執行詳細信息,從中找出耗時較多的環節(沒有 s)。
show profile;
此處時間6.2E-5表示小數點左移 5 位,代表 0.000062 秒。
 
也可以根據 ID 查看執行詳細信息,在後面帶上 for query + ID。
show profile for query 1;
 
除了慢日誌和 show profile,如果要分析出當前數據庫中執行的慢的 SQL,還可以
通過查看運行線程狀態和服務器運行信息、存儲引擎信息來分析。
 

其他系統命令

show processlist 運行線程
show processlist;
用於顯示用戶運行線程。可以根據 id 號 kill 線程。
也可以查表,效果一樣:
select * from information_schema.processlist;
 
Id : 線程的唯一標誌,可以根據它 kill 線程
User : 啓動這個線程的用戶,普通用戶只能看到自己的線程
Host : 哪個 IP 端口發起的連接
db : 操作的數據庫
 
show status 服務器運行狀態
SHOW STATUS 用於查看 MySQL 服務器運行狀態(重啓後會清空),有 session 和 global 兩種作用域,格式:參數-值。
可以用 like 帶通配符過濾。
SHOW GLOBAL STATUS LIKE 'com_select'; -- 查看 select 次數
 
show engine 存儲引擎運行信息
show engine 用來顯示存儲引擎的當前運行信息,包括事務持有的表鎖、行鎖信息;
事務的鎖等待情況;線程信號量等待;文件 IO 請求;buffer pool 統計信息。
例如:
show engine innodb status;
 
如果需要將監控信息輸出到錯誤信息 error log 中(15 秒鐘一次),可以開啓輸出。
show variables like 'innodb_status_output%';
-- 開啓輸出:
SET GLOBAL innodb_status_output=ON;
SET GLOBAL innodb_status_output_locks=ON;
 
其實很多開源的慢查詢日誌監控工具,他們的原理其實也都是讀取的系統的變量和狀態。
 
那麼,現在我們已經知道哪些 SQL 慢了,爲什麼慢呢?慢在哪裏?
 
MySQL 提供了一個執行計劃的工具,
通過 EXPLAIN 我們可以模擬優化器執行 SQL 查詢語句的過程,來知道 MySQL 是
怎麼處理一條 SQL 語句的。通過這種方式我們可以分析語句或者表的性能瓶頸。
 

4.3 EXPLAIN 執行計劃

 

先創建三張表。一張課程表,一張老師表。

先創建如下三張表以供測試。三張表沒有任何索引。

CREATE TABLE `course` (
  `cid` int(3) DEFAULT NULL,
  `cname` varchar(20) DEFAULT NULL,
  `tid` int(3) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `teacher` (
  `tid` int(3) DEFAULT NULL,
  `tname` varchar(20) DEFAULT NULL,
  `tcid` int(3) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

CREATE TABLE `teacher_contact` (
  `tcid` int(3) DEFAULT NULL,
  `phone` varchar(200) DEFAULT NULL
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

1)id

執行計劃中id不同

EXPLAIN SELECT tc.phone FROM teacher_contact tc 
WHERE tcid = (
 SELECT tcid FROM teacher t WHERE t.tid = (
 SELECT c.tid FROM course c WHERE c.cname = 'java' 
    )
);
-- 查詢 java 課程的老師手機號

SQL執行結果:

可以看到執行計劃中的id值不同,在id 值不同的時候,會先查詢 id 值大的(先大後小)。

查詢順序:course c——teacher t——teacher_contact tc。

子查詢只能以這種方式進行,只有拿到內層的結果之後才能進行外層的查詢。
 
 
執行計劃中id相同
EXPLAIN SELECT
	t.tname,
	c.cname,
	tc.phone
FROM
	teacher t,
	course c,
	teacher_contact tc
WHERE
	t.tid = c.tid
AND t.tcid = tc.tcid
AND (c.cid = 2 OR tc.tcid = 3);
-- 查詢課程 ID 爲 2,或者聯繫表 ID 爲 3 的老師 

id 值相同時,表的查詢順序是從上往下順序執行。
例如這次查詢的 id 都是 1,查詢的順序是 teacher t(3 條)——teacher_contact tc(3 條)——course c(4 條)。
 
嘗試對 teacher 表插入 3 條數據後:
INSERT INTO `teacher`
VALUES
	(4, 'John', 4);

INSERT INTO `teacher`
VALUES
	(5, 'Tyler', 5);

INSERT INTO `teacher`
VALUES
	(6, 'David', 6);

COMMIT;

id 也都是 1,但是從上往下查詢順序變成了:teacher_contact tc(3 條)——course c(4 條)——teacher t(6 條)。
 
注意,爲什麼和插入三條數據前的執行順序不同了?
這是mysql因爲對笛卡爾積的處理。
假如有 a、b、c 三張表,分別有 2、3、4 條數據,如果做三張表的聯合查詢,
當查詢順序是 a→b→c 的時候,它的笛卡爾積是:2*3*4 = 6*4 = 24。
如果查詢順序是 c→b→a,它的笛卡爾積是 4*3*2 = 12*2 = 24。
因爲 MySQL 要把查詢的結果,包括中間結果和最終結果都保存到內存,
所以 MySQL會優先選擇中間結果數據量比較小的順序進行查詢。
所以最終聯表查詢的順序是 a→b→ c。

這就是插入數據影響的執行順序的原因。(小表驅動大表的思想)

既有相同也有不同
如果 ID 有相同也有不同,就是 ID 不同的先大後小,ID 相同的從上往下

 

2)select type 查詢類型
SIMPLE :簡單查詢,不包含子查詢,不包含關聯查詢 union。

 

PRIMARY
子查詢 SQL 語句中的主查詢,也就是最外面的那層查詢。
SUBQUERY
子查詢中所有的內層查詢都是 SUBQUERY 類型的。
 
 
DERIVED
衍生查詢,表示在得到最終查詢結果之前會用到臨時表。
UNION
用到了 UNION 查詢。
UNION RESULT
主要是顯示哪些表之間存在 UNION 查詢。<union2,3>代表 id=2 和 id=3 的查詢存在 UNION。

 

3)type 連接類型
https://dev.mysql.com/doc/refman/5.7/en/explain-output.html#explain-join-types
在常用的鏈接類型中(效率高低排序):system > const > eq_ref > ref > range > index > all
這 裏 並 沒 有 列 舉 全 部 ( 其 他 : fulltext 、 ref_or_null 、 index_merger 、unique_subquery、index_subquery)。
除了all,都能用到索引。
 
const
條件爲主鍵索引或者唯一索引,且只能查到一條數據的 SQL。

 

system
system 是 const 的一種特例,只有一行滿足條件。例如:只有一條數據的系統表。不過在我們開發應用時基本不會去查系統表。
 
eq_ref
通常出現在多表的 join 查詢,表示對於前表的每一個結果,,都只能匹配到後表的一行結果。
一般是唯一性索引的查詢(UNIQUE 或 PRIMARY KEY)。
eq_ref 是除 const 之外最好的訪問類型。
DELETE FROM teacher where tid in (4,5,6); 
commit; -- 刪除多餘的三條記錄

ALTER TABLE teacher_contact ADD PRIMARY KEY(tcid);
-- 爲 teacher_contact 表的 tcid 創建主鍵索引。

ALTER TABLE teacher ADD INDEX idx_tcid (tcid);
-- 爲 teacher 表的 tcid 創建普通索引。
 
以上三種 system,const,eq_ref,都是可遇而不可求的,基本上很難優化到這個狀態。
 
ref
查詢用到了非唯一性索引,或者關聯操作只使用了索引的最左前綴。
例如:使用 tcid 上的普通索引查詢:
 
range
索引範圍掃描。
如果 where 後面是條件是索引,且範圍爲 between and 或 <或 > 或 >= 或 <=或 in 這些,type 類型就爲 range。
注意 沒有索引type就是ALL 全表掃描了。

index
Full Index Scan,查詢全部索引中的數據(比不走索引要快)。
 
all
Full Table Scan,如果沒有索引或者沒有用到索引,type 就是 ALL。代表全表掃描。
 
NULL
不用訪問表或者索引就能得到結果,例如:
EXPLAIN select 1 from dual where 1=1;
 
小結:
一般來說,需要保證查詢至少達到 range 級別,最好能達到 ref。
ALL(全表掃描)和 index(查詢全部索引)都是需要優化的。
 
4) possible_keykey
possible_key : 可能用到的索引
key : 實際用到的索引
如果是 NULL 就代表沒有用到索引。
possible_key 可以有一個或者多個,可能用到索引並不代表一定用到索引。
 
possible_key 爲空,key 可能有值嗎?
ALTER TABLE user_innodb add INDEX comidx_name_phone (name,phone);--爲name和phone建立聯合索引
explain select phone from user_innodb where phone='126'; --以索引爲條件查詢建立了索引的字段

因此,是有可能的,這是使用到了覆蓋索引的情況(無需回表)。

如果優化時,通過分析發現沒有用到索引,就要檢查 SQL 或者創建索引。

 

5)key_len

索引的長度(使用的字節數)。跟索引字段的類型、長度有關。

 

6)rows

MySQL 認爲掃描多少行才能返回請求的數據,是一個預估值。一般來說行數越少越好。
 
7)filtered
這個字段表示存儲引擎返回的數據在 server 層過濾後,剩下多少滿足查詢的記錄數量的比例,它是一個百分比。
 
8)ref
使用哪個列或者常數和索引一起從表中篩選數據。
 
9)Extra
執行計劃給出的額外的信息說明。
 
using index:
用到了覆蓋索引,不需要回表。

using where:
使用了 where 過濾,表示存儲引擎返回的記錄並不是所有的都滿足查詢條件,
需要在 server 層進行過濾(跟是否使用索引沒有關係)。
 
Using index condition(索引條件下推):使用到了第二篇中提到的索引條件下推
 
using filesort:不能使用索引來排序,用到了額外的排序(跟磁盤或文件沒有關係)。需要優化。
 
using temporary :用到了臨時表。
比如:
distinct 非索引列:
EXPLAIN select DISTINCT(tid) from teacher t;
group by 非索引列:
EXPLAIN select tname from teacher group by tname;
使用 join 的時候,group 任意列:
EXPLAIN select t.tid from teacher t join course c on t.tid = c.tid group by t.tid;
 
總結一下,模擬優化器執行 SQL 查詢語句的過程,來知道 MySQL 是怎麼處理一條 SQL 語句的。
通過這種方式我們可以分析語句或者表的性能瓶頸。
分析出問題之後,就是對 SQL 語句進行鍼對性的具體優化。
 

五、存儲引擎

5.1 存儲引擎的選擇

爲不同的業務表選擇不同的存儲引擎。比如查詢插入操作多或事物一致性要求低的用MyISAM,臨時數據用Memory,常規的併發大更新多的表用InnoDB。

5.2 分區或者分表

分區不推薦。

交易歷史表:在年底爲下一年度建立12個分區,每個月一個分區。

渠道交易表:分成當日表;當月表;歷史表,歷史表再做分區。

5.3 字段定義

原則:使用可以正確存儲數據的最小數據類型,爲每一列選擇合適的字段類型。

5.3.1 整數類型

INT有8種類型,不同類型的最大存儲範圍是不一樣的。

性別?用TINYINT,因爲ENUM也是整形存儲。

5.3.2 字符類型

變長情況下,varchar更節省時間,但是對於varchar字段,需要一個字節來記錄長度。

固定長度的用char,不要用varchar。

5.3.3 非空

非空字段儘量定義成NOT NULL,提供默認值,或者使用特殊值、控制代替null。

NULL類型的存儲、優化、使用都會存在問題。

5.3.4 不要用外鍵、觸發器、視圖

降低了可讀性;

影響數據庫性能,應該把計算的事交給程序,數據庫只做存儲;

數據的完整性應該在程序中檢查。

5.3.5 大文件存儲

不要用數據庫存儲圖片(比如base64)或者大文件;

把文件放在NAS上,數據庫只存儲URI,在應用中配置NAS服務器地址。

5.3.6 表拆分

將不常用的字段拆分出去,避免列數過多和數據量過大。

比如在業務系統中,要記錄所有接收和發送的消息,這個消息是XML格式的,用blob或者text存儲,用來追蹤和判斷重複,可以建立一張表專門用來存儲報文。

 

六、總結:優化體系

如圖,對於優化的方向,優化難度從上到下是依次增加的,但是優化得到的效益卻不一定。

因此我們優化的方向選擇儘量從上而下開始。

除了對於代碼、SQL 語句、表定義、架構、配置優化之外,業務層面的優化也不能忽視。舉幾個例子:
1)雙十一時,爲什麼在凌晨精緻查詢今天之外的賬單?
這是一種降級措施,用來保證當前最核心的業務。
2)爲什麼在雙十一之前,提前一個多星期就已經有雙十一的預售價格了?
這是通過預售的手段實現了分流。
在應用層面同樣有很多其他的方案來優化,達到儘量減輕數據庫的壓力的目的,比
如限流,或者引入 MQ 削峯,等等。
 
 
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章