1. 程式人生 > >注意使用 BTREE 複合索引各欄位的 ASC/DESC 以優化 order by 查詢效率

注意使用 BTREE 複合索引各欄位的 ASC/DESC 以優化 order by 查詢效率

tbl_direct_pos_201506 表有 190 萬資料,DDL:
CREATE TABLE `tbl_direct_pos_201506` (
  `acq_ins_code` char(13) NOT NULL DEFAULT '' COMMENT '機構程式碼',
  `trace_num` char(6) NOT NULL DEFAULT '' COMMENT '跟蹤號',
  `trans_datetime` char(10) NOT NULL DEFAULT '' COMMENT '交易時間',
  `process_flag` char(1) DEFAULT NULL COMMENT '處理標識',
  `rev_flag` char(1) DEFAULT NULL COMMENT '接收標識',
  `before_trans_code` char(3) DEFAULT NULL COMMENT '交易型別',
  `trans_amt` decimal(15,3) DEFAULT NULL COMMENT '交易金額',
  `acct_num` char(21) DEFAULT NULL COMMENT '卡號',
  `mer_type` char(4) DEFAULT NULL COMMENT '商戶型別',
  `recv_ins_code` char(13) DEFAULT NULL COMMENT '髮卡行程式碼',
  `retrivl_ref_num` char(12) DEFAULT NULL COMMENT '檢索參考號',
  `resp_auth_code` char(6) DEFAULT NULL COMMENT '授權碼',
  `resp_code` char(2) DEFAULT NULL COMMENT '應答碼',
  `term_id` char(8) DEFAULT NULL COMMENT '終端程式碼',
  `mer_code` char(15) DEFAULT NULL COMMENT '商戶程式碼',
  `mer_addr_name` char(40) DEFAULT NULL COMMENT '商戶名稱和地址,前 25 位元組是名稱,後面是地址',
  `self_define` varchar(300) DEFAULT NULL COMMENT '第 259 位元組是卡片型別',
  `sys_date` char(8) NOT NULL DEFAULT '' COMMENT '交易日期',
  `sa_sav2` varchar(300) DEFAULT NULL COMMENT '第 243 位元組是 DCC 標識',
  `rec_create_time` datetime DEFAULT NULL COMMENT '聯機入庫時間',
  `rec_update_time` datetime DEFAULT NULL COMMENT '最後修改時間',
  PRIMARY KEY (`sys_date`,`trans_datetime`,`acq_ins_code`,`trace_num`),
  KEY `idx_direct_pos_create_time` (`rec_create_time`)
) ENGINE=MyISAM DEFAULT CHARSET=utf8 COMMENT='交易月表模板';

關於該表的一個慢查詢日誌如下:
# Time: 150701 15:45:28
# [email protected]: test[test] @ localhost [127.0.0.1]  Id:     1
# Query_time: 2.478195  Lock_time: 0.010007 Rows_sent: 20  Rows_examined: 450612
SET timestamp=1435736728;
select substr(t.acq_ins_code, 3) merAcqInsCode, t.mer_code, t.term_id, substr(t.mer_addr_name, 1, 12) merName,
tt.trans_desc, t.rev_flag, t.trans_amt, concat(substr(t.sys_date, 1, 4), t.trans_datetime) transTime,t.before_trans_code,
t.acct_num, t.retrivl_ref_num,  t.resp_code, t.resp_auth_code,  r.recv_ins_name,t.acq_ins_code,t.trace_num,t.trans_datetime,
case substr(t.sa_sav2,259,1)  when 1 then '借記卡' when 2 then '貸記卡'
when 3 then '準貸記卡' when 4 then '私有預付卡' else '' end  cardType,
case 
when locate('VIS',t.sa_sav2) > 0 then 'VISA' 
when locate('JCB',t.sa_sav2) > 0 then 'JCB' 
when locate('DNC',t.sa_sav2) > 0 then '大萊卡' 
when locate('CUP',t.sa_sav2) > 0 then '銀聯境內卡' 
when locate('UPI',t.sa_sav2) > 0 then '銀聯境外卡'
else '' end cardBrand 
from tbl_direct_pos_201506 t
left join trans_recv_ins r on r.recv_ins_code = t.recv_ins_code
left join tbl_trans_type tt on tt.trans_code = t.before_trans_code
where t.sys_date between '20150622' and '20150628' 
order by
t.sys_date desc, t.trans_datetime desc, t.acq_ins_code, t.trace_num 
limit 0, 20;

日誌中可以看出該 sql 的執行時間是 2.478 s。
我們來檢視一下該 sql 的執行計劃:
idselect_typetabletypepossible_keyskeykey_lenrefrowsExtra
1SIMPLEtrangePRIMARYPRIMARY24299392Using index condition; Using filesort
1SIMPLEreq_refPRIMARYPRIMARY41msp.t.recv_ins_code1Using where
1SIMPLEtteq_refPRIMARYPRIMARY14msp.t.before_trans_code1Using where

