什么是數(shù)據(jù)庫的索引?
索引
當(dāng)數(shù)據(jù)庫中數(shù)據(jù)量比較少的時候,哪怕全部檢索也可以很快,但如果數(shù)據(jù)量達(dá)到了百萬,千萬,上億的時候,還是全表掃描,那么數(shù)據(jù)查詢的速度會慢的讓人無法忍受。
索引的作用,就是為了加快數(shù)據(jù)查詢,類似于我們查不認(rèn)識的字時,使用字典的目錄一樣,在字典里面快速查詢出不認(rèn)識的字。字典可以根據(jù)讀音的首字母,偏旁部首,筆畫來查詢。同樣的,索引也有Hash索引,B-Tree索引,GIN索引等不同索引類型,根據(jù)查詢的場景不同,可以選擇創(chuàng)建對應(yīng)的索引類型。
索引分類
數(shù)據(jù)結(jié)構(gòu)實現(xiàn)
Postgresql支持豐富的索引類型,并且根據(jù)索引框架支持用戶開發(fā)自定義的索引,下面列舉下常用的索引類型及適用范圍
| 索引類型 | 實現(xiàn)方法 | 適用范圍 |
|---|---|---|
| b-tree | 使用b-tree數(shù)據(jù)結(jié)構(gòu)來存儲索引數(shù)據(jù) | 等值查詢或范圍查詢,以及in、between、is null、order by等,默認(rèn)索引類型 |
| hash | 基于hash表實現(xiàn) | 等值查詢,尤其索引列值非常長的情況 |
| gist | 使用一種平衡的樹形結(jié)構(gòu)訪問方法 | 多維數(shù)據(jù)類型和集合數(shù)據(jù)類型 |
| gin | 通用倒排索引,存儲的是鍵值與倒排表 | 數(shù)組、jsonb、全文檢索、模糊查詢等 |
| brin | 塊范圍索引 | 索引列的值與物理存儲相關(guān)性很強,比如時序數(shù)據(jù) |
mysql的索引類型和數(shù)據(jù)庫引擎相關(guān)性較強,不過最常用的B樹索引是支持的
| 索引類型 | MyISAM | InnoDB |
|---|---|---|
| b-tree | yes | yes |
| hash | no | no |
| R-Tree | yes | no |
| Full-Text(類似gin) | yes | no |
聚簇索引與非聚簇索引
InnoDB 默認(rèn)創(chuàng)建的主鍵索引是聚族索引(Clustered Index),其它索引都屬于輔助索引(Secondary Index),也被稱為二級索引或非聚族索引。
聯(lián)合索引與單列索引
create index i1 on t2 (c1);
create index i2 on t2 (c1,c2);
pg的多列(聯(lián)合)索引僅支持b-tree、gist、gin、brin類型,其中b-tree的多列索引,僅在索引的第一個字段出現(xiàn)在查詢條件中才有效(最左匹配原則),而其他類型的多列索引可以支持任意字段查詢 對于多字段查詢,多列索引要比單列索引的查詢速度快,可以避免回表查詢,但對于單字段查詢,多列索引就要比單列索引查詢速度慢了,這里需要根據(jù)表的實際查詢sql類型、頻率,綜合考慮是否需要使用多列索引。
部分索引
部分索引是指支持在指定條件的記錄上創(chuàng)建索引,通過where條件指定這部分記錄,比如:
postgres=# create table test(id int, c1 varchar(10));
CREATE TABLE
postgres=# insert into test select generate_series(100001,100100),'invalid';
INSERT 0 100
postgres=# create index i1 on test (c1) where c1 = 'invalid';
CREATE INDEX
postgres=# explain analyze select * from test where c1 = 'invalid';
QUERY PLAN
----------------------------------------------------------------------------------------------------------
Index Scan using i1 on t3 (cost=0.14..4.16 rows=1 width=11) (actual time=0.035..0.079 rows=100 loops=1)
Planning Time: 0.485 ms
Execution Time: 0.135 ms
(3 rows)
實際上對于數(shù)據(jù)分布不均的字段,創(chuàng)建正常的索引,在查詢占比較小值時也是可以走索引的,查詢占比較大值時無法走索引,如下所示,部分索引的優(yōu)勢在于索引體積小,維護代價也比較小
函數(shù)索引
函數(shù)索引指可以使用一個函數(shù)或者表達(dá)式的結(jié)果作為索引的字段,比如:
postgres=# create index i1 on test ((lower(c1)));
CREATE INDEX
postgres=# explain analyze select * from test where lower(c1) = 'xxx';
QUERY PLAN
--------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on test (cost=20.29..783.05 rows=500 width=37) (actual time=0.063..0.063 rows=0 loops=1)
Recheck Cond: (lower(c1) = 'xxx'::text)
-> Bitmap Index Scan on i1 (cost=0.00..20.17 rows=500 width=0) (actual time=0.060..0.060 rows=0 loops=1)
Index Cond: (lower(c1) = 'xxx'::text)
Planning Time: 0.406 ms
Execution Time: 0.095 ms
(6 rows)
postgres=# explain analyze select * from test where c1 = 'xxx';
QUERY PLAN
--------------------------------------------------------------------------------------------------
Seq Scan on test (cost=0.00..2084.00 rows=1 width=37) (actual time=19.016..19.016 rows=0 loops=1)
Filter: (c1 = 'xxx'::text)
Rows Removed by Filter: 100000
Planning Time: 0.121 ms
Execution Time: 19.048 ms
(5 rows)
此時如果直接使用c1字段作為查詢條件是無法走索引的,同理如果創(chuàng)建的是普通索引,在查詢時對字段加上了函數(shù)或者表達(dá)式,都不會走索引,我們應(yīng)始終避免出現(xiàn)這樣的問題
排序索引
在涉及order by操作的sql時,b-tree索引返回的結(jié)果是有序的,可以直接返回,而其他索引類型,需要對索引返回結(jié)果再進(jìn)行一次排序。b-tree索引的默認(rèn)排序為升序,空值放在最后,創(chuàng)建索引時可以指定排序方式,如按倒序排序時,空值默認(rèn)是放在最前的,但往往我們的查詢并不想展示空值的結(jié)果,此時可以在創(chuàng)建索引時指定排序desc nulls last以達(dá)到和查詢sql切合的目的。

