一個例子與InnoDB索引的幾個概念

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優化無效,可自行分析。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章