SQL優化的一般步驟
在MySQL資料庫中,當面對一個有sql效能問題的資料庫時,我們可以使用show status、explain、show profile等命令進行系統分析,儘快定位問題SQL並解決問題。
一、通過show status命令瞭解各種SQL的執行情況
mysql> show status like 'Com_%';
+---------------------------+-------+
| Variable_name | Value |
+---------------------------+-------+
| Com_admin_commands | 0 |
| Com_assign_to_keycache | 0 |
| Com_alter_db | 0 |
| Com_alter_db_upgrade | 0 |
| Com_alter_event | 0 |
| Com_alter_function | 0 |
| Com_alter_procedure | 0 |
| Com_alter_server | 0 |
| Com_alter_table | 0 |
| Com_alter_tablespace | 0 |
| Com_alter_user | 0 |
| Com_analyze | 0 |
...
Com_xxx表示xxx語句執行的次數,通過觀察Com_insert、Com_select、Com_update、Com_delete等引數可以瞭解當前資料庫是以插入更新為主還是以查詢為主。以及各種型別的SQL大致的執行比例是多少。
對於事務性應用,通過Com_commit、Com_rollback可以瞭解事務提交和回滾的情況,對於事務回滾非常頻繁的資料庫,意味著應用編寫可能存在問題。
二、通過explain分析低效SQL的執行計劃
通過慢查詢日誌可以定位那些執行效率較低的SQL語句。之後可以通過explain或者desc命令獲取MySQL執行select語句的資訊。
mysql> explain select * from demo where id>5 and id<10 \G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: demo
type: range
possible_keys: PRIMARY
key: PRIMARY
key_len: 4
ref: NULL
rows: 3
Extra: Using where
1 row in set (0.00 sec)
explain命令顯示select語句的執行資訊,下面對每個欄位進行說明
1、select_type
表示select的型別,常見取值有:
- select_type=SIMPLE:簡單表,即不使用表連線和子查詢
- select_type=PRIMARY:主查詢,即外層查詢
- select_type=UNION:UNION中的第二個或者後面的查詢
- select_type=SUBQUERY:子查詢中的第一個select
2、table
輸出結果集的表
3、type
表示MySQL在表中找到所需行的方式,或者叫訪問型別。常見取值有:ALL、index、range、ref、eq_ref、const/system、NULL。從左到右,效能由最差到最好。
- type=ALL:全表掃描,MySQL遍歷全表來找到匹配的行
- type=index:索引全掃描。MySQL遍歷整個索引來查詢匹配的行
- type=range:索引範圍掃描。常見於<、<=、>、>=、between等操作符
- type=ref:使用非唯一索引掃描或唯一索引字首掃描,返回匹配某個單獨值的記錄行
- type=eq_ref:類似ref,區別在於使用的索引是唯一索引,對於每個索引的鍵值,表中只有一條記錄匹配。
- type=const/system:但表中最多有一個匹配行,查詢起來非常迅速。常見於根據主鍵primary key或唯一索引unique index進行的查詢。
- type=NULL:MySQL不用訪問表或者索引,直接就能得到結果。比如 select 2 > 1;
- 其他
4、possible_keys
表示查詢時可能使用的索引
5、key
表示實際使用的索引
6、key_len
使用到索引欄位的長度
7、rows
掃描行的數量
8、Extra
執行情況的說明和描述,包含不適合在其他列中顯示,但對執行計劃非常重要的額外資訊
使用explain extended命令加上show warnings,可以看到SQL真正被執行前優化做了哪些改寫:
mysql> explain extended select sum(salary) from demo a , role b where 1=1 and a.id=b.id and a.sex='男
' \G;
*************************** 1. row ***************************
id: 1
select_type: SIMPLE
table: b
type: index
possible_keys: PRIMARY
key: roleName_id
key_len: 63
ref: NULL
rows: 6
filtered: 100.00
Extra: Using index
*************************** 2. row ***************************
id: 1
select_type: SIMPLE
table: a
type: eq_ref
possible_keys: PRIMARY
key: PRIMARY
key_len: 4
ref: test.b.id
rows: 1
filtered: 100.00
Extra: Using where
2 rows in set, 1 warning (0.00 sec)
ERROR:
No query specified
mysql> show warnings \G;
*************************** 1. row ***************************
Level: Note
Code: 1003
Message: /* select#1 */ select sum(`test`.`a`.`salary`) AS `sum(salary)` from `test`.`demo` `a` join
`test`.`role` `b` where ((`test`.`a`.`id` = `test`.`b`.`id`) and (`test`.`a`.`sex` = '男'))
1 row in set (0.00 sec)
通過show warning命令的message欄位可以看到優化器去除了1=1的恆等條件,在遇到複雜SQL可以通過explain extended命令加show warning命令迅速獲得一個清晰易讀的語句
三、通過show profile分析SQL
通過profile,我們可以更清楚地瞭解SQL的執行過程,及各個過程所花時間。
檢視當前MySQL是否支援profile:
mysql> select @@have_profiling;
+------------------+
| @@have_profiling |
+------------------+
| YES |
+------------------+
1 row in set, 1 warning (0.00 sec)
預設profiling是關閉的,可以通過set語句在Session級別開啟profiling:
mysql> select @@profiling;
+-------------+
| @@profiling |
+-------------+
| 0 |
+-------------+
1 row in set, 1 warning (0.00 sec)
mysql> set profiling=1;
Query OK, 0 rows affected, 1 warning (0.00 sec)
在一個InnoDB表上查詢總行數:
mysql> select count(*) from tb_item;
+----------+
| count(*) |
+----------+
| 3096 |
+----------+
1 row in set (0.01 sec)
通過show profiles語句檢視當前查詢語句的Query ID:
mysql> show profiles;
+----------+------------+------------------------------+
| Query_ID | Duration | Query |
+----------+------------+------------------------------+
| 1 | 0.00025375 | SELECT DATABASE() |
| 2 | 0.00505650 | select count(*) from tb_item |
+----------+------------+------------------------------+
2 rows in set, 1 warning (0.00 sec)
通過show profile for query 2 看到執行過程中執行緒的執行狀態和消耗的時間:
mysql> show profile for query 2;
+----------------------+----------+
| Status | Duration |
+----------------------+----------+
| starting | 0.000117 |
| checking permissions | 0.000012 |
| Opening tables | 0.000028 |
| init | 0.000021 |
| System lock | 0.000015 |
| optimizing | 0.000009 |
| statistics | 0.000023 |
| preparing | 0.000019 |
| executing | 0.000005 |
| Sending data | 0.004631 |
| end | 0.000017 |
| query end | 0.000013 |
| closing tables | 0.000022 |
| freeing items | 0.000099 |
| cleaning up | 0.000028 |
+----------------------+----------+
15 rows in set, 1 warning (0.00 sec)
為了更加直觀看到排序結果,可以查詢information_schema.profiling表並按消耗時間做個desc排序:
mysql> select state,sum(duration) as Total_R ,
-> round(
-> 100*sum(duration)/(select sum(duration) from information_schema.profiling
-> where query_id = 2),2
-> )as Pct_R,
-> count(*)
-> from information_schema.profiling
-> where query_id=2
-> group by state
-> order by Total_R desc;
+----------------------+----------+-------+----------+
| state | Total_R | Pct_R | count(*) |
+----------------------+----------+-------+----------+
| Sending data | 0.004631 | 91.54 | 1 |
| starting | 0.000117 | 2.31 | 1 |
| freeing items | 0.000099 | 1.96 | 1 |
| cleaning up | 0.000028 | 0.55 | 1 |
| Opening tables | 0.000028 | 0.55 | 1 |
| statistics | 0.000023 | 0.45 | 1 |
| closing tables | 0.000022 | 0.43 | 1 |
| init | 0.000021 | 0.42 | 1 |
| preparing | 0.000019 | 0.38 | 1 |
| end | 0.000017 | 0.34 | 1 |
| System lock | 0.000015 | 0.30 | 1 |
| query end | 0.000013 | 0.26 | 1 |
| checking permissions | 0.000012 | 0.24 | 1 |
| optimizing | 0.000009 | 0.18 | 1 |
| executing | 0.000005 | 0.10 | 1 |
+----------------------+----------+-------+----------+
15 rows in set (0.00 sec)
可以清晰地看到時間主要消耗在Sending data這個狀態上。原因是MySQL執行緒要在該狀態做大量的磁碟讀取操作。