MySQL實(shí)戰(zhàn)45講閱讀筆記-索引

系列
MySQL實(shí)戰(zhàn)45講閱讀筆記-MySQL入門(mén)
MySQL實(shí)戰(zhàn)45講閱讀筆記-日志
MySQL實(shí)戰(zhàn)45講閱讀筆記-鎖
MySQL實(shí)戰(zhàn)45講閱讀筆記-索引
MySQL實(shí)戰(zhàn)45講閱讀筆記-MVCC

索引用于快速查找具有特定值的行。如果沒(méi)有索引,MySQL必須從第一行開(kāi)始,然后讀取整個(gè)表以查找相關(guān)行。表越大,成本越高。如果表中有相關(guān)??列的索引,MySQL可以快速確定要在數(shù)據(jù)文件中間尋找的位置,而無(wú)需進(jìn)行全表掃描,這比按順序讀取每一行要快得多。

索引模型

B Tree是一種平衡多叉樹(shù),是為了磁盤(pán)或其它存儲(chǔ)設(shè)備而設(shè)計(jì)的一種多叉平衡查找樹(shù);為什么這么說(shuō)呢;磁盤(pán)的最小存儲(chǔ)單位是扇區(qū)(sector),而操作系統(tǒng)的塊(block)通常是整數(shù)倍的sector,操作系統(tǒng)以頁(yè)(page)為單位管理內(nèi)存,一頁(yè)(page)通常默認(rèn)為4K,數(shù)據(jù)庫(kù)的頁(yè)通常設(shè)置為操作系統(tǒng)頁(yè)的整數(shù)倍;數(shù)據(jù)庫(kù)操作是以數(shù)據(jù)頁(yè)為基本單位進(jìn)行操作的,讀取某一行數(shù)據(jù)時(shí)是去尋找該行所在的頁(yè),然后把這整個(gè)數(shù)據(jù)頁(yè)(一般數(shù)據(jù)頁(yè)大小為16k,可以查詢(xún)Innodb_page_size)加載進(jìn)內(nèi)存里面,所以說(shuō)加載的內(nèi)存頁(yè)的數(shù)量決定了加載速度;B Tree相比其他的數(shù)據(jù)模型例如平衡二叉樹(shù),它的樹(shù)高遠(yuǎn)遠(yuǎn)小于平衡二叉樹(shù),樹(shù)高小意味著IO次數(shù)少,每個(gè)節(jié)點(diǎn)保存的數(shù)據(jù)也遠(yuǎn)多于二叉樹(shù),這就意味著訪問(wèn)一次磁盤(pán)訪問(wèn)能加載更多的內(nèi)存頁(yè);

B-Tree

B樹(shù)對(duì)比平衡二叉樹(shù)來(lái)看,每個(gè)節(jié)點(diǎn)允許有更多的子節(jié)點(diǎn),每個(gè)節(jié)點(diǎn)都可以?xún)?chǔ)存數(shù)據(jù);
B樹(shù)具有以下的特點(diǎn):

  • 每個(gè)節(jié)點(diǎn)都儲(chǔ)存盡量多的數(shù)據(jù),保證樹(shù)的層級(jí)盡量的少;
  • 查找時(shí)有可能在非葉子節(jié)點(diǎn)結(jié)束,查找性能接近二分查找算法;
B+Tree

相比于B樹(shù),B+樹(shù)的所有數(shù)據(jù)都儲(chǔ)存在葉子節(jié)點(diǎn),非葉子節(jié)點(diǎn)只儲(chǔ)存關(guān)鍵字,各葉子節(jié)點(diǎn)指針進(jìn)行連接;
因?yàn)锽+樹(shù)所有數(shù)據(jù)都儲(chǔ)存在葉子節(jié)點(diǎn),所以在查找關(guān)鍵字的時(shí)候一次性加載進(jìn)內(nèi)存的關(guān)鍵字就越多(因?yàn)榉侨~節(jié)點(diǎn)不保存數(shù)據(jù),每次能加載的key更多),相對(duì)來(lái)說(shuō)讀取IO的次數(shù)就越少;每次查詢(xún)的路徑長(zhǎng)度都是相同的所以查詢(xún)效率相當(dāng),因?yàn)槿~子節(jié)點(diǎn)連接起來(lái)了,可以實(shí)現(xiàn)遍歷葉子節(jié)點(diǎn)就完成整個(gè)樹(shù)的遍歷,這樣在范圍查詢(xún)的時(shí)候效率較高;

