1. 程式人生 > >mysql常用sql語句優化

mysql常用sql語句優化

在資料庫日常維護中,最常做的事情就是SQL語句優化,因為這個才是影響效能的最主要因素。當然還有其他方面的,比如OS優化,硬體優化,MySQL Server優化,資料型別優化,應用層優化,但是這些都沒有SQL語句優化來的重要。下面將介紹INSERT,GROUP BY,LIMIT等的優化方法。

1. 優化大批量插入資料

當用load命令匯入資料的時候,適當的設定可以提高匯入的速度。

對於MyISAM儲存引擎的表

對於MyISAM儲存引擎的表,可以通過如下方式快速匯入大量的資料:

ALTER TABLE tablename DISABLE KEYS;
loading the data;
ALTER
TABLE tablename ENABLE KEYS;

DISABLE KEYS和ENABLE KEYS用來關閉或者開啟MyISAM表非唯一索引的更新。
在匯入大量的資料到一個非空的MyISAM表示,通過設定這兩個命令,可以提高匯入的效率。
對於匯入大量資料到一個空的MyISAM表時,預設就是先匯入資料然後才建立索引的,所以不用設定。

對於InnoDB儲存引擎表

對於InnoDB儲存引擎表,上面的方式並不能提高匯入資料的效率。可以有以下幾種方式提高Innodb表的匯入效率:

  1. 因為InnoDB型別的表是按照主鍵的順序儲存的,所以將匯入的資料按照主鍵的順序排列,可以有效的提高匯入資料的效率。
  2. 在匯入資料前執行 SET UNIQUE_CHECKS=0,關閉唯一性效驗,在匯入資料結束以後執行SET UNIQUE_CHECKS=1,恢復唯一性效驗,可以提高匯入效率。
  3. 如果使用自動提交的方式,建議在匯入前執行SET AUTOCOMMIT=0,關閉自動提交,匯入結束後再執行SET AUTOCOMMIT=1,開啟自動提交,也可以提高匯入的效率。
  4. 對於有外來鍵約束的表,我們在匯入資料之前也可以忽略外來鍵檢查,因為innodb的外來鍵是即時檢查的,所以對匯入的每一行都會進行外來鍵的檢查。
set foreign_key_checks = 0;
load data ............
set
foreign_key_checks = 1;

2. 優化INSERT語句

  • 如果同時從同一客戶端插入大量資料,應該儘量使用多個值的表的INSERT 語句,這種方式將大大減少客戶端與資料庫伺服器之間的連線,關閉等消耗,使得效率比分開執行的單個INSERT語句快(大部分情況下,使用多個值表的INSERT語句能比單個INSERT語句快上好幾倍),比如下面一次插入多行:
INSERT INTO user(name,age) VALUES ('yayun',23),('tom',26),('atlas',32),('david',25).......
  • 插入延遲。如果從不同客戶端插入很多行,可以通過使用INSERT DELAYED語句得到更高的速度。DELAYED的意思是讓INSERT語句馬上執行,其實資料都被放在記憶體的佇列中,並沒有真正寫入磁碟,這比每條語句分別插入要快的多;LOW_PRIORITY剛好相反,在所有其他使用者對錶的讀寫完成後才進行插入。
  • 將索引檔案和資料檔案放在不同的磁碟(利用建表中的選項)
  • 如果進行批量插入,可以通過增加bulk_insert_buffer_size 變數值的方法來提高速度,這隻對MyISAM表有用。
  • 當從一個文字檔案裝載一個表時,使用LOAD DATA INFILE。通常比使用很多的INSERT語句快。

無法使用索引的情況

  1. 以%開頭的like查詢
  2. 資料型別出現隱式轉換的時候也不會使用索引,特別是當列型別是字串,那麼一定記得在where條件中把字串常量值用引號引起來,否則即便這個列上有索引,MySQL也不會用到,因為MySQL預設把輸入的常量值進行轉換以後才進行檢索
  3. 複合索引的情況下,如果查詢條件不包含索引列的最左邊部分,即不滿足最左字首原則,則不會使用索引
  4. 如果mysql估計使用索引掃描比全表掃描更慢,則不使用索引。(掃描資料超過30%,都會走全表)
  5. 用or分割開的條件,如果 or前的條件中的列有索引,而後面的列中沒有索引,那麼涉及的索引都不會被用到
  6. 欄位使用函式,將無法使用索引
  7. Join 語句中 Join 條件欄位型別不一致的時候 MySQL 無法使用索引

