首先咱們來看一下mysql的邏輯架構圖mysql
server層包括鏈接器,查詢緩存,分析器,優化器,執行器等,內置函數(例如時間函數,數學函數等)和存儲過程,觸發器,事務等都在這一層實現。redis
引擎層負責數據的存儲和提取,包括不少咱們經常使用的引擎如Innodb,MyISAM,Memory等。sql
咱們使用下面命令來鏈接數據庫:數據庫
mysql -uxxxx -pxxxx
複製代碼
鏈接器接收到命令後,完成以下操做:json
一個鏈接默認保存時間是8個小時,由參數wait_timeout決定。也就是說若是不操做,過了8小時鏈接器會自動斷開鏈接,再次操做,則會報 "LOST CONNECTION"的錯誤。數組
mysql拿到一個請求後會先看緩存中是否有,若是有則直接返回,若是沒有,再往下執行,執行完成後,把結果放入緩存,若是是複雜的查詢,效率會很是的高。 但mysql的緩存有一個很是大的問題: 只要對一個表更新,這個表上的全部緩存都會失效。 因此緩存的命中率很是低。在大多數狀況下,不建議使用緩存。緩存
mysql8.0以上版本直接將查詢緩存的整快功能都去掉了。性能優化
分析器主要是對sql語句進行解析,分析出關鍵字,讓mysql知道你要作什麼,若是sql語句有語法錯誤,會拋錯。bash
優化器主要目的是生成執行計劃。好比語句:服務器
SELECT a,b FROM t WHERE a=1 AND b=1
複製代碼
至於具體使用哪一種執行計劃,就是優化器根據效率最高來作判斷的了。
執行期在拿到執行計劃後,會先作一個權限的判斷,看用戶是否對錶有操做權限,若是沒有權限,會拋錯。若是有權限,就打開表調用引擎接口獲取數據。
mysql當中已經有了緩存,爲何咱們還要用redis,memcache等第三方的緩存呢?
在工做中,咱們是否是有時會遇到查詢返回很是慢的狀況,那麼這種狀況如何定位慢sql,而且優化呢?
定位慢sql有如下兩種方案:
mysql慢查詢日誌是記錄執行時間超過設置的閥值的SQL語句,可使用以下命令來查看是否開啓
show variables like '%slow_query_log%';
複製代碼
慢查詢日誌有四個比較關鍵的參數:
在開啓慢查詢日誌以前,咱們先在表裏插入一下數據
drop table if exists `slow_log`;
CREATE TABLE `slow_log` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`a` int(11) DEFAULT NULL,
`b` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `a` (`a`)
) ENGINE=InnoDB;
drop procedure if exists slow_log_insert;
delimiter ;;
create procedure slow_log_insert()
begin
declare i int;
set i=1;
while(i<=10000)do
insert into slow_log(a,b) values(i,i);
set i=i+1;
end while;
end;;
delimiter ;
call slow_log_insert();
複製代碼
咱們來開啓一下慢查詢:
set global slow_query_log=1; -- 在本會話中打開慢日誌
set global long_query_time=0.005; -- 在本會話中設置閥值時間爲5ms
set global slow_query_log_file='/tmp/mysql_slow.log'; -- 設置慢日誌的文件路徑
複製代碼
而後執行sql語句
select * from slow_log;
select * from slow_log where a = 5;
複製代碼
發現只有前一條sql語句記錄到了慢查詢日誌裏面
日誌中比較重要的參數以下:
固然在生成上,慢日誌中的內容會不少,咱們可使用mysqldumpslow 來對慢日誌進行分析和彙總。
有時候,慢查詢還在進行,但數據庫負載已經偏高了,這時候能夠用 show processlist 來找出慢查詢。 若是有PROCESS權限,能夠看到在執行的語句。若是沒有則只能看到本次會話中的執行語句。
咱們開啓兩個會話,而後執行下面語句
會話1 | 會話2 |
---|---|
SELECT SLEEP(100) | |
SHOW PROCESSLIST |
會話2的顯示結果以下:
這裏對幾個重要參數解釋一下:
咱們可使用 "kill [query] id" 命令來終止執行
kill 27
或
kill query 27
複製代碼
"kill 27 和 kill query 27" 的區別在於"kill 27"是結束id爲27的會話。"kill query 27"表示結束會話27的本次操做,而保留會話27。
經過上面的兩個步驟咱們已經找到了慢sql語句,如今咱們要進行進行分析了,那麼如何分析SQL語句呢?咱們可使用如下三種工具進行分析:
EXPLAIN SELECT * FROM slow_log WHERE a=1
複製代碼
執行結果以下:
咱們重點看如下以下幾個參數:
當key爲空的時候表示沒有用到索引,能夠考慮優化了。 當Extra出現以下幾個狀況,也能夠考慮優化。
值 | 解釋 | sql例子 |
---|---|---|
Using filesort | 是用外部排序,而非索引排序 | EXPLAIN select b from slow_log order by b |
Using temporary | 建立了臨時表 | EXPLAIN SELECT b FROM slow_log group by b order by null |
Using join buffer (flat, BNL join) | 關聯查詢中,被驅動表字段沒有索引 | EXPLAIN SELECT * FROM slow_log AS s1 INNER JOIN slow_log AS s2 ON s1.b=s2.b |
有的時候,咱們須要確認究竟是哪一個環節出問題了,此時explain就不是那麼好用了。咱們須要使用show profile。
show profile使用步驟以下:
SHOW VARIABLES LIKE '%profiling%';
複製代碼
SET profiling=1
複製代碼
上面的命令只是在本次會話中開啓,若是須要全局開的,能夠在命令中加上"GLOBAL"
SET GLOBAL profiling=1
複製代碼
SELECT a FROM slow_log WHERE a=1
複製代碼
SHOW PROFILES;
複製代碼
SHOW PROFILE FOR QUERY 2;
複製代碼
咱們使用explain能夠看到執行計劃,可是explain並不能告訴咱們爲何選擇了A方案而不是B方案。 咱們可使用trace來知道執行方案的細節。
ps:
trace 使用步驟以下:
接下來咱們用一個例子來講明一下trace是怎麼使用的。 有下面一條sql語句:
SELECT a,b FROM slow_log WHERE a>8000;
複製代碼
由於a上面有索引,按照大部分人的常識,應該會走a索引,可是咱們用explain工具查看,發現這條語句,居然使用了全表掃描。
那麼接下來咱們用trace來分析一下這條sql的細節。
1.打開trace,而且以json格式輸出
SET SESSION optimizer_trace="enabled=on",end_markers_in_json=on;
複製代碼
2.執行sql語句:
SELECT a,b FROM slow_log WHERE a>8000;
複製代碼
3.獲取結果:
SELECT * FROM information_schema.OPTIMIZER_TRACE
複製代碼
返回的結果以下:
{
"steps": [
{
"join_preparation": {
"select#": 1,
"steps": [
{
"expanded_query": "/* select#1 */ select `slow_log`.`a` AS `a`,`slow_log`.`b` AS `b` from `slow_log` where (`slow_log`.`a` > 8000)"
}
] /* steps */
} /* join_preparation */
},
{
"join_optimization": {
"select#": 1,
"steps": [
{
"condition_processing": {
"condition": "WHERE",
"original_condition": "(`slow_log`.`a` > 8000)",
"steps": [
{
"transformation": "equality_propagation",
"resulting_condition": "(`slow_log`.`a` > 8000)"
},
{
"transformation": "constant_propagation",
"resulting_condition": "(`slow_log`.`a` > 8000)"
},
{
"transformation": "trivial_condition_removal",
"resulting_condition": "(`slow_log`.`a` > 8000)"
}
] /* steps */
} /* condition_processing */
},
{
"substitute_generated_columns": {
} /* substitute_generated_columns */
},
{
"table_dependencies": [
{
"table": "`slow_log`",
"row_may_be_null": false,
"map_bit": 0,
"depends_on_map_bits": [
] /* depends_on_map_bits */
}
] /* table_dependencies */
},
{
"ref_optimizer_key_uses": [
] /* ref_optimizer_key_uses */
},
{
"rows_estimation": [
{
"table": "`slow_log`",
"range_analysis": {
"table_scan": {
"rows": 10337,
"cost": 2092.5
} /* table_scan */,
"potential_range_indexes": [
{
"index": "PRIMARY",
"usable": false,
"cause": "not_applicable"
},
{
"index": "a",
"usable": true,
"key_parts": [
"a",
"id"
] /* key_parts */
}
] /* potential_range_indexes */,
"setup_range_conditions": [
] /* setup_range_conditions */,
"group_index_range": {
"chosen": false,
"cause": "not_group_by_or_distinct"
} /* group_index_range */,
"analyzing_range_alternatives": {
"range_scan_alternatives": [
{
"index": "a",
"ranges": [
"8000 < a"
] /* ranges */,
"index_dives_for_eq_ranges": true,
"rowid_ordered": false,
"using_mrr": false,
"index_only": false,
"rows": 2000,
"cost": 2401,
"chosen": false,
"cause": "cost"
}
] /* range_scan_alternatives */,
"analyzing_roworder_intersect": {
"usable": false,
"cause": "too_few_roworder_scans"
} /* analyzing_roworder_intersect */
} /* analyzing_range_alternatives */
} /* range_analysis */
}
] /* rows_estimation */
},
{
"considered_execution_plans": [
{
"plan_prefix": [
] /* plan_prefix */,
"table": "`slow_log`",
"best_access_path": {
"considered_access_paths": [
{
"rows_to_scan": 10337,
"access_type": "scan",
"resulting_rows": 10337,
"cost": 2090.4,
"chosen": true
}
] /* considered_access_paths */
} /* best_access_path */,
"condition_filtering_pct": 100,
"rows_for_plan": 10337,
"cost_for_plan": 2090.4,
"chosen": true
}
] /* considered_execution_plans */
},
{
"attaching_conditions_to_tables": {
"original_condition": "(`slow_log`.`a` > 8000)",
"attached_conditions_computation": [
] /* attached_conditions_computation */,
"attached_conditions_summary": [
{
"table": "`slow_log`",
"attached": "(`slow_log`.`a` > 8000)"
}
] /* attached_conditions_summary */
} /* attaching_conditions_to_tables */
},
{
"refine_plan": [
{
"table": "`slow_log`"
}
] /* refine_plan */
}
] /* steps */
} /* join_optimization */
},
{
"join_execution": {
"select#": 1,
"steps": [
] /* steps */
} /* join_execution */
}
] /* steps */
}
複製代碼
這個結果主要分爲三個部分:
這裏咱們重點來看一下join_optimization的內容:
上面的表格中,rows和cost最爲重要
因此咱們能夠得出
類型 | 掃描行數 | 消耗資源 |
---|---|---|
全表掃描 | 10337 | 2092.5 |
使用"a"索引 | 2000 | 2401 |
咱們發現全表掃描比使用"a"索引消耗的資源少,因此mysql使用了全表掃描的方案。
4.關閉trace
SET SESSION optimizer_trace="enabled=off"
複製代碼
你們都知道,在mysql中使用的最多索引就是B+樹索引,那麼今天咱們就來了解一下B+樹索引他的數據結構究竟是什麼樣子的。在介紹B+樹以前咱們先來聊一聊其餘咱們經常使用的索引。 好比咱們如今有數據[1,2,3,5,6,7,9]。
咱們先把這個數據放在順序表中。獲得以下結構圖:
接下來咱們要查找是否存在數字6, 可使用二分法查找。
他的時間複雜度爲O{logn}。查詢效率很是高。可是插入的效率就不是特別好了,每次插入都須要把後面的元素向後移動一位,而刪除則是把後面的元素向前一位,修改能夠看做是刪除和插入的合集操做。 插入數字4的邏輯結構圖以下:
刪除數字5的邏輯結構圖以下:
因此由於順序表他的插入效率過低,最好是用來存儲那些一次插入不常變的或只作增量遞增的數據。
二叉樹特色:左節點的值小於根節點,右節點的值大於根節點,而且每一個節點共有兩課樹 咱們根據二叉樹的邏輯結構創建以下結構的二叉樹:
二叉樹的插入不像順序表那麼複雜,他只須要找到插入數字在樹中的位置,修改根節點和自身的索引便可。 舉個栗子: 咱們要插入數字8,我找到值爲7的節點,把7的右節點指向8,8的右節點指向9。
二叉樹的刪除也比較簡單,只須要把刪除節點左子樹最大節點或右子樹最小節點移上來代替本身便可。 舉個栗子: 咱們要刪除數字數字5,咱們能夠把3移動上來,或用6移動上來。
聊完了二叉樹的插入與刪除,咱們再來聊一下二叉樹的查詢,和層級相關,上圖中二叉樹的層級爲3,因此他查詢一個數字,最多隻要三次。可是二叉樹並不僅只有這一種創建方法,他只要知足「左節點的值小於根節點,右節點的值大於根節點,而且每一個節點共有兩課樹」就能夠,咱們來看一個比較極端的二叉樹結構圖。他的層級爲7,並且沒辦法使用二分法,只能逐個查找,效率就很是低了。
上一節咱們知道,二叉樹的搜索和自身的層級有很大的關係,層級越少,檢索效率越高。咱們這裏引出了平衡二叉樹: 平衡二叉樹是一種二叉樹,其中每個節點的左子樹和右子樹的高度差之多等於1。而左子樹深度減去右子樹的深度的值稱爲平衡因子BF。 BF只能夠是(-1,0,1),若是不在這三個值的範圍內,則須要翻轉。 咱們來看一個平衡二叉樹的栗子,如今要構建用平衡二叉樹構建 [3,2,1,4,5]的數組:
剛開始插入「3」和「2」的時候咱們很正常的構建,到插入「1」後,發現3節點的平衡因子變成了2 須要調整,因而向右旋轉。再插入「4」,沒有發生變化,插入「5」時,「3」節點爲「-2」右不平衡了,因而向左旋轉。使樹繼續達到平衡。
上面的栗子是徹底平衡二叉樹(AVL),但平衡二叉樹維護樹平衡的效率太高,因此不少系統中採用紅黑樹,紅黑樹是AVL樹的改進版本。具體實現方法,這裏就不介紹了。
咱們前面討論的各類數據結構,處理樹都是在內存中,所以考慮的都是內存中的運算時間複雜度。但對於mysql而言,大部分數據是存放在硬盤上的,因此硬盤的讀取次數是影響性能的關鍵因素。對於通一個檢索,咱們讀取硬盤幾百次和讀取硬盤幾回是有本質差異的。 咱們以前所說的樹都只存放一個元素。當數據很是多的時候,樹一定會很是大,並且深度很是的深,使得讀硬盤的次數很是多,這是很是影響檢索效率的。因此這使咱們不得不打破一個節點只存一個元素的限制,這就是B樹的由來。 咱們用B樹來構建[1,2,3,5,6,7,9]:
雖然咱們上面說了B樹的不少優勢,但B樹仍是有不少優化的空間,這裏咱們拿B+樹說明一下。
咱們來用B+樹來構建數組[1,2,3,5,6,7,9]。
咱們來分析一下B+樹相對於B樹有什麼不一樣點,以及有什麼優點:
咱們先來建立一張表,而且插入一些數據:
drop table if exists t8;
CREATE TABLE `t8` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`a` int(11) NOT NULL,
`b` char(2) NOT NULL,
PRIMARY KEY (`id`),
KEY `idx_a` (`a`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
insert into t8(a,b) values (1,'a'),(2,'b'),(3,'c'),(5,'e'),(6,'f'),(7,'g'),(9,'i');
複製代碼
而後使用查詢語句,獲得以下結果:
SELECT * FROM t8
複製代碼
「聚簇索引」又稱爲「主鍵索引」,主要是以下結構:
能夠看到,聚簇索引有兩個特色:
「二級索引」也稱爲「普通索引」,主要結構以下:
能夠看到,二級索引有如下兩個特色:
咱們用下面sql語句查詢數據:
SELECT a,b FROM t8 WHERE a=1
複製代碼
這條語句的執行順序以下:
咱們把t8表的二級索引a,改爲(a,b)索引,二級索引結構以下:
alter table t8 drop index idx_a;
create index idx_ab on t8(a,b);
複製代碼
咱們再用下面的sql語句查詢數據:
SELECT a,b FROM t8 WHERE a=1
複製代碼
這條語句查詢順序以下
咱們看,這裏只查詢了二級索引,沒有回表。 咱們把沒有回表的查找稱爲「覆蓋索引」。 由於覆蓋索引能夠減小查詢硬盤的次數,顯著提高性能,因此覆蓋索引是一個經常使用的性能優化手段。
在show_log表中,咱們執行sql語句:
SELECT a,b FROM slow_log WHERE a>8000;
複製代碼
爲何全表掃描相比於"a"索引掃描的行數多,但消耗的資源反而少?如何優化?
mysql有兩種讀的方式,快照讀和當前讀。
mysql有四種隔離級別:
在這一章,咱們不考慮當前讀,重點分析一下快照讀在四種隔離級別的應用。 快照讀在四個隔離級別中應用,會存在三種讀的問題:
類型 | 說明 |
---|---|
髒讀 | 事務A讀取了事務B未提交的數據。 |
不可重複讀 | 事務 A 屢次讀取同一數據,事務 B 在事務A屢次讀取的過程當中,對數據做了更新並提交,致使事務A屢次讀取同一數據時,結果 不一致。 |
幻讀 | 事務A首先根據條件索引獲得N條數據,而後事務B增添了M條符合A搜索條件的數據,致使事務A再次搜索發現有N+M條數據 |
接下來咱們就來分析一下這四種隔離級別和這三個讀問題的關係: 咱們先建立一張表:
DROP TABLE if exists `tran`;
CREATE TABLE `tran` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`a` int(11) DEFAULT NULL,
`b` int(11) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB;
INSERT INTO `tran`(a,b) VALUES(1,1);
複製代碼
而後咱們開啓兩個事務,分別執行以下sql語句:
事務A | 事務B |
---|---|
設置隔離級別 | 設置隔離級別 |
BEGIN; | BEGIN |
SELECT b FROM `tran` WHERE a=1; | |
UPDATE `tran` SET b=2 WHERE a=1; | |
INSERT INTO `tran`(a,b) VALUES(1,3); | |
SELECT b FROM `tran` WHERE a=1; // 記爲VAL1 | |
COMMIT; | |
SELECT b FROM `tran` WHERE a=1; // 記爲VAL2 | |
COMMIT; |
當VAL1中包含 b=2時,表示讀到了未提交的數據,有「髒讀」的問題。 當VAL2中包含 b=2時,表示兩次讀取的數據不一致,有「不可重複讀」的問題。 當VAL2中包含 b=3時,表示讀到了新插入的行,有「幻讀」的問題。
咱們使用下面語句來設置隔離級別
set session transaction isolation level [隔離級別];
// read uncommitted,read committed,repeatable read, serializable
複製代碼
在執行結束以後咱們可使用下面的sql語句來初始化表的狀態
TRUNCATE TABLE `tran`;
INSERT INTO `tran`(a,b) VALUES(1,1);
複製代碼
咱們分別設置四種不一樣的隔離級別,執行上面的sql,獲得以下的結果:
隔離級別 | VAL1 | VAL2 | 髒讀 | 不可重複讀 | 幻讀 |
---|---|---|---|---|---|
read uncommitted | 2,3 | 2,3 | Y | Y | Y |
read committed | 1 | 2,3 | N | Y | Y |
repeatable read | 1 | 1 | N | N | N |
serializable | 1 | 1 | N | N | N |
PS:
咱們在上一節瞭解到,mysql四個隔離級別中,只有RC和RR用到了快照讀。這一節咱們就來分析一下他們是怎麼實現的。
在系統中,每一個事務都有惟一的事務id,叫作"transaction id",在事務開始的時候,是向系統申請的,是嚴格遞增的。
每一行數據,也會用多個版本。每次更新一個事務都會產生一個新的版本,而且把transaction id 賦值給當前數據,叫作"row tx_id"。固然舊的版本會保留。
這裏咱們使用上一節的"tran",裏面有一條數據(id,a,b)= (1,1,1),這裏的"row tx_id" = 10,存儲的邏輯圖以下:
如今咱們執行下面的修改語句:
UPDATE `tran` SET b=2 WHERE a=1; // "transaction id"=20
複製代碼
存儲的邏輯圖以下:
PS:方框裏面的就是undo log(回滾日誌),當事務失敗時,咱們作逆向操做,把undo log中的數據回填就去就能夠實現事務的回滾了。
在瞭解了innerdb的存儲邏輯以後,咱們來分析「RR」隔離級別。在開啓一個新事務的時候,事務會生成一個一致性視圖(Read-View)。 裏面主要包含三個四個參數:
每一行的"row tx_id"在一致性視圖(Read_View)大概能夠分爲三種狀況
咱們仍是使用上一節裏的「tran」表,裏面有一條數據(id,a,b)= (1,1,1),這裏的"row tx_id" = 10,系統目前的事務id爲20,咱們執行以下語句:
事務A | 事務B | 事務C |
---|---|---|
Start transaction with consistent snapshot | ||
Start transaction with consistent snapshot | ||
UPDATE `tran` SET b=2 WHERE a=1; | ||
SELECT b FROM `tran` WHERE a=1; // VAL1 | ||
COMMIT; | ||
SELECT b FROM `tran` WHERE a=1; // VAL2 | ||
Start transaction with consistent snapshot | ||
UPDATE `tran` SET b=3 WHERE a=1; | ||
SELECT b FROM `tran` WHERE a=1; // VAL3 | ||
COMMIT; | ||
SELECT b FROM `tran` WHERE a=1; // VAL4 |
咱們把上面的sql語句轉換成邏輯圖以下:
在圖中咱們能夠知道Read-View是在事務開始的時候生成的。 咱們能夠獲得Read-View以下:
邏輯分析以下:
因此在RR隔離級別下面,VAL1,VAL2,VAL3, VAL4的值都爲「1」,
咱們再來分析一下RC隔離級別,RC隔離級別和RR隔離級別判斷邏輯是一致的,惟一區別是,RR隔離級別在事務開始的時候生成"Read-View", 而RC隔離級別是在每次「SELECT」語句時生成"Read-View"。
RC隔離級別邏輯圖以下:
WX20190926-201003.png
分析:
獲取VAL2時 Read-View以下:
分析:
獲取VAL3時 Read-View以下:
分析:
獲取VAL4時 Read-View以下:
分析:
因此在RC隔離級別下,咱們能夠得出結論: VAL1=1,VAL2=2,VAL3=2,VAL4=3。
慕課網 《一線數據庫工程師帶你深刻理解 MySQL》 s.imooc.com/W2749EM
極客時間 《MYSQL實戰45講》time.geekbang.org/column/intr…
《大話數據結構》
在公司作分享後,小夥伴raywang對於RR隔離級別下是存在幻讀,而且也給出了本身的例子,咱們仍是根據那張tran表,初始值仍是(a=1,b=1)
事務A | 事務B |
---|---|
BEGIN; | |
BEGIN; | |
SELECT b FROM tran WHERE a=1; | |
INSERT INTO tran(a,b) VALUES(1,2); | |
COMMIT; | |
update tran set b=b+10 where a=1; | |
SELECT b FROM tran WHERE a=1; // VAL | |
COMMIT; |
咱們能夠看到最終VAL結果有2個值 「11和12」,產生了幻讀
那麼爲何會幻讀?咱們來分析畫張邏輯圖分析一下
這裏的關鍵是update語句,把兩行數據都改了,使他們的tx_id爲事務A的id。 因此知足以前說的條件1(小於up_limit_id或爲本身自己時可見),因此能夠被查出來。
固然在RR隔離級別下面也一樣會出現不可重複讀的狀況。咱們依然根據那張tran表,初始值仍是(a=1,b=1),來看下面的例子:
事務A | 事務B |
---|---|
BEGIN; | |
BEGIN; | |
SELECT a,b FROM tran WHERE id=1; | |
update tran set a=2 where id=1; | |
COMMIT; | |
update tran set b=2 where id=1; | |
SELECT a,b FROM tran WHERE id=1; // VAL | |
COMMIT; |
咱們在事務A裏面只修改了b=2的值,並無修改a的值,但卻把B事務修改的a讀出來了。至於具體緣由和上面的同樣,這裏可交給各位讀者自行分析。 因此咱們能夠得出結論: 在RR隔離級別下,當一個事務修改了別的事務修改/新增過的數據時,可能會出現不可重複讀和幻讀。
最後,感謝 raywang 給出的很是寶貴的建議,使得此次分享更加的圓滿。