Innodb索引

Innodb索引類(lèi)型分為主鍵索引和非主鍵索引,主鍵索引也稱(chēng)為聚簇索引,葉子節(jié)點(diǎn)儲(chǔ)存的是整行的數(shù)據(jù);非主鍵索引也稱(chēng)二級(jí)索引,葉子節(jié)點(diǎn)儲(chǔ)存的是數(shù)據(jù)頁(yè);Innodb的索引和數(shù)據(jù)都是存儲(chǔ)在一個(gè)文件中的,不像MyISAM索引文件和數(shù)據(jù)文件是分離,索引文件只保存數(shù)據(jù)的地址;

假如一個(gè)表結(jié)構(gòu)是下面這個(gè)樣的

mysql> create table test(
id int primary key, 
age int not null, 
name varchar(16),
index (age))engine=InnoDB;
那么對(duì)應(yīng)的索引樹(shù)大概是這樣子的

二級(jí)索引樹(shù)儲(chǔ)存的內(nèi)容是對(duì)應(yīng)的主鍵,聚簇索引節(jié)點(diǎn)存儲(chǔ)的是數(shù)據(jù)頁(yè),所以一般情況下用二級(jí)索引查詢(xún)效率沒(méi)有主鍵查詢(xún)效率高;B+樹(shù)索引并不能找到具體的某一行,只能定位到具體的數(shù)據(jù)頁(yè),把數(shù)據(jù)頁(yè)加載到內(nèi)存后再通過(guò)二分查找法查找;

為了保證索引的有序性,在插入數(shù)據(jù)的時(shí)候可能會(huì)對(duì)樹(shù)做一些調(diào)整,如果現(xiàn)在要添加一個(gè)id為7的數(shù)據(jù),直接添加在5的屁股后面就好了,但是又再添加一個(gè)id為6的數(shù)據(jù)的時(shí)候則需要在邏輯上挪動(dòng)5后面的數(shù)據(jù),這時(shí)不巧5所在的數(shù)據(jù)頁(yè)滿了,按照B+樹(shù)的算法這時(shí)需要申請(qǐng)一個(gè)數(shù)據(jù)頁(yè),然后挪動(dòng)部分?jǐn)?shù)據(jù)過(guò)去,這種情況稱(chēng)為頁(yè)分裂,同時(shí)一個(gè)頁(yè)的數(shù)據(jù)分為了兩個(gè)頁(yè),整體的利用率下降了大約50%;
當(dāng)然有分裂就有合并,當(dāng)相鄰的兩個(gè)數(shù)據(jù)頁(yè)利用率很低的時(shí)候會(huì)將數(shù)據(jù)頁(yè)合并;
所以推薦建表用自增主鍵,這樣插入數(shù)據(jù)的時(shí)候都是追加操作,能帶來(lái)較好的性能;還有主鍵若是占用的字節(jié)較小的話,二級(jí)索引占用的空間也越小,因?yàn)槎?jí)索引節(jié)點(diǎn)的內(nèi)容是主鍵;

覆蓋索引

在查詢(xún)的時(shí)候在二級(jí)索引樹(shù)找到主鍵再回溯主鍵索引樹(shù)稱(chēng)為回表;而直接在二級(jí)索引樹(shù)上面查找到數(shù)據(jù)則稱(chēng)為覆蓋索引,因?yàn)樯倭嘶厮葸@一步驟,減少了樹(shù)的搜索次數(shù),查找性能有效的提高;

最左前綴匹配原則

當(dāng)表中有聯(lián)合索引時(shí),比如有一個(gè)表test是下面這個(gè)樣子

CREATE TABLE `test` (
  `id` int(11) NOT NULL,
  `age` int(11) NOT NULL,
  `name` varchar(16) DEFAULT NULL,
  `pwd` varchar(16) DEFAULT NULL,
  `email` varchar(16) DEFAULT NULL,
  PRIMARY KEY (`id`),
  KEY `name` (`name`,`age`,`pwd`)
) ENGINE=InnoDB DEFAULT CHARSET=latin1;

現(xiàn)在需要查詢(xún)的語(yǔ)句中包含name字段的時(shí)候是用到了索引的

