mysql索引設計的注意事項(大量示例,收藏再看)
mysql索引設計的注意事項(大量示例,收藏再看)
目錄
- 一、索引的重要性
- 二、執行計劃上的重要關注點
- (1).全表掃描,檢索行數
- (2).key,using index(覆蓋索引)
- (3).通過key_len確定究竟使用了複合索引的幾個索引欄位
- (4) order by和Using filesort
- 三、索引設計的注意事項
- (1). 關於INNODB表PRIMARY KEY的建議
- (2). 什麼列上適合建索引,什麼列上不適合建索引
- (3). 索引一定是有益的嗎?
- (4). where條件中不要在索引欄位側進行任何運算(包括隱式運算),否則會導致索引不可用,導致全表掃描
- (5). 不要使用%xxx%這種模糊匹配,會導致全表掃描/索引全掃描
- (6). 關於字首索引和冗餘索引
- (7). 關於索引定義中的欄位順序
- (8). 關於排序查詢的優化
- (9). 關於單列索引和複合索引
- (10). 關於多表關聯
- 四、慢查詢日誌的分析以及關注點
- (1). 使用pt-query-digest工具來統計
- (2). 對統計輸出進行分析
- 五、幾個優化案例
- 優化案例1
- 優化案例2
- 優化案例3
一、索引的重要性
索引對於MySQL資料庫的重要性是不言而喻的:
因為缺乏合適的索引,一個稍大的表全表掃描,稍微來些併發,就可能導致DB響應時間急劇飆升,甚至導致DB效能的雪崩;
既然索引對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)
避免全表掃描 全表掃描(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和它們的執行計劃

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,就可以確定索引使用了哪些欄位
第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個操作往往是很耗時的,是我們極力想要避免的.
三、索引設計的注意事項
關於索引的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迴圈實現.
四、慢查詢日誌的分析以及關注點
(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)來優化這類查詢。
五、幾個優化案例
優化案例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 | +----+-------------+------------------------+-------+-----------------