mysql索引設計的注意事項(大量示例,收藏再看)

mysql索引設計的注意事項(大量示例,收藏再看)

目錄

  • 1、索引的重要性
  • 2、執行計劃上的重要關注點
  • (1).全表掃描,檢索行數
  • (2).key,using index(覆蓋索引)
  • (3).經過key_len肯定究竟使用了複合索引的幾個索引字段
  • (4) order by和Using filesort
  • 3、索引設計的注意事項
  • (1). 關於INNODB表PRIMARY KEY的建議
  • (2). 什麼列上適合建索引,什麼列上不適合建索引
  • (3). 索引必定是有益的嗎?
  • (4). where條件中不要在索引字段側進行任何運算(包括隱式運算),不然會致使索引不可用,致使全表掃描
  • (5). 不要使用%xxx%這種模糊匹配,會致使全表掃描/索引全掃描
  • (6). 關於前綴索引和冗餘索引
  • (7). 關於索引定義中的字段順序
  • (8). 關於排序查詢的優化
  • (9). 關於單列索引和複合索引
  • (10). 關於多表關聯
  • 4、慢查詢日誌的分析以及關注點
  • (1). 使用pt-query-digest工具來統計
  • (2). 對統計輸出進行分析
  • 5、幾個優化案例
  • 優化案例1
  • 優化案例2
  • 優化案例3

1、索引的重要性

索引對於MySQL數據庫的重要性是不言而喻的:
由於缺少合適的索引,一個稍大的表全表掃描,稍微來些併發,就可能致使DB響應時間急劇飆升,甚至致使DB性能的雪崩;
如今你們廣泛使用的Innodb引擎的鎖機制依賴於索引,缺少適合的索引,會致使鎖範圍的擴大,甚至致使鎖表的效果,嚴重影響業務SQL的並行執行,影響業務的可伸縮性,只有在合適的索引條件下,纔是行鎖的效果.
既然索引對MySQL數據庫這麼重要,那麼在索引的設計上有什麼須要注意的事項嗎? 這篇文章就來聊聊這個.html

2、執行計劃上的重要關注點

既然涉及到索引,避免不了執行計劃的對比,先簡單說一下執行計劃上的重要關注點mysql

(1).全表掃描,檢索行數

mysql> show create table novel_agg_info\G *************************** 1. row ***************************
       Table: novel_agg_info Create Table: CREATE TABLE `novel_agg_info` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `rid` bigint(20) unsigned NOT NULL, `book_name` varchar(128) NOT NULL, `tag` varchar(128) NOT NULL, `dir_id` bigint(20) unsigned NOT NULL DEFAULT '0', `dir_url` varchar(512) NOT NULL DEFAULT '', `public_status` int(2) NOT NULL DEFAULT '1', PRIMARY KEY (`id`), UNIQUE KEY `rid` (`rid`), KEY `book_name` (`book_name`) ) ENGINE=InnoDB AUTO_INCREMENT=12096483 DEFAULT CHARSET=utf8 1 row in set (0.00 sec) mysql> select count(1) from novel_agg_info; +----------+
| count(1) |
+----------+
|  4298257 |
+----------+
1 row in set (0.00 sec) mysql> show table status like 'novel_agg_info'\G *************************** 1. row *************************** Name: novel_agg_info Engine: InnoDB Version: 10 Row_format: Compact Rows: 4321842 Avg_row_length: 130 Data_length: 565182464 Max_data_length: 0 Index_length: 374095872 Data_free: 35651584 Auto_increment: 12096483 Create_time: 2017-05-10 11:55:30 Update_time: NULL Check_time: NULL Collation: utf8_general_ci Checksum: NULL Create_options: Comment: 1 row in set (0.00 sec)

 

實際數據行數近430W,優化器估算Rows: 4321842 行記錄(這是一個估算值,來自於動態採樣,數量級沒有大的偏差便可,實際上屢次執行show table status,獲得的數據也是不一樣的)


where dir_id = 13301689388199959972 由於dir_id字段上沒有索引可用,致使了全表掃描(type:ALL),優化器估算檢索行數爲(rows:4290581)sql

避免全表掃描 全表掃描(type:ALL),大的檢索行數(rows:N,爲估算值),這些都是咱們應該儘可能避免的.數據庫

(2).key,using index(覆蓋索引)

再看下面的執行計劃對比:

key:book_name表明執行使用了KEY book_name (book_name),檢索行數爲1,這很好,是咱們想要的效果.
爲何第1個執行計劃中出現了Using index,而第2個執行計劃中卻沒有呢?
由於:第1個SQL中只須要檢索id,book_name字段,這在KEY book_name (book_name)中都存在了(索引葉節點中都會存儲PRIMARY KEY字段ID),不須要回訪表去獲取其它字段了,Using index即表明這個含義;而第2個SQL中還須要檢索tag字段,這在KEY book_name (book_name)中並不存在,就須要回訪表會獲取這個字段內容,因此沒有出現Using index.
緩存

key,Using index
key: 表明使用的索引名稱
Extra部分的Using index,表明只使用了索引便完成了查詢,並無回訪表去獲取索引外的字段,也就是咱們一般所說的使用了「覆蓋索引」;若是使用了key,但沒有出現Using index,說明索引並不能覆蓋檢索和核對的全部字段,須要回訪表去獲取其它字段內容,這相對於覆蓋索引增長了回訪表的成本,增長了隨機IO的成本安全

 

(3).經過key_len肯定究竟使用了複合索引的幾個索引字段

對於複合索引INDEX(a,b,c) 我如何肯定執行計劃到底使用了幾個索引字段呢? 這個須要經過key_len去肯定.網絡

*************************** 1. row ***************************
       Table: operationMenuInfo Create Table: CREATE TABLE `operationMenuInfo` ( `id` int(50) NOT NULL AUTO_INCREMENT, `operationMenuName` varchar(200) NOT NULL, `createTime` int(50) DEFAULT NULL, `startTime` int(50) DEFAULT NULL, `endTime` int(50) DEFAULT NULL, `appId` int(50) NOT NULL, `status` int(50) NOT NULL, `fromPlat` varchar(200) DEFAULT NULL, `appName` varchar(200) DEFAULT NULL, `packageId` int(20) DEFAULT NULL, `menuType` smallint(5) NOT NULL DEFAULT '0' COMMENT 'type', `entityId` int(11) NOT NULL DEFAULT '0' COMMENT 'entityId', `productId` int(11) NOT NULL DEFAULT '0' COMMENT 'pid', PRIMARY KEY (`id`), KEY `time_appid` (`appId`,`createTime`), KEY `idx_startTime` (`startTime`), KEY `idx_endTime` (`endTime`), KEY `t_eId_pId` (`entityId`,`menuType`,`productId`), KEY `idx_appId_createTime_fromPlat` (`appId`,`createTime`,`fromPlat`) ) ENGINE=InnoDB AUTO_INCREMENT=4656258 DEFAULT CHARSET=utf8 1 row in set (0.00 sec)

 

對比下面這兩個SQL和它們的執行計劃
mysql優化

where appId=927 and createTime=1494492062 按咱們的理解,應該是使用KEY idx_appId_createTime_fromPlat (appId,createTime,fromPlat)的前2個字段.
where appId=927 and fromPlat='dataman' 按咱們的理解,應該是使用KEY idx_appId_createTime_fromPlat (appId,createTime,fromPlat)的第1個字段.由於where條件中缺乏createTime字段,因此只能使用索引的第1個字段來access.
其實key_len反映的就是這些信息,不過沒有那麼直接(其實直接顯示使用哪些字段來access了會更好),要對應到字段上還須要一些換算:
併發


key_len的計算
經過key_len能夠知道複合索引都使用了哪些字段.key_len的計算上:
當字段定義能夠爲空時,須要額外的1個字節來記錄它是否爲空,當字段定義爲not null時,這額外的1個字節是不須要的.
當字段定義爲變長數據類型(好比說varchar)時,須要額外的2個字節來記錄它的長度; 當字段定義爲定長數據類型(好比說int,char,datetime等),這額外的2個字節是不須要的.
對於字符型數據,varchar(n),char(n), n都是定義的最大字符長度, gbk的話:2*n ,utf8的話:3*n
int 4個字節,bigint 8個字節,這些定長類型佔用的字節數,這裏只列舉這2個吧.
索引使用哪些字段,上述計算公式計算出的字節的和就是ken_len,就能夠肯定索引使用了哪些字段 
app

 

第1個SQL,使用了索引的前2個字段,appId(4) + createTime(4+1 這個字段定義爲能夠爲空,因此是4+1) =9 ,因此ken_len是9,標識索引使用了這2個字段.
第2個SQL,只使用了索引的第1個字段appId(4) =4,因此ken_len是4,標識索引只使用了第1個字段.

(4) order by和Using filesort

業務SQL常常會有order by,通常來講這須要真實的物理排序才能達到這個效果, 這就是咱們所說的Using filesort,通常來講它須要檢索出全部的符合where條件的數據記錄,然後在內存/文件層面進行物理排序,因此通常是一個很耗時的操做,是咱們極力想要避免的.
但其實對於MySQL來講,卻不必定非得物理排序才能達到order by的效果,也能夠經過索引達到order by的效果,卻不須要物理排序.
由於索引經過葉節點上的雙向鏈表實現了邏輯有序性,好比說對於where a=? order by b limit 1; 能夠直接使用index(a,b)來達到效果,不須要物理排序,從索引的根節點,走到葉節點,找到a=?的位置,由於這時b是有序的,只要順着鏈表向右走,掃描1個位置,就能夠找到想要的1條記錄,這樣既達到了業務SQL的要求,也避免了物理的排序操做。這種狀況下,執行計劃的Extra部分就不會出現Using filesort,由於它只掃描了極少許的索引葉節點就返回告終果,因此通常而言,執行很快,資源消耗不多,是咱們想要的效果.

由於存在KEY time_appid (appId,createTime), 第1個SQL能夠經過它快速的返回結果,由於沒有物理排序,因此執行計劃的Extra部分沒有出現Using filesort.
而第2個SQL是沒法經過任何索引達到上述效果的,必須掃描出全部的符合條件的記錄行後物理排序再返回TOP1的記錄,由於存在物理排序,因此執行計劃的Extra部分出現了Using filesort.
執行時間上,第1個SQL瞬間返回結果,第2個SQL須要0.7秒左右才能返回結果(由於它要檢索出符合條件的40W記錄,然後還要排序,這2個操做致使了它執行時間偏長).