explain select * from test where name = 'ss' and age = 10 and pwd = 'ss';
+----+-------------+-------+------------+------+---------------+------+---------+-------------------+------+----------+-------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref               | rows | filtered | Extra |
+----+-------------+-------+------------+------+---------------+------+---------+-------------------+------+----------+-------+
|  1 | SIMPLE      | test  | NULL       | ref  | name          | name | 42      | const,const,const |    1 |   100.00 | NULL  |
+----+-------------+-------+------------+------+---------------+------+---------+-------------------+------+----------+-------+

但是如果只使用pwd字段查找時(shí)

explain select * from test where pwd = 'ws'
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
| id | select_type | table | partitions | type | possible_keys | key  | key_len | ref  | rows | filtered | Extra       |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+
|  1 | SIMPLE      | test  | NULL       | ALL  | NULL          | NULL | NULL    | NULL |    1 |   100.00 | Using where |
+----+-------------+-------+------------+------+---------------+------+---------+------+------+----------+-------------+

可以看到并沒(méi)有使用到索引,mysql會(huì)一直向右匹配直到遇到范圍查詢(xún)(>、<、between、like)就停止匹配;最左前綴可以是聯(lián)合索引的最左N個(gè)字段,也可以是最左M個(gè)字符;

  • 什么時(shí)候索引會(huì)失效
    1. where條件中有or,即使其中有條件帶索引也不會(huì)使用;如果要想使用or又想讓索引生效,只能將or條件中的每個(gè)列都加上索引;
    2. 對(duì)于多列索引,不是使用的第一部分,則不會(huì)使用索引(即不符合最左前綴原則);
    3. like查詢(xún)是以%開(kāi)頭;
    4. 如果列類(lèi)型是字符串,那一定要在條件中將數(shù)據(jù)使用引號(hào)引用起來(lái),否則不使用索引;
    5. 如果mysql估計(jì)使用全表掃描要比使用索引快,則不使用索引;
索引下推(Index Condition Pushdown)

在Mysql5.6以后引入了索引下推優(yōu)化,可以在索引遍歷過(guò)程中對(duì)索引所包含的字段先做判斷,直接過(guò)濾不滿足的條件,旨在減少回表次數(shù)和減少M(fèi)ySQL server層和引擎層的交互次數(shù);

select * from test where name like 's%' and age = 19;

根據(jù)最左前綴原則,上面查詢(xún)語(yǔ)句只能用到's%'這個(gè)條件來(lái)搜索索引樹(shù)



Innodb在name,age索引樹(shù)內(nèi)部就已經(jīng)判斷了age是否等于19,只取需要的數(shù)據(jù)進(jìn)行回表;

唯一索引

UNIQUE KEY 可以保證不會(huì)出現(xiàn)重復(fù)的值( null 除外),如果能確定某個(gè)數(shù)據(jù)列只能存在彼此各不相同的值,可以使用唯一索引,在插入新數(shù)據(jù)的時(shí)候MySQL會(huì)檢查這個(gè)記錄是否存在;

唯一索引查找

如果使用查詢(xún)語(yǔ)句select * from test where age = 18;,首先根據(jù)索引樹(shù)的根節(jié)點(diǎn)開(kāi)始按層搜索到葉子節(jié)點(diǎn),找到age18所在的數(shù)據(jù)頁(yè),數(shù)據(jù)頁(yè)內(nèi)部通過(guò)二分法定位到所在行;

  • 對(duì)于唯一索引來(lái)說(shuō),查找到滿足第一個(gè)符合條件的行就會(huì)停止搜索;
  • 而對(duì)于普通索引來(lái)說(shuō),查找到滿足第一個(gè)符合條件的行后還會(huì)繼續(xù)搜索,直到找到第一個(gè)不滿足條件的行;

因?yàn)橐媸前错?yè)讀取到內(nèi)存的,意味著如果在數(shù)據(jù)頁(yè)內(nèi)查找一條數(shù)據(jù)和多條數(shù)據(jù)的效率是一樣的,所以唯一索引和普通索引在查找方面的效率沒(méi)有什么區(qū)別;

唯一索引更新

