1、一個簡單的sql語句問題
假設當前我們有一個表記錄用戶信息,結構如下:
a) 表結構
CREATE TABLE `u` (
`id` int(11) NOT NULL DEFAULT ‘0′,
`regdate` int(1) unsigned,
…..
PRIMARY KEY (`id`),
KEY `regdate` (`regdate`)
) ENGINE=InnoDB DEFAULT CHARSET=gbk
說明:1) 由於需要按照註冊時間單獨查詢,建了一個regdate的索引
2) 其他信息未列出, 一行長度100字節左右,錶行數百萬級。
b) 需求:需要一個語句查出表中id爲10000整數倍的記錄總數。
2、常規答案
一個正常想到的語句是 select sum(id % 10000 = 0) from u; —— (SQL1)
我們來看這個語句的執行流程:
a) 遍歷所有數據,取出id字段
b) 計算id%10000=0的值並通過sum累計。
在構造的環境中這個語句的執行時間爲2.6s.
3、查的多,查得快
假設我們同時要查出註冊時間在2007年之前的用戶總數,我們自然得到這個語句
select sum(id % 10000 = 0), sum(regdate<1167667200) from sbtest;—-(SQL2)
執行結果發現這個語句執行時間約0.5s 。 這個語句查的數據結果比SQL1多,但執行時間卻降爲1/5.
4、分析
可以直接從執行期間的磁盤參數,或者在os/os0file.c中將程序讀取的數據量輸出結果查看,直觀結果是SQL1讀取了更多的磁盤數據。
問題1:在SQL1執行過程中,遍歷所有數據,InnoDB只從磁盤讀取了id這個字段,還是全部讀入?
實際上由於id是聚簇索引,並沒有一個單獨的索引樹存id,因此在磁盤上,id索引樹的葉節點上就是數據。 InnoDB以page爲單位讀取,在取id的過程中,必須將所有的數據讀入。
於是我們發現,在SQL1中,我們只需要id字段,而每行額外讀入了幾百字節的數據。
問題2:SQL2避免了讀全數據?
確實如此。
我們對比兩個語句的explain結果, 發現僅有的不同是選用的key結果不同。
SQL1 | SQL2 |
key: PRIMARY | key: regdate |
由於regdate是非聚簇(secondary index)索引,單獨存於另一棵樹。 我們知道使用非聚簇索引時,需要讀行數據的時候,需要再到聚簇索引中取得。顯然SQL2不會再讀一遍全數據(否則性能必然低於SQL1)。
而其原因是覆蓋索引(covering index)。 非聚簇索引的葉節點上是聚簇索引的字段值,需要取數據時,根據這個值再去聚簇索引上取。而這時InnoDB變“聰明”了, 需要取的值只是id,而id作爲聚簇索引的key信息,已經得到,不需要再到聚簇索引中讀取數據。
由於regdate索引樹上只有regdate和主鍵(id)的信息,因此數據量遠小於全表數據,因此SQL2的讀盤量小於SQL1,執行速度快。
5、其他
這個例子涉及到幾個概念, 聚簇索引(cluster index)、非聚簇索引(secondary index), 覆蓋索引(covering index),還有磁盤的數據存放。都算是一些基本的內容,卻是平時見到的一些優化的理論基礎。舉幾個例子如下:
1) 我們經常被告誡select之後只填最必須的字段
其中的一個原因是減少網絡傳輸。但不一定能夠提升服務器執行性能。比如例子中的表,select * from u where id = n; 與select user_name from u where id =n一樣。
當然有些時候效果會很理想,比如 select id from u where regdate=xxx 就比select * from u where regdate=xxx快很多,原因已說明。
2) 查詢符合條件的第10w個記錄開始的10個記錄。
這個例子在其他博文上被多次提及,
select * from t order by a limit 100000, 10; 可以改進爲
select * from t where a>=(select a from t order by a limit 100000,1) limit 10;
在筆者環境中性能提升約1000倍。
原因即在於, 改進語句中,子查詢中的排序只在非聚餐索引a上執行,由於覆蓋索引,排序過程不需要訪問聚簇索引。實際讀讀取全數據的只有10條記錄,而原語句則需要讀所有記錄的全數據。
當然執行排序的過程消耗是一樣的。
6、結束
回到開頭,如果只需要查id滿足特定條件的記錄總數,可以使用select sum(id % 10000 = 0) from u force index (`regdate`);
把sum(id %10000=0)換成其他操作對執行效率均沒有影響。
但若查詢內容中出現除id和regdate外的其他字段,則force index優化無效,可自行分析。