order by和Using filesort
索引自己是邏輯有序的,因此能夠經過索引達到order by的效果要求,卻不須要真正的物理排序操做. 若是業務SQL中有order by,但執行計劃的Extra部分中卻沒有出現Using filesort,說明經過索引避免了物理的排序操做,對於TOPN SQL而言,這每每意味着經過索引快速的返回告終果,是咱們想要的.
若是執行計劃的Extra部分中出現了Using filesort,說明沒法經過索引達到效果,而使用了物理排序操做,對TOPN SQL而言,這意味着雖然只是返回極少的N條記錄,但須要檢索出符合where條件的全部記錄,然後物理排序,最終才能返回業務想要的N條記錄,若是符合where條件的記錄不少,這2個操做每每是很耗時的,是咱們極力想要避免的.

 

3、索引設計的注意事項

關於索引的2個知識點 關於索引,首先說2個應該知道的事項(其實上面也已經提到了): 1.如今廣泛使用的innodb存儲引擎中,索引的葉節點中除了存儲了索引定義中的字段外,還存儲了primary key,從而能夠找到對應的行記錄,這樣才能訪問索引外的字段. 2.索引的葉節點經過雙向鏈表實現了邏輯上的有序性,使得索引是有序的.

(1). 關於INNODB表PRIMARY KEY的建議

表設計層面,咱們通常建議使用自增ID作PRIMARY KEY,業務主鍵作UNIQUE KEY,緣由以下:
1.若是業務主鍵作PRIMARY KEY,業務主鍵的插入順序比較隨機,這樣會致使插入時間偏長,並且聚簇索引葉節點分裂嚴重,致使碎片嚴重,浪費空間;而自增ID作PRIMARY KEY的狀況下,順序插入,插入快,並且聚簇索引比較緊湊,空間浪費小。
2.通常表設計上除了PRIMARY KEY外,還會有幾個索引用來優化讀寫.而這些非PK索引葉節點中都要存儲PRIMARY KEY,以指向數據行,從而關聯非索引中的字段內容.這樣自增ID(定義爲bigint才佔用8個字節)和業務主鍵(一般字符串,多字段,空間佔用大)相比,作PRIMARY KEY在索引空間層面的優點也是很明顯的(同時也會轉換爲時間成本層面的優點),表定義中的索引越多,這種優點越明顯。

綜上所述,咱們通常建議使用自增ID作PRIMARY KEY,業務主鍵作UNIQUE KEY。

 

(2). 什麼列上適合建索引,什麼列上不適合建索引

這裏涉及到一個重要的概念:字段的選擇性
select count(1)/count(distinct col) 這個結果越接近數據總行數,那麼這個字段的選擇性越低; 越接近1,那麼這個字段的選擇性越高. 簡單舉例說就是:身份證ID字段的選擇性很高,而性別字段的選擇性很低.

通常來講,高選擇性字段上是適合建立索引的,而低選擇性字段上是不適合建立索引的


通常來講,status,type這類枚舉值不多的字段,就是低選擇性字段(或者說低基數字段),是不適合單獨做爲索引字段的.
例外的狀況就是: 這類字段數據分佈特別不均衡,而你常常要定位的是數據量極少的字段值,這種狀況下,仍是適合在這個字段上建立索引的.

 

mysql> show create table novel_agg_info\G *************************** 1. row ***************************
       Table: novel_agg_info Create Table: CREATE TABLE `novel_agg_info` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT, `rid` bigint(20) unsigned NOT NULL, `book_name` varchar(128) NOT NULL, `tag` varchar(128) NOT NULL, `dir_id` bigint(20) unsigned NOT NULL DEFAULT '0', `dir_url` varchar(512) NOT NULL DEFAULT '', `public_status` int(2) NOT NULL DEFAULT '1', PRIMARY KEY (`id`), UNIQUE KEY `rid` (`rid`), KEY `book_name` (`book_name`), KEY `idx_public_status` (`public_status`) ) ENGINE=InnoDB AUTO_INCREMENT=12096483 DEFAULT CHARSET=utf8 1 row in set (0.00 sec) mysql> select public_status,count(1) from novel_agg_info group by public_status; +---------------+----------+
| public_status | count(1) |
+---------------+----------+
|             0 |  3511945 |
|             1 |   367234 |
|             2 |   419062 |
|            12 |       16 |
+---------------+----------+
4 rows in set (1.35 sec) mysql> explain select * from novel_agg_info where public_status = 12; +----+-------------+----------------+------+-------------------+-------------------+---------+-------+------+-------+
| id | select_type | table          | type | possible_keys     | key               | key_len | ref   | rows | Extra |
+----+-------------+----------------+------+-------------------+-------------------+---------+-------+------+-------+
|  1 | SIMPLE      | novel_agg_info | ref  | idx_public_status | idx_public_status | 4       | const |   15 | NULL  |
+----+-------------+----------------+------+-------------------+-------------------+---------+-------+------+-------+
1 row in set (0.00 sec) mysql> explain select * from novel_agg_info where public_status = 0; +----+-------------+----------------+------+-------------------+-------------------+---------+-------+---------+-------+
| id | select_type | table          | type | possible_keys     | key               | key_len | ref   | rows    | Extra |
+----+-------------+----------------+------+-------------------+-------------------+---------+-------+---------+-------+
|  1 | SIMPLE      | novel_agg_info | ref  | idx_public_status | idx_public_status | 4       | const | 1955112 | NULL  |
+----+-------------+----------------+------+-------------------+-------------------+---------+-------+---------+-------+
1 row in set (0.00 sec) mysql> select sql_no_cache count(1) from (select * from novel_agg_info where public_status = 12 ) tmp; +----------+
| count(1) |
+----------+
|       16 |
+----------+
1 row in set (0.00 sec) mysql> select sql_no_cache count(1) from (select * from novel_agg_info where public_status = 0 ) tmp; +----------+
| count(1) |
+----------+
|  3511945 |
+----------+
1 row in set (11.60 sec)

 

能夠看到狀態值爲12的數據量極少,因此where public_status = 12 使用索引,快速的返回告終果. 但where public_status = 0 徹底是另一種狀況了.
其實下面能夠看到 where public_status = 0 不使用索引,使用全表掃描會更好些,但這裏也依然是選擇了使用索引的執行計劃. 優化器應該基於數據分佈的統計信息,對於不一樣的輸入值,使用更合理的執行計劃,而不是使用一個統一的執行計劃,這也是優化器層面須要繼續智能化,提高的地方.
它的一個典型的應用場景,就是任務處理表:
不斷有新任務插入進來,任務狀態初始化爲"未處理",後臺不斷的掃描出"未處理"的任務,進行調度處理,完成後,更新任務狀態爲"已處理",任務數據仍然保留下來.
這裏任務狀態字段就是這種狀況,不一樣值不多,但頻繁查詢的"未處理"狀態極少,絕大部分爲"已處理"狀態,它們又基本上不會被查詢,這種狀況下,就適合在任務狀態字段上建立索引.

爲何低選擇性字段上不適合建立索引呢? 其實也涉及到另外一個問題: 使用索引必定比全表掃描要好嗎? 答案是否認的.
繼續進行測試:

mysql> explain select * from novel_agg_info where public_status = 0;    ---默認走索引idx_public_status
+----+-------------+----------------+------+-------------------+-------------------+---------+-------+---------+-------+
| id | select_type | table          | type | possible_keys     | key               | key_len | ref   | rows    | Extra |
+----+-------------+----------------+------+-------------------+-------------------+---------+-------+---------+-------+
|  1 | SIMPLE      | novel_agg_info | ref  | idx_public_status | idx_public_status | 4       | const | 1955112 | NULL  |
+----+-------------+----------------+------+-------------------+-------------------+---------+-------+---------+-------+
1 row in set (0.00 sec) mysql> explain select * from novel_agg_info ignore index(idx_public_status) where public_status = 0; ---強制忽略索引idx_public_status,走全表掃描.
+----+-------------+----------------+------+---------------+------+---------+------+---------+-------------+
| id | select_type | table          | type | possible_keys | key  | key_len | ref  | rows    | Extra       |
+----+-------------+----------------+------+---------------+------+---------+------+---------+-------------+
|  1 | SIMPLE      | novel_agg_info | ALL  | NULL          | NULL | NULL    | NULL | 3910225 | Using where |
+----+-------------+----------------+------+---------------+------+---------+------+---------+-------------+
1 row in set (0.00 sec) mysql> select sql_no_cache count(1) from (select * from novel_agg_info where public_status = 0) tmp; +----------+
| count(1) |
+----------+
|  3511945 |
+----------+
1 row in set (11.59 sec) mysql> select sql_no_cache count(1) from (select * from novel_agg_info ignore index(idx_public_status)  where public_status = 0) tmp; +----------+
| count(1) |
+----------+
|  3511945 |
+----------+
1 row in set (8.46 sec)

 

上面2個SQL的執行時間均取屢次執行的平均執行時間,能夠忽略BUFFER POOL的影響.

爲何全表掃描反而快了,使用索引反而慢了呢?
必定程度上是由於回訪表的操做,使用索引,但提取了索引字段外的數據,因此須要回訪表數據,這裏符合條件的數據量特別大,因此致使了大量的回表操做,帶來了大量的隨機IO; 而全表掃描的話,雖說表空間比索引空間大,但可使用多塊讀特性,必定程度上使用順序讀; 此消彼長,致使全表掃描反而比使用索引還要快了.
這也解釋了低選擇性字段(低基數字段)爲何不適合建立索引(固然,使用覆蓋索引,不須要回訪表是另一種狀況了).

(3). 索引必定是有益的嗎?

答案是否認的,由於索引是有代價的:
每次的寫操做,都要維護索引,相應的調整索引數據,會在必定程度上下降寫操做的速度.因此大量的索引必然會下降寫性能,索引的建立要從總體考慮,在讀寫性能之間找到一個好的平衡點,在主要矛盾和次要矛盾之間找到平衡點.
因此說,索引並非越多越好,無用的索引要刪除,冗餘的索引(這在後面會提到)要刪除,由於它們只有維護上的開銷,卻沒有益處,因此在業務邏輯,SQL,索引結構變動的時候,要及時刪除無用/冗餘的索引.
索引使用不合理的狀況下,使用索引也不必定會比全表掃描快,上面也提到了.
總結說,索引不是萬能的,要合理的建立索引.

(4). where條件中不要在索引字段側進行任何運算(包括隱式運算),不然會致使索引不可用,致使全表掃描

select * from tab where id + 1 = 1000; 會致使全表掃描,應該修改成select * from tab where id = 1000 -1; 才能夠高效返回.