當(dāng)需要更新一個(gè)數(shù)據(jù)頁(yè)的時(shí)候,如果數(shù)據(jù)頁(yè)在內(nèi)存中則直接更新,但是數(shù)據(jù)頁(yè)不在內(nèi)存的時(shí)候,則會(huì)把更新操作緩存在Change Buffer中,下次查詢(xún)這個(gè)數(shù)據(jù)頁(yè)的時(shí)候就把該頁(yè)讀取到內(nèi)存中,然后執(zhí)行Change Buffer中與該頁(yè)相關(guān)的操作,通過(guò)這種方式能保證數(shù)據(jù)邏輯的正確性;

但是對(duì)于唯一索引來(lái)說(shuō),每一次的更新操作都需要判斷該記錄的唯一性,這就必須要把最新的數(shù)據(jù)頁(yè)讀取到內(nèi)存中,再執(zhí)行insert或update操作;
相比使用Change Buffer,這樣做會(huì)導(dǎo)致大量的隨機(jī)IO訪問(wèn),這就是Change Buffer可以避免很多隨機(jī)磁盤(pán)IO的原因;

所以在業(yè)務(wù)可以不需要去重或者可以用業(yè)務(wù)保證數(shù)據(jù)的唯一性的時(shí)候優(yōu)先使用普通索引;

  • ChangeBuffer和RedoLog的關(guān)系

假如現(xiàn)在需要執(zhí)行insert into test(id, name, age) values(1, 'ss', 33), (2, 'sss', 18),再假如前面所插入的values的數(shù)據(jù)頁(yè)1在內(nèi)存中,而后面的values的數(shù)據(jù)頁(yè)2不在內(nèi)存中;
那么會(huì)進(jìn)行以下操作

  1. 在數(shù)據(jù)頁(yè)1直接更新(1, 'ss', 33)
  2. 因?yàn)閿?shù)據(jù)頁(yè)2不在內(nèi)存,所以在內(nèi)存的ChangeBuffer中記錄insert into test(id, name, age) values(2, 'sss', 18)
  3. 將上面兩個(gè)動(dòng)作記錄到redo log中;(redolog同樣會(huì)記錄Change Buffer里面的操作)
    完成后事務(wù)就可以提交了,這里寫(xiě)了兩次內(nèi)存(數(shù)據(jù)頁(yè)1一次,ChangeBuffer一次),寫(xiě)磁盤(pán)一次(redo log寫(xiě)盤(pán));

讀取的時(shí)候假如行在數(shù)據(jù)頁(yè)1中,直接可以從內(nèi)存中返回,假如行在數(shù)據(jù)頁(yè)2中,則需要把數(shù)據(jù)頁(yè)2加載到內(nèi)存中然后根據(jù)Change Buffer里面的日志進(jìn)行更新后才會(huì)讀?。?/p>

  • 寫(xiě)ChangeBuffer時(shí)候MySQL異常退出了有什么影響?

如果在ChangeBuffer merge(ChangeBuffer應(yīng)用到舊數(shù)據(jù)頁(yè)得到新數(shù)據(jù)頁(yè)的過(guò)程稱(chēng)為merge)之后掉電,這部分已經(jīng)應(yīng)用到數(shù)據(jù)頁(yè)上面,所以不需要進(jìn)行恢復(fù);
主要是分析沒(méi)有merge的部分

  1. redolog沒(méi)有完成二階段提交,處于prepare-fsync階段,binlog未fsync,這部分?jǐn)?shù)據(jù)會(huì)丟失;
  2. redolog沒(méi)有完成二階段提交,redolog已經(jīng)fsync但是還未commit,binlog已經(jīng)fsync,這部分?jǐn)?shù)據(jù)可以通過(guò)binlog恢復(fù)redolog,然后通過(guò)redolog恢復(fù)ChangeBuffer;
  3. redolog已經(jīng)commit,可以直接使用redolog恢復(fù)ChangeBuffer;

前綴索引

索引是很方便,但是對(duì)于長(zhǎng)度過(guò)長(zhǎng)的字段建立索引是很耗空間的一種操作,所以就有了前綴索引,選取字段的前n個(gè)字符建立索引可以有效的縮小索引空間,但是也就意味著查詢(xún)時(shí)通過(guò)索引判斷的精度會(huì)有所變小,掃描的行數(shù)會(huì)變多;

  • 語(yǔ)法
ALTER TABLE table_name ADD KEY(column_name(length));