3. 優化ORDER BY語句

通過索引排序是效能最好的,通常如果SQL語句不合理,就無法使用索引排序,以下幾種情況是無法使用索引排序的。

  1. 查詢使用了兩種不同的排序方向,但是索引列都是正序排序的;
  2. 查詢的where和order by中的列無法組合成索引的最左字首;
  3. 查詢在索引列的第一列上是範圍條件;
  4. 查詢條件上有多個等於條件。對排序來說,這也是一種範圍查詢

在優化ORDER BY語句之前,先來看看MySQL中排序的方式。先看看MySQL官方提供的示例資料庫sakila中customer表上的索引情況。

這裡寫圖片描述

1.MySQL中有兩種排序方式

第一種通過有序索引順序掃描直接返回有序資料,這種方式在使用explain分析查詢時顯示為Using Index,不需要額外的排序,效能是最優的。

這裡寫圖片描述

因為查詢主鍵,然後store_id列是輔助索引(二級索引),輔助索引上存放了索引鍵值+對應行的主鍵,所以直接掃描輔助索引返回有序資料。

這裡寫圖片描述

這種排序方式直接使用了主鍵,也可以說成是使用了聚集索引。因為innodb是索引組織表(index-organized table),通過主鍵聚集資料,資料都是按照主鍵排序存放。而聚集索引就是按照沒張表的主鍵構造一顆B+樹,同時葉子節點中存放的即為正張表的行記錄資料,也將聚集索引的葉子節點稱為資料頁。聚集索引的這個特性決定了索引組織表中的資料也是索引的一部分。

第二種是通過對返回資料進行排序,也就是通常說的Filesort排序,所有不是通過索引直接返回排序結果的排序都叫Filesort排序。Filesort並不代表通過磁碟檔案進行排序,而只是說明進行了一個排序操作,至於排序操作是否使用了磁碟檔案或者臨時表,取決於mysql伺服器對排序引數的設定和需要排序資料的大小。

這裡寫圖片描述

那麼這裡優化器為什麼不使用store_id列上的輔助索引進行排序呢?

當通過輔助索引來查詢資料時,innodb儲存引擎會遍歷輔助索引並通過葉級別的指標獲得指向主鍵索引的主鍵,然後通過主鍵索引來找到一個完整的行記錄。舉例來說,如果在一棵高度為3的輔助索引樹中查詢資料,那麼需要對這個輔助索引樹遍歷3次找到指定主鍵,如果聚集索引樹的高度同樣為3,那麼還需要對聚集索引樹進行3次查詢,最終找到一個完整的行數所在的頁,因此一共需要6次邏輯IO訪問以得到最終的一個數據頁。

使用mysql5.6 的trace功能來檢視一下強制使用輔助索引和全表掃描的開銷。(mysql5.6的trace真心不錯,給個贊^_^)