select * from tab where from_unixtime(addtime) = '2017-05-11 00:00:00' 會致使index(addtime)不可用
應該調整爲select * from tab where addtime = unix_timestamp('2017-05-11 00:00:00') 這樣纔可使用index(addtime)

再好比說:

SELECT COUNT(*) FROM message WHERE (token = 'bed21e35b19fe40e71b3ba2ad080b10a') AND (date(create_time) = curdate());

會致使create_time上的索引不可用, 爲了使得create_time上的索引可用,應轉化爲以下的等效形式:

SELECT COUNT(*) FROM message WHERE (token = 'bed21e35b19fe40e71b3ba2ad080b10a') AND create_time>=curdate() and create_time<adddate(curdate(),1)

這裏的運算也包括隱式的運算,好比說隱式的類型轉換..業務上常常有類型不匹配致使隱式的類型轉換的狀況.這裏常常出現的狀況是字符串和整型比較.
好比說表定義字段類型爲BIGINT,但業務上傳進來一個字符串的; 或者是表定義字段類型爲varchar,但業務上傳進來一個整型的.這個字段上存在索引時,索引也許是不可用的.
爲何說也許呢?這取決於這種隱式的類型轉換髮生在了哪側?是表字段側,仍是業務傳入數據側?
整型和字符串比較,DB中和許多程序語言中的處理方式是同樣的,都是字符串轉換爲整型後和整型比較.
因此表定義字段類型爲BIGINT,但業務上傳進來一個字符串,字段上的索引依然可用,由於隱式的類型轉換髮生在業務傳入數據側(這隻能說是索引依然可用,沒有大的性能影響,但隱式的類型轉換照樣是有性能損耗的,因此仍是一致的好)。


表定義字段類型爲varchar,但業務上傳進來一個整型,會致使索引不可用,全表掃描.由於隱式的類型轉換髮生在表字段側。
建議可使用INT/BIGINT存儲的,儘可能定義爲INT/BIGINT,這樣相對於長的純數字字符串的VARCHAR定義,INT/BIGINT不只更節省空間(INT 4個字節,BIGINT 8個字節),性能更好;並且即便類型不匹配了,也不會致使索引不可用的問題.


還有表關聯,關聯字段上類型不一致,這種狀況下,索引是否可用,是否存在嚴重的性能問題,取決於哪一個表是驅動表,哪一個表是被驅動表.這裏不細論這個問題了.關聯字段類型定義一致了,什麼問題都沒有.這也是表設計階段須要注意的.
總結起來仍是一句話,類型一致了,什麼問題都沒有,不然可能存在嚴重的性能問題.


索引字段類型定義改變時的調整順序
這裏單獨的說一下這個,由於業務上確實存在字段類型調整的狀況,存在int/bigint和varchar定義轉換的狀況,若是這個字段上還存在着高效索引的話,必定要注意是業務代碼側先調整,仍是DB側先調整,若是順序弄反了,會致使這裏提到的全表掃描問題的:
原定義爲int/bigint,要修改成varchar的: 業務代碼側先調整,傳入數據都按字符串處理,確認都調整完畢後,DB端再修改表定義.
原定義爲varchar,要修改成 int/bigint的: DB端先修改表定義,DB端調整完畢,且確認從庫也同步完畢以後,業務代碼再調整,傳入數據都按整型處理


再說一下區分大小寫的字段比較
mysql的字符串比較默認是不區分大小寫的.因此有些業務上爲了嚴格匹配,區分大小寫,在SQL中使用了binary,確實達到了區分大小寫的目的,但致使索引不可用了(由於在字段側進行了運算)

 

表定義中存在合適的索引  KEY `idx_app_name_status` (`appname`,`status`)
mysql> explain select * from tbl_rtlc_conf where binary appname='LbsPCommon' and status = 1; +----+-------------+---------------+------+---------------+------+---------+------+------+-------------+
| id | select_type | table         | type | possible_keys | key  | key_len | ref  | rows | Extra       |
+----+-------------+---------------+------+---------------+------+---------+------+------+-------------+
|  1 | SIMPLE      | tbl_rtlc_conf | ALL  | NULL          | NULL | NULL    | NULL | 9156 | Using where |
+----+-------------+---------------+------+---------------+------+---------+------+------+-------------+
1 row in set (0.00 sec)

 

但由於binary的使用,致使了全表掃描.

那如何達到目的,又能高效呢?
mysql的字符串比較默認不區分大小寫,是由於它們默認的collation是不區分大小寫的

mysql> pager egrep -i "utf8|gbk|Default collation" PAGER set to 'egrep -i "utf8|gbk|Default collation"' mysql> show character set; | Charset  | Description                 | Default collation   | Maxlen |
| gbk      | GBK Simplified Chinese      | gbk_chinese_ci      |      2 |
| utf8     | UTF-8 Unicode               | utf8_general_ci     |      3 |
| utf8mb4  | UTF-8 Unicode               | utf8mb4_general_ci  |      4 |

 

gbk,utf8 字符集默認的collation分別爲gbk_chinese_ci,utf8_general_ci, caseignore 它們都是忽略大小寫的,致使字符串比較默認不區分大小寫了.


區分大小寫,且索引可用
解決的方案就是修改特定表/字段的collation,表collation的修改會影響到這個表的全部字段,因此通常都是隻修改特定目標字段的collation
表字符集爲utf8的話:
appname varchar(255) CHARACTER SET utf8 COLLATE utf8_bin NOT NULL COMMENT 'appname'
表字符集爲gbk的話:
appname varchar(255) CHARACTER SET gbk COLLATE gbk_bin NOT NULL COMMENT 'appname'
同時保證SQL中沒有包含binary, 這樣既達到了嚴格匹配的目的(utf8_bin ,gbk_bin這2個collation都是嚴格匹配的),也保證了索引的可用性.

 

(5). 不要使用%xxx%這種模糊匹配,會致使全表掃描/索引全掃描

where name like '%zhao%' 這種先後統配的模糊查詢,會致使索引不可用,全表掃描,稍好的狀況是,能使用覆蓋索引的話,是索引全掃描,但也高效不了.
若是確實存在這樣高頻執行的模糊匹配的業務需求,建議走全文檢索系統,不要使用MySQL來作這個事情.
但其實不少業務,使用模糊匹配是帶有很大的隨意性的,徹底能夠改成精確匹配,從而使用字段上的索引快速定位數據的.

另外where name like 'xxx%'這種,不前統配,只後統配的,確實是可使用索引的.
但它實際上是一個範圍匹配,下文會提到,這種範圍匹配(非等值匹配)會致使後面的索引字段不能(高效)使用,會致使索引不能用於避免物理排序等問題.
因此仍是要謹慎使用,若是能夠改成精確匹配的話,仍是建議使用精確匹配的好.

(6). 關於前綴索引和冗餘索引

index(a,b,c) 能同時優化下面幾類查詢:
where a=? and b=? and c=?
where a=? and b=?
where a=?
也能優化以下的排序查詢:
where a=? order by b[,c] limit 
where a=? and b=? order by c limit 
但不能優化 where b=? and c=? 由於索引定義index(a,b,c) 的前綴列a沒有出如今where條件中.
更不能優化where c=?

對於where a=? and c=? 查詢,它只能使用index(a,b,c)的第1個索引字段a.

因此,若是業務查詢爲以下2類:
where b=? and c=?
where c=?
**那麼就應該定義索引爲index(c,b),它能同時優化上面2類查詢 **,而不該該定義索引index(b,c)的,由於索引index(b,c)優化不了where c=? 由於這個索引的前綴列b沒有出如今where條件中.
也不建議建立2個索引: index(b,c) 和index(c) 由於前面提到了索引越少越好,能夠用一個index(c,b) 來完成的,就不要建立2個索引來完成.

在存在索引index(a,b,c)的狀況下,絕大多數狀況下,下面的這些索引就冗餘了,能夠DROP掉的:
index(a)
index(a,b)
上面提到了,這2個索引能優化的查詢,index(a,b,c)絕大多數狀況下也都能優化,因此它們就冗餘了,本着索引越少越好的原則,均可以DROP掉的.

上面提到了絕大多數狀況下,冗餘了,能夠DROP了,但也存在例外的狀況,它們的存在仍是必要的:
那就是存在下面的查詢:
where a=? order by id limit
這裏index(a) ( 實際爲index(a,id) ) 能夠優化上面的查詢,經過使用這個索引,避免物理排序而達到排序的實際效果.
但index(a,b,c) ( 實際爲index(a,b,c,id) ) 和index(a,b) (實際爲index(a,b,id)) 卻達不到這樣的效果.
這種狀況下,存在index(a,b,c)的狀況下,index(a) 是不冗餘的,是須要保留的.
若是不存在這種狀況,存在index(a,b,c)的狀況下,index(a) ,index(a,b) 都是冗餘的,建議drop掉.

但若是where a=? 後返回的數據行已經不多,也就是說對不多的數據進行order by id排序的話,也是可使用index(a,b)或者index(a,b,c) 來過濾行的,只不過還須要進行物理排序,但代價已經很小了,是否還須要建立一個index(a)須要業務折中考慮了.

(7). 關於索引定義中的字段順序

建議where條件中等值匹配的字段放到索引定義的前部,範圍匹配的字段(> < between in等爲範圍匹配)放到索引定義的後面.
由於前綴索引字段使用了範圍匹配後,會致使後續的索引字段不能高效的用於優化查詢.
來看一個例子:

mysql> show create table opLog\G *************************** 1. row ***************************
       Table: opLog Create Table: CREATE TABLE `opLog` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id', `listId` int(11) unsigned NOT NULL COMMENT '對應id', `listType` varchar(255) NOT NULL COMMENT '對應類型', `opName` varchar(255) NOT NULL COMMENT '操做人id', `operation` varchar(255) NOT NULL COMMENT '具體操做', `content` varchar(255) NOT NULL COMMENT '內容', `createTime` int(10) NOT NULL COMMENT '時間', PRIMARY KEY (`id`), KEY `idx_opName_createTime` (`opName`,`createTime`), KEY `idx_createTime_opName` (`createTime`,`opName`) ) ENGINE=InnoDB AUTO_INCREMENT=2515923 DEFAULT CHARSET=utf8 COMMENT='操做記錄表'

 

查詢2017-04-23到2017-05-23 這一個月內某個op發起的操做數量:
select sql_no_cache count(1) from opLog where opName='zhangyu21' and createTime between 1492876800 and 1495468800;
+----------+
| count(1) |
+----------+
|        0 |
+----------+
 