前綴索引只能用在普通索引上面,且使用了前綴索引就不能使用覆蓋索引了,因?yàn)橄到y(tǒng)不確定根據(jù)前綴索引查找出來(lái)的結(jié)果集是否全部滿足需求,必須回到主索引樹(shù)上面再過(guò)濾一遍;所以在選擇使用前綴索引的時(shí)候還是要根據(jù)業(yè)務(wù)來(lái)判斷是否是最優(yōu)選項(xiàng);

MRR優(yōu)化

Multi-Range Read(MRR)是MySQL5.6版本推出來(lái)性能優(yōu)化,主要功能是對(duì)于非聚簇索引查找時(shí)將隨機(jī)IO轉(zhuǎn)換為順序IO;
因?yàn)椴荒鼙WC二級(jí)索引上面儲(chǔ)存的主鍵是順序排序的,那么再回表的時(shí)候讀取的數(shù)據(jù)頁(yè)可能散落在各個(gè)節(jié)點(diǎn)上面,勢(shì)必會(huì)造成隨機(jī)IO讀;

MRR的優(yōu)化就是在回表的過(guò)程中,將二級(jí)索引樹(shù)上查找的主鍵放在一塊叫read_rnd_buffer的內(nèi)存中先保存起來(lái),然后對(duì)buffer的主鍵進(jìn)行排序,排序后的主鍵在表中查找時(shí)效率就會(huì)變高;
buffer的大小由read_rnd_buffer_size參數(shù)控制,如果說(shuō)一次MRR中buffer里面的主鍵越多那么優(yōu)化效果也就越好;

MRR只是在范圍查詢(xún)中有效,optimizer_switch中的mrr控制是否使用MRR優(yōu)化,mrr_cost_based表示優(yōu)化器是否會(huì)計(jì)算mrr的使用成本來(lái)決定是否使用mrr機(jī)制;

Order By

Innodb里面有兩種排序的方式,索引排序filesort排序,所謂的索引排序是依賴(lài)B+樹(shù)結(jié)構(gòu)中數(shù)據(jù)一定是有序的;而filesort排序則是讀出來(lái)的數(shù)據(jù)集需要經(jīng)過(guò)額外的排序操作;

select a, b, c from test where a(非聚簇索引字段)='a' order by b(非索引字段) limit 10000;

上面語(yǔ)句的流程大概是這樣的

  1. 初始化sort_buffer,sort_buffer是排序時(shí)候用到的內(nèi)存,是每個(gè)線程獨(dú)有的,由參數(shù)sort_buffer_size控制大??;(還有一個(gè)Innodb_sort_buffer_size參數(shù),這個(gè)參數(shù)是指在創(chuàng)建innodb索引期間用于對(duì)數(shù)據(jù)排序的排序緩存區(qū)大小)
  2. 根據(jù)索引a找到主鍵值,回到主索引樹(shù)上面取出a、b、c和主鍵的值存入sort_buffer中;
  3. 從索引a中繼續(xù)取下一個(gè)主鍵的值,重復(fù)步驟2和3直到找到所有滿足的行;
  4. 在sort_buffer中對(duì)數(shù)據(jù)按照b做快排;
  5. 取排序后的前10000條數(shù)據(jù)當(dāng)作結(jié)果集返回;

其中如果需要排序的數(shù)據(jù)小于sort_buffer_size,那么排序可以在內(nèi)存中完成,如果大于緩存區(qū),則需要借助磁盤(pán)的臨時(shí)文件輔助排序;

使用下面這個(gè)方法可以確定一個(gè)排序語(yǔ)句是否用到了臨時(shí)文件

/* 打開(kāi) optimizer_trace,只對(duì)本線程有效 */
SET optimizer_trace='enabled=on'; 
/* @a 保存 Innodb_rows_read 的初始值 */
select VARIABLE_VALUE into @a from  performance_schema.session_status where variable_name = 'Innodb_rows_read';
/* 執(zhí)行語(yǔ)句 */
select a, b, c from test where a(非聚簇索引字段)='a' order by b(非索引字段) limit 10000;
/* 查看 OPTIMIZER_TRACE 輸出 */
SELECT * FROM `information_schema`.`OPTIMIZER_TRACE`
/* @b 保存 Innodb_rows_read 的當(dāng)前值 */
select VARIABLE_VALUE into @b from performance_schema.session_status where variable_name = 'Innodb_rows_read';
/* 計(jì)算 Innodb_rows_read 差值 */
select @b-@a; #得到36764

