深入淺出MySQL讀書筆記(一)
前言
在某大神童靴的強烈安利下最近閱讀了深入淺出MySQL一書,這本書的第三部分,介紹了MySQL數據庫的一些優化方法,非常值得一讀,推薦大家如果有時間都可以閱讀一下,下面博客的主要內容實際是個人的讀書筆記。主要內容包括以下方面:
- 索引相關內容
- 常見SQL的優化方法
- MySQL排序的基礎知識
- 深分頁的兩種優化方法
本系列博文的主要針對對象是開發人員,而非DBA,有些內容不會進行詳細介紹。
准備工作
我們的的實踐主要是在MySQL提供的測試數據sakila上,測試數據的下載地址
下載地址,下載好數據后解壓,進入目錄通過MySQL運行命令source sakila-mv-schema.sql
和source sakila-mv-data.sql
,就可以將數據導入到MySQL數據庫中。
這里一定要在文件夾下進行數據導入,否則會有報錯。
優化SQL語句你需要知道的幾個命令
說起優化SQL語句,首先我們需要知道哪些SQL語句的效率是有問題的,我們要能夠在生產環境排查出有問題的SQL語句,這樣才能對SQL進行優化,所以,我們先來介紹幾個查看SQL語句執行效率方面的命令。
- 查看SQL執行頻率
我們可以通過show global status like "Com%"
命令來查看這個數據庫從啟動到現在為止所有SQL類型的執行頻率。同時可以將global更改為local,來查詢當前session的SQL執行頻率。
- 慢查詢日志定位慢SQL
MySQL提供了一種日志叫做慢查詢日志,通過這個日志,我們可以定位那些執行效率較低的SQL,這種方式一般由DBA管理,這里不做詳細介紹,如果想要了解可以自行上網查找相關介紹。
- 通過explain分析低效SQL的執行計划
當我們懷疑某個SQL有低效的問題,我們就可以通過explain命令來分析其執行計划,找出低效的原因。
下面解釋一下每列的含義:
-
select_type : 表示select類型,simple(簡單表,不需要表連接或子查詢),primary(主查詢,即外層的查詢,主要指的是嵌套查詢中的最外層,表連接中最前面的select語句),union(表連接中的第二個或后面的select語句),subquery(子查詢中的第一個select)等。
-
table : 輸出結果集的表
-
type : 表示表的連接類型,是一個重要的部分,性能由好到差分別為system、const、eq_ref、ref、 ref_or_null、index_merge、unique_subquery、index_subquery、range、index、all
-
possible_keys : 查詢時可能用到的索引
-
key : 最終實際使用的索引
-
key_len : 索引字段的長度
-
rows : 掃描行的數量,越少越好
-
extra : 執行情況的額外說明
通過sql的執行計划我們可以等到SQL實際的執行順序,使用的索引,當我們的查詢沒有使用到索引時,我們的查詢就會很慢,所以我們可以通過執行計划來適當修改或新建索引。
索引介紹
這部分我們來介紹索引的一些知識,包括建立索引需要考慮的一些點以及索引的一些基礎知識。
索引的分類
MySQL中的索引主要有四種,分別B樹索引、哈希索引、全文索引和R-tree索引,所有的存儲引擎都支持B樹索引,哈希索引只有MEMORY引擎支持,全文索引只有MyISAM引擎支持,MyISAM引擎支持R-tree索引,但使用較少
前綴索引
MyISAM和InnoDB默認創建的都是BTREE索引,索引實際上也是需要占用存儲,所以,如果我們在一個很大的表上建立聯合索引,那么索引數據會增長的十分快,這個時候我們可以考慮使用前綴索引,只對索引字段的前N個字符創建索引,這樣可以有效減少索引的大小。
前綴索引同樣存在缺點,在排序ORDER BY和分組GROUP BY時無法使用,因此需要根據實際情況處理。
HASH索引與BTEE索引
在默認情況下,Memory存儲引擎默認使用HASH索引,但也支持BTREE索引。
兩種不同的索引有其不同的適用范圍,HASH索引的適用范圍較小,需要注意。
- 只用於=或<=>操作符等式比較
- 無法加速ORDER BY操作
- 只能用整個關鍵字來搜索一行
- 當需要適用范圍查詢時需要建立BTREE索引
下面給出例子,首先創建一個Memory引擎的表。
范圍搜索實際不能適用hash索引,見下圖
將hash索引改為btree索引再查看,發現搜索過程中成功使用索引。
設計原則
- 最適合創建索引的是where語句后出現的字段
- 創建索引的列的值盡量是多樣性的,基數越大索引效果越好。例如,如果在記錄性別的列上創建索引,那么對此列索引沒有多大用處,因為不管搜索那個值,都會得到大約一半的行。
- 使用短索引。如果對字符創索引,而字符串的長度有比較長,那么就盡量使用前綴索引,方便節省空間,同時也會讓查詢更快。
- 利用最左前綴。當創建組合索引(a,b,c)時,該索引可以被a,(ab),(abc)等查詢利用到,但是不能夠被b,(bc)利用。這個原則是最左匹配原則。后面還會做詳細介紹。
- 不要過度索引,索引是需要占用磁盤空間的,當我們設計索引的時候一定要選擇最合適的索引,而不是什么都索引,這樣可能導致無法選中最佳索引。
- 對於InnoDB引擎的表,最好指定主鍵,並且盡量選擇較短的數據作為主鍵。因為InnoDB表的記錄默認會按照一定順序存儲,如果有明確主鍵,則會安札主鍵順序保存,如果沒有主鍵,但是有唯一索引,那么會按照唯一索引順序保存,如果兩者都沒有,那么表會自動生成一個內部列,按照這個順序保存,按照主鍵或內部列進行訪問時最快的。
能夠使用索引的經典場景
-
匹配全值,對索引中所有列都指定具體值。type字段為const,表示是常量。
-
匹配值的范圍查詢,對索引的值進行范圍查找。type字段為range,表示范圍。
-
匹配最左前綴,這個實際就是前面介紹過的最左前綴原則。
這里給出兩個例子。
-
僅僅對索引進行查詢,當查詢的所有列都在索引字段中時,查詢效率更高。
當查詢是僅對索引進行查詢時,extra字段為using index,即覆蓋索引掃描。 -
僅匹配列前綴,這里想要說明的是extra的另外一種,值為using where,表示優化器需要通過索引回表查詢數據。由於我們創建的索引是前綴索引,同時我們查詢的還是前綴索引的字段,所以使用索引后依然要回表中去查詢數據。
-
索引匹配部分精確而其他部分進行范圍匹配。范圍匹配不能通過索引確定唯一,仍需使用using where過濾元組
-
如果列名是索引,那么使用列名為null查詢也會使用索引。
-
MySQL的ICP特性,優化了查詢,將某些情況下的過濾操作下放到存儲引擎,也就是說當我們在低版本的MySQL上有些查詢最終可能是using where,但是到高版本上,相同的查詢,因為ICP特性就變成了using index condition,這也是MySQL的優化,降低了不必要的IO操作。
幾種需要注意的不會使用索引的情況
下面介紹幾種優化器不會使用索引的情況,我們在寫SQL語句和設計索引的時候應該盡量避免這些情況,提高效率。
- 第一個就是以%開頭的like查詢,所以我們應該盡量不要寫這樣的語句。這種全文檢索問題盡量使用全文索引來解決。
- 當數據類型出現隱式轉換時。例如列類型時字符串,但是where條件中沒有將其用引號括起來,這樣即便列上有索引,也無法使用。
- 復合索引時,SQL語句不符合最左前綴原則。
- 當MySQL估計使用索引比全表掃描更慢時,就不會使用索引。例如查詢“S”開頭的電影,這種返回記錄較多的情況。
- or分割開的條件必須每列都有索引,才會走索引。因為如果有沒有索引的列,那么最終肯定也要做全表掃描,那么不如直接一次全表掃描過濾條件,避免無用的IO。
常用SQL的優化
有關查詢的SQL通常的優化方法就是通過索引來進行優化,這些在上面已經介紹的差不多了,那么還有一些其他常用SQL的優化將在下面介紹。
大批量插入數據
當使用InnoDB引擎時,盡量關閉唯一性校驗,當導入結束后再打開,同時插入的數據應盡量按照主鍵順序排列,並且關閉自動提交,當導入數據結束后再打開。
相關命名如下
set UNIQUE_CHECK=0;
set SUTOCOMMIT=0;
當使用MyISAM引擎時,我們應該盡量關閉索引更新。
命令如下
alter table tbl_name disable keys;
ORDER BY 語句
MySQL中兩種排序方式
首先先了解一下customer表上的索引,方便后面的介紹。
首先介紹第一種,就是using index,通過查詢有序索引返回有序結果。
第二種就是using filesort,這種情況表示進行了額外的排序工作,這種情況並不表示使用了磁盤文件進行排序操作,這只是表示進行了額外的排序操作,至於排序操作時如何進行的,則取決於MySQL的決定。
下面這個例子可以清晰的表明這種情況,即使所有的數據都在索引中,依然有using filesort
FileSort
filesort是通過排序算法,將數據在sort_buffer_size系統變量設置的內存排序區中進行排序,如果排序區放不下,那么會將磁盤上的數據進行分塊,再分別進行排序,最后合並為有序的結果集。這個排序區是線程獨占的,同一時刻,MySQL中可能存在多個。
優化
那么,現在優化主要有兩個方法,一種是減少額外的排序,通過索引直接返回有序數據,一種是優化filesort。
首先介紹第一種。WHERE條件和ORDER BY條件使用的索引是同一個,並且索引順序和ORDER BY的順序是相同的時,並且ORDER BY 字段都是升序或降序。如果不滿足以上三個條件,那么肯定要進行額外的排序,就會出現filesort。因此,我們寫ORDER BY的時候盡量滿足。
有時,就算完全滿足條件,也不能確保不出現filesort,例如:
因此,這里又涉及到filesort的優化。
filesort有兩種排序算法。
- 兩次掃描算法:根據條件取出排序字段和行指針信息,在排序區中進行排序,如果排序區不夠,那么就在臨時表中先存儲結果,當完成后,在根據行指針回表取數據,這樣就涉及了兩次掃描,其中第二次掃描可能十分耗時,畢竟存在大量的隨機IO操作。
- 一次掃描算法:根據條件一次性取出所有字段和行指針信息,在排序區進行排序。排序效率高於兩次掃描算法。
兩種算法的選擇是通過max_length_for_sort_data的大小和query語句取出字段總大小來決定的。因此,通過增大max_length_for_sort_data的值,可以讓更多的排序可以采用一次掃描排序,但是,需要注意,這個值也不能過大,否則反而會造成效率降低。
同時,我們也可以增大排序區sort_buffer_size的大小,讓排序更多的在內存中完成,提高效率,同樣,也不可以過大,因為這個區域是線程獨占。
同樣的,我們也可以通過減少query中的字段來達到加快排序的目的。因此select語句盡量不適用select *,直接寫出需要的字段。
GROUP BY 優化
GROUP BY 語句實際上會對后面的字段進行排序,隱含操作ORDER BY,因此,當我們不需要進行排序時,我們可以增加字句ORDER BY NULL來禁止排序。
優化嵌套查詢
SQL的子查詢可以一次性完成很多邏輯上很多步驟才能完成的SQL操作,同時可以避免事物或表死鎖,寫起來也十分容易。但是,有些時候其效率較連接查詢更低。所以一般使用join來優化子查詢操作。
實際上子查詢的過程中產生了臨時表,因此相比join操作多了臨時變的建立和銷毀,同時臨時變中的數據無法使用索引,更進一步降低了速度,所以,當我們的SQL可以使用join時,盡量不要使用子查詢。
下面的例子給出同一個查詢數據的不同實現
select * from customer where customer_id not in (select customer_id from payment)
select * from customer c left join payment p on c.customer_id = p.customer_id where p.customer_id is null
OR條件的優化
其實,OR條件的優化,在前面已經提到過了,只有當OR連接的每一列都有索引時,才會使用索引,因此,在寫SQL的時候盡量讓每列都有索引,這樣速度會大大加快。
分頁查詢的兩種優化方法
一般分頁查詢的時候,我們都會通過創建覆蓋索引的方式提高性能。但是有些時候,我們的數據較多,會發生深分頁,也就是limit 1000,10
,這種情況,此時查詢了1010條記錄,排序后卻只需要返回1001到1010的10條記錄,造成了很大的浪費。
針對這種情況,我們給出兩種通用做法。
優化后我們按照索引分頁改寫SQL后,已經看不到全表掃描了。這里需要注意,我們的前提是已經建立了title上的索引。
- 第二種優化思路
第二種思路其實是將limit查詢操作轉化為某個位置的查詢。每次查詢后都記錄查詢結果的最后一個,下次查詢直接查詢該記錄后的n個結果就可以了。
給出優化前的sql
可以看出這個查詢使用全表掃描。
優化后
這種優化方法比較簡便,但是這種方法有一定的局限性。當排序字段存在重復元素時,這種方法會出現錯誤,遺漏數據。因此我們需要根據情況比較如何選擇。