這1個月內共有2.2W次的操做記錄,對應2.2W行記錄.
mysql> select count(1) from opLog  where createTime between 1492876800 and 1495468800;         
+----------+
| count(1) |
+----------+
|    22211 |
+----------+
 
我下面使用force index的hint強制走某個索引:
# Query_time: 0.009124  Lock_time: 0.000093 Rows_sent: 1  Rows_examined: 22211
select sql_no_cache count(1) from opLog force index(idx_createTime_opName) where opName='zhangyu21' and createTime between 1492876800 and 1495468800; 
 
# Query_time: 0.000220  Lock_time: 0.000077 Rows_sent: 1  Rows_examined: 0
select sql_no_cache count(1) from opLog force index(idx_opName_createTime) where opName='zhangyu21' and createTime between 1492876800 and 1495468800;
 
能夠看到第1個SQL,強制走KEY `idx_createTime_opName`(`createTime`,`opName`)時,檢索的行數是22211行,這個行數恰好是這個時間段內的總行數.爲何是這樣呢?
由於在前綴索引字段createTime上使用了範圍匹配,因此致使索引定義中後面的字段opName不能做爲高效的檢索字段(Access),只能做爲低效的過濾字段(Filter)了.
(在5.6推出ICP以前,這一點都很難知足,致使範圍匹配後的索引字段基本是無用的)
說白了,就是說索引上定位到createTime的起止,對期間的索引條目一行行的檢查是否知足opName='zhangyu21'的條件,知足的返回.
而第2個SQL,強制走KEY `idx_opName_createTime` (`opName`,`createTime`)時,這2個索引字段都是能夠做爲高效的Access條件的.
經過索引定位到opName='zhangyu21',createTime =1492876800 條目,向後掃描,直至opName='zhangyu21',createTime>1495468800或者opName!='zhangyu21'爲止.
它是至關高效的,掃描的條目就是返回的條目.
沒有帶force index這類hint的話,mysql優化器會默認使用idx_opName_createTime這個索引.

(8). 關於排序查詢的優化

前面提到了index(a,b) 邏輯上是有序的,因此能夠用於優化where a=? order by b [asc/desc] [limit n] 特別是對這種topN操做的優化效果很是好.

mysql> show create table opLog\G *************************** 1. row ***************************
       Table: opLog Create Table: CREATE TABLE `opLog` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id', `listId` int(11) unsigned NOT NULL COMMENT '對應id', `listType` varchar(255) NOT NULL COMMENT '對應類型', `opName` varchar(255) NOT NULL COMMENT '操做人id', `operation` varchar(255) NOT NULL COMMENT '具體操做', `content` varchar(255) NOT NULL COMMENT '內容', `createTime` int(10) NOT NULL COMMENT '時間', PRIMARY KEY (`id`), KEY `idx_opName_createTime` (`opName`,`createTime`) ) ENGINE=InnoDB AUTO_INCREMENT=2515923 DEFAULT CHARSET=utf8 COMMENT='操做記錄表' mysql> select count(1) from opLog where opName=''; +----------+
| count(1) |
+----------+
|  2511443 |
+----------+
1 row in set (1.08 sec)

 

一共有251W的匿名用戶,要查找他們最近的5個操做記錄:
# Query_time: 0.001566  Lock_time: 0.000084 Rows_sent: 5  Rows_examined: 5
select * from opLog where opName='' order by createTime desc limit 5;
  
從實際執行的統計信息看,它並無掃描出251W的記錄,排序,最終輸出5條記錄,而是隻掃描了5條記錄,就直接輸出了,執行時間很短的.
看一下執行計劃:
mysql> explain select * from opLog where opName='' order by createTime desc limit 5;
+----+-------------+-------+------+-----------------------+-----------------------+---------+-------+---------+-------------+
| id | select_type | table | type | possible_keys         | key                   | key_len | ref   | rows    | Extra       |
+----+-------------+-------+------+-----------------------+-----------------------+---------+-------+---------+-------------+
|  1 | SIMPLE      | opLog | ref  | idx_opName_createTime | idx_opName_createTime | 767     | const | 1252639 | Using where |
+----+-------------+-------+------+-----------------------+-----------------------+---------+-------+---------+-------------+
1 row in set (0.01 sec)
sql中有order by,但執行計劃的Extra部分並無出現Using filesort,說明經過KEY `idx_opName_createTime` (`opName`,`createTime`)這個索引達到了排序的效果,但避免了物理排序的操做.(rows部分的估算值能夠忽略呀)
若是沒有這個索引,就真的須要檢索出251W記錄(如何檢索出這些記錄,取決於其餘的索引,若是沒有合適的索引,可能須要全表掃描),對他們進行物理排序,並輸出須要的5行記錄.執行代價很大,執行時間很長.
但這裏經過索引,利用索引自己的邏輯有序性,避免了物理排序操做,快速的返回了topN行記錄.
mysql> show create table opLog\G *************************** 1. row ***************************
       Table: opLog Create Table: CREATE TABLE `opLog` ( `id` int(11) unsigned NOT NULL AUTO_INCREMENT COMMENT 'id', `listId` int(11) unsigned NOT NULL COMMENT '對應id', `listType` varchar(255) NOT NULL COMMENT '對應類型', `opName` varchar(255) NOT NULL COMMENT '操做人id', `operation` varchar(255) NOT NULL COMMENT '具體操做', `content` varchar(255) NOT NULL COMMENT '內容', `createTime` int(10) NOT NULL COMMENT '時間', PRIMARY KEY (`id`), KEY `idx_opName_listType_createTime` (`opName`,`listType`,`createTime`) ) ENGINE=InnoDB AUTO_INCREMENT=2515923 DEFAULT CHARSET=utf8 COMMENT='操做記錄表' 仍是上面的表數據,我修改了一下表的索引結構. mysql> select count(1) from opLog where opName=''; +----------+
| count(1) |
+----------+
|  2511443 |
+----------+
1 row in set (0.91 sec) 

 

# Query_time: 3.188810  Lock_time: 0.000088 Rows_sent: 1  Rows_examined: 2511444
select * from opLog where opName='' and listType in ('cronJob','cronJobNew') order by createTime desc limit 1;

 

從執行統計信息看,這個查詢並無經過索引快速的返回結果.
mysql> explain select * from opLog where opName='' and listType in ('cronJob','cronJobNew') order by createTime desc limit 1; +----+-------------+-------+------+--------------------------------+--------------------------------+---------+-------+---------+----------------------------------------------------+
| id | select_type | table | type | possible_keys                  | key                            | key_len | ref   | rows    | Extra                                              |
+----+-------------+-------+------+--------------------------------+--------------------------------+---------+-------+---------+----------------------------------------------------+
|  1 | SIMPLE      | opLog | ref  | idx_opName_listType_createTime | idx_opName_listType_createTime | 767     | const | 1252640 | Using index condition; Using where; Using filesort |
+----+-------------+-------+------+--------------------------------+--------------------------------+---------+-------+---------+----------------------------------------------------+

 

執行計劃來看,仍是有Using filesort,仍是須要物理排序的. 爲何不能經過這個索引避免物理排序,快速的返回結果呢?
緣由就在於listType in ('cronJob','cronJobNew')  在這個索引字段上使用了範圍匹配,從而致使索引層面上總體再也不有序了.

在排序字段前的全部索引字段上都必須是等值匹配,才能經過索引保證有序性,才能經過索引避免物理排序,快速的返回結果.

因此上面的查詢必須改造爲等效的等值匹配才能夠經過索引快速的返回結果的:
mysql> select *
    -> from
    -> ( -> select * from opLog where opName='' and listType = 'cronJob' order by createTime desc limit 1
    -> union all
    -> select * from opLog where opName='' and listType = 'cronJobNew' order by createTime desc limit 1
    -> ) tmp -> order by createTime desc limit 1; ERROR 1221 (HY000): Incorrect usage of UNION and ORDER BY

 

這樣還不行,必須再嵌套個外層,使用臨時表才能夠的:
select *
from ( select * from ( select * from opLog where opName='' and listType = 'cronJob' order by createTime desc limit 1 ) tmp_1 union all
    select * from ( select * from opLog where opName='' and listType = 'cronJobNew' order by createTime desc limit 1 ) tmp_2 ) tmp order by createTime desc limit 1;

 

這樣就能夠了.
  
改造後的SQL對應的執行統計信息以下:
# Query_time: 0.000765  Lock_time: 0.000332 Rows_sent: 1  Rows_examined: 4
通過改造爲等效的等值匹配,使用索引避免了大的物理排序操做,快速的返回告終果.

說到經過索引優化排序查詢,特別是TOPN操做,必須說一下MySQL在優化器層面的一個問題:
就是說在遇到order by時,myql會優先選擇一個能夠避免物理排序的索引來優化這個查詢,有時候,這種優先選擇是不合理的,會致使性能不好.
(特別在涉及到order by id limit N, 這裏id是primary key,優化器選擇使用PRIMARY KEY來避免物理排序時尤爲要注意是否合理了)