### 關(guān)鍵部分
"filesort_summary": {
    "rows": 36764,
    "examined_rows": 36764,
    "number_of_tmp_files": 6,
    "sort_buffer_size": 262144,
    "sort_mode": "<sort_key, packed_additional_fields>"
}

number_of_tmp_files表示使用了6個(gè)臨時(shí)文件,examined_rows表示有36764行數(shù)據(jù)參與排序,packed_additional_fields表示排序過(guò)程中對(duì)字符串做緊湊處理;

現(xiàn)在執(zhí)行下面這個(gè)語(yǔ)句

SET max_length_for_sort_data = 16;

然后再一次執(zhí)行一次上面的語(yǔ)句

select @b-@a; #得到46764
"filesort_summary": {
     "rows": 36764,
     "examined_rows": 36764,
     "number_of_tmp_files": 4,
     "sort_buffer_size": 262136,
     "sort_mode": "<sort_key, rowid>"
}

max_length_for_sort_data是控制用于排序行數(shù)據(jù)的最大長(zhǎng)度,假設(shè)abc三個(gè)字段長(zhǎng)度為36,改成16之后不能將abc都裝入sort_buffer中,所以MySQL會(huì)選擇只把主鍵和將要排序的b字段放入sort_buffer中,所以排序的流程變成了下面這樣

  1. 初始化sort_buffer,執(zhí)行器查看表定義,發(fā)現(xiàn)abc三個(gè)字段長(zhǎng)度大于max_length_for_sort_data,確認(rèn)放入的字段有b和主鍵字段;
  2. 執(zhí)行器調(diào)用儲(chǔ)存引擎的查找接口,根據(jù)索引a找到主鍵值,回到主索引樹(shù)上面取出b的值和主鍵值存入sort_buffer中;
  3. 從索引a中繼續(xù)取下一個(gè)主鍵的值,重復(fù)步驟2和3直到找到所有滿足的行;
  4. 在sort_buffer中對(duì)數(shù)據(jù)按照b做快排;
  5. 遍歷排序結(jié)果取前10000行并按照主鍵值回到原表取出abc三個(gè)字段返回;

sort_buffer不屬于innodb引擎內(nèi)部,所以需要兩次調(diào)用innodb引擎的查找接口,這就是方式二innodb讀取行數(shù)比方式一多1w行的原因;
兩種排序各有優(yōu)缺點(diǎn),方式一需要?jiǎng)?chuàng)建更多的臨時(shí)文件,但是查詢(xún)數(shù)據(jù)的次數(shù)少,方式二則相反;

當(dāng)然更好的方法就是不使用filesort排序直接使用索引排序,比如建立(city, name, age)的聯(lián)合索引;

JOIN

多表查詢(xún)的時(shí)候可以使用join關(guān)鍵字,合理的join可以帶來(lái)較好的性能,但是往往使用不當(dāng)?shù)那闆r較多,有些則干脆不推薦使用,像淘寶的阿里巴巴Java開(kāi)發(fā)手冊(cè)就有一條超過(guò)3個(gè)表就禁止使用join;到底join該怎么用還是需要了解關(guān)于它的基本信息;

JOIN的類(lèi)型

Cross Join

返回表a和表b的笛卡爾乘積,即是a中所有行*b中所有行;

select * from a cross join b
Inner Join

inner join(內(nèi)連接)是選取驅(qū)動(dòng)表的數(shù)據(jù)集去匹配被驅(qū)動(dòng)表的數(shù)據(jù)集中符合條件的集合;當(dāng)不存在任何條件時(shí),返回的結(jié)果和cross join是一樣的, 當(dāng)使用on ...返回的相當(dāng)于兩個(gè)表中符合條件的交集;

select * from a inner join b on condition where ...

當(dāng)使用join或inner join時(shí),mysql會(huì)選擇數(shù)據(jù)量比較小的表作為驅(qū)動(dòng)表,大表作為被驅(qū)動(dòng)表;

Left Join

left join(左連接)查找的結(jié)果集包括表a的所有行并顯示對(duì)應(yīng)匹配表b的行,對(duì)于表a存在而表b不存在的行則顯示null;如果要使用left\right join,對(duì)于等值判斷或不等值判斷就只能寫(xiě)到on里不能寫(xiě)在where中;

select * from a left join b on condition where ...

