索引是一種讓你快速找到數(shù)據(jù)的數(shù)據(jù)結(jié)構(gòu)
?人都是有慣性的,對于使用數(shù)據(jù)庫的人來說已經(jīng)習(xí)慣于使用索引,大多數(shù)人都只知道我們可以用索引來提高數(shù)據(jù)訪問的性能。對于索引是如何實現(xiàn)這一點的,大家可能也清楚,通過只在葉結(jié)點中存儲索引數(shù)據(jù)的B+TREE來快速定位到數(shù)據(jù)所在的位置,再從表中獲得數(shù)據(jù)就可以實現(xiàn)比全表掃描更快的獲得數(shù)據(jù)的目的了。
不過隨著慣性,我們一直都在把我們的業(yè)務(wù)模型與B樹去做融合,盡可能讓我們的應(yīng)用訪問數(shù)據(jù)的模式更符合B樹的結(jié)構(gòu),從而獲得更好的性能。比如說控制不會在索引中出現(xiàn),那么我們給創(chuàng)建一個(col,1)這樣的索引,讓索引中也能夠包含col的空值記錄。比如說我們的索引字段的獨立值數(shù)很少的時候,會發(fā)現(xiàn)使用索引可能還不如全表掃描快,使用位圖索引又容易出現(xiàn)并發(fā)寫入時鎖放大的性能問題。如果我們只訪問幾個占表的記錄數(shù)中較少的值的時候,我們發(fā)現(xiàn)可以用B樹索引來提升性能。只不過我們創(chuàng)建的索引包含了對所有數(shù)據(jù)的索引值對于應(yīng)用來說是沒有任何用途的。
實際上在使用索引的時候,我們已經(jīng)忘記了使用索引的目的就是為了更快速的找到數(shù)據(jù),索引并不是只能是B樹或者位圖,索引是一種輔助性的數(shù)據(jù)結(jié)構(gòu),它其實是可以被定義成任何樣式的。比如僅僅是為了解決你的某條SQL中幾張表的復(fù)雜關(guān)聯(lián)關(guān)系,或者僅僅為了某個應(yīng)用所需要的快速查找數(shù)據(jù)的需求。你可以自己設(shè)計一種最符合應(yīng)用特點的索引結(jié)構(gòu),來實現(xiàn)對此類應(yīng)用的加速。
實際上有一類對表關(guān)聯(lián)查詢特別有效的索引,這種索引出現(xiàn)了幾十年了,可能我們還從來沒有使用過,那就是連接位圖索引BMJ。這種索引在OLAP系統(tǒng)中可能用的更多一些,在OLTP系統(tǒng)中,因為會影響DML的性能而很少使用。不過如果你的數(shù)據(jù)是寫入后較少改動的,并且并發(fā)寫入不存在明顯瓶頸的時候,BMJ在OLTP中使用也是安全的。BMJ是一種專門用于表連接的索引,其性能高于一般的HASH JOIN或者NL。
如果你在使用PostgreSQL數(shù)據(jù)庫,那么你會對索引設(shè)計感到既興奮又迷茫。PG數(shù)據(jù)庫的索引類型太豐富了。哪怕我們排除一些用于全文檢索,JSON的索引類。我們也能發(fā)現(xiàn)很多有趣的索引類型。
比如說我們上面的這個例子,每次我們只是從上億條數(shù)據(jù)中找出幾百條特殊的數(shù)據(jù),那么在PG里就可以使用部分索引(Partial Index),這種索引我們可以看作是一種特殊的函數(shù)索引,其存儲結(jié)構(gòu)也是B-TREE的。部分索引也稱為過濾索引,它只覆蓋表數(shù)據(jù)的一個子集,是一個帶有 WHERE 子句的索引。部分索引有助于加快查詢速度,同時減少索引的大小,這些索引需要更少的存儲空間,它們更易于維護,掃描速度更快。比如在一張表上,STATUS=001的數(shù)據(jù)是我們要SELECT出來進行處理的,處理后STATUS就變成了002,因此這張表上的STATUS字段值域是傾斜的,001的記錄可能只有幾百條,而002的記錄有上千萬條。在PostgreSQL中,我們可以通過Partial索引獲得更好的效果。
這個索引中只有status=’001’的數(shù)據(jù),因此索引十分小。訪問的效率也十分高。再復(fù)雜一些,我們可以創(chuàng)建類似這樣的PartialIndex。
如果我們的where 條件是status in (’001’,’002’),那么這個索引就能夠發(fā)揮作用了。
另外一種比較有趣的PG索引類型是覆蓋索引。在做Oracle數(shù)據(jù)庫 的時候,對于回表數(shù)據(jù)量較大的查詢,如果不回表訪問那么可以大大提升性能。這種情況下Oracle有兩種方法來解決,一種是創(chuàng)建一個包含所有返回字段的索引,使執(zhí)行計劃變成INDEX ONLY SCAN,從而提升性能。不過如果要返回的字段數(shù)量很多,那么這個索引的冗余部分就很多,甚至有時候我們只能使用索引組織表(IOT)來替代索引了。實際上可能在SQL中用于定位數(shù)據(jù)的出現(xiàn)在WHERE條件中的字段數(shù)量并不多,大多數(shù)是為了避免回表而增加的額外字段,是不需要排序的。因此PG數(shù)據(jù)庫中出現(xiàn)了一種被稱為覆蓋索引。覆蓋索引(Covering index)是PostgreSQL 11開始引入的一種新的索引。這是一種特殊的復(fù)合索引,允許索引中存儲附加的非索引字段。比如:
在沒有覆蓋索引之前,我們需要創(chuàng)建一個(col2,col1)的復(fù)合索引,從而讓這條SQL使用Index Only Scan來提高執(zhí)行效率,減少對表的訪問。出現(xiàn)覆蓋索引后,可以創(chuàng)建一個(col2) include (col1)的索引。和傳統(tǒng)的復(fù)合索引不同的是,附加字段不需要參與B-TREE的構(gòu)建,讓索引的效率更高。
實際上在紛繁復(fù)雜的應(yīng)用場景中,PG提供的索引種類可能還無法覆蓋一些特殊的場景。不過也不用怕,PG提供了一個十分簡單的方法,讓你擴展自己的索引類型,從而來解決你應(yīng)用中很難解決的性能問題。只要你能夠想到,索引是一種讓你更快找到你所需要的數(shù)據(jù)的附加數(shù)據(jù)結(jié)構(gòu)。你可以使用標準的,通用的B樹、位圖等結(jié)構(gòu),也可以使用只有你的應(yīng)用能理解的數(shù)據(jù)結(jié)構(gòu)來查找到你所需要的數(shù)據(jù),因此如果使用的是PG數(shù)據(jù)庫,那么你很幸運,你可以自己去定義一種新的索引來適配你的應(yīng)用。
也許我在寫這篇文章的時候,也有一些其他的數(shù)據(jù)庫也具備了這個能力,如果這樣,那就對了,索引本來就是這樣的,索引并不是你平時理解的那種死板的數(shù)據(jù)結(jié)構(gòu)。也并不是你的應(yīng)用必須去適合B樹索引,索引也可以去適應(yīng)你的應(yīng)用。希望我們的基于PG開源代碼開發(fā)的國產(chǎn)數(shù)據(jù)庫,千萬要保留這個接口,有時候它真的能救命。?