mysql> show create table layer\G *************************** 1. row ***************************
       Table: layer Create Table: CREATE TABLE `layer` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'layer的id', `uuid` varchar(255) NOT NULL COMMENT 'layer的惟一標識', `type` tinyint(4) NOT NULL COMMENT 'layer的類型', `status` tinyint(4) NOT NULL COMMENT 'layer的狀態', `app_id` bigint(20) NOT NULL COMMENT 'layer所屬的app id', `src` varchar(1024) NOT NULL COMMENT 'layer的源地址', `oais_src` varchar(1024) NOT NULL DEFAULT '' COMMENT 'layer存在於oais的地址', `cmd` varchar(1024) NOT NULL DEFAULT '' COMMENT 'layer執行的命令', `skip_download` tinyint(1) NOT NULL DEFAULT '0' COMMENT '默認爲0,不跳過中轉', `extra` text NOT NULL COMMENT 'layer的額外信息', `create_time` int(10) unsigned NOT NULL DEFAULT '0' COMMENT 'layer建立時間戳', `last_update_time` int(10) unsigned NOT NULL DEFAULT '0' COMMENT 'layer更新時間戳', `finish_time` int(10) unsigned NOT NULL DEFAULT '0' COMMENT 'layer完成時間戳', `merge_latest_layer_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '該baseLayer最新merge的layerid', PRIMARY KEY (`id`), KEY `idx_uuid` (`uuid`), KEY `idx_app_id` (`app_id`) ) ENGINE=InnoDB AUTO_INCREMENT=2866980 DEFAULT CHARSET=utf8 COMMENT='layer表' # Query_time: 2.586674  Lock_time: 0.000084 Rows_sent: 1  Rows_examined: 1986479
SELECT * FROM `layer`  WHERE (app_id = 2183) ORDER BY `layer`.`id` ASC LIMIT 1; 輸出的ID:1998941 # Query_time: 1.442171  Lock_time: 0.000071 Rows_sent: 1  Rows_examined: 1095035
SELECT * FROM `layer`  WHERE (app_id = 139) ORDER BY `layer`.`id` ASC LIMIT 1; 輸出的ID: 1107497 # Query_time: 0.597380  Lock_time: 0.000077 Rows_sent: 1  Rows_examined: 464929
SELECT * FROM `layer`  WHERE (app_id = 1241) ORDER BY `layer`.`id` ASC LIMIT 1; 輸出的ID:465532 mysql> explain SELECT sql_no_cache* FROM `layer`  WHERE (app_id = 2183) ORDER BY `layer`.`id` ASC LIMIT 1; +----+-------------+-------+-------+-----------------------------+---------+---------+------+------+-------------+
| id | select_type | table | type  | possible_keys               | key     | key_len | ref  | rows | Extra       |
+----+-------------+-------+-------+-----------------------------+---------+---------+------+------+-------------+
|  1 | SIMPLE      | layer | index | idx_app_id | PRIMARY | 8       | NULL |  151 | Using where |
+----+-------------+-------+-------+-----------------------------+---------+---------+------+------+-------------+
1 row in set (0.00 sec)

 

能夠看到掃描的數據行數是大不一樣的.爲何呢? 源於它的執行計劃,使用了PRIMARY KEY (`id`)來避免物理排序操做.
說白了,就是順着PRIMARY KEY (`id`)的索引鏈表,從小往大掃描,找到第1條知足app_id = ?的記錄就返回了.
因此執行的時間長短,掃描的記錄行數的多少,徹底取決於app_id = ? 的整體數據量,數據分佈狀況.若是查找1個不存在的app_id最終的結果是掃描了整個表的數據行,也沒有找到數據,返回0行記錄,執行時間確定長.
下面也能夠驗證這1點:
mysql> SELECT count(1) FROM `layer`  WHERE (app_id = 2183)  and id<1998941; +----------+
| count(1) |
+----------+
|        0 |
+----------+
1 row in set (0.01 sec) mysql> SELECT count(1) FROM `layer`  WHERE id<=1998941; +----------+
| count(1) |
+----------+
|  1986479 |
+----------+
1 row in set (0.79 sec) 就是檢索的數據行數 mysql> SELECT count(1) FROM `layer`  WHERE (app_id = 139)  and id<1107497; +----------+
| count(1) |
+----------+
|        0 |
+----------+
1 row in set (0.00 sec) mysql> SELECT count(1) FROM `layer`  WHERE id<= 1107497; +----------+
| count(1) |
+----------+
|  1095035 |
+----------+
1 row in set (0.43 sec) 就是檢索的數據行數 mysql> SELECT count(1) FROM `layer`  WHERE (app_id = 1241)  and id<465532; +----------+
| count(1) |
+----------+
|        0 |
+----------+
1 row in set (0.00 sec) mysql> SELECT count(1) FROM `layer`  WHERE id<= 465532; +----------+
| count(1) |
+----------+
|   464929 |
+----------+
1 row in set (0.18 sec)  就是檢索的數據行數

 

這裏雖然經過索引避免了物理排序,但掃描的行數很大,實際執行時間很長,執行效果不好.
那這個SQL應該如何優化呢?
KEY idx_app_id (app_id) 等價於index(app_id,id) 徹底能夠經過它來高效的返回前N行記錄呀.但由於MySQL默認不選擇它,只能使用force index這個hint來強制mysql選擇這個索引了.

mysql> explain SELECT * FROM `layer` force index(idx_app_id) WHERE (app_id = 1241) ORDER BY `layer`.`id` ASC LIMIT 1; +----+-------------+-------+------+---------------+------------+---------+-------+--------+-------------+
| id | select_type | table | type | possible_keys | key        | key_len | ref   | rows   | Extra       |
+----+-------------+-------+------+---------------+------------+---------+-------+--------+-------------+
|  1 | SIMPLE      | layer | ref  | idx_app_id    | idx_app_id | 8       | const | 111142 | Using where |
+----+-------------+-------+------+---------------+------------+---------+-------+--------+-------------+
1 row in set (0.00 sec) 表名後跟 force index(idx_app_id) 提示mysql強制選擇這個索引. 經過這個索引也是能夠避免物理排序的,並且真的能夠快速的返回結果(即便這個app_id不存在,也會快速返回結果) # Query_time: 0.000213  Lock_time: 0.000082 Rows_sent: 1  Rows_examined: 1
SELECT * FROM `layer` force index(idx_app_id) WHERE (app_id = 2183) ORDER BY `layer`.`id` ASC LIMIT 1; # Query_time: 0.000202  Lock_time: 0.000075 Rows_sent: 1  Rows_examined: 1
SELECT * FROM `layer`force index(idx_app_id) WHERE (app_id = 139) ORDER BY `layer`.`id` ASC LIMIT 1; # Query_time: 0.000222  Lock_time: 0.000075 Rows_sent: 1  Rows_examined: 1
SELECT * FROM `layer`force index(idx_app_id) WHERE (app_id = 1241) ORDER BY `layer`.`id` ASC LIMIT 1;

 

使用force index 這個hint強制走某個索引後,真的高效返回了.
在遇到MYSQL蒙圈,選擇錯誤的執行計劃時,須要使用一些hint給mysql一些提示,使用頻率較高的hint有:
force index(index_name) 強制走某個索引
ignore index(index_name) 建議忽略某個索引不使用
但通常不建議使用這種hint,緣由以下:
hint是和索引名稱而不是索引字段綁定的,之後存在着很大的風險,把索引更名了,會致使提示無效的.
業務存在拼接SQL的狀況下,代碼考慮不周全,會致使一些不該該使用這種HINT的SQL也使用了這種HINT,致使它們的執行計劃變差.
隨着版本的升級,優化器的提高,數據量,數據分佈特色的變化,MYSQL本能夠選擇更好的執行計劃,但由於HINT致使MYSQL不能選擇更好的執行計劃.
因此使用這些提示前,請先和DBA溝通,也要進行詳盡的測試,確認HINT的引入只帶來了益處,沒有帶來壞處.

(9). 關於單列索引和複合索引

有時候會看到業務SQL是where a=? and b=? and c=? 
但3個列上分別建立了一個單列索引:
index(a) index(b) index(c)
這種建立是否合理呢?
前面提到高選擇性字段上適合建立索引,低選擇性字段上不適合建立單列索引(但能夠考慮做爲複合索引定義的一部分)
**若是a字段上的選擇性足夠高,b,c的選擇性低,徹底能夠只建立索引index(a) **, 這種狀況下,固然也能夠只建立index(a,b) 或者只建立index(a,b,c). (不要建立index(b), index(c) 這2個低選擇性字段上的單列索引了).
須要考慮到index(a,b) index(a,b,c) 相對於index(a),提高的收益並不大,但可能空間佔用卻大出很多去,須要業務在時空的矛盾中作出平衡,看建立哪一個索引更合適.

若是實際狀況是a,b,c單獨的選擇性通常,都不是很高,但3個組合到一塊兒的選擇性很高的話,那就建議建立index(a,b,c)的組合索引,不要3個字段上都建立一個單列索引.
爲何呢? mysql確實可使用index merge來使用多個索引,但不少時候是否比得上覆合索引效率高呢?
簡化一下: where a=? and b=?

a=? 返回1W行記錄, b=? 返回1W行記錄, where a=? and b=? 返回100行記錄.
若是是兩個單列索引: index(a) index(b) 的狀況下,index_merge會是一個什麼樣的執行計劃呢?
針對a=? 經過使用index(a) 返回1W行記錄,帶PRMIARY KEY
針對b=? 經過使用index(b) 返回1W行記錄,帶PRMIARY KEY
而後對primary key 取交集,無論是排序後取交集也好,仍是經過嵌套循環,關聯的方式取交集也好.都會是一個耗時耗費資源的操做.
綜合來講,掃描各自的索引返回1W行記錄,然後對這2W行記錄取交集,確定是一個耗時耗費資源的操做了.
但若是存在複合索引index(a,b) 經過索引的掃描定位,能夠快速的返回這100行記錄的.
因此針對這種狀況,建議建立複合索引,不要建立多個單列索引.

補充說一下:
where a=? or b=? 這種查詢, a列,b列上的選擇性都很高,這時候須要index(a) index(b),缺乏一個,都會致使全表掃描的.

(10). 關於多表關聯

ORACLE中有三種主要的表關聯方式:NESTED LOOP , HASH JOIN 和 SORT MERGE JOIN
其中最經常使用的仍是前兩種,ORACLE的優化器會根據統計獲得的錶行數,數據分佈狀況等信息,對各類關聯方式,關聯順序下的多個執行計劃進行評估,分別計算它們的cost,最後選擇一個cost最低(優化器認爲的最優)執行計劃做爲最終的執行計劃去執行.
至少到mysql官方的5.6版本,依然只有NESTED LOOP(嵌套循環)這樣一種關聯方式.
NESTED LOOP說白了就是FOR循環實現:

好比說針對下面的關聯查詢: select a.*, b *  
from EMP a,DEPT b where a.DEPTNO = b.DEPTNO; 它的嵌套循環的僞代碼大意是這樣的: declare 
begin 
  for outer_table in (select * from dept) loop for inner_table in (select * 
                          from emp where DEPTNO = outer_table.DEPTNO) loop dbms_output.put_line(inner_table.*, outer_table.*); end loop; end loop; end;

 

NESTED LOOP的適用場景是什麼?
外表(驅動表)通過過濾後返回較少的數據行(最好也能夠經過索引快遞的定位這些數據行,和表自己的數據行多少無關,只要求通過條件的過濾後返回較少的數據行),而內表(被驅動表)在表的關聯字段上存在着高效的索引可用.
由於這種狀況下,FOR循環的代價是小的,是適用NESTED LOOP的.
其它狀況,使用NESTED LOOP都不合適,好比內外表通過過濾後都返回上萬行甚至數十萬,百萬的記錄,這種狀況下,FOR循環的成本過高了(其實這種狀況下,HASH JION是適用的)
由於這個緣由(固然還有其它緣由了,好比說mysql沒有bitmap index等),mysql不適合作OLAP系統,不適合作複雜的多表關聯:
多表關聯,關聯的表越多,返回的行數越多,他們做爲外表,FOR循環的成本會愈來愈高,執行時間愈來愈長,很容易就超過業務設置的讀超時時間,或者超過DB端設置的超時時間,稍微來點兒併發,就可能會耗盡DB的資源,會致使雪崩,DB響應不了任何的業務請求.
因此不建議在MySQL上進行復雜的多表關聯查詢,低頻,基本無併發的查詢,能夠在線下庫進行;執行頻率稍高,存在併發的,就必須到hadoop,hbase等環境進行了.
由於mysql的表關聯實現就是for循環,因此簡單的表關聯,業務也能夠本身for循環實現.

4、慢查詢日誌的分析以及關注點

(1). 使用pt-query-digest工具來統計

可使用percona公司的開源工具pt-query-digest來進行統計,它能夠支持多種類型日誌文件的分析,包括binlog,genlog,slowlog,tcpdump的輸出進行統計.默認就是對slowlog進行分析的.
它也支持多種過濾條件,好比說執行時間,檢索行數等的過濾輸出,也支持過濾後裸數據的輸出,支持多種聚合排序輸出.
通常使用最簡單的調用形式便可,都使用默認定義:
/usr/local/bin/pt-query-digest slow.log > slow.log.fenxi
slow.log 是待分析的慢查詢日誌文件,將分析的結果重定向到文件slow.log.fenxi中.
它是去除字面值後對SQL進行分類彙總,而後按照每類SQL總的執行時間降序排序輸出的.而且每類SQL都給出了一個字面值SQL(期間執行時間最長的SQL).

(2). 對統計輸出進行分析

咱們通常重點分析執行時間佔比大的SQL,也就是前排的一些SQL,它們的執行時間長,系統資源消耗大,對業務的影響也大.
以一個輸出爲例:

# Profile # Rank Query ID Response time Calls R/Call  V/M Item # ==== ================== =============== ===== ======= ===== ============ # 1 0x426D0452190D3B9C 9629.1622 55.8%  5204  1.8503  0.01 SELECT queue_count # 2 0x52A6A31F2F3F0692 2989.7074 17.3%  2224  1.3443  0.03 SELECT server_info # 3 0x959209F179E16B2A  819.3819  4.8%   759  1.0796  0.00 SELECT server_info

 

第1類SQL總共耗時9629s,總的執行時間佔日誌中全部SQL執行時間的55.8%,在慢查詢日誌中出現了5204次,平均每次執行耗時爲1.85s
下面有這類SQL的詳盡信息,顯示的字面值SQL是其中執行時間最長的SQL

# Query 1: 0.11 QPS, 0.20x concurrency, ID 0x426D0452190D3B9C at byte 4615533 # This item is included in the report because it matches --limit.
# Scores: V/M = 0.01 # Time range: 2017-05-24 23:56:03 to 2017-05-25 13:37:15 # Attribute pct total min     max     avg     95% stddev median # ============ === ======= ======= ======= ======= ======= ======= ======= # Count         52    5204 # Exec time     55 9629s 2s 3s 2s 2s 110ms 2s # Lock time 23 185ms 20us 23ms 35us 40us 331us 25us # Rows sent 0   5.65k       0       2    1.11    1.96    0.45    0.99 # Rows examine 83  18.54G   3.65M   3.65M   3.65M   3.50M       0   3.50M # Query size 20 665.75k     131     131     131     131       0     131 # String: # Databases queue_center # Hosts 10.36.31.52 (696/13%), 10.36.31.31 (694/13%)... 6 more # Users queue_center_w # Query_time distribution # 1us # 10us # 100us # 1ms # 10ms # 100ms # 1s ################################################################ # 10s+ # Tables # SHOW TABLE STATUS FROM `queue_center` LIKE 'queue_count'\G # SHOW CREATE TABLE `queue_center`.`queue_count`\G # EXPLAIN /*!50100 PARTITIONS*/
select * from `queue_count` where `app_id` = '1' and `created_at` > '2017-05-25 11:42:01' and `created_at` <= '2017-05-25 11:43:01'\G

 

咱們重點關注avg,95分位的 #Rows examine 和 # Rows sent
Rows examine / Rows sent 對非聚合SQL而言,表明返回1行數據所要檢索的數據行數, 1 是想要的效果.
Rows examine檢索行數偏大的,若是同時Rows sent返回的數據行數不多(聚合函數除外),通常是能夠經過索引優化的。
對於update/delete類的寫操做,慢查詢日誌中Rows_examined仍是SQL執行過程當中檢索的行數,Rows_sent: 0 沒有意義,慢查詢日誌中沒有體現出來匹配/影響的行數來。
若是寫操做Rows_examined很大,同時匹配/影響的行數極少,通常是全表掃描,寫操做過程當中持有表鎖,影響併發的,並且執行時間長,容易致使同步延遲。但實際上是能夠經過索引優化這類寫操做的。
若是Rows examin,Rows sent都很小,但整體執行時間長的話,特別是讀取操做,極可能是受其它慢查詢影響的,能夠暫時先無論,把其它慢查詢優化完畢以後,這類慢查詢極可能也就消失了。
像上面這個SQL,3.65M/1.11 = 3.29M,也就是說平均須要掃描329W行數據才能返回1行記錄,過低效了.
表結構中除了主鍵ID外沒有任何的索引,其實業務都是查詢最近1分鐘內的數據,確實能夠經過index(app_id,created_at)或者index(created_at)來優化這類查詢。

5、幾個優化案例

優化案例1

mysql> show create table lc_day_channel_version\G *************************** 1. row ***************************
       Table: lc_day_channel_version Create Table: CREATE TABLE `lc_day_channel_version` ( `id` bigint(20) unsigned NOT NULL AUTO_INCREMENT COMMENT '主鍵', `prodline` varchar(50) NOT NULL DEFAULT '' COMMENT '產品線標識', `os` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '平臺類型,1:Android_Phone 2:Android_Pad 3:IPhone', `original_type` tinyint(3) unsigned NOT NULL DEFAULT '0' COMMENT '母包類型,1主線 3非主線', `dtime` int(10) unsigned NOT NULL DEFAULT '0' COMMENT 'date time,yyyymmdd', `version_name` varchar(50) NOT NULL DEFAULT '' COMMENT '來源版本號', `channel` varchar(50) NOT NULL DEFAULT '' COMMENT '渠道號', `request_pv` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '請求量', `request_uv` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '請求用戶量', `response_pv` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '請求成功量', `response_uv` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '請求成功用戶量', `download_pv` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '下載量', `download_uv` bigint(20) unsigned NOT NULL DEFAULT '0' COMMENT '下載用戶量', PRIMARY KEY (`id`), UNIQUE KEY `UNIQUE_poouvc` (`prodline`,`os`,`original_type`,`dtime`,`version_name`,`channel`), KEY `INDEX_d` (`dtime`) ) ENGINE=InnoDB AUTO_INCREMENT=135293125 DEFAULT CHARSET=utf8 COMMENT='升級版本渠道彙總信息'
1 row in set (0.00 sec) mysql> explain select version_name,sum(request_pv) as request_pv from lc_day_channel_version where dtime>=20170504 and dtime<=20170510 group by version_name order by request_pv desc; +----+-------------+------------------------+-------+-----------------------+---------+---------+------+---------+--------------------------------------------------------+
| id | select_type | table                  | type  | possible_keys         | key     | key_len | ref  | rows    | Extra                                                  |
+----+-------------+------------------------+-------+-----------------------+---------+---------+------+---------+--------------------------------------------------------+
|  1 | SIMPLE      | lc_day_channel_version | range | UNIQUE_poouvc,INDEX_d | INDEX_d | 4       | NULL | 2779470 | Using index condition; Using temporary; Using filesort |
+----+-------------+------------------------+-------+-----------------------+---------+---------+------+---------+--------------------------------------------------------+
1 row in set (0.00 sec) 

 

業務反饋執行上面的SQL,有索引可用呀,爲何還這麼慢呢?
  
問題在於:
mysql> select count(1) from lc_day_channel_version where dtime>=20170504 and dtime<=20170510; +----------+
| count(1) |
+----------+
|  1462991 |
+----------+
1 row in set (0.58 sec)

 

對應146W記錄,使用index(dtime),須要回訪表獲取version_name,request_pv字段,這樣要對應146W的隨機IO + 掃描的索引塊數量的隨機IO,
然後還要對這146W的結果集 group by version_name order by request_pv desc,代價仍是很高的. 屢次測試執行4.7s左右.
一種優化方案就是走覆蓋索引,避免回訪表:alter table lc_day_channel_version add key idx_dtime_version_name_request_pv(dtime,version_name,request_pv);
再看執行計劃:
mysql> explain select sql_no_cache version_name,sum(request_pv) as request_pv from lc_day_channel_version where dtime>=20170504 and dtime<=20170510 group by version_name order by request_pv desc; +----+-------------+------------------------+-------+---------------------------------------------------------+-----------------------------------+---------+------+---------+-----------------------------------------------------------+
| id | select_type | table                  | type  | possible_keys                                           | key                               | key_len | ref  | rows    | Extra                                                     |
+----+-------------+------------------------+-------+---------------------------------------------------------+-----------------------------------+---------+------+---------+-----------------------------------------------------------+
|  1 | SIMPLE      | lc_day_channel_version | range | UNIQUE_poouvc,INDEX_d,idx_dtime_version_name_request_pv | idx_dtime_version_name_request_pv | 4       | NULL | 2681154 | Using where; Using index; Using temporary; Using filesort |
+----+-------------+------------------------+-------+---------------------------------------------------------+-----------------------------------+---------+------+---------+-----------------------------------------------------------+
1 row in set (0.00 sec)

 

Using index 已經不須要回訪表了,總體的執行時間也下降了一半,平均執行時間爲2.35s左右.
  
再繼續優化下去,可否避免對這麼大量的數據(146W行記錄)進行排序操做呀? 能利用索引避免排序操做嗎? index(dtime,version_name,request_pv)爲何不能避免物理排序操做呢?(Using filesort顯示確實存在物理排序動做) 
緣由就在於dtime上使用了範圍匹配,使得索引數據總體上再也不有序了. 那我改爲等值匹配看看呢?
mysql> explain select version_name,sum(request_pv) as request_pv from lc_day_channel_version where dtime=20170504 group by version_name; +----+-------------+------------------------+------+---------------------------------------------------------+-----------------------------------+---------+-------+--------+--------------------------+
| id | select_type | table                  | type | possible_keys                                           | key                               | key_len | ref   | rows   | Extra                    |
+----+-------------+------------------------+------+---------------------------------------------------------+-----------------------------------+---------+-------+--------+--------------------------+
|  1 | SIMPLE      | lc_day_channel_version | ref  | UNIQUE_poouvc,INDEX_d,idx_dtime_version_name_request_pv | idx_dtime_version_name_request_pv | 4       | const | 402616 | Using where; Using index |
+----+-------------+------------------------+------+---------------------------------------------------------+-----------------------------------+---------+-------+--------+--------------------------+
1 row in set (0.00 sec)

 

確實沒有出現Using filesort,說明經過這個索引能避免物理排序操做.
固然,業務邏輯仍是不能變的,最終最初的SQL能夠修改成以下等效的SQL:
select version_name,sum(request_pv) as request_pv from ( select version_name,sum(request_pv) as request_pv from lc_day_channel_version where dtime=20170504 group by version_name union all
select version_name,sum(request_pv) as request_pv from lc_day_channel_version where dtime=20170505 group by version_name union all
select version_name,sum(request_pv) as request_pv from lc_day_channel_version where dtime=20170506 group by version_name union all
select version_name,sum(request_pv) as request_pv from lc_day_channel_version where dtime=20170507 group by version_name union all
select version_name,sum(request_pv) as request_pv from lc_day_channel_version where dtime=20170508 group by version_name union all
select version_name,sum(request_pv) as request_pv from lc_day_channel_version where dtime=20170509 group by version_name union all
select version_name,sum(request_pv) as request_pv from lc_day_channel_version where dtime=20170510 group by version_name ) tmp group by version_name order by request_pv desc;

 

天天對應21W左右的記錄,天天的記錄group by後對應2500行左右的記錄,這樣就將原來對146W記錄排序,變成了對2500*7 行記錄排序,排序量大幅降低,因此執行時間也有了提高,如今執行時間已經變爲1.01s左右了
  
單純從SQL的角度優化,彷佛只能優化到這個地步了.而實際的業務SQL要比上面的還要複雜多變,好比說:
select version_name, sum(request_uv) as request_uv from `lc_day_channel_version` where `dtime` >= 20170505 and `dtime` <= 20170511
group by version_name order by request_uv desc; select sql_calc_found_rows version_name, channel, sum(request_pv) as request_pv, sum(request_uv) as request_uv, sum(response_pv) as response_pv, sum(response_uv) as response_uv, sum(download_pv) as download_pv, sum(download_uv) as download_uv from `lc_day_channel_version` where `dtime` >= 20170505 and `dtime` <= 20170511
group by version_name, channel order by request_uv desc limit 0, 15
 
select version_name, sum(response_pv) as response_pv from `lc_day_channel_version` where `dtime` >= 20170505 and `dtime` <= 20170511
group by version_name order by response_pv desc
 
select sql_calc_found_rows version_name, channel, sum(request_pv) as request_pv, sum(request_uv) as request_uv, sum(response_pv) as response_pv, sum(response_uv) as response_uv, sum(download_pv) as download_pv, sum(download_uv) as download_uv from `lc_day_channel_version` where `dtime` >= 20170505 and `dtime` <= 20170511
 group by version_name, channel order by response_pv desc limit 0, 15

 

針對每類SQL都添加一個對應的索引? 那索引太多了,會嚴重影響寫入性能的.
index(dtime,version_name,全部的統計項字段)
index(dtime,version_name,channel,全部的統計項字段)
這樣全家桶式的索引,包含了全部的統計項字段,問題是每一個索引太大了.
  
不要光想着SQL優化,其實最大的殺手鐗: 業務優化尚未考慮呢。 那業務層面是否有優化的空間呢? 固然是有的,並且優化空間還不小.
天天的統計數據在插入後,基本就再也不變更了.天天插入21W左右的記錄,天天的數據統計後也就是2000多行的記錄,這樣在天天凌晨對前1天的數據進行異步統計,
統計結果放到一箇中間表中去,天天的這種統計報表,再也不掃描原始數據表,而掃描這類中間表,天天掃描的記錄行數能夠減小到1/100的數量級,再配以SQL層面的優化纔是王道呀!

優化案例2

mysql> show create table mc_state\G *************************** 1. row ***************************
       Table: mc_state Create Table: CREATE TABLE `mc_state` ( `state_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '機器的狀態ID', `transaction_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '維修週期ID', `ip` int(10) unsigned NOT NULL COMMENT '機器的IP', `state_name` varchar(255) NOT NULL COMMENT '狀態名稱', `start_time` datetime NOT NULL COMMENT '狀態開始時間', `update_time` datetime NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '狀態信息的更新時間', `end_time` datetime NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '狀態結束時間', `create_ip` int(10) unsigned NOT NULL COMMENT '建立該狀態的IP', `error_status` text NOT NULL COMMENT '機器當前的狀態錯誤信息(JSON)', PRIMARY KEY (`state_id`), KEY `idx_name` (`state_name`), KEY `idx_time` (`start_time`), KEY `idx_transaction_id` (`transaction_id`), KEY `idx_ip` (`ip`), KEY `idx_end_time` (`end_time`) ) ENGINE=InnoDB AUTO_INCREMENT=7614257 DEFAULT CHARSET=utf8 COMMENT='機器維修狀態表' mysql> show create table mc_machine\G *************************** 1. row ***************************
       Table: mc_machine Create Table: CREATE TABLE `mc_machine` ( `ip` int(10) unsigned NOT NULL COMMENT '機器的IP', `pool` varchar(255) NOT NULL COMMENT '機器所屬維修策略', `params` varchar(2047) NOT NULL DEFAULT '' COMMENT '其餘PER機器的維修參數信息', `create_time` datetime NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '機器信息的建立時間', `update_time` datetime NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '機器信息的更新時間', PRIMARY KEY (`ip`), KEY `idx_pool` (`pool`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8 COMMENT='機器信息表'

 

天天存在以下的慢查詢:
# Query_time: 6.799199  Lock_time: 0.000124 Rows_sent: 148  Rows_examined: 8394700
SELECT mc_state.state_id AS mc_state_state_id, mc_state.transaction_id AS mc_state_transaction_id, mc_state.ip AS mc_state_ip, mc_stat e.state_name AS mc_state_state_name, mc_state.start_time AS mc_state_start_time, mc_state.update_time AS mc_state_update_time, mc_stat e.end_time AS mc_state_end_time, mc_state.create_ip AS mc_state_create_ip, mc_state.error_status AS mc_state_error_status FROM mc_state INNER JOIN (SELECT max(mc_state.state_id) AS state_id FROM mc_state GROUP BY mc_state.ip) AS anon_1 ON mc_state.state_id = anon_1.state_id INNER JOIN mc_machine ON mc_state.ip = mc_machine.ip WHERE mc_machine.pool IN ('hadoop-repair-quick-repair') LIMIT 0, 999999999999; # Query_time: 6.826629  Lock_time: 0.000125 Rows_sent: 98  Rows_examined: 8394700
SELECT mc_state.state_id AS mc_state_state_id, mc_state.transaction_id AS mc_state_transaction_id, mc_state.ip AS mc_state_ip, mc_stat e.state_name AS mc_state_state_name, mc_state.start_time AS mc_state_start_time, mc_state.update_time AS mc_state_update_time, mc_stat e.end_time AS mc_state_end_time, mc_state.create_ip AS mc_state_create_ip, mc_state.error_status AS mc_state_error_status FROM mc_state INNER JOIN (SELECT max(mc_state.state_id) AS state_id FROM mc_state GROUP BY mc_state.ip) AS anon_1 ON mc_state.state_id = anon_1.state_id INNER JOIN mc_machine ON mc_state.ip = mc_machine.ip WHERE mc_machine.pool IN ('kuorong_beehive') LIMIT 0, 999999999999; # Query_time: 7.824977  Lock_time: 0.000139 Rows_sent: 148  Rows_examined: 8394700
SELECT mc_state.state_id AS mc_state_state_id, mc_state.transaction_id AS mc_state_transaction_id, mc_state.ip AS mc_state_ip, mc_stat e.state_name AS mc_state_state_name, mc_state.start_time AS mc_state_start_time, mc_state.update_time AS mc_state_update_time, mc_stat e.end_time AS mc_state_end_time, mc_state.create_ip AS mc_state_create_ip, mc_state.error_status AS mc_state_error_status FROM mc_state INNER JOIN (SELECT max(mc_state.state_id) AS state_id FROM mc_state GROUP BY mc_state.ip) AS anon_1 ON mc_state.state_id = anon_1.state_id INNER JOIN mc_machine ON mc_state.ip = mc_machine.ip LIMIT 0, 999999999999; # Query_time: 7.899820  Lock_time: 0.000095 Rows_sent: 98  Rows_examined: 8394700
SELECT mc_state.state_id AS mc_state_state_id, mc_state.transaction_id AS mc_state_transaction_id, mc_state.ip AS mc_state_ip, mc_stat e.state_name AS mc_state_state_name, mc_state.start_time AS mc_state_start_time, mc_state.update_time AS mc_state_update_time, mc_stat e.end_time AS mc_state_end_time, mc_state.create_ip AS mc_state_create_ip, mc_state.error_status AS mc_state_error_status FROM mc_state INNER JOIN (SELECT max(mc_state.state_id) AS state_id FROM mc_state GROUP BY mc_state.ip) AS anon_1 ON mc_state.state_id = anon_1.state_id INNER JOIN mc_machine ON mc_state.ip = mc_machine.ip WHERE mc_machine.pool IN ('kuorong_beehive') LIMIT 0, 999999999999;

 

業務的邏輯是什麼?獲取每一個池中,每臺機器最新的狀態數據.
其實最原始的業務需求是天天得到每臺機器最新的狀態數據,但一個SQL執行時間太長了,常常超時報錯,因此最後修改成這樣,按池獲取.
但其實這樣,每次執行都要執行SELECT max(mc_state.state_id) AS state_id FROM mc_state GROUP BY mc_state.ip
大量這這個pool不相關的數據也要獲取一遍,其實存在着明顯的資源浪費的.

其實業務邏輯能夠下面這樣實現:

$last_ip = 0; $result1 = $dbconn->prepare("select ip from mc_machine where pool='ps_diaoyan' and ip>? order by ip limit 1000"); $result2 = $dbconn->prepare("select mc_state.state_id AS mc_state_state_id, mc_state.transaction_id AS mc_state_transaction_id, mc_state.ip AS mc_state_ip, mc_state.state_name AS mc_state_state_name, mc_state.start_time AS mc_state_start_time, mc_state.update_time AS mc_state_update_time, mc_state.end_time AS mc_state_end_time, mc_state.create_ip AS mc_state_create_ip, mc_state.error_status AS mc_state_error_status from mc_state where ip=? order by state_id desc limit 1"); $v_file = fopen("matrix_mc1.result","w+"); $result1->bindParam(1,$last_ip,PDO::PARAM_INT); $result2->bindParam(1,$this_ip,PDO::PARAM_INT); while (true) { $result1->execute(); $iplist = $result1->fetchAll(PDO::FETCH_ASSOC); foreach ( $iplist as $row ) { $this_ip = intval($row["ip"]); $result2->execute(); foreach ( $result2->fetchAll(PDO::FETCH_NUM) as $row2 ) { $v_str = implode(",",$row2).PHP_EOL; fwrite($v_file,$v_str); } } if ( count($iplist) < 1000) { break; } $last_ip = intval($row["ip"]); } $result1 = null; $result2 = null; $dbconn = NULL; fclose($v_file);

 

原始SQL的執行計劃不在這裏展現了.
  
優化後的方案只涉及到2類SQL,都是很簡單的SQL,均可以經過高效的索引快速的返回結果:
mysql> explain select ip from mc_machine where ip>169524751 order by ip limit 1000; +----+-------------+------------+-------+---------------+---------+---------+------+--------+--------------------------+
| id | select_type | table      | type  | possible_keys | key     | key_len | ref  | rows   | Extra                    |
+----+-------------+------------+-------+---------------+---------+---------+------+--------+--------------------------+
|  1 | SIMPLE      | mc_machine | range | PRIMARY       | PRIMARY | 4       | NULL | 103043 | Using where; Using index |
+----+-------------+------------+-------+---------------+---------+---------+------+--------+--------------------------+
1 row in set (0.00 sec) mysql> explain select mc_state.state_id AS mc_state_state_id, mc_state.transaction_id AS mc_state_transaction_id, -> mc_state.ip AS mc_state_ip,mc_state.state_name AS mc_state_state_name, mc_state.start_time AS mc_state_start_time, -> mc_state.update_time AS mc_state_update_time, mc_state.end_time AS mc_state_end_time, mc_state.create_ip AS mc_state_create_ip, -> mc_state.error_status AS mc_state_error_status -> from mc_state where ip=169524751 order by state_id desc limit 1; +----+-------------+----------+------+---------------+--------+---------+-------+------+-------------+
| id | select_type | table    | type | possible_keys | key    | key_len | ref   | rows | Extra       |
+----+-------------+----------+------+---------------+--------+---------+-------+------+-------------+
|  1 | SIMPLE      | mc_state | ref  | idx_ip        | idx_ip | 4       | const |    1 | Using where |
+----+-------------+----------+------+---------------+--------+---------+-------+------+-------------+
1 row in set (0.00 sec) 

 

這樣經過SQL的拆分,經過循環的方式,將原來的一個複雜的自關聯查詢,變成2類簡單的SQL循環執行,從而達到了優化的目的.
針對由此帶來的應用端和DB端網絡交互太多帶來的時間成本,能夠考慮使用multiquery一次發送執行多條SQL來減小頻繁網絡交互帶來的影響(具體一次發送執行多少個SQL合適,須要業務層面進行測試肯定).

固然,業務最終沒有使用這裏的方案,而是根據業務邏輯,變爲簡單的讀取兩個表的記錄,然後代碼層進行關聯,也成功的消除了業務的讀取壓力問題. 
這也說明了業務層面的優化是很重要的.

優化案例3

手百的夏逗活動, 是手百爲了提高用戶黏度推出的一個活動,鼓勵用戶經過手百搜索一些奇葩的問題,並獎勵給用戶必定的豆幣,最終排名前1500名的用戶,能夠瓜分一筆現金大獎.
表結構以下:

mysql> show create table xiadou_user\G *************************** 1. row ***************************
       Table: xiadou_user Create Table: CREATE TABLE `xiadou_user` ( `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '用戶 id', `uid` bigint(20) NOT NULL COMMENT '百度帳號 uid', `uname` varchar(128) NOT NULL DEFAULT '' COMMENT '百度帳號名稱', `displayname` varchar(128) NOT NULL DEFAULT '' COMMENT '百度帳號顯示名稱', `securemobil` varchar(50) NOT NULL DEFAULT '' COMMENT '綁定的手機號', `score` int(10) NOT NULL DEFAULT '0' COMMENT '用戶當前的豆子數', `money` float NOT NULL DEFAULT '0' COMMENT '累積抽獎賺取的金額', `last_sign_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '最近一次簽到時間', `sign_continue_days` tinyint(3) NOT NULL DEFAULT '0' COMMENT '簽到連續天數', `create_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '建立時間', `update_time` timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新時間', `last_add_score_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' COMMENT '最近一次加豆子的時間', `cuid` varchar(255) NOT NULL DEFAULT '' COMMENT '用戶的 cuid 信息,可能包含多個,逗號分隔,最多存3個', `win` tinyint(1) NOT NULL DEFAULT '0' COMMENT '是不是最後大獎中獎用戶', `awarded` tinyint(1) NOT NULL DEFAULT '0' COMMENT '表示是否領取過最後大獎', `issafe` tinyint(2) NOT NULL DEFAULT '1' COMMENT '安全狀態,1 正常,0高危', `appos` varchar(100) NOT NULL DEFAULT '' COMMENT 'appos', PRIMARY KEY (`id`), UNIQUE KEY `uid_UNIQUE` (`uid`), KEY `SCORE_TIME_INDEX` (`score`,`last_add_score_time`), KEY `WIN_INDEX` (`win`), KEY `idx_appos` (`appos`) ) ENGINE=InnoDB AUTO_INCREMENT=9891815 DEFAULT CHARSET=utf8 COMMENT='用戶信息表'