索引非銀彈
索引需要占用額外的物理空間,如果表中的數(shù)據(jù)變化,也需要同步維護索引中的數(shù)據(jù),對數(shù)據(jù)庫的性能會有一定影響。考慮到索引的維護代價、空間占用和查詢時回表的代價,不能認(rèn)為索引越多越好。索引一定是按需創(chuàng)建的,并且要盡可能確保足夠輕量。一旦創(chuàng)建了多字段的聯(lián)合索引,我們要考慮盡可能利用索引本身完成數(shù)據(jù)查詢,減少回表的成本。不能認(rèn)為建了索引就一定有效,對于后綴的匹配查詢、查詢中不包含聯(lián)合索引的第一列、查詢條件涉及函數(shù)計算等情況無法使用索引。此外,即使SQL本身符合索引的使用條件,MySQL也會通過評估各種查詢方式的代價,來決定是否走索引,以及走哪個索引。
數(shù)據(jù)庫基于成本決定是否走索引
查詢數(shù)據(jù)可以直接在聚簇索引上進(jìn)行全表掃描,也可以走二級索引掃描后到聚簇索引回表。那么PostgreSQL/MySQL到底是怎么確定走哪種方案的呢。在滿足能走索引的條件下,最終是否走索引由計劃器生成的執(zhí)行計劃決定,PostgreSQL/MySQL中執(zhí)行計劃是完全基于代價估計的,如果估算的代價為全表掃描最優(yōu),則不會使用索引掃描
這里的代價,包括IO成本和CPU成本:
IO成本,是從磁盤把數(shù)據(jù)加載到內(nèi)存的成本。默認(rèn)情況下,讀取數(shù)據(jù)頁的IO成本常數(shù)是1(也就是讀取1個頁成本是1)。
CPU成本,是檢測數(shù)據(jù)是否滿足條件和排序等CPU操作的成本。默認(rèn)情況下,檢測記錄的成本是0.2。基于此,我們分析下全表掃描的成本。
全表掃描,就是把聚簇索引中的記錄依次和給定的搜索條件做比較,把符合搜索條件的記錄加入結(jié)果集的過程。要計算全表掃描的代價需要兩個信息:
1.聚簇索引占用的頁面數(shù),用來計算讀取數(shù)據(jù)的IO成本;
2.表中的記錄數(shù),用來計算搜索的CPU成本。
有時會因為統(tǒng)計信息的不準(zhǔn)確或成本估算的問題,實際開銷會和MySQL統(tǒng)計出來的差距較大,導(dǎo)致MySQL選擇錯誤的索引或是直接選擇走全表掃描,這個時候就需要人工干預(yù),使用強制索引了。
索引失效
-
對于 Hash 索引實現(xiàn)的列,如果使用到范圍查詢,那么該索引將無法被優(yōu)化器使用到。Hash 索引只有在“=”的查詢條件下,索引才會生效。如果涉及范圍查詢則應(yīng)建立b-tree索引 -
以 % 開頭的 LIKE 查詢將無法利用節(jié)點查詢數(shù)據(jù),這種情況下需要考慮gin索引或者es這種全文檢索的方式 -
使用復(fù)合索引時,需要使用索引中的最左邊的列進(jìn)行查詢,才能使用到復(fù)合索引。
例如我們在 order 表中建立一個復(fù)合索引 idx_user_order_status(order_no, status, user_id),如果我們使用 order_no、order_no+status、order_no+status+user_id 以及 order_no+user_id 組合查詢,則能利用到索引;而如果我們用 status、status+user_id 查詢,將無法使用到索引,這也是我們經(jīng)常聽過的最左匹配原則。
-
如果查詢條件中使用 or,且 or 的前后條件中有一個列沒有索引,那么涉及的索引都不會被使用到。
常見慢sql情況
-
沒有創(chuàng)建索引,建表的時候一定不要忘記建立可能的索引,創(chuàng)建索引需要按照ESR原則進(jìn)行 -
索引失效的情況,如查詢字段上使用表達(dá)式導(dǎo)致索引失效比如在c1字段上存在一個b-tree索引,where c1+1 > 10000這個查詢條件不會走索引,而where c1 > 10000-1可以走索引。 -
查詢列表數(shù)據(jù)不分頁,對于列表展現(xiàn)數(shù)據(jù),在數(shù)據(jù)量特別大的情況,一次性返回所有數(shù)據(jù)一般不具有實際的業(yè)務(wù)意義,此時應(yīng)通過limit offset進(jìn)行分頁,這樣有機會利用到索引掃描和排序,降低全表掃描的影響,同時也能減小返回數(shù)據(jù)包過大的負(fù)擔(dān)。 -
count (*) 時order by做無用排序由于列表展現(xiàn)與列表查數(shù)經(jīng)常成對兒出現(xiàn),有可能在復(fù)用列表展現(xiàn)的sql時在查數(shù)時也加入了排序操作,此時無論是否加上排序操作,得到的最終結(jié)果是一致的,但加上排序時大大增加了得到目標(biāo)結(jié)果的代價。 -
跨表進(jìn)行分組、排序,當(dāng)涉及到跨表分組、排序時,需要把兩個表的結(jié)果集匯總到一起進(jìn)行排序、分組,這里的消耗是非常大的,此時可以考慮去冗余部分字段,使分組、排序操作在一個表中完成,這樣能夠利用到索引,起到優(yōu)化效果。 -
慢sql對數(shù)據(jù)庫cpu消耗極大,嚴(yán)重時甚至?xí)礄C