執行計劃分析:
Using filesort。是的,看到它,說明我們的查詢需要優化了:檔案排序是通過相應的排序演算法,將取得的資料在記憶體中進行排序。
MyISAM 儲存引擎的主鍵索引和非主鍵索引差別很小,只不過是主鍵索引的索引鍵是一個唯一且非空的鍵而已。MyISAM 的索引預設為 B-TREE。也就是說,主鍵在這裡相當於一個普通的 B-TREE。
該 sql 一個 where 欄位,四個 order by 欄位,都在主鍵裡邊呀,而且 order by 的順序完全符合最左字首原則,為什麼還要 filesort?
MySql 索引建立手冊裡如是說:
索引列的定義可以跟隨 ASC 或者 DESC。這些關鍵字允許為未來擴充套件用於指定升序或降序索引值儲存。這個語法會被解析但卻被忽略。索引列總是以升序排列。——也就是說你寫了不會報錯,但寫了白寫。
這樣看來,我們的主鍵沒起排序作用,原因就在於我們的主鍵是各主鍵欄位 asc 儲存, order by 裡 desc 和 asc(預設是 asc) 混用。為了驗證這個說法,我們把該 order by 換為和主鍵一致的 asc:
select substr(t.acq_ins_code, 3) merAcqInsCode, t.mer_code, t.term_id, substr(t.mer_addr_name, 1, 12) merName,
				tt.trans_desc, t.rev_flag, t.trans_amt, concat(substr(t.sys_date, 1, 4), t.trans_datetime) transTime,t.before_trans_code,
				t.acct_num, t.retrivl_ref_num,  t.resp_code, t.resp_auth_code,  r.recv_ins_name,t.acq_ins_code,t.trace_num,t.trans_datetime,
				case substr(t.sa_sav2,259,1)  when 1 then '借記卡' when 2 then '貸記卡'
				when 3 then '準貸記卡' when 4 then '私有預付卡' else '' end  cardType,
				case 
					when locate('VIS',t.sa_sav2) > 0 then 'VISA' 
					when locate('JCB',t.sa_sav2) > 0 then 'JCB' 
					when locate('DNC',t.sa_sav2) > 0 then '大萊卡' 
					when locate('CUP',t.sa_sav2) > 0 then '銀聯境內卡' 
					when locate('UPI',t.sa_sav2) > 0 then '銀聯境外卡'
					else '' end cardBrand 
			from tbl_direct_pos_201506 t
			left join trans_recv_ins r on r.recv_ins_code = t.recv_ins_code
			left join tbl_trans_type tt on tt.trans_code = t.before_trans_code
			where t.sys_date between '20150622' and '20150628' 
			order by
			 t.sys_date, t.trans_datetime, t.acq_ins_code, t.trace_num
			limit 0, 20;

執行時間:0.023 s。
結果差強人意。檢視其執行計劃:
idselect_typetabletypepossible_keyskeykey_lenrefrowsExtra
1SIMPLEtrangePRIMARYPRIMARY24299392Using index condition
1SIMPLEreq_refPRIMARYPRIMARY41msp.t.recv_ins_code1Using where
1SIMPLEtteq_refPRIMARYPRIMARY14msp.t.before_trans_code1Using where

果然,我們利用到了主鍵索引,Using filesort 沒有了。
既然找的了問題的症兆所在,接下來的事情似乎只是走流程了。
問了下業務,分頁結果裡 sys_date 和 trans_datetime 兩個欄位必須降序排列,其餘兩個欄位倒不是很在意。
既然我們無法更改索引每一列的降序、升序(預設為升序),那麼我們可以在寫 order by 的時候讓索引各欄位降序/升序一致。最終的 sql 改寫為:
select substr(t.acq_ins_code, 3) merAcqInsCode, t.mer_code, t.term_id, substr(t.mer_addr_name, 1, 12) merName,
				tt.trans_desc, t.rev_flag, t.trans_amt, concat(substr(t.sys_date, 1, 4), t.trans_datetime) transTime,t.before_trans_code,
				t.acct_num, t.retrivl_ref_num,  t.resp_code, t.resp_auth_code,  r.recv_ins_name,t.acq_ins_code,t.trace_num,t.trans_datetime,
				case substr(t.sa_sav2,259,1)  when 1 then '借記卡' when 2 then '貸記卡'
				when 3 then '準貸記卡' when 4 then '私有預付卡' else '' end  cardType,
				case 
					when locate('VIS',t.sa_sav2) > 0 then 'VISA' 
					when locate('JCB',t.sa_sav2) > 0 then 'JCB' 
					when locate('DNC',t.sa_sav2) > 0 then '大萊卡' 
					when locate('CUP',t.sa_sav2) > 0 then '銀聯境內卡' 
					when locate('UPI',t.sa_sav2) > 0 then '銀聯境外卡'
					else '' end cardBrand 
			from tbl_direct_pos_201506 t
			left join trans_recv_ins r on r.recv_ins_code = t.recv_ins_code
			left join tbl_trans_type tt on tt.trans_code = t.before_trans_code
			where t.sys_date between '20150622' and '20150628' 
			order by
			 t.sys_date desc, t.trans_datetime desc, t.acq_ins_code desc, t.trace_num desc 
			limit 0, 20;

執行之,0.029 s,搞定。

參考資料