測試使用版本爲11g
若未特別說明,所說索引皆爲B*索引
創建語法
https://docs.oracle.com/cd/E11882_01/server.112/e41084/statements_5012.htm#SQLRF01209
簡易版
CREATE UNIUQE | BITMAP INDEX <schema>.<index_name>
ON <schema>.<table_name>
(<column_name> | <expression> ASC | DESC,
<column_name> | <expression> ASC | DESC,...)
TABLESPACE <tablespace_name>
STORAGE <storage_settings>
LOGGING | NOLOGGING
COMPUTE STATISTICS
NOCOMPRESS | COMPRESS<nn>
NOSORT | REVERSE
PARTITION | GLOBAL PARTITION<partition_setting>
應該考慮使用B*索引的情況
- 僅使用索引返回很小一部分行
- 僅使用索引來回答整個查詢(無關查詢行數的多少)
對第一點的解釋
下面來看這樣一個簡化的例子,假設我們通過索引讀取一個瘦表,而且要讀取表中的20%的行。若這個表中有100 000行,其中的20%就是20000行。如果行大小約爲80字節,在一個塊大小爲8kb的數據庫中,每個塊上則有大約100行。這說明,這個表大約有1000個塊。瞭解了這些情況,計算起來就非常容易了。我們要通過索引讀取20000行,這說明,大約是20000個table access by rowid操作。爲此要處理20000個塊來執行這個操作。不過整個表只有1000個塊,最終平均會把每個塊讀取和處理20次。即使把行的大小提高一個數量級,達到每行800字節,仍需要對每個塊執行2次。在這種情況下,全表掃描就比索引高效得多,因爲每個塊只會掃描一次。如果查詢使用這個索引來訪問數據,效率都不會高,除非對於800字節的行,至訪問5%的數據(算下來需要訪問5000個塊)。如果是80字節的行,則訪問的數據應當只佔更小的百分比(小於0.5%)。
摘自編程藝術p-443
oracle查詢中影響效率的一個最基本的因素就是讀取塊(block)的數量。一個塊通常包含多個行。在通過索引對錶進行查詢時,它的過程大概是:每在索引中找到一條數據,都會通過索引中記錄的地址去對應數據塊中查找數據,這就會導致通過索引來查詢大量數據有可能比全表掃描所讀取的塊還要多,這會花費更多的時間。
雖然優化器會從統計信息中知道所獲取數據的大致行數並決定是否最終使用索引來進行查詢,但是由於索引本身會耗費額外的資源(本身佔用的存儲空間、dml產生的重建索引等),所以創建索引前需要謹慎考慮一下。
對第二點的解釋
用一個例子來解釋一下,加入表與索引是這樣創建的
create table test0407(a number, b number);
create index idx_test0407 on test0407(a);
如果進行這樣一個查詢
select t1.a from test0407 t1 where t1.a > 5;
我只用了索引字段進行了搜索,並且也只用索引字段來返回查詢結果,這種情況下,即使返回了較多數據,索引的效率也是非常高的,因爲oracle會把這條索引當成一個較瘦的、帶有索引特性的表來處理。
如果查看一下執行計劃,也會發現select t1.a from test0407 t1 where t1.a > 5;
相比於select t1.a, t1.b from test0407 t1 where t1.a > 5;
少了這樣一條執行計劃 TABLE ACCESS BY INDEX ROWID,也就是常說的回表,這是很影響效率以操作。
索引與null
一個容易被忽視的特性:B索引(除了聚簇B索引以外)不會存儲完全爲null的條目。
create table test0410(x number, y number);
create unique index idx_test0410 on test0410(x, y);
insert into test0410 values(1, 1);
insert into test0410 values(1, null);
insert into test0410 values(null, 1);
insert into test0410 values(null, null);
commit;
這時如果執行這樣的語句
insert into test0410 values(1, null);
這毫無疑問會報錯,因爲我們創建的是唯一索引,這樣的數據違反了唯一性規則。但是像下面這樣的卻是可以插入的
insert into test0410 values(null, null);
這就是B索引(除了聚簇B索引以外)不會存儲完全爲null的條目
如果希望完全爲null的數據被唯一性所阻止,這會成爲一個隱藏的bug。
這也是另一個常見問題的原因,x is null 的查詢爲什麼無法使用索引,這是顯而易見的,因爲根據上面的例子,x爲空的數據有可能根本就沒有存儲在索引中,如果強行使用索引,將會得到錯誤的結果。
那麼要怎樣才能對x is null
這樣的條件使用索引呢?只要將任意一列設爲非空即可。
delete from test0410 t1 where t1.y is null;
alter table test0410 modify y not null;
外鍵是否應該加索引
需要,原因如下
死鎖
外鍵未加索引是引發死鎖的一大原因。更新父表的主鍵或者刪除父表中的一行都會導致子表產生一個全表鎖。
創建測試數據
create table test0414_1(a int primary key, b int);
create table test0414_2(a references test0414_1(a));
insert into test0414_1 values(1, 2);
insert into test0414_1 values(2, 3);
insert into test0414_1 values(3, 3);
insert into test0414_1 values(4, 3);
insert into test0414_2 values(1);
先說一種不會發生阻塞的情況
如果我在會話一執行(未提交)
update test0414_1 t1 set t1.a = 5 where t1.a = 2;
那麼在會話二執行下面的語句會發生阻塞
insert into test0414_2 values(2) ;
insert into test0414_2 values(5) ;
可以看得出oracle還是很智能的,它可以從未提交的數據中得知父表要將2改爲5,因此,在未對其進行提交時,子表無法確定父表會擁有這兩個數據中的哪一個,操作會被阻塞
(將上面的語句全部回滾)
但是如果向下面一樣執行就會出現意想不到問題
在會話一執行這句(注意執行完後不要提交)
insert into test0414_2 values(2) ;
在另一個會話中執行這一句時會出現阻塞
update test0414_1 t1 set t1.a = 8 where t1.a = 7;
這兩個會話所修改的數據是完全不一樣的,但是依然會發生阻塞,這是因爲,當對父表的主鍵進行修改時,會對子表加一個表級鎖。這時如果子表有未提交的語句,則會產生阻塞,在特定的情況下甚至會產生死鎖。
與上述情況相似,如果對父表執行delete語句,哪怕這個delete語句不會刪除任何數據,也會對子表加表級鎖。
delete from test0414_1 t1 where 1 = 2;
解決這種問題方法有兩個
-
把外鍵刪掉
-
子表外鍵列加索引
create index idx_test0414_2 on test0414_2(a);
這是在會話一執行以下語句後(不提交)
insert into test0414_2 values(2) ;
在會話二執行下面語句時不會阻塞
update test0414_1 t1 set t1.a = 8 where t1.a = 7;
delete from test0414_1 t1 where 1 = 2;
執行下面語句會產生阻塞
update test0414_1 t1 set t1.a = 5 where t1.a = 2;
oracle又可以自動判斷多個會話之間操作的數據是否有關聯了。
級聯刪除
對子表進行級聯刪除時,對於刪除父表的每一行,都會對子表產生一個全表掃描,如果對父表刪除的行比較多,同時子表又比較大,那麼這個查詢可能會變得非常非常慢。
父表與子表的關聯查詢
select *
from test0415_1 t1, test0415_2 t2
where t1.a = t2.a
and t1.b = 2
類似這種查詢,如果子表外鍵列未加索引,效率也會很低。(測試方法待補充)
小結
如果同時滿足下述情況
- 未刪除父表中的行
- 未更新父表主鍵
- 不會從父表聯結到子表,或者更一般的講,外鍵列不支持子表的一個重要的訪問路徑,而且你在謂詞中沒有使用這些外鍵列從子表中選擇數據
那麼就不必因爲外鍵的原因去對子表的外鍵列添加索引。
未使用索引的原因
概括來說,要麼是語句本身無法使用索引,要麼是使用索引會產生錯誤結果,要麼就是使用索引的效率更低。下面來分情況詳細討論。
情況1
對於一個B*索引(x, y),沒有使用索引的最前列,如使用and y = 1
這樣的條件來查詢,很可能會導致不會使用索引。但是有兩個例外
- 只使用索引數據來應答整個查詢
- x列的值很少,優化器可能會選用跳躍式掃描(skip scan)
對於第二點,用形象一點的例子來表達就是,假設x只有兩種值,那麼and y = 1
就會相當於
select... and x = 1 and y = 1
union all
select... and x = 2 and y = 1
情況2
對於select count(*) from tab
這樣的查詢,如果索引列允許爲null,則可能不會使用索引。這是因爲完全null的列不會存在索引中,使用索引會產生錯誤的結果。
情況3
select * from tab where f(indexed_column) = value
indexed_column列建有索引,但是沒有使用,這是因爲我們是對indexed_column列建的索引,而不是f(indexed_column)。改進方法:創建f(indexed_column)函數索引,或者想辦法將表達式全部移到等號右邊。
情況4
注意隱式轉換!
在select語句中,oracle會把字段的數據類型隱式轉換爲變量的數據類型。例如,val的數據類型爲varchar2,那麼
and val = 1
實際在執行中會被轉換爲
and to_number(val) = 1
因此不會使用索引。(具體原因建情況3)
順帶提一句,oracle不會將1轉換成字符串(to_char(1))是因爲具體轉換方式取決於NLS參數,不同環境下轉換規則不同
情況5
如果使用索引,效率會變得更低。在統計信息正確的前提下,oracle的CBO幾乎總會選擇最優的執行計劃來執行語句,如果判斷使用索引會使效率變低,則不會使用索引。具體原因在文章開頭已經舉了一個例子。
情況6
在沒有正確統計信息時,oracle可能會選擇錯誤的執行計劃,導致本應使用索引的地方沒有使用索引。舉個例子,在兩次信息收集之間,表的內容發生了特定的變化,比如數據量激增,將可能導致CBO判斷上的失誤。解決辦法是修改數據庫信息收集的規則,或者直接對指定表進行手動信息收集。
附個百度搜索關鍵字:oracle 表統計信息收集