索引優(yōu)化
子查詢優(yōu)化
實際的業(yè)務(wù)sql中,往往要涉及多個表進(jìn)行關(guān)聯(lián)查詢,這里既可以使用子查詢,也可以使用表連接,一般我們認(rèn)為子查詢方式的查詢層次較多,且關(guān)聯(lián)時的結(jié)果集較大,所以性能會差一些,執(zhí)行計劃器會對子查詢進(jìn)行邏輯優(yōu)化,將子查詢上提到父查詢中,與父查詢合并,過濾出較小的結(jié)果集再進(jìn)行關(guān)聯(lián)
子查詢類型是否支持優(yōu)化
any,some,exists,not exists基本支持,in部分情況支持,all,not in不支持.
寫法優(yōu)化
-
連接優(yōu)化裁剪 利用left join消除無用的連接,當(dāng)連表查詢時,只輸出左表字段,且連接條件的右表字段具有唯一性,那么可以使用left join消除部分連接
-
union all 代替 unionunion all不會進(jìn)行去重,union會去重,如果在明確查詢結(jié)果不存在重復(fù)數(shù)據(jù)時,union all的效率會高很多
-
避免使用select * 首先,如果select的字段被索引字段覆蓋,那么可能就會使用僅索引掃描,這種掃描性能會更高一些。其次,select字段的多少直接影響著結(jié)果集數(shù)據(jù)包的大小,對于前臺來說數(shù)據(jù)包越大,返回就越慢。還有對于一些復(fù)雜的查詢,比如涉及子查詢、連接、分組、聚合、排序等,過程中如果select字段過多,那么大概率會影響sql整體使用的work_mem,超出work_mem時則需使用磁盤,性能更低。
創(chuàng)建合適的索引
單表索引不應(yīng)該超過5個。復(fù)合索引字段數(shù)量一定不可超過4個。復(fù)合索引字段數(shù)量多主要有以下2個影響:1.字段數(shù)量越多,對查詢的要求越苛刻。查詢必須按照索引的命中規(guī)則來安排。2.字段數(shù)量越多,索引的體積越大。數(shù)據(jù)的扇出度(單次IO能得到的數(shù)據(jù)條數(shù))越低,IO效率也越低,而且索引被更新的概率越大,由于二級索引大部分情況下是隨機更新,所以會引起B(yǎng)+樹的平衡維護操作。
-
ESR原則
E 即Equal。查詢中等于條件的字段優(yōu)先考慮。S 即Sort,排序字段其次考慮。R 即Range,范圍查詢字段最后考慮 在經(jīng)常用于查詢的字段上創(chuàng)建索引,在經(jīng)常用于連接的字段上創(chuàng)建索引,在經(jīng)常用于排序的字段上創(chuàng)建索引
-
在選擇性好的字段上創(chuàng)建索引
低基數(shù)字段不應(yīng)該建立單獨的索引。(該字段的不重復(fù)值個數(shù)低于總行數(shù)的 10%的稱為低基數(shù)字段)。比如性別字段,只有男、女兩種取值,認(rèn)為選擇性不好,不建議創(chuàng)建索引分布不均勻的字段不應(yīng)該建立索引。如果一定需要,應(yīng)該避免使用分布較高的值作為查詢條件。分布不均勻指不同的列值占總體的比例差異很大(通常超過50%),即某一個列值或者某幾個列值在整個數(shù)據(jù)集合中占比非常大。例如幼兒園學(xué)生年齡分段:年齡段占比3~5:95% ,6~8:3%, 9~12:1%,12~20:1%,20以上0%
-
適當(dāng)創(chuàng)建聯(lián)合索引,并將選擇性好的字段作為第一個字段 -
對于頻繁更新的表避免創(chuàng)建過多索引
高頻更新字段不應(yīng)該建立索引,高頻更新字段,會以更新頻率同步去更新索引。這會引起索引的刪除、插入操作。頻繁地刪除索引上的數(shù)據(jù),索引頁會造成大量的空洞,進(jìn)而引發(fā)樹的平衡維護。
-
不建議在小表上創(chuàng)建索引 -
一定不可存在冗余索引。
例如 同時存在 idx_A_B(A,B) ,idx_A(A) 兩個索引
-
索引單行長度不應(yīng)該 超過200字節(jié)
按數(shù)據(jù)頁16K計算,我們期望單個索引頁至少應(yīng)該存納70個索引。默認(rèn)的innodb的頁上會留有1/16的空閑區(qū)域。這個空閑區(qū)域的主要作用是減少樹的平衡操作。
InnoDB是如何存儲和查詢數(shù)據(jù)的
MySQL把數(shù)據(jù)存儲和查詢操作抽象成了存儲引擎,不同的存儲引擎,對數(shù)據(jù)的存儲和讀取方式各不相同。MySQL支持多種存儲引擎,并且可以以表為粒度設(shè)置存儲引擎。因為支持事務(wù),我們最常使用的是InnoDB。
-
雖然數(shù)據(jù)保存在磁盤中,但其處理是在內(nèi)存中進(jìn)行的。為了減少磁盤隨機讀取次數(shù),InnoDB采用頁而不是行的粒度來保存數(shù)據(jù),即數(shù)據(jù)被分成若干頁,以頁為單位保存在磁盤中。InnoDB的頁大小,一般是16KB。 -
各個數(shù)據(jù)頁組成一個雙向鏈表
每個數(shù)據(jù)頁中的記錄按照主鍵順序組成單向鏈表;每一個數(shù)據(jù)頁中有一個頁目錄,方便按照主鍵查詢記錄。
-
數(shù)據(jù)頁的結(jié)構(gòu)如下:
頁目錄通過槽把記錄分成不同的小組,每個小組有若干條記錄。如圖所示,記錄中最前面的小方塊中的數(shù)字,代表的是當(dāng)前分組的記錄條數(shù),最小和最大的槽指向2個特殊的偽記錄。有了槽之后,我們按照主鍵搜索頁中記錄時,就可以采用二分法快速搜索,無需從最小記錄開始遍歷整個頁中的記錄鏈表。
如果要搜索主鍵(PK)=15的記錄:先二分得出槽中間位是(0+6)/2=3,看到其指向的記錄是12<15,所以需要從#3槽后繼續(xù)搜索記錄;再使用二分搜索出#3槽和#6槽的中間位是(3+6)/2=4.5取整4,#4槽對應(yīng)的記錄是16>15,所以記錄一定在#4槽中;再從#3槽指向的12號記錄開始向下搜索3次,定位到15號記錄。
-
B+樹 
B+樹的特點包括:1.最底層的節(jié)點叫做葉子節(jié)點,用來存放數(shù)據(jù);2.其他上層節(jié)點叫作非葉子節(jié)點,僅用來存放目錄項,作為索引;3.非葉子節(jié)點分為不同層次,通過分層來降低每一層的搜索量;4.所有節(jié)點按照索引鍵大小排序,構(gòu)成一個雙向鏈表,加速范圍查找。因此,InnoDB使用B+樹,既可以保存實際數(shù)據(jù),也可以加速數(shù)據(jù)搜索,這就是聚簇索引。如果把上圖葉子節(jié)點下面方塊中的省略號看作實際數(shù)據(jù)的話,那么它就是聚簇索引的示意圖。由于數(shù)據(jù)在物理上只會保存一份,所以包含實際數(shù)據(jù)的聚簇索引只能有一個,這也就是為什么主鍵只能有一個的原因。
-
InnoDB會自動使用主鍵
(唯一定義一條記錄的單個或多個字段)作為聚簇索引的索引鍵(如果沒有主鍵,就選擇第一個不包含NULL值的唯一列)。上圖方框中的數(shù)字代表了索引鍵的值,對聚簇索引而言一般就是主鍵。
-
我們再看看B+樹如何實現(xiàn)快速查找主鍵。
比如,我們要搜索PK=4的數(shù)據(jù),通過根節(jié)點中的索引可以知道數(shù)據(jù)在第一個記錄指向的2號頁中,通過2號頁的索引又可以知道數(shù)據(jù)在5號頁,5號頁就是實際的數(shù)據(jù)頁,然后再通過二分法查找頁目錄馬上可以找到記錄的指針。
-
為了實現(xiàn)非主鍵字段的快速搜索,就引出了二級索引,也叫作非聚簇索引、輔助索引。
二級索引,也是利用的B+樹的數(shù)據(jù)結(jié)構(gòu),如下圖所示:
這次二級索引的葉子節(jié)點中保存的不是實際數(shù)據(jù),而是主鍵,獲得主鍵值后去聚簇索引中獲得數(shù)據(jù)行。這個過程就叫作回表。比如有個索引是針對用戶名字段創(chuàng)建的,索引記錄上面方塊中的字母是用戶名,按照順序形成鏈表。如果我們要搜索用戶名為b的數(shù)據(jù),經(jīng)過兩次定位可以得出在#5數(shù)據(jù)頁中,查出所有的主鍵為7和6,再拿著這兩個主鍵繼續(xù)使用聚簇索引進(jìn)行兩次回表得到完整數(shù)據(jù)。
?? 總結(jié)
以上就是索引的創(chuàng)建及使用時注意事項,最后匯總了一些索引優(yōu)化方式,并分析InnoDB是如何存儲和查詢數(shù)據(jù)的。下一期將用2個真實案例分析索引在實際生產(chǎn)中的注意事項。
——End——