當(dāng)使用left join時(shí),左表是驅(qū)動(dòng)表,右表是被驅(qū)動(dòng)表;

Right Join

right join和left join相反,顯示表b所有行和對(duì)應(yīng)匹配表a的行,不存在則顯示null;

select * from a right join b on condition where ...

當(dāng)使用right join時(shí),右表時(shí)驅(qū)動(dòng)表,左表是驅(qū)動(dòng)表;

Join的原理

Join使用的是Nested-Loop Join算法,即是驅(qū)動(dòng)表的數(shù)據(jù)作為循環(huán)主體,然后把符合條件的行再拿去和被驅(qū)動(dòng)表中符合條件的行組成結(jié)果集;NLJ算法總共細(xì)分了3種類(lèi)型;

  • Simple Nested-Loop Join
    拿驅(qū)動(dòng)表所有的數(shù)據(jù)去被驅(qū)動(dòng)表逐條匹配查找符合條件的行,每一次去被驅(qū)動(dòng)表查找的時(shí)候走的是全表掃描,假設(shè)驅(qū)動(dòng)表行數(shù)是n,被驅(qū)動(dòng)表行數(shù)位m,那么總共掃描的行數(shù)為n*m;

  • Index Nested-Loop Join
    和上面的相比最大的不同是被驅(qū)動(dòng)表中關(guān)聯(lián)的字段是有索引的,在查找的時(shí)候走的是樹(shù)搜索,一次樹(shù)查詢(xún)的時(shí)間復(fù)雜度為log?m,如果被驅(qū)動(dòng)表所關(guān)聯(lián)的字段是非聚簇索引的話,還需要回表到主索引樹(shù)上,那么時(shí)間復(fù)雜度是2 * log?m,所以整個(gè)查找是驅(qū)動(dòng)表行數(shù)n * 2 * log?m;在explain中發(fā)現(xiàn)沒(méi)有關(guān)鍵字Using join buffer(Block Nested Loop)表明使用的就是NLJ算法;

  • Block Nested-Loop Join
    被驅(qū)動(dòng)表上面沒(méi)有可用索引,那么是不會(huì)選擇Simple Nested-Loop Join,而是Block Nested-Loop Join
    會(huì)把驅(qū)動(dòng)表的數(shù)據(jù)逐一放到一個(gè)叫做join_buffer的內(nèi)存中,然后再到被驅(qū)動(dòng)表的每一行數(shù)據(jù)拿出來(lái)到join_buffer中進(jìn)行比較,符合條件的數(shù)據(jù)作為結(jié)果集返回;
    join_buffer的大小由參數(shù)join_buffer_size控制的,如果buffer中不能一次性裝下驅(qū)動(dòng)表的數(shù)據(jù),則會(huì)再buffer裝滿的時(shí)候和被驅(qū)動(dòng)表數(shù)據(jù)對(duì)比,對(duì)比完后清空buffer再繼續(xù)裝驅(qū)動(dòng)表的數(shù)據(jù),直到完成join;
    Simple Nested-Loop Join對(duì)比這個(gè)算法的優(yōu)勢(shì)是判斷是在內(nèi)存中完成的,效率較高,join_buffer也是影響join速度的因素之一,越大的buffer,被驅(qū)動(dòng)表被掃描次數(shù)就越少,IO次數(shù)也就越少;如果被驅(qū)動(dòng)表掃描次數(shù)變多,可能會(huì)造成Innodb buffer pool中的Young區(qū)域被被驅(qū)動(dòng)表的數(shù)據(jù)頁(yè)填滿,以至于正常訪問(wèn)的熱點(diǎn)數(shù)據(jù)頁(yè)被淘汰掉影響內(nèi)存命中率;

  • Batched Key Access
    MySQL5.6后引入了BKA算法,是對(duì)NLJ算法的優(yōu)化,NLJ算法時(shí)從驅(qū)動(dòng)表中遍歷的數(shù)據(jù)一行行的拿去和被驅(qū)動(dòng)表中匹配,而B(niǎo)KA算法則是從驅(qū)動(dòng)表中拿出來(lái)的數(shù)據(jù)先儲(chǔ)存在join_buffer中進(jìn)行排序,然后使用MRR接口中去被驅(qū)動(dòng)表中查找對(duì)應(yīng)行;BKA算法提升性能的地方在于將原本一行一行的與被驅(qū)動(dòng)表進(jìn)行對(duì)比改成了批量拿去和被驅(qū)動(dòng)表對(duì)比,將隨機(jī)IO轉(zhuǎn)換成了順序IO;
    如果是多表join,那么每一次join得到的數(shù)據(jù)集會(huì)新增一個(gè)join_buffer用來(lái)存在下一次join的數(shù)據(jù)集;
    BNL和BKA都是用到了join_buffer,區(qū)別在于BKA用于被驅(qū)動(dòng)表有可用索引的情況下,而B(niǎo)NL是用于被驅(qū)動(dòng)表上沒(méi)有可用索引;
    啟用BKA之前需要啟動(dòng)MRR優(yōu)化,然后設(shè)置set global optimizer_switch = 'batched_key_access=on';

  • Join的優(yōu)化

  1. 用小表做驅(qū)動(dòng)表,盡量減少join語(yǔ)句中的Nested Loop的循環(huán)總次數(shù);
  2. 對(duì)被驅(qū)動(dòng)表的join字段上建立索引;
  3. 使用臨時(shí)表建立索引,把原本被驅(qū)動(dòng)表上的數(shù)據(jù)插入到臨時(shí)表中,使用臨時(shí)表作為被驅(qū)動(dòng)表從而用上索引;
  4. 當(dāng)被驅(qū)動(dòng)表的join字段上無(wú)法建立索引的時(shí)候,設(shè)置足夠的Join Buffer Size。

