一、 問題背景
給一個業務表online建索引時遇到了ORA-01450 maximum key length (3215) exceeded報錯,看字面意思是字段太長了,檢查表字段類型發現基本都是nvarchar2(2000),有些字段(例如unit)明顯是不需要這麼長的,表的設計有問題,聯繫開發按實際需求改短後能正常創建。
奇怪的是表的id字段類型也是nvarchar2(2000),但上面是有索引的,好奇爲啥這個字段就能建上,以及爲啥maximum key length是3215。
二、 報錯分析
根據網上文章,9i之後每個index key最大隻能爲block size的80%。理論上8k的塊可以創建最大長度爲8096*80%約爲6400左右長度的index。但是,online創建(包括rebuild)的過程中會生成一箇中間的IOT表,用來記錄創建過程中的變化。IOT表的限制比較嚴格,導致8k的block size最大長度只能有3215。當然普通創建的索引也是有限制的:ORA-01450: maximum key length (6398) exceeded。
按上面的建測試表和索引,發現的確online創建報錯,而普通創建可以成功。因爲nvarchar2(2000)字段最大可能長度是4000,創建索引時並不會看實際字段長度,直接按的最大長度。
建聯合索引會報錯,因爲nvarchar2(2000)+nvarchar2(2000)字段最大可能長度是8000
再測試varchar2(2000)類型,發現普通創建和online都能成功,因爲varchar2(2000)字段最大可能長度是2000
關於索引key最大長度,在文檔 ID 136158.1中給出了不同BLOCK SIZE的限制,你會發現8K BLOCK SIZE的maximum key length 文檔中寫的是3218而不是我們遇到的3215。這可能是由於文檔是針對8i的版本,在新版本中這個值變成了3215。
ORA-01450 maximum key length (758) exceeded ->(2K Block)
ORA-01450 maximum key length (1578) exceeded ->(4K block)
ORA-01450 maximum key length (3218) exceeded ->(8K Block)
ORA-01450 maximum key length (6498) exceeded ->(16K Block)
報錯中限制的KEY SIZE包含:索引的長度+存儲索引長度的空間(2字節) + ROWID (6字節)+存儲ROWID長度佔用空間 (1字節),所以真正能夠存放的列數據的長度只有3218-2-6-1=3209,新版本應該是3215-2-6-1=3206。
- 這裏的3209並不是實際的數據的長度,而是定義的列的長度。就像前面的例子,定義NVARCHAR2(2000),即使裏面只存放了一個字符,創建索引時也會報這個錯。ORACLE擔心以後裏面存放的數據萬一超過了,索引那邊沒辦法交代,所以乾脆從源頭上掐死。
- 那能不能先定義一個小列,創建完索引後再把這個列的值改大?不行,你會遇到報錯:ORA-01404: ALTER COLUMN will make an index too large,告訴你加大列長度的命令可能會導致索引太大
- 這裏面還有一個陷阱,就是你索引創建好了,一直使用也沒問題,但是當你ONLINE REBUILD的時候卻發現他的KEY SIZE超過限制了,導致索引只能不ONLINE的REBUILD,這對於24*7的系統而且必須REBUILD的情況比較痛苦。
常用的數據類型的KEY長度計算如下:
日期類型的長度是7;字符類型就是字段定義時候的長度;數字類型是22(數字類型的長度=精度/2+1),如果是負數,那麼長度要再加1;如果是函數索引,那就要按照函數索引的返回值來進行計算。
爲什麼ONLINE創建只能使用不到BLOCK SIZE一半的空間?
ORACLE的管理手冊中指明瞭索引的大小不能大於BLOCK_SIZE的一半,然後這一半的空間去掉ORACLE自己的PCTFREE、INITRANS以及BLOCK HEADER等等預留空間,實際可以使用的空間比一半要小很多。
當ONLINE創建一個索引,ORACLE爲這個表的變化創建一箇中間表,創建好後,ORACLE用表數據的一致性拷貝去創建一個新的索引,然後再把變化的記錄拷貝到新創建的索引中,最後更新數據字典,刪除臨時段並刪除這個中間表。這個過程將會鎖表兩次(ROW SHARE MODE)。一次是開始創建中間表時,另一次是結束時刪除中間表。
中間表是一個名字類似SYS_JOURNAL_NNNNN的IOT表,其中的NNNNN是ONLINE REBUILD的索引的OBJECT_ID。因爲IOT表的限制只能使用BLOCKSIZE的40%左右,而且這個IOT表的KEY就是索引中使用的KEY並加上ROWID的值,所以只有ONLINE創建或者REBUILD索引的時候會碰到這個問題。
下面來做一個演示,先創建一個表:
create table test(a varchar2(10),b varchar2(11),c varchar2(12),d number(10),e varchar2(13));
然後打開跟蹤並ONLINE的創建索引:
create index idx_test on test(a,b,c,d,e) online;
關閉跟蹤並查看TRACE文件,可以發現如下語句:
create table "SYS"."SYS_JOURNAL_74346" (C0 VARCHAR2(10), C1 VARCHAR2(11), C2 VARCHAR2(12), C3 NUMBER(10,0), C4
VARCHAR2(13), opcode char(1), partno number, rid rowid, primary key( C0, C1, C2, C3, C4 , rid )) organization
index TABLESPACE "SYSTEM"
其中前面的C0,C1等列就是索引的KEY值,索引由幾列組成,臨時IOT表也會對應創建,後面三列是不變的,根據字面意思推測應該是操作的代碼(增加、刪除、更新) 、分區號(分區索引用到)、ROWID。而主鍵是由所有的KEY值和ROWID列組成,這也正好跟前面的長篇大論相吻合。至於IOT爲啥只能用一半,有些說是爲了B*TREE的分裂,有些說是ORACLE老版本的小問題,結果爲了兼容一直沒改。
四、解決方法
查詢文檔,這個問題其實有挺多繞過的方法,整理學習一下。
1. 改短字段後創建
對於表設計明顯不合理的情況,這是比較合理的方法。
查看字段實際最大長度
select max ( length ( text_column ) ) mx_char_length,
max ( lengthb ( text_column ) ) mx_byte_length
from some_table;
改短字段長度
alter table some_table modify text_column nvarchar2(100);
如果確實不能改短,下面有一些workaround,但它們都有各自的限制,需要根據實際選擇。
2. 不使用online創建
對於前面的例子,nvarchar2(2000)的字段可以用這個方法繞過最大長度限制。但也就像之前寫的,這個長度沒辦法創建聯合索引,以後也不能使用online rebuild,對於7*24小時的系統,如果是大表可能難以接受。
3. 使用更大的block size存儲索引
將索引存放在單獨的表空間,並將表空間block size設爲16k或32k,當然也需要先設置好DB_
n
K_CACHE_SIZE
參數。這種方法打破了DB的標準化,可能會使運維管理更加複雜。
alter system set DB_32K_CACHE_SIZE=256M;
create tablespace tblsp_32k_blocks datafile 'tblsp_32k_blocks' size 1m blocksize 32768;
create index text_index on some_table(text_column) tablespace tblsp_32k_blocks;
4. 創建基於STANDARD_HASH函數的索引
standard_hash函數會返回一個固定長度的值,在等值查詢時能用到該索引。
create index text_index on some_table(standard_hash(text_column));
select * from some_table where text_column = 'this';
----------------------------------------------------------
| Id | Operation | Name |
----------------------------------------------------------
| 0 | SELECT STATEMENT | |
|* 1 | TABLE ACCESS BY INDEX ROWID BATCHED| SOME_TABLE |
|* 2 | INDEX RANGE SCAN | TEXT_INDEX |
----------------------------------------------------------
但範圍查詢和like查詢都用不到
select * from some_table where text_column >= 't' and text_column<'u';
----------------------------------------
| Id | Operation | Name |
----------------------------------------
| 0 | SELECT STATEMENT | |
|* 1 | TABLE ACCESS FULL| SOME_TABLE |
----------------------------------------
select * from some_table where text_column like 'this%';
----------------------------------------
| Id | Operation | Name |
----------------------------------------
| 0 | SELECT STATEMENT | |
|* 1 | TABLE ACCESS FULL| SOME_TABLE |
----------------------------------------
5. 創建基於substr函數的索引
對於範圍查詢,可以使用基於substr函數的索引(like依然用不到),但是它可能會導致查詢效率較低。
create index text_substr_index on some_table(substr(text_column,1,10));
select * from some_table where text_column = 'this';
-----------------------------------------------------------------
| Id | Operation | Name |
-----------------------------------------------------------------
| 0 | SELECT STATEMENT | |
|* 1 | TABLE ACCESS BY INDEX ROWID BATCHED| SOME_TABLE |
|* 2 | INDEX RANGE SCAN | TEXT_SUBSTR_INDEX |
-----------------------------------------------------------------
select * from some_table where text_column >= 't' and text_column < 'u';
-----------------------------------------------------------------
| Id | Operation | Name |
-----------------------------------------------------------------
| 0 | SELECT STATEMENT | |
| 1 | TABLE ACCESS BY INDEX ROWID BATCHED| SOME_TABLE |
| 2 | INDEX RANGE SCAN | TEXT_SUBSTR_INDEX |
-----------------------------------------------------------------
select * from some_table where text_column like 'this%';
----------------------------------------
| Id | Operation | Name |
----------------------------------------
| 0 | SELECT STATEMENT | |
| 1 | TABLE ACCESS FULL| SOME_TABLE |
----------------------------------------
6. 定義virtual列
虛擬列其實就是對列進行運算或者在列上使用函數,oracle在運行時纔會計算該列的值。我們可以用standard_hash函數建虛擬列,然後對該列建普通索引。虛擬列相比函數索引有以下好處:
- 優化器可獲得虛擬列的統計信息
- 能夠看到索引值,更易於理解
alter table some_table add text_hash varchar2(40 char) as (standard_hash(text_column));
create index vc_text_hash_index on some_table (text_hash);
7. 使用全文索引(Oracle Text Index)
創建時需要指定indextype子句,查詢時使用contains操作符
create index oracle_text_index on some_table(text_column) indextype is ctxsys.context;
select * from some_table where contains (text_column,'value')>0;
參考
https://blog.csdn.net/tnndwdl/article/details/78452967
https://blogs.oracle.com/sql/how-to-fix-ora-01450-maximum-key-length-6398-exceeded-errors
ORA-01450 and Maximum Key Length - How it is Calculated (文檔 ID 136158.1)