說明:此文章並不是原創,參考極客時間文章《MySQL實戰45講》作的一些筆記,方便本身查閱,有興趣能夠自行去極客時間閱讀,內容很是給力。java
Innodb:mysql
Page是Innodb存儲的最基本結構,也是Innodb磁盤管理的最小單位,與數據庫相關的全部內容都存儲在Page結構裏。Page分爲幾種類型:數據頁(B-Tree Node)
算法
Undo頁(Undo Log Page)
,系統頁(System Page)
,事務數據頁(Transaction System Page)
等;sql
每一個數據頁的大小爲16kb
,每一個Page使用一個32位(一位表示的就是0或1)的int值來表示,正好對應Innodb最大64TB的存儲容量(16kb * 2^32=64tib)數據庫
1)能夠經過自動增加列,方法是auto_increment。 2)支持事務。默認的事務隔離級別爲可重複度,經過MVCC(併發版本控制)來實現的。 3)使用的鎖粒度爲行級鎖,能夠支持更高的併發; 4)支持外鍵約束;外鍵約束其實下降了表的查詢速度,可是增長了表之間的耦合度。 5)在InnoDB中存在着緩衝管理,經過緩衝池,將索引和數據所有緩存起來,加快查詢的速度; 6)對於InnoDB類型的表,其數據的物理組織形式是聚簇表。全部的數據按照主鍵來組織。數據和索引放在一塊,都位於B+數的葉子節點上; InnoDB的存儲表和索引也有下面兩種形式: 1:使用共享表空間存儲:全部的表和索引存放在同一個表空間中。 2:使用多表空間存儲:表結構放在frm文件,數據和索引放在IBD文件中。分區表的話,每一個分區對應單獨的IBD文件,分區表的定義能夠查看個人其餘文章。使用分區表 的好處在於提高查詢效率。
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------數組
MyISAM緩存
MyISAM擁有較高的插入、查詢速度,但不支持事物,不支持外鍵,鎖的最小粒度爲表鎖,不支持行鎖。安全
使用這個存儲引擎,每一個MyISAM在磁盤上存儲成三個文件;session
(1)frm文件:存儲表的定義數據mysql優化
(2)MYD文件:存放表具體記錄的數據
(3)MYI文件:存儲索引
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Memory
Memory 採用的方案式使用內存當存儲介質,優勢式響應速度快。但存儲到內存上也致使了一個致命的問題就是當mySql進程崩潰的時數據會丟失。此外Memory對
存儲的數據有必定的要求,要求存儲的是長度不變的數據。
Memory索引支持 1)散列索引:散列索引的使用場景是數據查找時使用 == 匹配,但範圍查詢(<=, >=, <, >)較慢 2)B樹索引:B樹索引可使用部分查詢和通配查詢,範圍查詢較快 Memory使用場景: 1)數據量小、訪問很是頻繁、在內存中存放數據,數據量過大會致使內存溢出。能夠經過參數max_heap_table_size控制Memory表的大小,限制Memory表的最大的大小。 2)數據是臨時數據,並且當即可用到。那麼就比較合適存放在內存中。 3)存儲在表中的數據若是丟失也沒太大關係,不會形成損失。
注意:這裏講的是基於Innodb引擎下的狀況。
redo log 和 binlog 區別
redo log 是 InnoDB 引擎特有的;binlog 是 MySQL 的 Server 層實現的,全部引擎均可以使用。
redo log 是物理日誌,記錄的是「在某個數據頁上作了什麼修改」;binlog 是邏輯日誌,記錄的是這個語句的原始邏輯,好比「給 ID=2 這一行的 c 字段加 1 」。
redo log 是循環寫的,空間固定會用完;binlog 是能夠追加寫入的。「追加寫」是指 binlog 文件寫到必定大小後會切換到下一個,並不會覆蓋之前的日誌。
兩階段提交圖:
兩階段提交:先把此次更新寫入到redolog中,並設redolog爲prepare狀態,而後再寫入binlog,寫完binlog以後再提交事務,並設redolog爲commit狀態。也就是把relolog拆成了prepare和commit兩段!
爲何必須有「兩階段提交」呢?這是爲了讓兩份日誌之間的邏輯一致。
不使用兩階段提交的狀況下:
1)先寫 redo log 後寫 binlog
假設在 redo log 寫完,binlog 尚未寫完的時候,MySQL 進程異常重啓。因爲咱們前面說過的,redo log 寫完以後,系統即便崩潰,仍然可以把數據恢復回來,因此恢復後這一行 c 的值是 1。 可是因爲 binlog 沒寫完就 crash 了,這時候 binlog 裏面就沒有記錄這個語句。所以,以後備份日誌的時候,存起來的 binlog 裏面就沒有這條語句。 而後你會發現,若是須要用這個 binlog 來恢復臨時庫的話,因爲這個語句的 binlog 丟失,這個臨時庫就會少了這一次更新,恢復出來的這一行 c 的值就是 0,與原庫的值不一樣。 2)先寫 binlog 後寫 redo log
若是在 binlog 寫完以後 crash,因爲 redo log 還沒寫,崩潰恢復之後這個事務無效,因此這一行 c 的值是 0。可是 binlog 裏面已經記錄了「把 c 從 0 改爲 1」這個日誌。因此,在以後用 binlog 來
恢復的時候就多了一個事務出來,恢復出來的這一行 c 的值就是 1,與原庫的值不一樣。
mysql崩潰恢復時的判斷規則: 1:若是 redo log 裏面的事務是完整的,也就是已經有了 commit 標識,則直接提交; 2:若是 redo log 裏面的事務只有完整的 prepare,則判斷對應的事務 binlog 是否存在並完整: a. 若是是,則提交事務; b. 不然,回滾事務。
redo log 和 binlog 是怎麼關聯起來的?
它們有一個共同的數據字段,叫 XID。崩潰恢復的時候,會按順序掃描 redo log:
1) 若是碰到既有 prepare、又有 commit 的 redo log,就直接提交;
2) 若是碰到只有 parepare、而沒有 commit 的 redo log,就拿着 XID 去 binlog 找對應的事務。
InnoDB 的 redo log 是固定大小的,好比能夠配置爲一組 4 個文件,每一個文件的大小是 1GB,那麼這塊「粉板」總共就能夠記錄 4GB 的操做。從頭開始寫,寫到末尾就又回到開頭
循環寫,以下面這個圖所示。
innodb事務日誌包括 redo log 和undo log。redo log是重作日誌,提供前滾操做,undo log是回滾日誌,提供回滾操做。
undo log不是redo log的逆向過程,其實它們都算是用來恢復的日誌:
1.redo log一般是物理日誌,記錄的是數據頁的物理修改,而不是某一行或某幾行修改爲怎樣怎樣,它用來恢復提交後的物理數據頁(恢復數據頁,且只能恢復到最後一次提交的位置)。 2.undo log用來回滾行記錄到某個版本。undo log通常是邏輯日誌,根據每行記錄進行記錄
write pos 是當前記錄的位置,一邊寫一邊後移,寫到第 3 號文件末尾後就回到 0 號文件開頭。
checkpoint 是當前要擦除的位置,也是日後推移而且循環的,擦除記錄前要把記錄更新到數據文件。 write pos 和 checkpoint 之間的是「粉板」上還空着的部分,能夠用來記錄新的操做。若是 write pos 追上 checkpoint,表示「粉板」滿了,這時候不能再執行新的更新,得停下來先擦掉一些記錄,把 checkpoint 推動一下。 有了 redo log,InnoDB 就能夠保證即便數據庫發生異常重啓,以前提交的記錄都不會丟失,這個能力稱爲crash-safe。
redo log 可能存在的三種狀態提及。這三種狀態,對應的就是圖 中的三個顏色塊。
這三種狀態分別是:
存在 redo log buffer 中,物理上是在 MySQL 進程內存中,就是圖中的紅色部分;
寫到磁盤 (write),可是沒有持久化(fsync),物理上是在文件系統的 page cache 裏面,也就是圖中的黃色部分;
持久化到磁盤,對應的是 hard disk,也就是圖中的綠色部分。
日誌寫到 redo log buffer 是很快的,wirte 到 page cache 也差很少,可是持久化到磁盤的速度就慢多了。
爲了控制 redo log 的寫入策略,InnoDB 提供了 innodb_flush_log_at_trx_commit 參數,它有三種可能取值: 1) 設置爲 0 的時候,表示每次事務提交時都只是把 redo log 留在 redo log buffer 中 ; 2) 設置爲 1 的時候,表示每次事務提交時都將 redo log 直接持久化到磁盤; 3) 設置爲 2 的時候,表示每次事務提交時都只是把 redo log 寫到 page cache(硬盤緩存區,mysql掛掉數據不會丟失,只有斷電時纔會丟失)。 InnoDB 有一個後臺線程,每隔 1 秒,就會把 redo log buffer 中的日誌,調用 write 寫到文件系統的 page cache,而後調用 fsync 持久化到磁盤。
-------------------------------------------------------------------------------------------------------------------------------------------------------------------------
實際上,除了後臺線程每秒一次的輪詢操做外,還有兩種場景會讓一個沒有提交的事務的 redo log 寫入到磁盤中。
1. 一種是,redo log buffer 佔用的空間即將達到 innodb_log_buffer_size 一半的時候,後臺線程會主動寫盤。注意,因爲這個事務並無提交,因此這個寫盤動做只是 write,而沒有調用 fsync,也就是隻留在了
文件系統的 page cache。
2. 另外一種是,並行的事務提交的時候,順帶將這個事務的 redo log buffer 持久化到磁盤。假設一個事務 A 執行到一半,已經寫了一些 redo log 到 buffer 中,這時候有另一個線程的事務 B 提交,若是
innodb_flush_log_at_trx_commit 設置的是 1,那麼按照這個參數的邏輯,事務 B 要把 redo log buffer 裏的日誌所有持久化到磁盤。這時候,就會帶上事務 A 在 redo log buffer 裏的日誌一塊兒持久化到磁盤。
咱們說 MySQL 的「雙 1」配置,指的就是 sync_binlog 和 innodb_flush_log_at_trx_commit 都設置成 1。也就是說,一個事務完整提交前,須要等待兩次刷盤,一次是 redo log(prepare 階段),一次是 binlog。
若是你的 MySQL 如今出現了性能瓶頸,並且瓶頸在 IO 上,能夠經過哪些方法來提高性能呢? 針對這個問題,能夠考慮如下三種方法: 1) 設置 binlog_group_commit_sync_delay 和 binlog_group_commit_sync_no_delay_count 參數,減小 binlog 的寫盤次數。這個方法是基於「額外的故意等待」來實現的,所以可能會增長語句的響應時間,但沒有丟失數據的風險。
binlog_group_commit_sync_delay 參數,表示延遲多少微秒後才調用 fsync;
binlog_group_commit_sync_no_delay_count 參數,表示累積多少次之後才調用 fsync。
當 binlog_group_commit_sync_delay 設置爲 0 的時候,binlog_group_commit_sync_no_delay_count 無效。
2) 將 sync_binlog 設置爲大於 1 的值(比較常見是 100~1000)。這樣作的風險是,主機掉電時會丟 binlog 日誌。 3) 將 innodb_flush_log_at_trx_commit 設置爲 2。這樣作的風險是,主機掉電的時候會丟數據。
redo log 是 InnoDB 引擎特有的日誌,而 Server 層也有本身的日誌,稱爲 binlog(歸檔日誌)。
最開始 MySQL 裏並無 InnoDB 引擎。MySQL 自帶的引擎是 MyISAM,可是 MyISAM 沒有 crash-safe 的能力,binlog 日誌只能用於歸檔。而 InnoDB 是另外一個公司以插件形式
引入 MySQL 的,既然只依靠 binlog 是沒有 crash-safe 能力的,因此 InnoDB 使用另一套日誌系統——也就是 redo log 來實現 crash-safe 能力。
事務執行過程當中,先把日誌寫到 binlog cache,事務提交的時候,再把 binlog cache 寫到 binlog 文件中。
系統給 binlog cache 分配了一片內存,每一個線程一個,參數 binlog_cache_size 用於控制單個線程內 binlog cache 所佔內存的大小。若是超過了這個參數規定的大小,就要暫存到磁盤。
事務提交的時候,執行器把 binlog cache 裏的完整事務寫入到 binlog 中,並清空 binlog cache。每一個線程有本身 binlog cache,可是共用同一份 binlog 文件。
write:指的就是指把日誌寫入到文件系統的 page cache,並無把數據持久化到磁盤,因此速度比較快。
fsync:纔是將數據持久化到磁盤的操做。通常狀況下,咱們認爲 fsync 才佔磁盤的 IOPS。
write 和 fsync 的時機,是由參數 sync_binlog 控制的: 1)sync_binlog=0 的時候,表示每次提交事務都只 write,不 fsync; 2)sync_binlog=1 的時候,表示每次提交事務都會執行 fsync; 3)sync_binlog=N(N>1) 的時候,表示每次提交事務都 write,但累積 N 個事務後才 fsync 在實際的業務場景中,考慮到丟失日誌量的可控性,通常不建議將這個參數設成 0,比較常見的是將其設置爲 100~1000 中的某個數值。 將 sync_binlog 設置爲 N,對應的風險是:若是主機發生異常重啓,會丟失最近 N 個事務的 binlog 日誌。
事務的四大特性:原子性、一致性、隔離性、持久性
事務隔離級別:(隔離級別越高,並行性能依次下降,安全性依次提升),mysql InnoDB引擎默認隔離級別是可重複讀
可重複讀中的版本快照(事務視圖):InnoDB 裏面每一個事務有一個惟一的事務 ID,叫做 transaction id。它是在事務開始的時候向 InnoDB 的事務系統申請的,是按申請順序嚴格遞增的。
而每行數據也都是有多個版本的。每次事務更新數據的時候,都會生成一個新的數據版本,而且把 transaction id 賦值給這個數據版本的事務 ID,記爲 row trx_id。
同時,舊的數據版本要保留,而且在新的數據版本中,可以有信息能夠直接拿到它。
對於一個事務視圖來講,除了本身的更新老是可見之外,有三種狀況:
1 版本未提交,不可見;
2 版本已提交,可是是在視圖建立後提交的,不可見;
3 版本已提交,並且是在視圖建立前提交的,可見。
讀已提交:事務啓動以後,事務內的查詢語句獲取到的數據是已經提交的最新數據,可能形成幻讀,也就是屢次讀取到的可能不一致。
可重複讀:查詢會獲取在事務啓動前就已經提交完成的數據,不過更新的時候會先獲取最新的數據版本號,而後再更新,避免丟失其餘事務已經提交的數據,而且更新完成以後,
本身成爲最新的版本號,因此更新語句以後的查詢的數據是更新語句執行完的數據,也就是最新的數據。
幻讀:幻讀僅專指「新插入的行」,一個事務在先後兩次查詢同一個範圍的時候,後一次查詢看到了前一次查詢沒有看到的行。
MySQL 的行鎖是在引擎層由各個引擎本身實現的。但並非全部的引擎都支持行鎖,好比 MyISAM 引擎就不支持行鎖,InnoDB 是支持行鎖的,
這也是 MyISAM 被 InnoDB 替代的重要緣由之一。
顧名思義,行鎖就是針對數據表中行記錄的鎖。這很好理解,好比事務 A 更新了一行,而這時候事務 B 也要更新同一行,則必須等事務 A 的操做完成後才能進行更新。
兩階段鎖:在 InnoDB 事務中,行鎖是在須要的時候才加上的,但並非不須要了就馬上釋放,而是要等到事務結束時(提交或回滾)才釋放。這個就是兩階段鎖協議,因此同一個事
務中,將update操做放到最後,減小行鎖時間,提升併發。
使用條件:InnoDB行鎖是經過給索引上的索引項加鎖來實現的,只有經過索引條件檢索數據,InnoDB才使用行級鎖,不然InnoDB將使用表鎖,因此建議全部增刪改語句的條件最好
使用索引字段,避免行鎖轉換成表鎖。
死鎖和死鎖檢測:當併發系統中不一樣線程出現循環資源依賴,涉及的線程都在等待別的線程釋放資源時,就會致使這幾個線程都進入無限等待的狀態,稱爲死鎖
如上圖(圖來自極客時間mysql實戰45講)所示,事務 A 在等待事務 B 釋放 id=2 的行鎖,而事務 B 在等待事務 A 釋放 id=1 的行鎖。
事務 A 和事務 B 在互相等待對方的資源釋放,就是進入了死鎖狀態。
死鎖解決兩種策略:一種策略是,直接進入等待,直到超時。這個超時時間能夠經過參數 innodb_lock_wait_timeout 來設置。另外一種
策略是,發起死鎖檢測,發現死鎖後,主動回滾死鎖鏈條中的某一個事務,讓其餘事務得以繼續執行。將參數 innodb_deadlock_detect 設
置爲 on(默認值),表示開啓這個邏輯。死鎖應該儘可能避免,不出現則最好。
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
InnoDB行鎖的鎖定模式實際上能夠分爲四種:共享鎖(S),排他鎖(X),意向共享鎖(IS)和意向排他鎖(IX)。
四種鎖之間的衝突關係:若是一個事務請求的鎖模式與當前的鎖兼容,InnoDB就將請求的鎖授予該事務;反之,若是二者不兼容,該事務就要等待鎖釋放。
意向鎖是InnoDB自動加的,不需用戶干預(隱式加鎖)。對於UPDATE、DELETE和INSERT語句,InnoDB會自動給涉及數據集加排他鎖(X);對於普通SELECT語句,
InnoDB不會加任何鎖,可是用戶能夠主動加鎖(顯式加鎖)。以下:
共享鎖(S):SELECT * FROM table_name WHERE ... LOCK IN SHARE MODE
排他鎖(X):SELECT * FROM table_name WHERE ... FOR UPDATE
共享鎖(讀鎖):事務都加,都能讀。修改是唯一的(發生覆蓋索引的狀況下除外),必須等待前一個事務 commit,纔可繼續執行
排他鎖(寫鎖):事務之間不容許其它排他鎖或共享鎖讀取,修改更不可能,一次只能有一個排他鎖執行 commit 以後,其它事務纔可執行
行鎖也分紅讀鎖和寫鎖。下圖就是這兩種類型行鎖的衝突關係,也就是說,跟行鎖有衝突關係的是「另一個行鎖」。
間隙鎖(GAP Lock):當咱們用範圍條件而不是相等條件檢索數據,並請求共享或排他鎖時,InnoDB會給符合條件的已有數據記錄的索引項加鎖;
對於鍵值在條件範圍內但並不存在的記錄,叫作「間隙(GAP)」,InnoDB也會對這個「間隙」加鎖,這種鎖機制就是所謂的間隙鎖。
間隙鎖,鎖定一個範圍,但不包括記錄自己。GAP鎖的目的,是爲了防止幻讀、防止間隙內有新數據插入、防止已存在的數據更新爲間隙內的數據。
和間隙鎖存在衝突關係的,是「往這個間隙中插入一個記錄」這個操做,好比在索引字段1和5之間的間隙插入2/3/4。
Next-Key Lock:1+2,鎖定一個範圍,而且鎖定記錄自己。對於行的查詢,都是採用該方法,主要目的是解決幻讀的問題。InnoDB默認加鎖方式是next-key 鎖。
總結:
可重複讀隔離級別加鎖規則:
原則 1:加鎖的基本單位是 next-key lock。next-key lock 是前開後閉區間。
原則 2:查找過程當中訪問到的對象纔會加鎖。
優化 1:索引上的等值查詢,給惟一索引加鎖的時候,next-key lock 退化爲行鎖。
優化 2:索引上的等值查詢,向右遍歷時且最後一個值不知足等值條件的時候,next-key lock 退化爲間隙鎖。
惟一索引上的範圍查詢會訪問到不知足條件的第一個值爲止。
測試數據:
CREATE TABLE `t` (
`id` int(11) NOT NULL,
`c` int(11) DEFAULT NULL,
`d` int(11) DEFAULT NULL,
PRIMARY KEY (`id`),
KEY `c` (`c`)
) ENGINE=InnoDB;
insert into t values(0,0,0),(5,5,5),
(10,10,10),(15,15,15),(20,20,20),(25,25,25);
案例一:等值查詢間隙鎖 update t set d=d+1 where id = 7 解釋:因爲表 t 中沒有 id=7 的記錄。根據原則 1,加鎖單位是 next-key lock, 加鎖範圍就是 (5,10];同時根據優化 2,這是一個等值查詢 (id=7),而 id=10 不知足查詢條件,next-key lock 退化成
間隙鎖,所以在事務未提交以前加鎖的範圍是 (5,10)。
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
案例二:非惟一索引等值鎖 select id from t where c = 5 lock in share mode 解釋:這裏要給索引 c 上 c=5 的這一行加上讀鎖。根據原則 1,加鎖單位是 next-key lock,所以會給 (0,5] 加上 next-key lock。要注意 c 是普通索引,所以僅訪問 c=5 這一條記錄是不能立刻停下來的,需
要向右遍歷,查到 c=10 才放棄。根據原則 2,訪問到的都要加鎖,所以要給 (5,10] 加 next-key lock。可是同時這個符合優化 2:等值判斷,向右遍歷,最後一個值不知足 c=5 這個等值條件,所以退化成間隙鎖 (5,10)。 根據原則 2 ,只有訪問到的對象纔會加鎖,這個查詢使用覆蓋索引,並不須要訪問主鍵索引,因此主鍵索引上沒有加任何鎖。
須要注意,在這個例子中,lock in share mode 只鎖覆蓋索引,可是若是是 for update 就不同了。執行 for update 時,系統會認爲你接下來要更新數據,所以會順便給主鍵索引上知足條件的行加上行鎖。 若是你要用 lock in share mode 來給行加讀鎖避免數據被更新的話,就必須得繞過覆蓋索引的優化,在查詢字段中加入索引中不存在的字段。例如:
select id,d from t where c = 5 lock in share mode
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
案例三:主鍵索引範圍鎖 select * from t where id=10 for update; select * from t where id>=10 and id<11 for update; 解釋:第二句sql開始執行的時候,要找到第一個 id=10 的行,所以本該是 next-key lock(5,10]。根據優化 1, 主鍵 id 上的等值條件,退化成行鎖,只加了 id=10 這一行的行鎖。範圍查找就日後繼續找,找到 id=15 這
一行停下來,所以須要加 next-key lock(10,15]。這時候鎖的範圍就是主鍵索引上,行鎖 id=10 和 next-key lock(10,15]。全部第一條和第二條的鎖是不同的範圍。
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
案例四:非惟一索引範圍鎖
select * from t where c>=10 and c<11 for update;
解釋:此次用字段 c 來判斷,加鎖規則跟案例三惟一的不一樣是:在第一次用 c=10 定位記錄的時候,索引 c 上加了 (5,10] 這個 next-key lock 後,因爲索引 c 是非惟一索引,沒有優化規則,也就是說不會蛻變爲行鎖,所以
最終加的鎖是,索引 c 上的 (5,10] 和 (10,15] 這兩個 next-key lock。
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
案例五:非惟一索引上存在"等值"的例子
insert into t values(30,10,30);
新插入的這一行 c=10,也就是說如今表裏有兩個 c=10 的行。雖然有兩個 c=10,可是它們的主鍵值 id 是不一樣的(分別是 10 和 30),所以這兩個 c=10 的記錄之間,也是有間隙的。
delete from t where c = 10
解釋:這時在遍歷的時候,先訪問第一個 c=10 的記錄。一樣地,根據原則 1,這裏加的是 (c=5,id=5) 到 (c=10,id=10) 這個 next-key lock。而後向右查找,直到碰到 (c=15,id=15) 這一行,循環才結束。根據優
化 2,這是一個等值查詢,向右查找到了不知足條件的行,因此會退化成 (c=10,id=10) 到 (c=15,id=15) 的間隙鎖。即 (c=5,id=5) 和 (c=15,id=15) 這兩行上都沒有鎖。
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
案例六:limit 語句加鎖
delete from t where c = 10 limit 2
解釋:這個例子裏的delete 語句加了 limit 2。你知道表 t 裏 c=10 的記錄其實只有兩條,所以加不加 limit 2,刪除的效果都是同樣的,可是加鎖的效果卻不一樣。這是由於delete 語句明確加了 limit 2 的限制,所以
在遍歷到 (c=10, id=30) 這一行以後,知足條件的語句已經有兩條,循環就結束了。所以,索引 c 上的加鎖範圍就變成了從(c=5,id=5) 到(c=10,id=30) 這個前開後閉區間。
!這個例子對咱們實踐的指導意義就是,在刪除數據的時候儘可能加 limit。這樣不只能夠控制刪除數據的條數,讓操做更安全,還能夠減少加鎖的範圍。
普通索引和惟一索引選擇:
按照業務須要,若是某個字段的值是惟一的,好比身份證號碼,那麼可使用惟一索引去實現惟一性,也能夠在業務代碼裏面作到惟一性。
不過阿里的java開發手冊泰山版裏面提到 「即便在應用層作了很是完善的校驗控制,只要沒有惟一索引,根據墨菲定律,必然有髒數據產生」。
不過我我的以爲Mysql實戰45講的做者說的更加好一點。
對於惟一索引來講,須要將數據頁讀入內存(因此不須要使用change buffer),判斷到沒有衝突,而後再插入值;對於普通索引來講,則是將更新記錄在 change buffer,語句執行就結束了。當須要更新一個數據頁時,
若是數據頁在內存中就直接更新,而若是這個數據頁尚未在內存中的話,在不影響數據一致性的前提下,InooDB 會將這些更新操做緩存在 change buffer 中,這樣就不須要從磁盤中讀入這個數據頁了。
在下次查詢須要訪問這個數據頁的時候,將數據頁讀入內存,而後執行 change buffer 中與這個頁有關的操做。change buffer是能夠持久化的數據,也就是說,change buffer 在內存中有拷貝,
也會被寫入到磁盤上。change buffer 用的是 buffer pool 裏的內存,所以不能無限增大。change buffer 的大小,能夠經過參數 innodb_change_buffer_max_size 來動態設置
總結:對於寫多讀少的業務來講,頁面在寫完之後立刻被訪問到的機率比較小,此時 change buffer 的使用效果最好。這種業務模型常見的就是帳單類、日誌類的系統。假設一個業務的更新模式是寫入之
後立刻會作查詢,那麼即便知足了條件,將更新先記錄在 change buffer,但以後因爲立刻要訪問這個數據頁,會當即觸發 merge(持久化) 過程。這樣隨機訪問 IO 的次數不會減小,反而增長了 change buffer 的維護代價。
marge:change buffer 中的操做應用到原數據頁,獲得最新結果的過程稱爲 merge。除了訪問這個數據頁會觸發 merge 外,系統有後臺線程會按期 merge。在數據庫正常關閉(shutdown)的過程當中,也會執行 merge 操做。
字符串加索引
直接建立完整索引,這樣可能比較佔用空間;
建立前綴索引,節省空間,但會增長查詢掃描次數,而且不能使用覆蓋索引;
倒序存儲,再建立前綴索引,用於繞過字符串自己前綴的區分度不夠的問題;
建立 hash 字段索引,查詢性能穩定,有額外的存儲和計算消耗,跟第三種方式同樣,都不支持範圍掃描。
索引觸發和失效
函數中使用:對索引字段作函數操做,可能會破壞索引值的有序性,所以優化器就決定放棄走樹搜索功能。
隱式類型轉換:索引字段是字符串類型,傳入進去的值是int,就會對索引字段作函數操做,將字符串轉成int再去判斷,優化器會放棄走樹搜索功能。
可是若是字段是int,傳入的值是字符串,則不受影響,由於直接把傳入的字段轉成int便可,不須要屢次轉換。
字符集不一樣:utf8的字段在utf8mb4表中沒法使用索引,會先進行字符集轉換,而後再比較值。
explain語句詳解:
id:id列的編號是select的序列號,有幾個select就有幾個id,而且id的順序是按select出現的順序增加的。id越大執行優先級越高,id相同則從上往下執行,id爲NULL最後執行
select type:select type表示對應行是簡單仍是複雜的查詢。
simple:簡單查詢。查詢不包含子查詢和union。
primary:複雜查詢中最外層的select
subquery:包含在select中的子查詢(不在from子句中)
derived:包含在from子句中的子查詢。MySQL會將結果存放在一個臨時表中,也稱爲派生表。
union:在union關鍵字隨後的selelct。
table:這一列表示explain的一行正在訪問哪一個表。
當from子句中有子查詢時,table列是格式,表示當前查詢依賴id=N的查詢,因而先執行id=N的查詢。
當有union時,UNION RESULT的table列的值爲<union 1,2>,1和2表示參與union的select行id。
type:這一列表示關聯類型或訪問類型,即MySQL決定如何查找表中的行,查找數據行對應的大概範圍。
依次從最優到最差的分別爲:system>const>eq_ref>ref>range>index>All
通常來講,得保證查詢達到range級別,最好達到ref。
爲null時,MySQL可以在優化階段分解查詢語句,在執行階段用不着在訪問表或索引。例如:在索引列中選取最小值,能夠單獨查找索引來完成,不需在執行時訪問表。
const,system:
一般狀況下,若是將一個主鍵放置到where後面做爲條件查詢,mysql優化器就能把此次查詢優化轉化爲一個常量。用於primay key或unique key的全部列與常數 比較時,因此表最多有一個匹配行,讀取1次,速讀較快。system 是const的特例,表中只有一行元素匹配時爲system。 EXPLAIN select * from film where id= 1 ; eq_ref:
primay key或 unique key索引的全部部分被鏈接使用,最多隻會返回一條符合條件的記錄。這是const以外最好的聯接類型,簡單的select查詢不會出現這種type。 ref:
相比eq_ref,不適用惟一索引,而是使用普通索引或者惟一索引的部分前綴,索引要和某個值相比較,可能會找到多個符合條件的行。 簡單select查詢,name是普通索引(非主鍵索引或惟一索引) EXPLAIN select * from film where name='film1'; range:
range指的是有範圍的索引掃描,相對於index的全索引掃描,它有範圍限制,所以要優於index。關於range比較容易理解,須要記住的是出現了range,則必定是基於 索引的。同時除了顯而易見的between,and以及'>','<'外,in和or也是索引範圍掃描 index:
這種鏈接類型只是另一種形式的全表掃描,只不過它的掃描順序是按照索引的順序。這種掃描根據索引而後回表取數據,和all相比,他們都是取得了全表的數據,並且 index要先讀索引找到主鍵id,而後回表讀取數據。 all:
即全表掃描,意味着MySQL須要從頭至尾去查找所須要的行。這種狀況下須要增長索引來進行優化。
possible_keys:這一列顯示select可能會使用哪些索引來查找。
explain時可能會出現possible_keys有列,而key顯示爲NULL的狀況,這種狀況是由於表中的數據很少,MySQL認爲索引對此查詢幫助不大,選擇了全表掃描。
若是該列爲NULL,則沒有相關的索引。這種狀況下,能夠經過檢查where子句看是否能夠創造一個適當的索引來提升查詢性能,而後用explain查看效果
key:這一列顯示MySQL實際採用哪一個索引對該表的訪問。
若是沒有使用索引,則改列爲NULL。若是想強制MySQL使用或忽視possible_keys列中的索引,在查詢中使用force index、 ignore index。
key_len:這一列顯示了mysql在索引裏使用的字節數,經過這個值能夠估算出具體使用了索引中的哪些列。
ken_len計算規則以下:
字符串:char(n):n字節長度,varchar(n):n字節存儲字符串長度,若是是utf-8, 則長度是3n+2
數值類型:tinyint:1字節,smallint:2字節,int:4字節,bigint:8字節
時間類型:date:3字節,timestamp:4字節,datetime:8字節
若是字段容許爲NULL,須要1字節記錄是否爲NULL
索引最大長度是768字節,當字符串過長時,MySQL會作一個相似作前綴索引的處理,將前半部分的字符串提取出來作索引
ref:這一列顯示了在key列記錄的索引中,表查找值所用到的列或常量,常見的有: const(常量),字段名等。
通常是查詢條件或關聯條件中等號右邊的值,若是是常量那麼ref列是const,很是量的話ref列就是字段名。
rows:這一列是mysql估計要讀取並檢測的行數,注意這個不是結果集的行數
Extra:這一列是額外信息
Using index:使用覆蓋索引,結果集的字段是索引,索引除了有本索引包含的字段,還有主鍵id(主鍵索引)的記錄。
explain select id from film_actor where film_id=1;
Using index condition:查詢的列不徹底被索引覆蓋,where條件中是一個前導的範圍
explain select * from film_actor where film_id > 1;
Using where:使用where語句來處理結果,查詢的列未被索引覆蓋
explain select * from actor where name ='a'
Using temporary:mysql須要建立一張臨時表來處理查詢。出現這種狀況通常要進行優化,首先要想到是索引優化。
sing filesort:將用外部排序而不是索引排序,數據較小時從內存排序,不然須要在磁盤完成排序。這種狀況下通常也是要考慮使用索引來優化的。
explain select * from actor order by name;
select tables optimized away:使用某些聚合函數(好比:max、min)來訪問存在索引的某個字段
select min(id) from film ;
count函數:
在不一樣的 MySQL 引擎中,count(*) 有不一樣的實現方式。MyISAM 引擎把一個表的總行數存在了磁盤上,所以執行 count(*) 的時候會直接返回這個數,效率很高;
而 InnoDB 引擎就麻煩了,它執行 count(*) 的時候,須要把數據一行一行地從引擎裏面讀出來,而後累積計數。
count() 是一個聚合函數,對於返回的結果集,一行行地判斷,若是 count 函數的參數不是 NULL,累計值就加 1,不然不加。最後返回累計值。因此,count(*),
count(主鍵 id) 和 count(1) 都表示返回知足條件的結果集的總行數;而 count(字段),則表示返回知足條件的數據行裏面,參數「字段」不爲 NULL 的總個數。
mysql對count(*)作了專門的優化,不取值,不判空。因此按照效率排序的話,count(字段)<count(主鍵 id)<count(1)≈count(*),因此我建議你,儘可能使用 count(*)
order by語句
1)全字段排序:
mysql的排序有可能在內存,也有可能在外部,取決於排序所需的內存和參數 sort_buffer_size。sort_buffer_size,就是 MySQL 爲排序開闢的內存(sort_buffer)的大小。
若是要排序的數據量小於 sort_buffer_size,排序就在內存中完成。但若是排序數據量太大,內存放不下,則不得不利用磁盤臨時文件輔助排序。
select city,name,age from t where city='杭州' order by name limit 1000 ;
能夠用下面介紹的方法,來肯定一個排序語句是否使用了臨時文件。
/* 打開 optimizer_trace,只對本線程有效 */
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';
/* 執行語句 */
select city, name,age from t where city='杭州' order by name limit 1000;
/* 查看 OPTIMIZER_TRACE 輸出 */
SELECT * FROM `information_schema`.`OPTIMIZER_TRACE`\G
/* @b 保存 Innodb_rows_read 的當前值 */
select VARIABLE_VALUE into @b from performance_schema.session_status where variable_name = 'Innodb_rows_read';
/* 計算 Innodb_rows_read 差值 */
select @b-@a;
這個方法是經過查看 OPTIMIZER_TRACE 的結果來確認的,你能夠從 number_of_tmp_files 中看到是否使用了臨時文件。
number_of_tmp_files 表示的是,排序過程當中使用的臨時文件數。你必定奇怪,爲何須要多個文件?內存放不下時,就須要使用外部排序,外部排序通常使用歸併排序算法。
能夠這麼簡單理解,MySQL 將須要排序的數據分紅n 份,每一份單獨排序後存在這些臨時文件中。而後把這 n 個有序文件再合併成一個有序的大文件。若是 sort_buffer_size超過了
須要排序的數據量的大小,number_of_tmp_files就是 0,表示排序能夠直接在內存中完成。不然就須要放在臨時文件中排序。sort_buffer_size 越小,須要分紅的份數越多,number_of_tmp_files 的值就越大。
2)rowid 排序:
查詢要返回的字段不少的話,那麼 sort_buffer 裏面要放的字段數太多,這樣內存裏可以同時放下的行數不多,要分紅不少個臨時文件,排序的性能會不好。max_length_for_sort_data,是
MySQL 中專門控制用於排序的行數據的長度的一個參數。它的意思是,若是單行的長度超過這個值,MySQL 就認爲單行太大,要換一個算法。新的算法放入 sort_buffer 的字段,只有要排序的列(即 name 字段)
和主鍵 id。但這時,排序的結果就由於少了 city 和 age 字段的值,不能直接返回了。 MySQL 服務端從排序後的 sort_buffer 中依次取出 id,而後到原表查到 city、name 和 age 這三個字段的結果。
order by 優化
覆蓋索引是指,索引上的信息足夠知足查詢請求,不須要再回到主鍵索引上去取數據,因此能夠利用覆蓋索引這一特性,將須要返回的字段建立一個聯合索引,這樣能夠避免查詢回表。 而且由於索引自己就已經排序了,這樣就不用再次排序,直接返回就行了。不過這種辦法只適合字段較少的場景。
例如:alter table t add index city_user_age(city, name, age); 索引排序:除了索引覆蓋,還能夠利用索引自己就有序,將排序的字段設置成索引,這樣就能夠避免再次排序。須要注意的是,若是order by 的
字段不少,須要保證所有字段都加了索引,而且統一升序或降序,不然失效
舉例:SELECT * FROM t1 WHERE key_part1=1 ORDER BY key_part1 DESC, key_part2 DESC;
SELECT * FROM t1 ORDER BY key_part1 DESC, key_part2 DESC;
1)無條件查詢若是隻有order by create_time,即使create_time上有索引,也不會使用到。由於優化器認爲走二級索引再去回表成本比全表掃描排序更高。
因此選擇走全表掃描。
2)無條件查詢可是是order by create_time limit m.若是m值較小,是能夠走索引的.由於優化器認爲根據索引有序性去回表查數據,而後獲得m條數據,就能夠終止循環,那麼成本比全表掃描小,則選擇走二級索引。
即使沒有二級索引,mysql針對order by limit也作了優化,採用堆排序。
3)bigint和int加數字都不影響能存儲的值。
bigint(1)和bigint(19)都能存儲2^64-1範圍內的值,int是2^32-1。建議不加varchar()就必須帶,由於varchar()括號裏的數字表明能存多少字符。假設varchar(2),就只能存兩個字符,無論是中文仍是英文。
目前來看varchar()這個值能夠設得稍稍大點,由於內存是按照實際的大小來分配內存空間的,不是按照值來預分配的。注意的是255這個邊界。小於255都須要一個字節記錄長度,超過255就須要兩個字節。
group by
若是對 group by 語句的結果沒有排序要求,要在語句後面加 order by null;
儘可能讓 group by 過程用上表的索引,確認方法是 explain 結果裏沒有 Using temporary 和 Using filesort;
若是 group by 須要統計的數據量不大,儘可能只使用內存臨時表;也能夠經過適當調大 tmp_table_size 參數,來避免用到磁盤臨時表;
若是數據量實在太大,使用 SQL_BIG_RESULT 這個提示,來告訴優化器直接使用排序算法獲得 group by 的結果。
根據計算的條件去建立索引:若是group by 的字段是計算出來的字段,那麼將沒法利用索引排序。
解決辦法:使用 generated column 建立一計算以後的列,而後在計算後的列上加入索引。
例:alter table t1 add column z int generated always as(id % 100), add index(z);
直接使用:select z, count(*) as c from t1 group by z;
group by 優化方法 -- 直接排序 一個 group by 語句中須要放到臨時表上的數據量特別大,卻仍是要按照「先放到內存臨時表,插入一部分數據後,發現內存臨時表不夠用了再轉成磁盤臨時表」。 因此咱們能夠直接走磁盤臨時表。 group by 語句中加入 SQL_BIG_RESULT 這個提示(hint),就能夠告訴優化器:這個語句涉及的數據量很大,請直接用磁盤臨時表。
MySQL 的優化器一看,磁盤臨時表是 B+ 樹存儲,存儲效率不如數組來得高。因此,既然你告訴我數據量很大,那從磁盤空間考慮,仍是直接用數組來存吧。因此最終是直接輸出排序結果,不須要使用臨時表。 使用:select SQL_BIG_RESULT id%100 as m, count(*) as c from t1 group by m;
!= 和not in
儘可能避免使用由於條件範圍過大會致使數據庫引擎放棄索引進行全表掃描。
SELECT * FROM t WHERE id IN (2,3) 索引有效
SELECT * FROM t1 WHERE name NOT IN (SELECT username FROM t2) 索引失效
select * from t where status!=0 and stauts!=1 索引失效
or:
當or兩邊的字段都有索引時,索引生效,任一字段無索引,索引失效,走全文搜索。此時可使用 UNION 代替,這樣有索引的字段觸發索引,無索引的字段則走全文搜索。
例: SELECT * FROM broadcast WHERE id =100 OR money = '2000' 改爲 SELECT * FROM broadcast WHERE id =100 UNION SELECT * FROM broadcast WHERE money = '2000'
limit:
肯定搜索一條記錄的時候,加上limit 1 ,以此讓遊標查到第一條結果時中止,不須要遍歷下面的結果。
A:SELECT * FROM broadcast WHERE request_id = '1587458829595059d3ce6cfcf4b7f8'
B:SELECT * FROM broadcast WHERE request_id = '1587458829595059d3ce6cfcf4b7f8' limit 1
實測(表數據60萬)不創建索引狀況下:A:0.233s B:0.038s
能夠看到相差仍是很明顯的。若是request_id創建了索引,那麼A和B基本上是沒什麼區別。因此在一些比較複雜的查詢,而且知道返回的只有一條的狀況下加一個limit能夠避免繼續搜索後面的數據。
limit覆蓋索引
A:SELECT * FROM broadcast ORDER BY id LIMIT 500000,100 B:SELECT * FROM broadcast WHERE id >= (SELECT id FROM broadcast ORDER BY id LIMIT 500000,1) limit 100 也能夠用join代替 >=
SELECT * FROM broadcast a JOIN (select id from broadcast ORDER BY id limit 500000, 100) b ON a.id = b.id;
使用覆蓋索引先查出數據是50000的id,而後直接在id的範圍內查詢數據,因此不須要遍歷前面的50000條數據
A:0.2s B:0.114s
join
能不能使用 join 語句?
join算法:
簡單嵌套循環鏈接實際上就是簡單粗暴的嵌套循環,若是table1有1萬條數據,table2有1萬條數據,那麼數據比較的次數=1萬 * 1萬 =1億次,這種查詢效率會很是慢,mysql
已經不在使用。使用BNLJ算法代替
索引嵌套循環鏈接是基於索引進行鏈接的算法,索引是基於被驅動表的,經過驅動表匹配條件直接與被驅動表索引進行匹配,避免和被驅動表的每條記錄
進行比較, 從而利用索引的查詢減小了對被驅動表的匹配次數,優點極大的提高了 join的性能
原來的匹配次數 = 外層錶行數 * 內層錶行數
優化後的匹配次數= 外層表的行數 * (內層表索引的高度+索引回表查詢)
緩存塊嵌套循環鏈接經過一次性緩存多條數據,把參與查詢的列緩存到Join Buffer 裏,而後拿join buffer裏的數據批量與內層表的數據進行匹配,從而減
少了內層循環的次數(遍歷一次內層表就能夠批量匹配一次Join Buffer裏面的外層表數據)。當不使用Index Nested-Loop Join的時候,默認使用Block Nested-Loop Join。
----------------------------------------------------------------------------------------------------------------------------------------------
在選擇Join算法時,會有優先級,理論上會優先判斷可否使用INLJ、BNLJ:
Batched Key Access > Index Nested-LoopJoin > Block Nested-Loop Join > Simple Nested-Loop Join
1: 若是可使用 Index Nested-Loop Join 算法,也就是說能夠用上被驅動表上的索引,可使用;
2: 若是使用 Block Nested-Loop Join 算法,掃描行數就會過多。尤爲是在大表上的 join 操做,這樣可能要掃描被驅動表不少次,會佔用大量的系統資源。因此這種 join 儘可能不要用。
在判斷要不要使用 join 語句時,就是看 explain 結果裏面,Extra 字段裏面有沒有出現「Block Nested Loop」字樣。
-----------------------------------------------------------------------------------------------------------------------------------------------
若是要使用 join,應該選擇大表作驅動表仍是選擇小表作驅動表?(其實優化器會幫咱們選擇合適的表作驅動表,但不排除翻車的可能性,使用straight_join代替join讓優化器按照咱們寫的順序去執行join)
若是是 Index Nested-Loop Join(可使用被驅動表的索引)算法,應該選擇小表作驅動表;
若是是 Block Nested-Loop Join 算法:在 join_buffer_size 足夠大的時候,是同樣的;在 join_buffer_size 不夠大的時候(這種狀況更常見),應該選擇小表作驅動表。
若是join_buffer 不夠大,須要對被驅動表作屢次全表掃描,也就形成了「長事務」。被驅動表就被屢次讀,而被驅動表又是大表,循環讀取的間隔確定得超1秒,就會致使「數據頁在LRU_old的存在時間超過1秒,就會移到young區」。
最終結果就是把大部分熱點數據都淘汰了,致使「Buffer pool hit rate」命中率極低,其餘請求須要讀磁盤,所以系統響應變慢,大部分請求阻塞。
總結:不能使用被驅動表的索引,只能使用 Block Nested-Loop Join 算法,這樣的語句就儘可能不要使用,使用也要用小表作驅動表。
因此,老是應該使用小表作驅動表。在決定哪一個表作驅動表的時候,應該是兩個表按照各自的條件過濾,過濾完成以後,計算參與 join 的各個字段的總數據量,數據量小的那個表,就是「小表」,應該做爲驅動表。
join優化總結:
1:用小結果集驅動大結果集,減小外層循環的數據量:
2:若是小結果集和大結果集鏈接的列都是索引列,mysql在內鏈接時也會選擇用小結果集驅動大結果集,由於索引查詢的成本是比較固定的,這時候外層的循環越少,join的速度便越快。
爲匹配的條件增長索引:爭取使用INLJ,減小內層表的循環次數
3:增大join buffer size的大小:當使用BNLJ時,一次緩存的數據越多,那麼外層表循環的次數就越少
4:減小沒必要要的字段查詢:
(1)當用到BNLJ時,字段越少,join buffer 所緩存的數據就越多,外層表的循環次數就越少;
(2)當用到INLJ時,若是能夠不回表查詢,即利用到覆蓋索引
5:Multi-Range Read(MRR) 優化:
由於大多數的數據都是按照主鍵遞增順序插入獲得的,因此咱們能夠認爲,若是按照主鍵的遞增順序查詢的話,對磁盤的讀比較接近順序讀,可以提高讀性能。MRR 可以提高性能的核心在於,查詢語句在索引上作的一個範圍查詢(也就是說,這是一
個多值查詢),能夠獲得足夠多的主鍵 id。這樣經過排序之後,再去主鍵索引查數據,才能體現出「順序性」的優點。
執行流程:根據索引 a 定位到知足條件的記錄,將 id 值放入 read_rnd_buffer 中 ,將 read_rnd_buffer 中的 id 進行遞增排序,排序後的 id 數組,依次到主鍵 id 索引中查記錄,並做爲結果返回。
須要注意的是:查詢語句在索引 a 上作的是一個範圍查詢(也就是說,這是一個多值查詢),能夠獲得足夠多的主鍵 id。這樣經過排序之後,再去主鍵索引查數據,才能體現出「順序性」的優點。
你想要穩定地使用 MRR 優化的話,須要設置set optimizer_switch="mrr_cost_based=off"
6:Batched Key Access(BKA)算法
mySQL 在 5.6 版本後開始引入的 Batched Key Access(BKA) 算法了。這個 BKA 算法,其實就是對 NLJ 算法的優化。 NLJ 算法執行的邏輯是從驅動表 t1,一行行地取出 a 的值,再到被驅動表 t2 去作 join。也就是說,對於表 t2 來
說,每次都是匹配一個值,這時,MRR 的優點就用不上了。既然如此,咱們就把表 t1 的數據取出來一部分,先放到一個臨時內存。這個臨時內存也是 join_buffer。
若是要使用 BKA 優化算法的話,你須要在執行 SQL 語句以前,先設置,而且被驅動表須要索引。(前兩個參數的做用是要啓用 MRR。這麼作的緣由是,BKA 算法的優化要依賴於 MRR。)
set optimizer_switch='mrr=on,mrr_cost_based=off,batched_key_access=on';
BNL 轉 BKA
一些狀況下,咱們能夠直接在被驅動表上建索引,這時就能夠直接轉成 BKA 算法了。可是,有時候你確實會碰到一些不適合在被驅動表上建索引的狀況。好比下面這個語句: select * from t1 join t2 on (t1.b=t2.b) where t2.b>=1 and t2.b<=2000; 表 t2 中插入了 100 萬行數據,可是通過 where 條件過濾後,須要參與 join 的只有 2000 行數據。若是這條語句同時是一個低頻的 SQL 語句,那麼再爲這個語句在表 t2 的字段 b 上建立一個索引就很浪費了 sql執行流程: 1:把表 t1 的全部字段取出來,存入 join_buffer 中。這個表只有 1000 行,join_buffer_size 默認值是 256k,能夠徹底存入。 2:掃描表 t2,取出每一行數據跟 join_buffer 中的數據進行對比, 3:若是不知足 t1.b=t2.b,則跳過;若是知足 t1.b=t2.b, 再判斷其餘條件,也就是是否知足 t2.b 處於 [1,2000] 的條件,若是是,就做爲結果集的一部分返回,不然跳過。 判斷 join 是否知足的時候,都須要遍歷 join_buffer 中的全部行。所以判斷等值條件的次數是 1000*100 萬 =10 億次 explain 結果裏 Extra 字段顯示使用了 BNL 算法,總體sql語句的執行時間超過了1分鐘。 這時候,咱們能夠考慮使用臨時表。使用臨時表的大體思路是: 1:把表 t2 中知足條件的數據放在臨時表 temp_t 中; 2:爲了讓 join 使用 BKA 算法,給臨時表 temp_t 的字段 b 加上索引; 3:讓表 t1 和 temp_t 作 join 操做。 create temporary table temp_t(id int primary key, a int, b int, index(b))engine=innodb; insert into temp_t select * from t2 where b>=1 and b<=2000; select * from t1 join temp_t on (t1.b=temp_t.b);