join查詢(xún)?cè)谟兴饕龡l件下

  • 驅(qū)動(dòng)表有索引不會(huì)使用到索引
  • 被驅(qū)動(dòng)表建立索引會(huì)使用到索引

Group By

Group By主要有3種方式實(shí)現(xiàn)

  • 松散索引掃描(Loose Index Scan)
    依賴(lài)于索引的有序性直接使用索引,且只需掃描部分的索引而不用掃描全部索引,所以稱(chēng)為松散索引掃描;使用松散索引的條件是

    1. 只能單表查詢(xún)
    2. group by選擇的列還滿足最左前綴原則且沒(méi)有其他多余的列,如果有多余的列必須是以常量形式存在;
    3. 使用聚合函數(shù)(min、max)的列必須在group by的列里面;
    4. 只能對(duì)列的整個(gè)值進(jìn)行g(shù)roup by,比如c varchar(20),索引必須是index(c(20))而不能是前綴索引如index(c(10));
  • 緊湊索引掃描(Tight Index Scan)
    緊湊索引掃描可以是完整索引掃描也可以是范圍索引掃描,如果不滿足松散索引掃描的條件,則使用緊湊索引掃描,緊湊索引掃描仍然可以避免使用臨時(shí)表來(lái)進(jìn)行額外的排序;

  • 創(chuàng)建臨時(shí)表實(shí)現(xiàn)Group by
    創(chuàng)建一張內(nèi)存臨時(shí)表用來(lái)保存這個(gè)groupby的所有關(guān)聯(lián)字段然后再在臨時(shí)表中排序后得到結(jié)果集返回給客戶端;
    在explain中顯示Using index; Using temporary; Using filesort分別表示使用覆蓋索引不需要回表,使用臨時(shí)表和使用filesort排序;如果不需要排序可以在語(yǔ)句后面添加order by null;
    參數(shù)tmp_table_size是控制臨時(shí)表的大小,默認(rèn)16M,如果大于這個(gè)則會(huì)在磁盤(pán)中創(chuàng)建臨時(shí)表;

  • 優(yōu)化總結(jié)

    1. 盡量使用索引,確保explain中不會(huì)出現(xiàn)Using temporary; Using filesort
    2. 如果group by沒(méi)有排序要求,在語(yǔ)句后面添加order by null,在使用臨時(shí)表的時(shí)候能避免使用sort_buffer;
    3. 使用臨時(shí)表時(shí)可以適當(dāng)調(diào)大tmp_table_size來(lái)滿足groupby,避免使用磁盤(pán)臨時(shí)文件;

參考

https://draveness.me/sql-index-intro

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
平臺(tái)聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點(diǎn),簡(jiǎn)書(shū)系信息發(fā)布平臺(tái),僅提供信息存儲(chǔ)服務(wù)。

推薦閱讀更多精彩內(nèi)容