{
   "rows_estimation": [
     {
       "table": "`customer` FORCE INDEX (`idx_fk_store_id`)",
       "table_scan": {
         "rows": 599,
         "cost": 5
       } /* table_scan */
     }
   ] /* rows_estimation */
 },
 {
   "considered_execution_plans": [
     {
       "plan_prefix": [
       ] /* plan_prefix */,
       "table": "`customer` FORCE INDEX (`idx_fk_store_id`)",
       "best_access_path": {
         "considered_access_paths": [
           {
             "access_type": "scan",
             "rows": 599,
             "cost": 719.8,
          "chosen": true,
             "use_tmp_table": true
           }

可以清楚的看見優化器使用全表掃描開銷更小。

再來看一種情況

這裡寫圖片描述

這裡寫圖片描述

這裡為什麼又是filesort呢?不是使用了using index嗎?雖然使用了覆蓋索引(只訪問索引的查詢,即查詢只需要訪問索引,而無須訪問資料行,最簡單的理解,比如翻開一本書,從目錄頁查詢某些內容,但是目錄就寫的比較詳細,我們在目錄就找到了自己想看的內容)。但是請別忘記了,idx_stored_email是複合索引,必須遵循最左字首的原則。

我們改成如下SQL,就可以看見效果了:
這裡寫圖片描述

Filesort是通過相應的排序演算法,將取得的資料在sort_buffer_size系統變數設定的記憶體排序區中進行排序,如果記憶體裝載不下,它就會將磁碟上的資料進行分塊,再對各個資料塊進行排序,然後將各個塊合併成有序的結果集。sort_buffer_size設定的排序區是每個執行緒獨佔的,所以在同一個時刻,mysql中存在多個sort buffer排序區。該值不要設定的太大,避免耗盡伺服器記憶體。

簡單來說,儘量減少額外排序,通過索引直接返回有序資料。where條件和order by使用相同的索引,並且order by的順序和索引順序相同。並且order by的欄位都是升序或者降序。否則肯定需要額外的排序操作,這樣就會出現Filesort。

這裡寫圖片描述

針對Filesort優化MySQL Server

通過建立合適的索引當然能夠減少Filesort的出現。但是在某些特殊情況下,條件限制不能讓Filesort消失,那就需要想辦法加快Filesort的操作。對於Filesort,mysql有兩種排序演算法。

1.兩次掃描演算法(Two Passes)

首先 根據條件取出排序欄位和行指標資訊,之後在排序區sort buffer中排序。如果sort buffer不夠,則在臨時表Temporary Table中儲存排序結果。完成排序後根據行指標回表讀取記錄。該演算法是MySQL 4.1之前採用的演算法,需要兩次訪問資料,第一次獲取排序欄位和行指標資訊,第二次根據行指標記錄獲取記錄,尤其是第二次讀取操作可能導致大量隨機I/O操作,優點是排序的時候記憶體開銷比較小。

2.一次掃描演算法(Single passes)

一次性取出滿足條件的行的所有欄位,然後在排序區sort buffer中排序後直接輸出結果集。排序的時候記憶體開銷比較大,但是排序效率比兩次掃描演算法高。

MySQL通過比較系統變數max_length_for_sort_data的大小和Query語句取出的欄位總大小來判斷使用哪種排序演算法。如果max_length_for_sort_data更大,那麼使用第二種排序演算法,否則使用第一種。

適當加大系統變數max_length_for_sort_data的值,能夠讓MySQL選擇更優化的排序演算法,即第二種演算法。當然設定max_length_for_sort_data 過大,會造成CPU利用率過低和磁碟I/O過高,CPU和I/O利用平衡就足夠了。

適當加大sort_buffer_size排序區,儘量讓排序在記憶體中完成,而不是通過建立臨時表放在檔案中進行,當然也不能無限制加大sort_buffer_size排序區,因為sort_buffer_szie引數是每個執行緒獨佔,設定過大,會導致伺服器SWAP嚴重。

儘量只使用必要的欄位,SELECT具體的欄位名稱,而不是SELECT * 選擇所有欄位,這樣可以減少排序區的使用。提高SQL效能。

4.優化GROUP BY 語句

預設情況下,mysql對所有GROUP BY col1,col2,的欄位進行排序。這與在查詢中指定ORDER BY col1,col2類似。因此,如果顯式包括一個 包含相同列的ORDER BY子句,則對mysql的實際效能沒有什麼影響。如果查詢包括GROUP BY,但我們想要避免排序帶來的效能損耗,則可以指定ORDER BY NULL禁止排序,示例如下:

這裡寫圖片描述

可以看見使用了Filesort,還使用了記憶體臨時表,這條SQL嚴重影響效能,所以需要優化:

首先禁止排序,ORDER BY NULL

這裡寫圖片描述

可以看見已經沒有使用Filesort,但是還是使用了記憶體臨時表,這是我們可以建立一個複合索引來優化效能

這裡寫圖片描述

5.優化子查詢

MySQL 4.1開始支援SQL的子查詢。這個技術可以使用SELECT語句來建立一個單列的查詢結果,然後把這個結果作為過濾條件用在另外一個SELECT語句中。使用子查詢可以一次性完成很多邏輯上需要多個步驟才能完成的SQL操作,同時也可以避免事務或者表鎖死,並且寫起來也非常easy,but,在有些情況下,子查詢效率非常低下,我們可以使用比較高大上的寫法,那就是連線(JOIN)取而代之.^_^,下面是一個列子:

這裡寫圖片描述

我解釋一下這裡的執行計劃:

第二行,id為2,說明優先順序最高,最先執行,DEPENDENT SUBQUERY子查詢中的第一個SELECT(意味著select依賴於外層查詢中的資料),type為index_subquery,與unique_subquery類似,區別在於in的後面是查詢非唯一索引欄位的子查詢,using index使用了覆蓋索引。

第一行,id為1,說明優先順序最低,可以看見select_type列是PRIMARY,意思是最外層的SELECT查詢,可以看見使用了全表掃描。

如果使用連線(join)來完成這個查詢,速度將會快很多。尤其是連線條件有索引的情況下:

這裡寫圖片描述

從執行計劃看出查詢關聯型別從index_subquery調整為了ref,在mysql5.5(包含mysql5.5),子查詢效率還是不如關聯查詢(join),連線之所以更有效率,是因為MySQL不需要在記憶體中建立臨時表來完成這個邏輯上需要兩個步驟的查詢工作。

6.優化OR條件

對於含有OR的查詢語句,則無法使用單列索引,但是可以使用複合索引

這裡寫圖片描述

下面看一個較簡單明瞭的例子:
這裡寫圖片描述

可以看見表tt有兩個單列索引,我們使用如下SQL查詢,看是否會使用索引

這裡寫圖片描述

可以看見雖然顯示有id,age索引可用,但是沒有使用,即全表掃描。我們可以這樣優化:

這裡寫圖片描述

可以看見已經使用了索引,至於這裡的執行計劃,我就不再說明。有機會我會寫一篇mysql執行計劃的文章。

看看使用複合索引查詢的情況:

這裡寫圖片描述

這裡寫圖片描述

7.優化分頁查詢(LIMIT)

一般分頁查詢時,通過建立覆蓋索引能夠比較好的提高效能。比較常見的一個分頁查詢是limit 1000,20,這種最蛋碎了,此時mysql排序出前1020條記錄後僅僅返回第1001到1020條記錄,前1000條記錄都會被拋棄,查詢和排序的代價非常高。

在索引上完成排序分頁操作,最後根據主鍵關聯回表查詢所需要的其他列內容。(使用到了自連線)例如下面的SQL語句:

這裡寫圖片描述

可以看見實際上使用了全表掃描,如果表有上百萬記錄,那麼這將是一條致命SQL

我們改寫成按照索引分頁後回表讀取行的方式,從執行計劃中看不到全表掃描了

這裡寫圖片描述

這裡我大概解釋一下執行計劃:

第三行:

id為2,優先順序最高,最先執行
select_type為DERIVED 用來表示包含在from子句中的子查詢的select,mysql會遞迴執行並將結果放到一個臨時表中。伺服器內部稱為“派生表”,因為該臨時表是從子查詢中派生出來的。
type列為index表示索引樹全掃描,mysql遍歷整個索引來查詢匹配的行,這裡就把film_id查詢出來了。
Extra列為using index 表示使用覆蓋索引

第二行:
select_type為PRIMARY,即複雜查詢的最外層,當然這裡還不算是最最外層。
table列為a,即film表的別名a,
type列為eq_ref,類似ref,區別就在使用的索引是唯一索引,對於每個索引鍵值,表中只有一條記錄匹配,簡單來說,就是多表連線中使用primary key或者 unique key作為關聯條件

第一行:
select_type列的primary表示該查詢為外層查詢
table列被標記為<derived2>,表示查詢結果來自一個衍生表,其中2代表該查詢衍生自第2個select查詢,即id為2的select

7.其他優化手段

當然還有其他的優化手段,比如索引提示,我這裡簡單列舉一下就行了,因為大部分的時候mysql 優化器都工作的很好。

USE INDEX
提供給優化器參考的索引列表(優化器不一定給你面子哦)

IGNORE INDEX
提示優化器忽略一個或者多個索引

FORCE INDEX
強制優化器使用某個特定索引

總結一下:
其實SQL語句優化的過程中,無非就是對mysql的執行計劃理解,以及B+樹索引的理解,其實只要我們理解執行計劃和B+樹以後,優化SQL語句還是比較簡單的,當然還有特別複雜的SQL,我這裡只是一些簡單例子,當然再複雜的SQL,還是逃脫不了原理性的東西。呵呵。^_^