1 row in set (0.00 sec)

 

具體的排名規則是: ORDER BY score DESC, last_add_score_time ASC 優先按照分數降序排名, 分數相同的, 早得到這個分數的用戶排名靠前.
用戶參與活動,搜索了奇葩問題後,極可能想查看本身目前的積分,距離瓜分大獎的資格還差多少分.
因此業務提供了這樣一個功能:
SELECT score FROM xiadou_user ORDER BY score DESC, last_add_score_time ASC LIMIT 1500, 1;  
查詢目前第1500名(其實應該是limit 1499,1 的) 的分數,而後顯示目前用戶的分數距離它還差多少分數.
這個查詢應用端是有CACHE的,但每次只要用戶積分有變化,排名就可能發生變化,因此業務會del相關的CACHE,因此對於這個查詢而言,CACHE是沒有用的,白天時段,讀取基本上還都是要實時的走DB的.
可是很快DB端CPU就打滿了,DB端都是上面這個查詢,爲何呢?

mysql> explain SELECT score FROM xiadou_user ORDER BY score DESC, last_add_score_time ASC LIMIT  1500, 1; +----+-------------+-------------+-------+---------------+------------------+---------+------+---------+-----------------------------+
| id | select_type | table       | type  | possible_keys | key              | key_len | ref  | rows    | Extra                       |
+----+-------------+-------------+-------+---------------+------------------+---------+------+---------+-----------------------------+
|  1 | SIMPLE      | xiadou_user | index | NULL          | SCORE_TIME_INDEX | 8       | NULL | 9255660 | Using index; Using filesort |
+----+-------------+-------------+-------+---------------+------------------+---------+------+---------+-----------------------------+
1 row in set (0.00 sec)

 

