全表掃描
假設,現在對一個200G的innodb的表,做全表掃描,把掃描結果保存在客戶端。
mysql ‑h$host ‑P$port ‑u$user ‑p$pwd ‑e "select * from db1.t" > $target_file
這個語句的結果集存在哪里呢?
- 實際上,服務端並不需要保存一個完整的結果集。取數據和發數據流程如下:
- 獲取一行,寫到net_buffer中。這塊內存的大小是由參數net_buffer_length定義的,默認是
16k。 - 重復獲取行,直到net_buffer寫滿,調用網絡接口發出去。
- 如果發送成功,就清空net_buffer,然后繼續取下一行,並寫入net_buffer。
- 如果發送函數返回EAGAIN或WSAEWOULDBLOCK,就表示本地網絡棧(socket send
buffer)寫滿了,進入等待。直到網絡棧重新可寫,再繼續發送。
- 獲取一行,寫到net_buffer中。這塊內存的大小是由參數net_buffer_length定義的,默認是
- 一個查詢在發送過程中,占用mysql內部的內存最大的就是net_buffer_length,並不會達到200G
- socket send buffer 也不可能達到200G
- 也就是說,MySQL是“邊讀邊發的”,這個概念很重要。這就意味着,如果客戶端接收得慢,會
導致MySQL服務端由於結果發不出去,這個事務的執行時間變長。
全表掃描對innodb的影響
- innodb內存的一個作用是保存更新的結果,再配合redo log,避免了隨機斜盤。
- 內存的數據頁在Buffer Pool中,在wal里,buffer pool即起到了加速更新的作用,也有個作用是加速查詢。其依賴一個重要指標:內存命中率。
- 執行 show engine innodb status 可以看到命中率
- 如果所有查詢需要的數據頁都能夠直接從內存得到,那是最好的,對應的命中率就是100%。
但,這在實際生產上是很難做到的。 - InnoDB Buffer Pool的大小是由參數 innodb_buffer_pool_size確定的,一般建議設置成可用物理內存的60%~80%。
- InnoDB內存管理用的是最近最少使用 (Least Recently Used, LRU)算法,這個算法的核心就是淘汰最久未使用的數據。
- 全表掃描還是比較耗費IO資源的,所以業務高峰期還是不能直接在線上主庫執行全表掃描的。
mysql如何使用join
CREATE TABLE `t2` (
`id` int(11) NOT NULL,
`a` int(11) DEFAULT NULL,
`b` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `a` (`a`)
) ENGINE=InnoDB;
可以看到,這兩個表都有一個主鍵索引id和一個索引a,字段b上無索引。假設表t2有1000行數據,表t1是100行數據。
-
select * from t1 straight_join t2 on (t1.a=t2.a);
straight_join可以讓mysql使用固定的連接方式執行join,這個語句里,ti是驅動表,t2是被驅動表。執行流程如下:
- 從表t1讀入一行數據R
- 從R中,取出a到t2里查找
- 取出t2中滿足的行和R一行,作為結果集的一部分
- 重復以上步驟,直到t1的循環結束。
這個過程是先遍歷表t1,然后根據從表t1中取出的每行數據中的a值,去表t2中查找滿足條件的記
錄。在形式上,這個過程就跟我們寫程序時的嵌套查詢類似,並且可以用上被驅動表的索引,所
以我們稱之為“Index Nested-Loop Join”,簡稱NLJ。流程圖如下:
t1做了全表掃描,100行,t2是走了樹搜索過程,也是100行。整個執行流程,總掃描行數是200。
-
假設不適用join,用單表查詢。
- 執行select * from t1,查出表t1的所有數據,這里有100行;
- 循環遍歷這100行數據:
- 從每一行R取出字段a的值$R.a;
- 執行select * from t2 where a=$R.a;
- 把返回的結果和R構成結果集的一行。
可以看到,在這個查詢過程,也是掃描了200行,但是總共執行了101條語句,比直接join多了
100次交互。除此之外,客戶端還要自己拼接SQL語句和結果。
如何選擇驅動表
- 之前的join語句執行過程中,驅動表走的是全表掃描,被驅動表走樹搜索。
- 假設被驅動表的行數是M,每次在被驅動表插一行數據,先搜索索引a,再搜索主鍵索引,每次搜索一次近似復雜度是logM,在被驅動表上查一行的時間復雜度是2*logM。
- 假設驅動表行數是N,搜索過程就要掃描驅動表N行,對於每一行,到被驅動表匹配一次,因此整個過程復雜度近似N+N*2*logM,顯然,N對掃描行數的影響更大,應該讓小表做驅動表。
- 小結:
- 使用join語句,性能比強行拆成多個單表執行sql語句性能要好
- 如果使用join語句,需要讓小表做驅動表。
- 注意:前提是可以使用被驅動表的索引。
Simple Nested-Loop Join
select * from t1 straight_join t2 on (t1.a=t2.b);
由於表t2的字段b沒有索引,因此再用上圖的流程去匹配的時候,就要做一次全表掃描。
那么這一次掃描的行數就是100*1000 = 10萬行。這個就叫Simple Nested-Loop Join,
很笨重,下面是mysql使用的另一種算法Block Nested-Loop Join
Block Nested-Loop Join
-
把表t1的數據讀入線程內存join_buffer中,由於是select * ,因此是把整個表t1放入內存
-
掃描表t2,把t2中每一行取出來,和join_buffer中的數據做對比,滿足join條件的,作為結果集的一部分返回。
所以,在整個過程中,只對t1和t2都做了一次全表掃描,總行數是1100,由於join_buffer是無序數組的方式組織的,因此對表t2中的每一行,都要做100次判斷,總共次數是10萬次,從時間復雜度來看,兩者是一樣的,但BNL的算法是內存上的操作,速度會更快,性能頁更好。
-
如何選擇驅動表
假設小標N行,大表M行,掃描的總行數是M+N,內存判斷次數是M*N,所以選擇大表還是小標,耗時是一樣的。
如果t1是大表,join_buffer會不會放不下呢?
join_buffer可以設置大小,默認是256k,如果放不下t1的所有數據,就分段放。流程如下:
- 掃描表t1,順序讀取數據行放入join_buffer中,放完第88行join_buffer滿了,繼續第2步;
- 掃描表t2,把t2中的每一行取出來,跟join_buffer中的數據做對比,滿足join條件的,作為結果集的一部分返回;
- 清空join_buffer;
- 繼續掃描表t1,順序讀取最后的12行數據放入join_buffer中,繼續執行第2步。
所以這個算法體現了BLOCK,表示分塊去join,判讀的總次數是不變的。再看一下驅動表的選擇問題。比如100*1000 = (88+12)*1000,分塊越多,被驅動表掃描次數也就越多。
驅動表的數據行數是N,需要分K段才能完成算法流程,被驅動表的數據行數是M。
這里的K不是常數,N越大K就會越大,因此把K表示為λ*N,顯然λ的取值范圍是(0,1)。
- 掃描行數是 N+λ*N*M;
- 內存判斷 N*M次。
內存判斷次數是不受選擇哪個表作為驅動表影響的。而考慮到掃描行數,在M和N大小確定的情況下,N小一些,整個算式的結果會更小。所以結論是,應該讓小表當驅動表。
-
小表的定義
我們前面的例子是沒有加條件的。如果我在語句的where條件加上 t2.id<=50這個限定條件,再來看下這兩條語句:
select * from t1 straight_join t2 on (t1.b=t2.b) where t2.id<=50; select * from t2 straight_join t1 on (t1.b=t2.b) where t2.id<=50;
如果是用第二個語句的話,join_buffer只需要放入t2的前50行,顯然是更好的。所以這里,“t2的前50行”是那個相對小的表,也就是“小表”。
select t1.b,t2.* from t1 straight_join t2 on (t1.b=t2.b) where t2.id<=100; select t1.b,t2.* from t2 straight_join t1 on (t1.b=t2.b) where t2.id<=100;
這個例子里,表t1 和 t2都是只有100行參加join。但是,這兩條語句每次查詢放入join_buffer中的數據是不一樣的:
- 表t1只查字段b,因此如果把t1放到join_buffer中,則join_buffer中只需要放入b的值;
- 表t2需要查所有的字段,因此如果把表t2放到join_buffer中的話,就需要放入三個字段id、a和b。我們應該選擇表t1作為驅動表。也就是說在這個例子里,“只需要一列參與join的表t1”是那個相對小的表。
- 在決定哪個表做驅動表的時候,應該是兩個表按照各自的條件過濾,過濾完
成之后,計算參與join的各個字段的總數據量,數據量小的那個表,就是“小表”,應該作為驅動表。