SQL 優化——一般步驟、索引問題、優化方法(ANALYZE、CHECK、OPTIMIZE)、常用 SQL 的優化
一、優化 SQL 語句的一般步驟
1 通過 show status 命令瞭解各種 SQL 的執行頻率
MySQL 客戶端連線成功後,通過【 show [session|global] status 】命令可以提供伺服器狀態資訊,也可以在作業系統上使用 mysqladmin extended-status 命令獲得這些訊息。show [session|global] status 可以根據需要加上引數“session”或者“global”來顯示 session 級(當前連線)的統計結果和 global 級(自資料庫上次啟動至今)的統計結果。如果不寫,預設使用引數是“session”。
Com_xxx 表示每個 xxx 語句執行的次數,我們通常比較關心的是以下幾個統計引數。
- Com_select:執行 select 操作的次數,一次查詢只累加 1。
- Com_insert:執行 INSERT 操作的次數,對於批量插入的 INSERT 操作,只累加一次。
- Com_update:執行 UPDATE 操作的次數。
- Com_delete:執行 DELETE 操作的次數。
上面這些引數對於所有儲存引擎的表操作都會進行累計。
下面這幾個引數只是針對InnoDB 儲存引擎的,累加的演算法也略有不同。
- Innodb_rows_read:select 查詢返回的行數。
- Innodb_rows_inserted:執行 INSERT 操作插入的行數。
- Innodb_rows_updated:執行 UPDATE 操作更新的行數。
- Innodb_rows_deleted:執行 DELETE 操作刪除的行數。
通過以上幾個引數,可以很容易地瞭解當前資料庫的應用是以插入更新為主還是以查詢操作為主,以及各種型別的 SQL 大致的執行比例是多少。對於更新操作的計數,是對執行次數的計數,不論提交還是回滾都會進行累加。
對於事務型的應用,通過 Com_commit 和 Com_rollback 可以瞭解事務提交和回滾的情況,對於回滾操作非常頻繁的資料庫,可能意味著應用編寫存在問題。
此外,以下幾個引數便於使用者瞭解資料庫的基本情況。
- Connections:試圖連線 MySQL 伺服器的次數。
- Uptime:伺服器工作時間。
- Slow_queries:慢查詢的次數。
2 定位執行效率較低的 SQL 語句
可以通過以下兩種方式定位執行效率較低的 SQL 語句。
- 通過慢查詢日誌定位那些執行效率較低的 SQL 語句,用--log-slow-queries[=file_name]選項啟動時,mysqld 寫一個包含所有執行時間超過 long_query_time 秒的 SQL 語句的日誌檔案。具體可以檢視本書第 26 章中日誌管理的相關部分。
- 慢查詢日誌在查詢結束以後才紀錄,所以在應用反映執行效率出現問題的時候查詢慢查詢日誌並不能定位問題,可以使用 show processlist 命令檢視當前 MySQL 在進行的執行緒,包括執行緒的狀態、是否鎖表等,可以實時地檢視 SQL 的執行情況,同時對一些鎖表操作進行優化。
3 通過 EXPLAIN 分析低效 SQL 的執行計劃
通過以上步驟查詢到效率低的 SQL 語句後,可以通過 EXPLAIN 或者 DESC 命令獲取 MySQL如何執行 SELECT 語句的資訊,包括在 SELECT 語句執行過程中表如何連線和連線的順序,比如想計算 2006 年所有公司的銷售額,需要關聯 sales 表和 company 表,並且對 moneys 欄位做求和(sum)操作,相應 SQL 的執行計劃如下:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
每個列的簡單解釋如下:
- select_type:表示 SELECT 的型別,常見的取值有 SIMPLE(簡單表,即不使用表連線或者子查詢)、PRIMARY(主查詢,即外層的查詢)、UNION(UNION 中的第二個或者後面的查詢語句)、SUBQUERY(子查詢中的第一個 SELECT)等。
- table:輸出結果集的表。
- type:表示表的連線型別,效能由好到差的連線型別為
- system(表中僅有一行,即常量表)
- const(單表中最多有一個匹配行,例如 primary key 或者 unique index)
- eq_ref(對於前面的每一行,在此表中只查詢一條記錄,簡單來說,就是多表連線中使用 primary key 或者 unique index)
- ref(與 eq_ref 類似,區別在於不是使用 primary key 或者 unique index,而是使用普通的索引)
- ref_or_null(與 ref 類似,區別在於條件中包含對 NULL 的查詢)
- index_merge(索引合併優化)
- unique_subquery(in的後面是一個查詢主鍵欄位的子查詢)
- index_subquery (與 unique_subquery 類似,區別在於 in 的後面是查詢非唯一索引欄位的子查詢)
- range (單表中的範圍查詢)
- index (對於前面的每一行,都通過查詢索引來得到資料)
- all (對於前面的每一行,都通過全表掃描來得到資料)
- possible_keys:表示查詢時,可能使用的索引。
- key:表示實際使用的索引。
- key_len:索引欄位的長度。
- rows:掃描行的數量。
- Extra:執行情況的說明和描述。
4 確定問題並採取相應的優化措施
經過以上步驟,基本就可以確認問題出現的原因。此時使用者可以根據情況採取相應的措施,進行優化提高執行的效率。在上面的例子中,已經可以確認是對 a 表的全表掃描導致效率的不理想,那麼對 a 表的year 欄位建立索引:
1 |
|
建立索引後,再看一下這條語句的執行計劃:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
|
可以發現建立索引後對 a 表需要掃描的行數明顯減少(從 1000 行減少到 1 行),可見索引的使用可以大大提高資料庫的訪問速度,尤其在表很龐大的時候這種優勢更為明顯。
二、索引問題
索引是資料庫優化中最常用也是最重要的手段之一,通過索引通常可以幫助使用者解決大多數的 SQL 效能問題。
1 索引的儲存分類
MyISAM 儲存引擎的表的資料和索引是自動分開儲存的,各自是獨立的一個檔案; InnoDB儲存引擎的表的資料和索引是儲存在同一個表空間裡面,但可以有多個檔案組成。
MySQL 中索引的儲存型別目前只有兩種(BTREE 和 HASH),具體和表的儲存引擎相關:MyISAM 和 InnoDB 儲存引擎都只支援 BTREE 索引;MEMORY/HEAP 儲存引擎可以支援 HASH和 BTREE 索引。
MySQL 目前不支援函式索引,但是能對列的前面某一部分進索引(字首索引),例如 name 欄位,可以只取 name 的前 4 個字元進行索引,這個特性可以大大縮小索引檔案的大小,使用者在設計表結構的時候也可以對文字列根據此特性進行靈活設計。下面是建立字首索引的一個例子:
1 |
|
2 MySQL 如何使用索引
索引用於快速找出在某個列中有一特定值的行。對相關列使用索引是提高 SELECT 操作效能的最佳途徑。
查詢要使用索引最主要的條件是查詢條件中需要使用索引關鍵字,如果是多列索引,那麼只有查詢條件使用了多列關鍵字最左邊的字首時,才可以使用索引,否則將不能使用索引。
1.使用索引
在 MySQL 中,下列幾種情況下有可能使用到索引:
(1)對於建立的多列索引,只要查詢的條件中用到了最左邊的列,索引一般就會被使用
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 |
|
(2)對於使用 like 的查詢,後面如果是常量並且只有%號不在第一個字元,索引才可能會被使用:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
|
(3)如果對大的文字進行搜尋,使用全文索引而不用使用 like ‘%...%’。
(4)如果列名是索引,使用 column_name is null 將使用索引。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
2.存在索引但不使用索引
在下列情況下,雖然存在索引,但是 MySQL 並不會使用相應的索引。
(1)如果 MySQL 估計使用索引比全表掃描更慢,則不使用索引。例如,如果列key_part1 均勻分佈在 1 和 100 之間,下列查詢中使用索引就不是很好:
1 |
|
(2)如果使用 MEMORY/HEAP 表並且 where 條件中不使用“=”進行索引列,那麼不會用到索引。heap 表只有在“=”的條件下才會使用索引。
(3)用 or 分割開的條件,如果 or 前的條件中的列有索引,而後面的列中沒有索引,那麼涉及到的索引都不會被用到,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
|
從上面可以發現只有 year 列上面有索引,來看如下的執行計劃:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
可見雖然在 year 這個列上存在索引 ind_sales_year,但是這個 SQL 語句並沒有用到這個索引,原因就是 or 中有一個條件中的列沒有索引。
(4)如果不是索引列的第一部分,如下例子:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
可見雖然在 money 上面建有複合索引,但是由於 money 不是索引的第一列,那麼在查詢中這個索引也不會被 MySQL 採用。
(5)如果 like 是以%開始,例如:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
可見雖然在 name 上建有索引,但是由於 where 條件中 like 的值的“%”在第一位了,那麼MySQL 也不會採用這個索引。
(6) 如果列型別是字串,那麼一定記得在 where 條件中把字元常量值用引號引起來,否則的話即便這個列上有索引,MySQL 也不會用到的,因為,MySQL 預設把輸入的常量值進行轉換以後才進行檢索。如下面的例子中 company2 表中的 name 欄位是字元型的,但是 SQL 語句中的條件值是一個數值型值,因此即便在 name 上有索引, MySQL 也不能正確地用上索引,而是繼續進行全表掃描。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 |
|
從上面的例子中可以看到,第一個 SQL 語句中把一個數值型常量賦值給了一個字元型的列name,那麼雖然在 name 列上有索引,但是也沒有用到;而第二個 SQL 語句就可以正確使用索引。
3 檢視索引使用情況
如果索引正在工作,Handler_read_key 的值將很高,這個值代表了一個行被索引值讀的次數,很低的值表明增加索引得到的效能改善不高,因為索引並不經常使用。
Handler_read_rnd_next 的值高則意味著查詢執行低效,並且應該建立索引補救。這個值的含義是在資料檔案中讀下一行的請求數。如果正進行大量的表掃描,Handler_read_rnd_next 的值較高,則通常說明表索引不正確或寫入的查詢沒有利用索引,具體如下。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
從上面的例子中可以看出,目前使用的 MySQL 資料庫的索引情況並不理想。
三、兩個簡單實用的優化方法
1 定期分析表和檢查表
分析表的語法如下:
1 |
|
本語句用於分析和儲存表的關鍵字分佈,分析的結果將可以使得系統得到準確的統計資訊,使得 SQL 能夠生成正確的執行計劃。如果使用者感覺實際執行計劃並不是預期的執行計劃,執行一次分析表可能會解決問題。在分析期間,使用一個讀取鎖定對錶進行鎖定。這對於 MyISAM, BDB 和 InnoDB 表有作用。對於 MyISAM 表,本語句與使用 myisamchk -a 相當,下例中對錶 sales 做了表分析:
1 2 3 4 5 6 7 8 9 10 11 |
|
檢查表的語法如下:
1 |
|
檢查表的作用是檢查一個或多個表是否有錯誤。 CHECK TABLE 對 MyISAM 和 InnoDB 表有作用。對於 MyISAM 表,關鍵字統計資料被更新,例如:
1 2 3 4 5 6 7 8 9 10 11 |
|
CHECK TABLE 也可以檢查檢視是否有錯誤,比如在檢視定義中被引用的表已不存在,舉例如下。
(1)首先我們建立一個檢視。
1 2 |
|
(2)然後 CHECK 一下該檢視,發現沒有問題。
1 2 3 4 5 6 7 8 9 10 11 |
|
(3)現在刪除掉檢視依賴的表。
1 2 |
|
(4)再來 CHECK 一下剛才的檢視,發現報錯了。
1 2 3 4 5 6 7 8 |
|
2 定期優化表
優化表的語法如下:
1 |
|
如果已經刪除了表的一大部分,或者如果已經對含有可變長度行的表(含有 VARCHAR、BLOB 或 TEXT 列的表)進行了很多更改,則應使用 OPTIMIZE TABLE 命令來進行表優化。這個命令可以將表中的空間碎片進行合併,並且可以消除由於刪除或者更新造成的空間浪費,但OPTIMIZE TABLE 命令只對 MyISAM、BDB 和 InnoDB 表起作用。
1 2 3 4 5 6 7 8 9 10 11 |
|
注意:ANALYZE、CHECK、OPTIMIZE 執行期間將對錶進行鎖定,因此一定注意要在資料庫不繁忙的時候執行相關的操作。
四、常用 SQL 的優化
1 大批量插入資料
當用 load 命令匯入資料的時候,適當的設定可以提高匯入的速度。
對於 MyISAM 儲存引擎的表,可以通過以下方式快速的匯入大量的資料。
ALTER TABLE tbl_name DISABLE KEYS; loading the data ALTER TABLE tbl_name ENABLE KEYS;
DISABLE KEYS 和 ENABLE KEYS 用來開啟或者關閉 MyISAM 表非唯一索引的更新。在匯入大量的資料到一個非空的 MyISAM 表時,通過設定這兩個命令,可以提高匯入的效率。對於匯入大量資料到一個空的 MyISAM 表,預設就是先匯入資料然後才建立索引的,所以不用進行設定。
mysql> alter table film_test2 disable keys; Query OK, 0 rows affected (0.00 sec) mysql> load data infile '/home/mysql/film_test.txt' into table film_test2; Query OK, 529056 rows affected (6.34 sec) Records: 529056 Deleted: 0 Skipped: 0 Warnings: 0 mysql> alter table film_test2 enable keys; Query OK, 0 rows affected (12.25 sec)
上面是對MyISAM表進行資料匯入時的優化措施,對於InnoDB型別的表,這種方式並不能提高匯入資料的效率,可以有以下幾種方式提高InnoDB表的匯入效率。
(1)因為 InnoDB 型別的表是按照主鍵的順序儲存的,所以將匯入的資料按照主鍵的順序排列,可以有效地提高匯入資料的效率。
(2)在匯入資料前執行 SET UNIQUE_CHECKS=0,關閉唯一性校驗,在匯入結束後執行SET UNIQUE_CHECKS=1,恢復唯一性校驗,可以提高匯入的效率。
SET UNIQUE_CHECKS=0; #loading the data
load data infile '/home/mysql/film_test.txt' into table film_test2;
SET UNIQUE_CHECKS=1;
(3)如果應用使用自動提交的方式,建議在匯入前執行 SET AUTOCOMMIT=0,關閉自動提交,匯入結束後再執行 SET AUTOCOMMIT=1,開啟自動提交,也可以提高匯入的效率。
SET AUTOCOMMIT=0; #loading the data
load data infile '/home/mysql/film_test.txt' into table film_test2;
SET AUTOCOMMIT=1;
2 優化 INSERT 語句
當進行資料 INSERT 的時候,可以考慮採用以下幾種優化方式。
- 如果同時從同一客戶插入很多行,儘量使用多個值表的 INSERT 語句,這種方式將大大縮減客戶端與資料庫之間的連線、關閉等消耗,使得效率比分開執行的單個 INSERT 語句快(在一些情況中幾倍)。下面是一次插入多值的一個例子:
insert into test values(1,2),(1,3),(1,4)...
- 如果從不同客戶插入很多行,能通過使用 INSERT DELAYED 語句得到更高的速度。DELAYED 的含義是讓 INSERT 語句馬上執行,其實資料都被放在記憶體的佇列中,並沒有真正寫入磁碟,這比每條語句分別插入要快的多;LOW_PRIORITY 剛好相反,在所有其他使用者對錶的讀寫完後才進行插入;
- 將索引檔案和資料檔案分在不同的磁碟上存放(利用建表中的選項);
- 如果進行批量插入,可以增加 bulk_insert_buffer_size 變數值的方法來提高速度,但是,這隻能對 MyISAM 表使用;
- 當從一個文字檔案裝載一個表時,使用 LOAD DATA INFILE。這通常比使用很多 INSERT 語句快 20 倍。
3 優化 GROUP BY 語句
預設情況下,MySQL 對所有 GROUP BY col1,col2....的欄位進行排序。這與在查詢中指定ORDER BY col1,col2...類似。因此,如果顯式包括一個包含相同的列的 ORDER BY 子句,則對 MySQL 的實際執行效能沒有什麼影響。
如果查詢包括 GROUP BY 但使用者想要避免排序結果的消耗,則可以指定 ORDER BY NULL禁止排序。
4 優化 ORDER BY 語句
在某些情況中, MySQL 可以使用一個索引來滿足 ORDER BY 子句,而不需要額外的排序。WHERE 條件和 ORDER BY 使用相同的索引,並且 ORDER BY 的順序和索引順序相同,並且ORDER BY 的欄位都是升序或者都是降序。
例如,下列 SQL 可以使用索引。
SELECT * FROM t1 ORDER BY key_part1,key_part2,... ; 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;
但是在以下幾種情況下則不使用索引:
SELECT * FROM t1 ORDER BY key_part1 DESC, key_part2 ASC; --order by 的欄位混合 ASC 和 DESC SELECT * FROM t1 WHERE key2=constant ORDER BY key1; --用於查詢行的關鍵字與 ORDER BY 中所使用的不相同 SELECT * FROM t1 ORDER BY key1, key2; --對不同的關鍵字使用 ORDER BY:
5 優化巢狀查詢
MySQL 4.1 開始支援 SQL 的子查詢。這個技術可以使用 SELECT 語句來建立一個單列的查詢結果,然後把這個結果作為過濾條件用在另一個查詢中。使用子查詢可以一次性地完成很多邏輯上需要多個步驟才能完成的 SQL 操作,同時也可以避免事務或者表鎖死,並且寫起來也很容易。但是,有些情況下,子查詢可以被更有效率的連線(JOIN)替代。
連線(JOIN)之所以更有效率一些,是因為 MySQL 不需要在記憶體中建立臨時表來完成這個邏輯上的需要兩個步驟的查詢工作。
6 MySQL 如何優化 OR 條件
對於含有 OR 的查詢子句,如果要利用索引,則 OR 之間的每個條件列都必須用到索引;如果沒有索引,則應該考慮增加索引。
MySQL 在處理含有 OR字句的查詢時,實際是對 OR 的各個欄位分別查詢後的結果進行了 UNION。
7 使用 SQL 提示
SQL 提示(SQL HINT)是優化資料庫的一個重要手段,簡單來說就是在 SQL 語句中加入一些人為的提示來達到優化操作的目的。
1.USE INDEX
在查詢語句中表名的後面,新增 USE INDEX 來提供希望 MySQL 去參考的索引列表,就可以讓 MySQL 不再考慮其他可用的索引。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
2.IGNORE INDEX
如果使用者只是單純地想讓 MySQL 忽略一個或者多個索引,則可以使用 IGNORE INDEX 作為 HINT。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
3.FORCE INDEX
為強制 MySQL 使用一個特定的索引,可在查詢中使用 FORCE INDEX 作為 HINT。
例如,
當不強制使用索引的時候,因為 id 的值都是大於 0 的,因此 MySQL 會預設進行全表掃描,而不使用索引,如下所示:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
但是,當使用 FORCE INDEX 進行提示時,即便使用索引的效率不是最高,MySQL 還是選擇使用了索引,這是 MySQL 留給使用者的一個自行選擇執行計劃的權力。加入 FORCE INDEX 提示後再次執行上面的 SQL:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
|
執行計劃中使用了 FORCE INDEX 後的索引。
2.IGNORE INDEX
如果使用者只是單純地想讓 MySQL 忽略一個或者多個索引,則可以使用 IGNORE INDEX 作
為 HINT。