存在KEY  (,) 可是它只能優化這2個字段的同向排序,都升序或者都降序均可以經過這個索引避免物理排序,快速的返回TOPN記錄.SCORE_TIME_INDEXscorelast_add_score_time

由於MYSQL自己只有升序索引,沒有降序索引,但索引葉節點是經過雙向鏈表來保證邏輯有序的,因此SQL層面兩個排序字段都升序或者都降序,都是能夠經過索引來優化的,就是正向掃描和逆向掃描索引而已.
但對於2個排序字段排序方向不一樣的狀況,是沒法經過索引優化的,只能進行物理排序了,因此執行計劃中出現了Using filesort ,也就是說讀取出幾百W的記錄,然後物理排序,最後輸出第1500個記錄,因此SQL性能不好的.
大併發的狀況,狀況進一步惡化,從而致使DB主機CPU打滿的狀況(隨着數據的持續增長,狀況只會是進一步的惡化).
優化方案:
由於排序時優先按分數排序,分數相同的,再按照時間排序,這裏並非要得到確切的第1500名的用戶信息,而只是要得到第1500名的分數而已,因此上面的SQL在業務邏輯層其實等價於下面的SQL:

SELECT score FROM xiadou_user ORDER BY score DESC LIMIT 1500, 1; mysql> explain SELECT score FROM xiadou_user ORDER BY score DESC LIMIT  1500, 1; +----+-------------+-------------+-------+---------------+------------------+---------+------+------+-------------+
| id | select_type | table       | type  | possible_keys | key              | key_len | ref  | rows | Extra       |
+----+-------------+-------------+-------+---------------+------------------+---------+------+------+-------------+
|  1 | SIMPLE      | xiadou_user | index | NULL          | SCORE_TIME_INDEX | 8       | NULL | 1501 | Using index |
+----+-------------+-------------+-------+---------------+------------------+---------+------+------+-------------+
1 row in set (0.00 sec)

 

 

 

這個SQL是可使用KEY SCORE_TIME_INDEX (score,last_add_score_time) 來優化的,是不須要物理排序的.
業務改寫爲這個SQL,上線後,DB主機CPU恢復正常,並且業務響應時間大幅提高.
固然,最終的用戶排名仍是要調用上面的2個字段的排序SQL的.
不過業務21點結束活動,22點公佈排名,這中間徹底能夠執行SQL,把結果插入到一個結果表去,然後只是讀取這個結果表就能夠了.並且這樣也方便業務干預最終的排名結果.
固然,應用端使用cache緩存上面2個字段的排序SQL的執行結果,也是徹底可行的,由於數據再也不變更,徹底能夠經過cache擋住所有的讀取流量.
總結:
本來並不等價的2個SQL,但在業務層面是徹底等價的,經過SQL的改寫,達到了優化的目的.

轉自:

mysql索引設計的注意事項(大量示例,收藏再看)






原文出處:https://www.cnblogs.com/wangtcc/p/mysql-suo-yin-she-ji-de-zhu-yi-shi-xiang-da-liang-.html

相關文章
相關標籤/搜索