2025年6月25日 (水)

VECTOR INDEX はどこ?、見積もりサイズだとそれなりのサイズだったのに... の謎を探るべく、我々は洞窟の奥へ向かった!(完結編)

Previously on Mac De Oracle
前回はVECTOR INDEX はどこ?、見積もりサイズだとそれなりのサイズだったのに... の謎を探るべく、我々は洞窟の奥へ向かった!(後編) でした。
謎は、ほぼ解決したかな?と、思ったのですが、モヤモヤは晴れずwww
ということで、完結編的な何か。という位置付けのまとめを書いておくことにしました。(なお、23.5以降VECTOR INDEX関連も機能追加があったり。。。まあ、追いかけるの大変です)

注)ちょい古いリリースなので最新リリースとは挙動など異なる点がある可能性がある点、ご了承ください

SCOTT@localhost:1521/freepdb1> select banner_full from v$version;

BANNER_FULL
--------------------------------------------------------------------------------
Oracle Database 23ai Free Release 23.0.0.0.0 - Develop, Learn, and Run for Free
Version 23.4.0.24.05


インメモリー近傍グラフ・ベクトル索引の特徴まとめ (的なもの)

インメモリー近傍グラフ・ベクトル索引は、ディクショナリービューからは、OBJECTでもSEGMENTも無い扱いになっているよう見えてしまう。
しかし、vecsys.vector$indexにリストされるOBJECT_IDは、*_OBJECTSと共通で、vecsys.vector$indexにはあるが、*_OBJECTS上はIDが欠番のように見える(後述)。無いけどあるみたいな不思議な扱いになっている。(今のところ)
なぜそうしたのだろう?....


  • Oraclerならお馴染みの *_OBJECTS や *_SEGMENTS には現れないが、in-memory onlyのオブジェクトとして専用のプール上にポピューレートされた場合に存在している in-memory indexである。
    (ここテストにでますよ。嘘w)

    また、in-memory vector indexの補助表及び関連する索引はリストされる(セグメントサイズは、deferred segment creationであるものも含むため状況次第)
    メモリー上とは言っても他と同じ扱いで、属性としてin-memory onlyのような列を追加すればよかったのでは?と、個人的には思っていたりする。。。


参考)
(上記の時事検証 23ai 23.4で行ったため、最新のリリースとは差異がある可能性があり、気づいた差分について随時補足追記してあります)
作成したin-memory vector index ( SEARCH_DATA_HNSW_IX ) の IDX_OBJN(列名の短縮系も*_OBJECTSで利用されているOBJECT_IDとな異なり、IDX_OBJN ( N は Numberの略と思われる) という、いわゆるシノニムになってしまっている(この点も状況をわかりにくくしている原因でしょうね。Oraclerじゃない方が作ったような違和感を感じる)

SCOTT@localhost:1521/freepdb1> select idx_objn, idx_name from vecsys.vector$index;

IDX_OBJN IDX_NAME
---------- -------------------------------
80113 SEARCH_DATA_HNSW_IX

で、この IDX_OBJN = OBJECT_ID ( *_OBJECTS で使われている )、いきなりシノニムになってる! と仮説をたて UESR_OBJECTSを検索してみると、存在しない! (何ぃ〜〜〜っ!)
ちがうのか?(*_OBJECTSにはリストされないだけで、採番元は同じなので、はやり仮説は正しかったw。後述)

SCOTT@localhost:1521/freepdb1> select object_name,object_type from user_objects where object_id = 80113;

レコードが選択されませんでした。


念の為、data_object_idでも検索してみる(こちらにもないですよねーw)

SCOTT@localhost:1521/freepdb1> select object_name,object_type from user_objects where data_object_id = 80113;

レコードが選択されませんでした。


USER_SEGMENTSをVECTOR INDEX NAMEで検索してみるがやはり無い。ストレージ上のデータをそのままインメモリー化しているわけではないいうことだろうと想像するが。ではメモリー上の情報は。。。

SCOTT@localhost:1521/freepdb1> select segment_name from user_segments where segment_name = 'SEARCH_DATA_HNSW_IX';

レコードが選択されませんでした。


VECTOR INDEXとしては、 *_INDEXES から確認は可能であるが”!。。。

SCOTT@localhost:1521/freepdb1> select index_name,index_type,table_name from user_indexes where table_name='SEARCH_DATA';

INDEX_NAME INDEX_TYPE TABLE_NAME
------------------------------ --------------------------- ------------------------------
SYS_IL0000078074C00009$$ LOB SEARCH_DATA
SEARCH_DATA_HNSW_IX VECTOR SEARCH_DATA


in-memory vector indexはSGA上の専用メモリープール上に造られる。
では、v$vector_memory_poolからポピュレートされていることを確認。

SCOTT@localhost:1521/freepdb1> l
1 SELECT
2 pool
3 , alloc_bytes
4 , used_bytes
5 , populate_status
6 FROM
7* v$vector_memory_pool
SCOTT@localhost:1521/freepdb1> /

POOL ALLOC_BYTES USED_BYTES POPULATE_STATUS
-------------------------- ----------- ---------- --------------------------
1MB POOL 369098752 236978176 DONE
64KB POOL 150994944 2686976 DONE
IM POOL METADATA 16777216 16777216 DONE


ちなみに、vector indexがポピュレートされる前の状態は以下。1MB/64KB Poolが消費されたことがわかります。つまり、236,978,176 + 2,686,976 = 239,665,152 = 228.6MB

POOL                       ALLOC_BYTES USED_BYTES POPULATE_STATUS
-------------------------- ----------- ---------- --------------------------
1MB POOL 369098752 0 DONE
64KB POOL 150994944 0 DONE
IM POOL METADATA 16777216 16777216 DONE


in-memory vector indexのメモリセグメントの詳細を確認するには v$vector_mem_segments_detail を参照する。vecsys.vector$indexのidx_objnにリストされていた 80113 が 239MB 程度使用されていることがわかる。
(このサイズ、覚えておいてくださいね。後で使います。 239,534,080 = 228.4MB)

SCOTT@localhost:1521/freepdb1> l
1 SELECT
2 obj
3 , membytes
4 FROM
5* v$vector_mem_segments_detail
SCOTT@localhost:1521/freepdb1> /

OBJ MEMBYTES
---------- ----------
0 131072
80113 239534080

in-memory vector index (HNSW)の補助表と関連索引の object_id を見てみましょう。
vecsys.vector$indexでは、idx_objn となっていた列の値と連続していること点が確認できます。つまり、object_id を採番しているシーケンスを利用している。。。in-memory vector index (HNSW) も *_OBJECTSに含まれている扱い。。。だが、*_objectsには含まれていません!!!!

CREATE VECTOR INDEXで作成された、in-memory vector index( SEARCH_DATA_HNSW_IX )の idx_objn = 80113 , その後に作成される補助表及び索引の *_objects.object_id は、80114 以降が採番されています。
かつ、80113 という object_id は、*_objects にはリストされず。
妙ですよね。内部的には *_objectsに含まれていてもおかしくないと思われる扱いにであるかのように見えますよね。うーーむ。

SCOTT@localhost:1521/freepdb1> l
1 WITH
2 vector_idx_auxiliary_tables
3 AS (
4 SELECT
5 idx_name AS vector_index_name
6 , REPLACE(aux_table_name,'"','') AS aux_table_name
7 FROM
8 (
9 SELECT
10 vvi.idx_name AS idx_name
11 ,vvi.idx_auxiliary_tables.rowid_vid_map_name
12 ,vvi.idx_auxiliary_tables.shared_journal_transaction_commits_name
13 ,vvi.idx_auxiliary_tables.shared_journal_change_log_name
14 FROM
15 vecsys.vector$index vvi
16 ) objs
17 UNPIVOT (
18 aux_table_name FOR related_obj_name IN
19 (
20 rowid_vid_map_name
21 , shared_journal_transaction_commits_name
22 , shared_journal_change_log_name
23 )
24 )
25 )
26 SELECT
27 object_id
28 ,object_name
29 ,object_type
30 FROM
31 user_objects
32 WHERE
33 object_name in (
34 SELECT
35 aux_table_name AS segment_name
36 FROM
37 vector_idx_auxiliary_tables
38 WHERE
39 vector_idx_auxiliary_tables.vector_index_name = 'SEARCH_DATA_HNSW_IX'
40 UNION ALL
41 SELECT
42 index_name AS segment_name
43 FROM
44 user_indexes
45 WHERE
46 EXISTS
47 (
48 SELECT
49 1
50 FROM
51 vector_idx_auxiliary_tables
52 WHERE
53 vector_idx_auxiliary_tables.aux_table_name = user_indexes.table_name
54 AND vector_idx_auxiliary_tables.vector_index_name = 'SEARCH_DATA_HNSW_IX'
55 )
56* )
SCOTT@localhost:1521/freepdb1> /

OBJECT_ID OBJECT_NAME OBJECT_TYPE
---------- -------------------------------------------------------------------------------------------------------------------------------- -----------------------
80114 VECTOR$SEARCH_DATA_HNSW_IX$78074_80113_0$HNSW_ROWID_VID_MAP TABLE
80115 SYS_C0013840 INDEX
80117 VECTOR$SEARCH_DATA_HNSW_IX$78074_80113_0$HNSW_SHARED_JOURNAL_TRANSACTION_COMMITS TABLE PARTITION
80116 VECTOR$SEARCH_DATA_HNSW_IX$78074_80113_0$HNSW_SHARED_JOURNAL_TRANSACTION_COMMITS TABLE
80118 PK_XID_80113 INDEX
80120 VECTOR$SEARCH_DATA_HNSW_IX$78074_80113_0$HNSW_SHARED_JOURNAL_CHANGE_LOG TABLE PARTITION
80119 VECTOR$SEARCH_DATA_HNSW_IX$78074_80113_0$HNSW_SHARED_JOURNAL_CHANGE_LOG TABLE
80124 SYS_IL0000080119C00007$$ INDEX PARTITION
80123 SYS_IL0000080119C00007$$ INDEX

9行が選択されました。

SCOTT@localhost:1521/freepdb1> l
1 select object_id, data_object_id from user_objects where object_name = 'SEARCH_DATA'
SCOTT@localhost:1521/freepdb1> /

OBJECT_ID DATA_OBJECT_ID
---------- --------------
78074 78796

ここまでで、in-memory vector indexはどこ?
って話の場所的なところは見切れたかなと思いますが、
もう一つ、explain plan for create vector index及び、dbms_space.create_index_costプロシージャでの見積もりサイズは、信じて良いのか?、よくないのか?

エラーにはなってないが、信用してよいのだろうか。。。
ちなみに、23.6以降だが、INDEX_VECTOR_MEMORY_ADVISORプシージャによりインメモリーサイズを見積もることができるようになった。とか。。。すぐには用意できないのでw 23.4で一旦確認してみる


  • CREATE VECTOR INDEXで作成されるINMEMORYの索引オブジェクトに加え、補助表とよばれる表が複数存在する。未検証だが変更をトラックするタイプの補助表は更新量によってはセグメントサイズが一時的に増加する可能性あり

変更をトラッキングするのための2つのパーティション表と関連索引に関しては変更が無い限りセグメントは作成されない。はず。(deferred segment creationになっているようなので)
つまり新規作成時にはオブジェクトとしては存在するものの、セグメントは存在しない。
ということは、残る一つの補助表(仮でMAP表と呼ぶことにする)とその索引はある程度のセグメントを保持することになるだろうと予想。
また、インメモリー索引自体はベース表のVECTOR列のサイズに依存するのではないだろうか。。。と。

とりあえず、補助表のセグメントサイズを改めて確認してみる(in-memory vector index を何度かdrop/createしていためセグメント名に含まれるobject_idが異なりますが気になさらず。。)
今回のケース 125,000行のデータでは合計で 9MB 程度です。誤差の範囲程度のサイズではあります。ではメモリ上のサイズを占めるデータのベースとなる表のセグメントサイズも今一度確認しておきましょう。

SEGMENT_NAME                                                                     SEGMENT_TYPE               MB
-------------------------------------------------------------------------------- ------------------ ----------
VECTOR$SEARCH_DATA_HNSW_IX$78074_79519_0$HNSW_ROWID_VID_MAP TABLE 3
SYS_C0013800 INDEX 6


VECTORデータ型の列以外もありますが、ベース表のセグメントサイズは、 249MB あります。。。おやおや、サイズ、近似してますよね。。

SCOTT@localhost:1521/freepdb1> r
1* select segment_name,segment_type,bytes / 1024 /1024 "MB" from user_segments where segment_name = 'SEARCH_DATA'

SEGMENT_NAME SEGMENT_TYPE MB
------------------------------ ------------------ ----------
SEARCH_DATA TABLE 248


もう少しベース表のセグメントサイズを調べてみましょう。

SCOTT@localhost:1521/freepdb1> l
1 create table search_data_without_vector_desc
2 as
3 select
4 id
5 ,primary_description
6 ,description
7 ,location_desc
8 ,district
9 ,ward
10 ,community
11 ,c_year
12* from search_data
SCOTT@localhost:1521/freepdb1> /

表が作成されました。

SCOTT@localhost:1521/freepdb1> exec dbms_stats.gather_table_stats(ownname=>'SCOTT',tabname=>upper('search_data_without_vector_desc'),cascade=>true,no_invalidate=>false);

PL/SQLプロシージャが正常に完了しました。

おお、10MBなので、VECTORデータ型の列を含むSEARCH_DATA表のVECTORデータ型部分のサイズは、238MB 程度みたいですね。むむむ。。。

SCOTT@localhost:1521/freepdb1> r
1 select
2 segment_name
3 ,segment_type
4 ,bytes / 1024 /1024 "MB"
5 from
6 user_segments
7 where
8* segment_name = upper('search_data_without_vector_desc')

SEGMENT_NAME SEGMENT_TYPE MB
------------------------------- ------------------ ----------
SEARCH_DATA_WITHOUT_VECTOR_DESC TABLE 10


in-memory vector index (HNSW)のメモリー消費サイズは、 228.4MB
(これに、補助表のMAP表と索引のセグメントサイズ 9MB を加算すると、237.4MB おおおおおおおおーっ)

ベース表のVECTORデータ型だけのセグメントサイズは、238MB 

で、ですねぇ〜、EXPLAIN PLAN FOR CREATE VECTOR INDEX ...の見積もりサイズと、DBMS_SPACE.CREATE_INDEX_COSTプロシージャによる見積もり。。。VECTOR INDEXに対応できているという情報は未確認のままですが。。。

EXPLAIN PLAN FOR CREATE VECTOR INDEX ...
- estimated index size: 293M bytes


DBMS_SPACE.CREATE_INDEX_COSTプロシージャによるセグメントサイズ見積もり...
Segment Size (MB) :280

という結果になりました。偶然近い値だったの??。。ということでもなさそうな気はします。。

ただし、ストレージのセグメントサイズではなくて、VECTOR MEMORY AREAで消費される索引サイズの見積もりとして....

みなさんは、どう思います????


おまけの疑問というか違和感(これまでも少しだけ触れたが、妙な違和感がある)


  • これまでのIN-MEMORYとはことなり、VECTOR INDEX (HNSW)の場合、実行計画のオペレーション ( VECTOR INDEX HNSW SCAN ) には、INMEMORYというワードが含まれず勘違いしやすい気がするのに、
    なぜ? INMEMORYを含めなかったのだろう。また、*_OBJECTS/*_SEGMENTSを使わず、OBJECT_IDをIDX_OBJNというシノニムまで作り、vecsys.vector$index で別管理かつ、補助表をJSONに書き込んでいる。
    SQL文面倒なんすけど。。というのとこれまでの*_OBJECTSの存在と、vecsys.vector$indexのidx_objnの分離(*_objectsのobject_idの採番と同じなのに。。)といい、これまでと違う空気感が強い。

少し調べたのですが、
以下メッセージからも読み取れるように INMEMORY というワードは、Oracle Database上、In-Memory Column Store Architectureをイメージさせるものではないことは確かようだ、
では、なぜ、INMEMORYというキーワードを実行計画のオペレーション名に含めなかった理由は少々理解しにくい。INMEMORY VECTOR INDEX HNSW SCANのほうがイメージしやすいと思うのは私だけだろうか。。。
このフワフワしてるところが違和感の原因でもあるな。。。

ORA-51815
INMEMORY NEIGHBOR GRAPH HNSW vector index snapshot is too old.
Oracle Database 23ai / Error Messages Oracle Database / Database Error Messages / ORA-51815

最後に、vector_index_neighbor_graph_reload を restartにすると再ロードされるよ(scope=bothでspfileにも同時に反映できるよ!)


  • 23.4までは初期化パラメータ vector_index_neighbor_graph_reload のデフォルトが OFF であった。
    インスタンス再起動で VECTOR INDEX (HNSW) がインメモリー上に再作成されない!!!w(なんだとーーー!)
    23.6以降では、デフォルトが変更になり、 restart がデフォルトになっています。ここ試験にでるよ(知らんけどw

おまけに、ADB-Sだと、V$VECTOR_INDEXなんてビューが提供されていたり。。。なぜ全てで提供しないのだろう。。。23.4だからって話でもなさそうで。。


  • 関連するディクショナリービューなどが整備されてないように見える(今後整備されるような気はする)。対応しきれていない部分はJSON化して回避しているようにも見える(やめて〜〜w)

ということで、最終的な私の理解のビジュアル化w 2025/6/25時点、かつ、Oracle Database 23ai 23.4 を元にした理解は以下の通り(まだ理解不足な箇所はあるかもしれない)

Create_vector_index_and_aux_tables_etc

なお、
DBMS_VECTOR.GET_INDEX_STATUSプロシージャが返すステータスをみると興味深い内容が載っている
Oracle Database 23 / Oracle AI Vector Search User's Guide / Vector Index Status, Checkpoint, and Advisor Procedures / GET_INDEX_STATUS

CREATE VECTOR INDEX (HNSW)に関する状態を取得するプロシージャが返すステータスには以下のように記載されている。In-memory vector index (HNSW)のDDL発行からvector memory poolにロードされ、multi-layered HNSW graphができあがるまでのステータスがわかります!!!


そしてここでも、なんでプロシージャ必要だったのだろう。。。。と、言う素朴な疑問が!!!

Oracle Databaseには昔から、V$SESSION_LONGOPSってビューがあって。。。。
長時間操作のステータスがわかるようになっているのだが.....

ますます、なぜ、これまでの機能やビューを有効に再利用していないのだろう。。。という点が気になる...。が、現状は見切れたかな、と。
(もう一つのVector Indexのタイプと更新トラッキングと反映、いくつかの実行計画パターンの確認を除く)


  • HNSW Index Initialization
    Initialization phase for the HNSW vector index creation

  • HNSW Index Auxiliary Tables Creation
    Creation of the internal auxiliary tables for the HNSW Neighbor Graph vector index

  • HNSW Index Graph Allocation
    Allocation of memory from the vector memory pool for the HNSW graph

  • HNSW Index Loading Vectors
    Loading of the base table vectors into the vector pool memory

  • HNSW Index Graph Construction
    Creation of the multi-layered HNSW graph with the previously loaded vectors

  • HNSW Index Creation Completed
    HNSW vector index creation finished


では、また。

Enjoy SQL! and AI vector search!



Oracle Database 23ai freeで試すVector Search - ONNXモデル準備編
Oracle Database 23ai freeで試すVector Search - データ準備編
実行計画は, SQL文のレントゲン写真だ! No.66 / AI Vector Search - VECTOR INDEX HNSW SCAN
VECTOR INDEX はどこ?、見積もりサイズだとそれなりのサイズだったのに... の謎を探るべく、我々は洞窟の奥へ向かった!(前編)
VECTOR INDEX はどこ?、見積もりサイズだとそれなりのサイズだったのに... の謎を探るべく、我々は洞窟の奥へ向かった!(後編)


| | | コメント (0)

2025年6月18日 (水)

VECTOR INDEX はどこ?、見積もりサイズだとそれなりのサイズだったのに... の謎を探るべく、我々は洞窟の奥へ向かった!(後編)

Previously on Mac De Oracle
前回は、VECTOR INDEX はどこ?、見積もりサイズだとそれなりのサイズだったのに... の謎を探るべく、我々は洞窟の奥へ向かった!(前編) でした。
今日はその後編です。

10046トレースよりVECTOR INDEX(HNSW)作成では前回の通り、補助表3つ(うち2つは変更をトラックするためなので作成直後は空)、それらの表に加え、各1つの索引が作成されるということがハッキリしました。
ただ、VECTOR INDEXの補助表はvecsys.vector$indexという表にJSONとして保持されている点と、USER_INDEXESから確認できるVECTOR INDEX自体は実態を持たない、さらに、Oraclerにはお馴染みの*_INDEXESなどから単純に取得できないことも見えてきました(今後もっと便利になることを期待したいですね)

(2025/6/18追記)
これ、HNSWってインメモリーと言っても、元ネタは永続化されているわけで、それをメモリープールにポピュレートしてるって理解(間違ってないとは思うけど、違ってたらコメントもらえるとありがたいです)なので、後編ではそのあたりを見ておこうかと。。。
インメモリー近傍グラフ・ベクトル索引

(作成される補助表の名称にVECTOR INDEXのベース表の名称が入っているようなので中間一致で検索していますが、これだとノイズも多くなるので検索キツイですよね。索引名はシステム生成名称なのでベース表の名称で中間一致検索はできません!。このケースに限ればなんとか拾えてるけどという感じではありますね。。。ということで後半ではJSONから引っこ抜いてなんとかするパズルもwやってますw)

SCOTT@localhost:1521/freepdb1> select index_name , table_name, index_type from user_indexes where table_name like '%SEARCH_DATA%';

INDEX_NAME TABLE_NAME INDEX_TYPE
------------------------------ -------------------------------------------------------------------------------- ---------------------------
SYS_IL0000079525C00007$$ VECTOR$SEARCH_DATA_HNSW_IX$78074_79519_0$HNSW_SHARED_JOURNAL_CHANGE_LOG LOB
SYS_IL0000078074C00009$$ SEARCH_DATA LOB
SEARCH_DATA_HNSW_IX SEARCH_DATA VECTOR
SYS_C0013800 VECTOR$SEARCH_DATA_HNSW_IX$78074_79519_0$HNSW_ROWID_VID_MAP NORMAL
PK_XID_79519 VECTOR$SEARCH_DATA_HNSW_IX$78074_79519_0$HNSW_SHARED_JOURNAL_TRANSACTION_COMMITS NORMAL

SCOTT@localhost:1521/freepdb1> r
1 SELECT
2 idx_name
3 ,(
4 SELECT
5 table_name
6 FROM
7 user_tables
8 WHERE
9 user_tables.table_name = (
10 SELECT
11 object_name
12 FROM
13 user_objects
14 WHERE
15 user_objects.object_id = vecsys.vector$index.idx_base_table_objn
16 )
17 ) AS tab_name
18 ,JSON_SERIALIZE(
19 idx_auxiliary_tables
20 RETURNING VARCHAR2 PRETTY
21 ) AS idx_auxiliary_tables
22 FROM
23 vecsys.vector$index
24 WHERE
25* idx_name = 'SEARCH_DATA_HNSW_IX'

IDX_NAME TAB_NAME IDX_AUXILIARY_TABLES
------------------------------ ------------------------------ ----------------------------------------------------------------------------------------------------------------------------------
SEARCH_DATA_HNSW_IX SEARCH_DATA {
"rowid_vid_map_objn" : 79520,
"shared_journal_transaction_commits_objn" : 79522,
"shared_journal_change_log_objn" : 79525,
"rowid_vid_map_name" : "VECTOR$SEARCH_DATA_HNSW_IX$78074_79519_0$HNSW_ROWID_VID_MAP",
"shared_journal_transaction_commits_name" : "VECTOR$SEARCH_DATA_HNSW_IX$78074_79519_0$HNSW_SHARED_JOURNAL_TRANSACTION_COMMITS",
"shared_journal_change_log_name" : "VECTOR$SEARCH_DATA_HNSW_IX$78074_79519_0$HNSW_SHARED_JOURNAL_CHANGE_LOG"
}

 

これまでの調査の結果、VECTOR INEDEX (HNSW) であるSEARCH_DATA_HNSW_IXはオブジェクトでもなく、セグメントもない(当然、シノニムにもなれない。仮にも索引ですから)
VECTOR INDEX (HNSW)の索引セグメントのサイズは、作成直後のIDX_AUXILIARY_TABLESと、索引があればそれら索引のセグメントサイズの合計ということになりますよね!!!
(やっと見えてきたw、なんなんだこれ)

 

こんな感じでJSONからIDX_AUXILIARY_TABLESにある関連表のオブジェクトID、または、テーブル名を取り出しUSER_INDEXESから索引も合わせて取得したうえで、それぞれのセグメントサイズの合計を取得すれば物理的なサイズは見えますよね。。。。(めんどくさいw)

SCOTT@localhost:1521/freepdb1> l
1 WITH
2 vector_idx_auxiliary_tables
3 AS (
4 SELECT
5 idx_name AS vector_index_name
6 , REPLACE(aux_table_name,'"','') AS aux_table_name
7 FROM
8 (
9 SELECT
10 vvi.idx_name AS idx_name
11 ,vvi.idx_auxiliary_tables.rowid_vid_map_name
12 ,vvi.idx_auxiliary_tables.shared_journal_transaction_commits_name
13 ,vvi.idx_auxiliary_tables.shared_journal_change_log_name
14 FROM
15 vecsys.vector$index vvi
16 ) objs
17 UNPIVOT (
18 aux_table_name FOR related_obj_name IN
19 (
20 rowid_vid_map_name
21 , shared_journal_transaction_commits_name
22 , shared_journal_change_log_name
23 )
24 )
25 )
26 SELECT
27 aux_table_name AS segment_name
28 FROM
29 vector_idx_auxiliary_tables
30 WHERE
31 vector_idx_auxiliary_tables.vector_index_name = 'SEARCH_DATA_HNSW_IX'
32 UNION ALL
33 SELECT
34 index_name AS segment_name
35 FROM
36 user_indexes
37 WHERE
38 EXISTS
39 (
40 SELECT
41 1
42 FROM
43 vector_idx_auxiliary_tables
44 WHERE
45 vector_idx_auxiliary_tables.aux_table_name = user_indexes.table_name
46 AND vector_idx_auxiliary_tables.vector_index_name = 'SEARCH_DATA_HNSW_IX'
47 )
48*
SCOTT@localhost:1521/freepdb1> /

SEGMENT_NAME
---------------------------------------------------------------------------------------
VECTOR$SEARCH_DATA_HNSW_IX$78074_79519_0$HNSW_ROWID_VID_MAP
VECTOR$SEARCH_DATA_HNSW_IX$78074_79519_0$HNSW_SHARED_JOURNAL_TRANSACTION_COMMITS
VECTOR$SEARCH_DATA_HNSW_IX$78074_79519_0$HNSW_SHARED_JOURNAL_CHANGE_LOG
SYS_C0013800
PK_XID_79519
SYS_IL0000079525C00007$$

6行が選択されました。

経過: 00:00:00.16

 

では、あらためて、EXPLAIN PLAN FOR CREATE VECTOR INDEX...の見積もりサイズと、実際のサイズを比較してみましょう!

SCOTT@localhost:1521/freepdb1> l
1 explain plan for
2 CREATE VECTOR INDEX search_data_hnsw_ix ON search_data ( vector_desc )
3 ORGANIZATION
4 INMEMORY NEIGHBOR GRAPH
5 DISTANCE COSINE
6* WITH TARGET ACCURACY 90
SCOTT@localhost:1521/freepdb1> /

解析されました。

経過: 00:00:00.02
SCOTT@localhost:1521/freepdb1> @?/rdbms/admin/utlxpls

PLAN_TABLE_OUTPUT
-----------------------------------------------------------------------------------------------
Plan hash value: 2727344110

----------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
----------------------------------------------------------------------------------------------
| 0 | CREATE INDEX STATEMENT | | 125K| 188M| 14689 (1)| 00:00:01 |
| 1 | VECTOR INDEX BUILD | SEARCH_DATA_HNSW_IX | | | | |
----------------------------------------------------------------------------------------------

Note
-----
- estimated index size: 293M bytes



なんどかコメントしてますが、SEARCH_DATA_HNSW_IXという索引セグメントは無く!、複数の補助表と索引群から構成されている。それらの作成直後のサイズは....

SCOTT@localhost:1521/freepdb1> r
1 WITH
2 vector_idx_auxiliary_tables
3 AS (
4 SELECT
5 idx_name AS vector_index_name
6 , REPLACE(aux_table_name,'"','') AS aux_table_name
7 FROM
8 (
9 SELECT
10 vvi.idx_name AS idx_name
11 ,vvi.idx_auxiliary_tables.rowid_vid_map_name
12 ,vvi.idx_auxiliary_tables.shared_journal_transaction_commits_name
13 ,vvi.idx_auxiliary_tables.shared_journal_change_log_name
14 FROM
15 vecsys.vector$index vvi
16 ) objs
17 UNPIVOT (
18 aux_table_name FOR related_obj_name IN
19 (
20 rowid_vid_map_name
21 , shared_journal_transaction_commits_name
22 , shared_journal_change_log_name
23 )
24 )
25 )
26 SELECT
27 user_segments.segment_name
28 , user_segments.segment_type
29 , user_segments.bytes / 1024 / 1024 AS "MB"
30 FROM
31 user_segments
32 INNER JOIN
33 (
34 SELECT
35 aux_table_name AS segment_name
36 FROM
37 vector_idx_auxiliary_tables
38 WHERE
39 vector_idx_auxiliary_tables.vector_index_name = 'SEARCH_DATA_HNSW_IX'
40 UNION ALL
41 SELECT
42 index_name AS segment_name
43 FROM
44 user_indexes
45 WHERE
46 EXISTS
47 (
48 SELECT
49 1
50 FROM
51 vector_idx_auxiliary_tables
52 WHERE
53 vector_idx_auxiliary_tables.aux_table_name = user_indexes.table_name
54 AND vector_idx_auxiliary_tables.vector_index_name = 'SEARCH_DATA_HNSW_IX'
55 )
56 ) vector_idx_aux_objects
57 ON
58 vector_idx_aux_objects.segment_name = user_segments.segment_name
59*

SEGMENT_NAME SEGMENT_TYPE MB
-------------------------------------------------------------------------------- ------------------ ----------
VECTOR$SEARCH_DATA_HNSW_IX$78074_79519_0$HNSW_ROWID_VID_MAP TABLE 3
SYS_C0013800 INDEX 6

 

誤差が大きいですよね。正しく見積もれてない可能性が高いように思いますが、少なく出るよりマシ程度かもしれません。今後のチューニングに期待。。。。。というところでしょうか。。。

SCOTT@localhost:1521/freepdb1> l
1 explain plan for
2 CREATE VECTOR INDEX search_data_hnsw_ix ON search_data ( vector_desc )
3 ORGANIZATION
4 INMEMORY NEIGHBOR GRAPH
5 DISTANCE COSINE
6* WITH TARGET ACCURACY 90
SCOTT@localhost:1521/freepdb1> /

 

だと、

- estimated index size: 293M bytes
ですが、
実際には以下の通り、 9M Bytes程度でした。
(もしかして、VECTOR INDEX (HNSW)ってINMEMORYだからメモリーサイズ? んなことないか、いや、あったりしてw と思ったりし始めているw  23ai 23.6では、DBMS_VECTOR.INDEX_VECTOR_MEMORY_ADVISORプロシージャによりメモリーサイズを見積もれるようになっていたりする。。怪しい。2025/6/19追記)

SEGMENT_NAME                                                                     SEGMENT_TYPE               MB
-------------------------------------------------------------------------------- ------------------ ----------
VECTOR$SEARCH_DATA_HNSW_IX$78074_79519_0$HNSW_ROWID_VID_MAP TABLE 3
SYS_C0013800 INDEX 6

 

Bツリー索引の見積り(19cで確かめたときは、これほど大きな差にはなっていませんでした)に比べると精度は低そうですよね。と言うより、VECTOR INDEXには対応できてないのかもしれませんね....

おまけで、
DBMS_SPACEパッケージの索引サイズ見積もりも試しておきましょう。。。。。やはり、explain plan for文同様にVECTOR INDEXのセグメントサイズ見積もりには対応してなさそう。(小さくでるわけではないのですがw)

SCOTT@localhost:1521/freepdb1> set serveroutput on
SCOTT@localhost:1521/freepdb1> l
1 DECLARE
2 used_bytes NUMBER;
3 segment_bytes NUMBER;
4 BEGIN
5 DBMS_SPACE.CREATE_INDEX_COST (
6 ddl=> 'CREATE VECTOR INDEX search_data_hnsw_ix ON search_data ( vector_desc )'
7 || ' ORGANIZATION'
8 || ' INMEMORY NEIGHBOR GRAPH'
9 || ' DISTANCE COSINE'
10 || ' WITH TARGET ACCURACY 90'
11 , used_bytes => used_bytes
12 , alloc_bytes => segment_bytes
13 );
14 DBMS_OUTPUT.PUT_LINE('Segment Size (MB) :'||segment_bytes/1024/1024);
15 DBMS_OUTPUT.PUT_LINE('Index data Size (MB) :'||used_bytes/1024/1024);
16* END;
SCOTT@localhost:1521/freepdb1> /
Segment Size (MB) :280
Index data Size (MB) :188.94672393798828125

PL/SQLプロシージャが正常に完了しました。

 

Enjoy SQL, PL/SQL and VECTOR SEARCH!

VECTOR INDEXの謎はもう少し探る必要があるようだ...w (後編のおまけにつづくw)

めちゃ快適な気温の場所なので、
東京は猛暑と聞いて恐怖している

では、また。

 


参考)
今回利用したコードの一部

WITH
vector_idx_auxiliary_tables
AS (
SELECT
idx_name AS vector_index_name
, REPLACE(aux_table_name,'"','') AS aux_table_name
FROM
(
SELECT
vvi.idx_name AS idx_name
,vvi.idx_auxiliary_tables.rowid_vid_map_name
,vvi.idx_auxiliary_tables.shared_journal_transaction_commits_name
,vvi.idx_auxiliary_tables.shared_journal_change_log_name
FROM
vecsys.vector$index vvi
) objs
UNPIVOT (
aux_table_name FOR related_obj_name IN
(
rowid_vid_map_name
, shared_journal_transaction_commits_name
, shared_journal_change_log_name
)
)
)
SELECT
aux_table_name AS segment_name
FROM
vector_idx_auxiliary_tables
WHERE
vector_idx_auxiliary_tables.vector_index_name = 'SEARCH_DATA_HNSW_IX'
UNION ALL
SELECT
index_name AS segment_name
FROM
user_indexes
WHERE
EXISTS
(
SELECT
1
FROM
vector_idx_auxiliary_tables
WHERE
vector_idx_auxiliary_tables.aux_table_name = user_indexes.table_name
AND vector_idx_auxiliary_tables.vector_index_name = 'SEARCH_DATA_HNSW_IX'
)
;
WITH
vector_idx_auxiliary_tables
AS (
SELECT
idx_name AS vector_index_name
, REPLACE(aux_table_name,'"','') AS aux_table_name
FROM
(
SELECT
vvi.idx_name AS idx_name
,vvi.idx_auxiliary_tables.rowid_vid_map_name
,vvi.idx_auxiliary_tables.shared_journal_transaction_commits_name
,vvi.idx_auxiliary_tables.shared_journal_change_log_name
FROM
vecsys.vector$index vvi
) objs
UNPIVOT (
aux_table_name FOR related_obj_name IN
(
rowid_vid_map_name
, shared_journal_transaction_commits_name
, shared_journal_change_log_name
)
)
)
SELECT
user_segments.segment_name
, user_segments.segment_type
, user_segments.bytes / 1024 / 1024 AS "MB"
FROM
user_segments
INNER JOIN
(
SELECT
aux_table_name AS segment_name
FROM
vector_idx_auxiliary_tables
WHERE
vector_idx_auxiliary_tables.vector_index_name = 'SEARCH_DATA_HNSW_IX'
UNION ALL
SELECT
index_name AS segment_name
FROM
user_indexes
WHERE
EXISTS
(
SELECT
1
FROM
vector_idx_auxiliary_tables
WHERE
vector_idx_auxiliary_tables.aux_table_name = user_indexes.table_name
AND vector_idx_auxiliary_tables.vector_index_name = 'SEARCH_DATA_HNSW_IX'
)
) vector_idx_aux_objects
ON
vector_idx_aux_objects.segment_name = user_segments.segment_name
;
set serveroutput on
DECLARE
used_bytes NUMBER;
segment_bytes NUMBER;
BEGIN
DBMS_SPACE.CREATE_INDEX_COST (
ddl=> 'CREATE VECTOR INDEX search_data_hnsw_ix ON search_data ( vector_desc )'
|| ' ORGANIZATION'
|| ' INMEMORY NEIGHBOR GRAPH'
|| ' DISTANCE COSINE'
|| ' WITH TARGET ACCURACY 90'
, used_bytes => used_bytes
, alloc_bytes => segment_bytes
);
DBMS_OUTPUT.PUT_LINE('Segment Size (MB) :'||segment_bytes/1024/1024);
DBMS_OUTPUT.PUT_LINE('Index data Size (MB) :'||used_bytes/1024/1024);
END;
/

 


Oracle Database 23ai freeで試すVector Search - ONNXモデル準備編
Oracle Database 23ai freeで試すVector Search - データ準備編
実行計画は, SQL文のレントゲン写真だ! No.66 / AI Vector Search - VECTOR INDEX HNSW SCAN
VECTOR INDEX はどこ?、見積もりサイズだとそれなりのサイズだったのに... の謎を探るべく、我々は洞窟の奥へ向かった!(前編)

 

 

| | | コメント (0)

2025年6月17日 (火)

VECTOR INDEX はどこ?、見積もりサイズだとそれなりのサイズだったのに... の謎を探るべく、我々は洞窟の奥へ向かった!(前編)

Previously on Mac De Oracle
前回は、実行計画は, SQL文のレントゲン写真だ! No.66 / AI Vector Search - VECTOR INDEX HNSW SCANでした。
今日はそれに絡んで気づいたVECTOR INDEXの謎を追いかけてみることにします。

前回の最後の宿題というか謎の一つ目、覚えてますか?  そう、VECTOR INDEXを作成後、まあよくある、USER_INDEXESを検索して作成されたVECTOR INDEXの名称を確認して、EXPLAIN PLAN FORで見積もった索引サイズとどの程度乖離しているのだろうと、手癖でUSER_SEGMENTSをアクセスしてみると、なな、なーーーーーんと、ない、無い!、なーーーい!。 (INMEMORYとはいっても元ネタがあってINMEMORY化されるわけで仕組み的に理解しにくいのと、これまでのディクショナリービューの使い方と異なっていてものすごく追いにくい)
なんで? という謎。

SCOTT@localhost:1521/freepdb1> select index_name,index_type,table_name from user_indexes where table_name='SEARCH_DATA';

INDEX_NAME INDEX_TYPE TABLE_NAME
------------------------------ --------------------------- ------------------------------
SYS_IL0000078074C00009$$ LOB SEARCH_DATA
SEARCH_DATA_HNSW_IX VECTOR SEARCH_DATA

経過: 00:00:00.05
SCOTT@localhost:1521/freepdb1> select segment_name from user_segments where segment_name = 'SEARCH_DATA_HNSW_IX';

レコードが選択されませんでした。

 

その後、アッ! 
いつもと違うビューがあったことを思い出し、覗いてみると、
作成したVECTOR INDEX (HNSW)の名称とともに、IDX_AUXILIARY_TABLES列(JSONデータ型)に、なにやらそれらしい値があるじゃありませんか!!!
そーなんだーーーーっ。VECTOR INDEXの名称は、VECTOR INDEXの補助表の集まりをまとめる為だけの存在?!!!
というところまででした。

SCOTT@localhost:1521/freepdb1> r
1* SELECT * FROM vecsys.vector$index

IDX_OBJN IDX_OBJD IDX_OWNER# IDX_NAME IDX_BASE_TABLE_OBJN IDX_BASE_TABLE_OWNER# IDX_PARAMS IDX_AUXILIARY_TABLES
---------- ---------- ---------- ------------------------------ ------------------- --------------------- ------------------------------ ------------------------------
78797 153 SEARCH_DATA_HNSW_IX 78074 153 {"type":"HNSW","num_neighbors" {"rowid_vid_map_objn":78798,"s
:32,"efConstruction":200,"dist hared_journal_transaction_comm
ance":"COSINE","accuracy":90," its_objn":78800,"shared_journa
vector_type":"FLOAT32","vector l_change_log_objn":78803,"rowi
_dimension":384,"degree_of_par d_vid_map_name":"VECTOR$SEARCH
allelism":1,"pdb_id":3,"indexe _DATA_HNSW_IX$78074_78797_0$HN
d_col":"VECTOR_DESC"} SW_ROWID_VID_MAP","shared_jour
nal_transaction_commits_name":
"VECTOR$SEARCH_DATA_HNSW_IX$78
074_78797_0$HNSW_SHARED_JOURNA
L_TRANSACTION_COMMITS","shared
_journal_change_log_name":"VEC
TOR$SEARCH_DATA_HNSW_IX$78074_
78797_0$HNSW_SHARED_JOURNAL_CH
ANGE_LOG"}

SCOTT@localhost:1521/freepdb1> r
1 SELECT
2 JSON_SERIALIZE(idx_params RETURNING VARCHAR2 PRETTY) AS "INDEX PARAM"
3 ,JSON_SERIALIZE(idx_auxiliary_tables RETURNING VARCHAR2 PRETTY) AS "INDEX AUX"
4* FROM vecsys.vector$index

INDEX PARAM INDEX AUX
-------------------------------------------------- --------------------------------------------------------------------------------------------------------------------------------------------
{ {
"type" : "HNSW", rowid_vid_map_objn" : 78798,
"num_neighbors" : 32, "shared_journal_transaction_commits_objn" : 78800,
"efConstruction" : 200, "shared_journal_change_log_objn" : 78803,
"distance" : "COSINE", "rowid_vid_map_name" : "VECTOR$SEARCH_DATA_HNSW_IX$78074_78797_0$HNSW_ROWID_VID_MAP",
"accuracy" : 90, "shared_journal_transaction_commits_name" : "VECTOR$SEARCH_DATA_HNSW_IX$78074_78797_0$HNSW_SHARED_JOURNAL_TRANSACTION_COMMITS",
"vector_type" : "FLOAT32", "shared_journal_change_log_name" : "VECTOR$SEARCH_DATA_HNSW_IX$78074_78797_0$HNSW_SHARED_JOURNAL_CHANGE_LOG"
"vector_dimension" : 384, }
"degree_of_parallelism" : 1,
"pdb_id" : 3,
"indexed_col" : "VECTOR_DESC"
}

 

一旦ここまでの情報を整理すると

VECTOR INDEX(この例ではHNSW)の実態は、複数の補助表から構成されており、それらをまとめているのが 作成時に指定したVECTOR INDEXの名称。
オブジェクトとしては存在しているが、それ自体は、セグメントを持たない!!!(IN MEMORY展開される索引だからとはいっても。。なにかしらあるのでは。。?)

現時点の23aiには、使い勝手の良いディクショナリービューは提供されていない。。。。まじか!

その代わりに、vecsys.vector$indexディクショナリー表(ビューではない)から詳細を追うことができる。(vecsysスキーマ)

さらに、補助表を見つけるためには、該当ディクショナリー表のIDX_AUXILIARY_TABLES列に格納されているJSONの*_objnや、補助表のオブジェクトID、*_nameから探れ!、と。
ようするに、このJSONに対象となるVECTOR INDEXとその実態の依存関係が入っている!!!!!
(ちなみに、表しかないが、おそらく、関連索引もありそうだ、というかあるだろうな。補助表の索引は補助表をキーにしてUSER_INDEXESから探すしかないだろうけども。)

ここまで見てきたところで、
めんどくせーーーーーーーーーーーーーっ! なぜ、依存関係をJSONに突っ込んだの?。。。。という顔をしているところwwwww

そしてもう一つの謎、EXPLAIN PLAN FOR CREATE VECTOR INDEX ...foo bar ..として見積もられたVECTOR INDEXの見積もりサイズって、こいつら補助表含めた合計値? 
なのか?  。。。だよなw 精度的に微妙な気がしなくも無い

全部かき集めて、合計して、見積もりサイズとの差分を見てみよう!
B*Treeの索引見積もりって、結構いい感じのサイズを弾き出してくれるわけだが。。。果たして。。。こいつは、どうなんだ? SEARCH_DATA_HNSW_IXって実態持ってないし。。。

20250613-142549

 

では、もっと探ってみなければw

 

 

そのまえに、CREATE VECTOR INDEXの謎を探るべくw
10046トレースで洞窟の奥に潜入することにします! ww (わからなくなったら、これしかない!)

SCOTT@localhost:1521/freepdb1> desc search_data
名前 NULL? 型
----------------------------------------- -------- ----------------------------
ID NUMBER
PRIMARY_DESCRIPTION VARCHAR2(40)
DESCRIPTION VARCHAR2(100)
LOCATION_DESC VARCHAR2(100)
DISTRICT VARCHAR2(30)
WARD NUMBER
COMMUNITY VARCHAR2(30)
C_YEAR NUMBER
VECTOR_DESC VECTOR(*, *)

SCOTT@localhost:1521/freepdb1> select count(*) from search_data

COUNT(*)
----------
125000

 

作成済みVECTOR INDEX (HNSW)を削除しておきます。

SCOTT@localhost:1521/freepdb1> drop index search_data_hnsw_ix;

索引が削除されました。

経過: 00:00:01.77

SCOTT@localhost:1521/freepdb1> select index_name , table_name, index_type from user_indexes where table_name like '%SEARCH_DATA%';

INDEX_NAME TABLE_NAME INDEX_TYPE
------------------------------ ------------------------------ ---------------------------
SYS_IL0000078074C00009$$ SEARCH_DATA LOB

SCOTT@localhost:1521/freepdb1> select table_name,column_name,segment_name,index_name from user_lobs where table_name like '%SEARCH_DATA%';

TABLE_NAME COLUMN_NAME SEGMENT_NAME INDEX_NAME
------------------------------ ------------------------------ ------------------------------ ------------------------------
SEARCH_DATA VECTOR_DESC SYS_LOB0000078074C00009$$ SYS_IL0000078074C00009$$

経過: 00:00:00.07

 

10046トレースの準備ができたので、トレースを設定しCREATE VECTOR INDEX文を実行して洞窟の奥へw

SCOTT@localhost:1521/freepdb1> show parameter timed_statistics

NAME TYPE VALUE
------------------------------------ ----------- ------------------------------
timed_statistics boolean TRUE

SCOTT@localhost:1521/freepdb1> show parameter max_dump_file_size

NAME TYPE VALUE
------------------------------------ ----------- ------------------------------
max_dump_file_size string 32M
SCOTT@localhost:1521/freepdb1> alter session set max_dump_file_size = unlimited;

セッションが変更されました。

経過: 00:00:00.00
SCOTT@localhost:1521/freepdb1> show parameter tracefile_identifier

NAME TYPE VALUE
------------------------------------ ----------- ------------------------------
tracefile_identifier string
SCOTT@localhost:1521/freepdb1> alter session set tracefile_identifier = 'create_vector_index';

セッションが変更されました。

経過: 00:00:00.00
SCOTT@localhost:1521/freepdb1> alter session set events '10046 trace name context forever, level 12';

セッションが変更されました。

経過: 00:00:00.02
SCOTT@localhost:1521/freepdb1> l
1 CREATE VECTOR INDEX search_data_hnsw_ix ON search_data ( vector_desc )
2 ORGANIZATION
3 INMEMORY NEIGHBOR GRAPH
4 DISTANCE COSINE
5* WITH TARGET ACCURACY 90
SCOTT@localhost:1521/freepdb1> /

索引が作成されました。

経過: 00:00:39.24
SCOTT@localhost:1521/freepdb1> alter session set events '10046 trace name context off';

セッションが変更されました。

経過: 00:00:00.01

...略...

[oracle@localhost ~]$ echo $ORACLE_BASE
/opt/oracle
[oracle@localhost ~]$ cd $ORACLE_BASE/diag/rdbms/free/FREE/trace
[oracle@localhost trace]$ pwd
/opt/oracle/diag/rdbms/free/FREE/trace
[oracle@localhost trace]$
[oracle@localhost trace]$ ll *create_vector_index*
-rw-r-----. 1 oracle oinstall 2836064 6月 12 00:24 FREE_ora_4133_create_vector_index.trc
-rw-r-----. 1 oracle oinstall 602896 6月 12 00:24 FREE_ora_4133_create_vector_index.trm
[oracle@localhost trace]$
[oracle@localhost trace]$ tkprof FREE_ora_4133_create_vector_index.trc FREE_ora_4133_create_vector_index.txt waits=yes explain=scott@localhost:1521/freepdb1 sys=yes

...略...

[oracle@localhost trace]$ ll *create_vector_index*
-rw-r-----. 1 oracle oinstall 2836064 6月 12 00:24 FREE_ora_4133_create_vector_index.trc
-rw-r-----. 1 oracle oinstall 602896 6月 12 00:24 FREE_ora_4133_create_vector_index.trm
-rw-rw-r--. 1 oracle oracle 500986 6月 12 00:30 FREE_ora_4133_create_vector_index.txt
[oracle@localhost trace]$
[oracle@localhost trace]$ view FREE_ora_4133_create_vector_index.txt

 

ということで、整形したトレースファイルからCREATE文を抜き出してみた。
これは私が実行したDDL文なので参考程度に...

CREATE VECTOR INDEX search_data_hnsw_ix ON search_data ( vector_desc )
ORGANIZATION
INMEMORY NEIGHBOR GRAPH
DISTANCE COSINE
WITH TARGET ACCURACY 90

 

以降、トレースから抜き出した補助表及関連する索引を作成するDDL
VECTOR$SEARCH_DATA_HNSW_IX$78074_79461_0$HNSW_ROWID_VID_MAP表、および、主キー索引の作成(基本的にこの補助表が主役)

CREATE TABLE SCOTT.VECTOR$SEARCH_DATA_HNSW_IX$78074_79461_0$HNSW_ROWID_VID_MAP 
(base_table_rowid ROWID PRIMARY KEY, vertex_id NUMBER)

CREATE UNIQUE INDEX "SCOTT"."SYS_C0013760" on
"SCOTT"."VECTOR$SEARCH_DATA_HNSW_IX$78074_79461_0$HNSW_ROWID_VID_MAP"("BASE_TABLE_ROWID")
NOPARALLEL



VECTOR$SEARCH_DATA_HNSW_IX$78074_79461_0$HNSW_SHARED_JOURNAL_TRANSACTION_COMMITS表、および、主キー索引の作成

CREATE TABLE SCOTT.VECTOR$SEARCH_DATA_HNSW_IX$78074_79461_0$HNSW_SHARED_JOURNAL_TRANSACTION_COMMITS 
(usn NUMBER NOT NULL, slot NUMBER NOT NULL, seq
NUMBER NOT NULL, commit_scn NUMBER NOT NULL,
CONSTRAINT pk_xid_79461 PRIMARY KEY(usn, slot, seq))
PARTITION BY RANGE(commit_scn)
INTERVAL(100)
(PARTITION pdefault VALUES LESS THAN (0))

CREATE UNIQUE INDEX "SCOTT"."PK_XID_79461" on
"SCOTT"."VECTOR$SEARCH_DATA_HNSW_IX$78074_79461_0$HNSW_SHARED_JOURNAL_TRANSACTION_COMMITS"("USN","SLOT","SEQ")
NOPARALLEL


VECTOR$SEARCH_DATA_HNSW_IX$78074_79461_0$HNSW_SHARED_JOURNAL_CHANGE_LOG表の作成(HNSW_SHARED_JOURNAL_TRANSACTION_COMMITS表の参照パーティションになっている)

CREATE TABLE SCOTT.VECTOR$SEARCH_DATA_HNSW_IX$78074_79461_0$HNSW_SHARED_JOURNAL_CHANGE_LOG 
(usn NUMBER NOT NULL, slot NUMBER NOT NULL, seq NUMBER NOT
NULL, xcn NUMBER, base_table_rowid ROWID, dml_op VARCHAR2(10),
data_vector vector(384, FLOAT32),
CONSTRAINT fk_xid_79461 FOREIGN KEY(usn, slot, seq)
REFERENCES
SCOTT.VECTOR$SEARCH_DATA_HNSW_IX$78074_79461_0$HNSW_SHARED_JOURNAL_TRANSACTION_COMMITS(usn, slot, seq))
PARTITION BY REFERENCE(fk_xid_79461)

各表をdescribeしてみると...

VECTOR列を持つ表のROWIDとVERTEX_IDをマップしていますね、表を見てなんとなく想像できる列名で素敵w。RDBMSのモデリングで列からなにやってるか想像もできねー設計しているのを見たりしてると心が清くなった気がしますね。
これがVECTOR INDEXの主役といってもよいでしょうね。

SCOTT@localhost:1521/freepdb1> desc VECTOR$SEARCH_DATA_HNSW_IX$78074_79461_0$HNSW_ROWID_VID_MAP
名前 NULL? 型
----------------------------------------- -------- ----------------------------
BASE_TABLE_ROWID NOT NULL ROWID
VERTEX_ID NUMBER

 

次の2つの表は、元の表のVECTORに影響のある更新がトラックされているみたい。詳しい資料読んではいないのですが、変更をトラックしていく表っぽうので、変更のない状況では空なんじゃないだろうか。。。あとで確認します。

SCOTT@localhost:1521/freepdb1> desc VECTOR$SEARCH_DATA_HNSW_IX$78074_79461_0$HNSW_SHARED_JOURNAL_TRANSACTION_COMMITS
名前 NULL? 型
----------------------------------------- -------- ----------------------------
USN NOT NULL NUMBER
SLOT NOT NULL NUMBER
SEQ NOT NULL NUMBER
COMMIT_SCN NOT NULL NUMBER

SCOTT@localhost:1521/freepdb1> desc VECTOR$SEARCH_DATA_HNSW_IX$78074_79461_0$HNSW_SHARED_JOURNAL_CHANGE_LOG
名前 NULL? 型
----------------------------------------- -------- ----------------------------
USN NOT NULL NUMBER
SLOT NOT NULL NUMBER
SEQ NOT NULL NUMBER
XCN NUMBER
BASE_TABLE_ROWID ROWID
DML_OP VARCHAR2(10)
DATA_VECTOR VECTOR(384, FLOAT32)

 

一応user_indexesビューから全体を見てみます。USER_INDEXESビューを通して見ると新たに7つのオブジェクトが作成されたことがわかります!!!!(赤字部分)
vecsys.vector$index表からは、補助表の存在しか見えませんでしたが、索引も作成されてます。実際にはこの表と索引のセグメントサイズの合計が、VECTOR INDEX作成時のセグメントサイズってことですよね?!(INMEMORYだからメモリーサイズって可能性もなくはないけど、どっちかわからんw) とはいえ、CREATE VECTOR INDEX ほげほげ索引で、指定したほげほげ索引自体はセグメントは持たない!(INMEMORYテーブルとは異なる持ち方をしているだけなのかもしれないが、わかりにくいw)


SCOTT@localhost:1521/freepdb1> select index_name , table_name from user_indexes where table_name like '%SEARCH_DATA%';

INDEX_NAME TABLE_NAME
------------------------------ ----------------------------------------------------------------------------------------------------
SYS_IL0000078074C00009$$ SEARCH_DATA
SEARCH_DATA_HNSW_IX SEARCH_DATA
SYS_C0013720 VECTOR$SEARCH_DATA_HNSW_IX$78074_79421_0$HNSW_ROWID_VID_MAP
PK_XID_79421 VECTOR$SEARCH_DATA_HNSW_IX$78074_79421_0$HNSW_SHARED_JOURNAL_TRANSACTION_COMMITS
SYS_IL0000079427C00007$$ VECTOR$SEARCH_DATA_HNSW_IX$78074_79421_0$HNSW_SHARED_JOURNAL_CHANGE_LOG

経過: 00:00:00.29

表名を中間一致検索すると関連する索引と表をリストすることができますが、これだとなんかイケてない感じが強いのでvecsys.vector$index表より扱いやすいビューを提供してもらいたいですね! まじで (苦笑

長くなってしまったので、一旦、謎の1つをまとめておきたいと思います。
VECTOR INDEX (HNSW)のセグメントサイズとEXPLAIN PLAN FOR CREATE VECTOR INDEX...の見積もりサイズの差分確認は次回のおたのしみということで。

CREATE VECTOR INDEX search_data_hnsw_ix ON search_data ( vector_desc )
ORGANIZATION
INMEMORY NEIGHBOR GRAPH
DISTANCE COSINE
WITH TARGET ACCURACY 90

 

作成されるベクター索引(今回の例では、search_data_hnsw_ix)は *_INDEXESにはVECTOR索引としてリストされるが実体は持たない(INMEMORYとはいっても何がどうINMEMORY化されるかディクショナリーから追いやすかったテーブルのINMEMORY化とは傾向が違いそう)。
補助表とその索引を含めたオブジェクト群をアクセスするためのキーのような存在。
ベクター索引(今回のHNSW索引の例では、マップ表とベクターの更新をトラックするパーティション表と参照パーティション表からなる3表と付随する3つの索引から構成されている。 ベクター索引の主役は、ベクター列をもつベース表のROWIDをVERTEX_IDをマップするマップ表とその索引。変更がない状態であれば、おそらくこの2つのオブジェクトがセベクター索引のセグメントサイズの総量になりそう。

また、
これらオブジェクト(索引も含め)を簡単に一覧するビューは無く、中間一致検索で無理やり検索するか、vecsys.vector$index表のJSONデータを使って他のビューと結合して取得する必要がありそう(めんどくせぇw)

ということまでは見えた。(以下は、洞窟の奥へ潜入したときの手書きメモww)

もうひとつ、前述の通り、search_data_hnsw_ixというベクター索引の便宜上のオブジェクトは同一名のセグメントは持っていません。しかし、以下のようなSELECT文では、該当ベクター索引が利用されていることを示すオブジェクトとして現れてくるというのも特徴的ですね。
SQL文と実行計画によってはMAP表が現われてくることもあるようなので、その例についてもいずれ書こうと思っています:) (それはそれで謎いですよね。突然裏の主役が実行計画に顔を出してくるわけなのでw)

SCOTT@localhost:1521/freepdb1> r
1 SELECT
2 id
3 , description
4 , community
5 , location_desc
6 , district
7 , TO_NUMBER( v_distance ) AS v_distance
8 FROM
9 (
10 SELECT
11 id
12 , description
13 , community
14 , location_desc
15 , district
16 , VECTOR_DISTANCE
17 (
18 vector_desc
19 , VECTOR_EMBEDDING
20 (
21 all_minilm_l6 USING 'Incident in which someone may have been murdered' AS data
22 )
23 , COSINE
24 ) v_distance
25 FROM
26 search_data
27 ORDER BY
28 v_distance
29 FETCH APPROX FIRST 10 ROWS ONLY
30* )


10行が選択されました。

経過: 00:00:00.32

実行計画
----------------------------------------------------------
Plan hash value: 2333665681

-------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10 | 1570 | 2 (50)| 00:00:01 |
| 1 | VIEW | | 10 | 1570 | 2 (50)| 00:00:01 |
|* 2 | COUNT STOPKEY | | | | | |
| 3 | VIEW | | 10 | 1570 | 2 (50)| 00:00:01 |
|* 4 | SORT ORDER BY STOPKEY | | 10 | 16400 | 2 (50)| 00:00:01 |
| 5 | TABLE ACCESS BY INDEX ROWID| SEARCH_DATA | 10 | 16400 | 1 (0)| 00:00:01 |
| 6 | VECTOR INDEX HNSW SCAN | SEARCH_DATA_HNSW_IX | 10 | 16400 | 1 (0)| 00:00:01 |
-------------------------------------------------------------------------------------------------------

 

なお、今回利用している Oracle Database 23.4は最近の更新ペースからするとすでに古いリリースになっていますw 
VECTOR関連追加など含め結構な差分がりそうですね。例えば、DBMS_VECTOR.INDEX_VECTOR_MEMORY_ADVISOR()プロシージャが無いなど。(マニュアルには記載されているんですが、リリースいくつから有効というような表記は見当たらず)
また、VirtualBox Applienceだと、現時点では、23.7 が最新みたいなので入れ替えないとな。。。

 

23って、機能的にいつ頃落ち着くんだろうなぁ〜っ。

次回へ、つづく!

Enjoy SQL! and JSON?!

 

 


Oracle Database 23ai freeで試すVector Search - ONNXモデル準備編
Oracle Database 23ai freeで試すVector Search - データ準備編
実行計画は, SQL文のレントゲン写真だ! No.66 / AI Vector Search - VECTOR INDEX HNSW SCAN

 

| | | コメント (0)

2025年6月 5日 (木)

実行計画は, SQL文のレントゲン写真だ! No.66 / AI Vector Search - VECTOR INDEX HNSW SCAN

Previously on Mac De Oracle
前回は、Oracle Database 23ai freeで試すVector Search - データ準備編でした。
今回は、AI Vector SearchでVECTOR INDEX HNSW SCANとそれが利用できない場合の実行計画という名のSQL文のレントゲンを診ていきたいと思います。:)

その前に、VECTOR INDEX HNSW を利用する前に必要な準備がまだ残っていました。
vector_memory_sizeの設定です。デフォルトでは 0 になっています。

Oracle Dataabase 23ai / Oracle AI Vector Search User's Guide/ Oracle AI Vector Search Parameters

https://docs.oracle.com/en/database/oracle/oracle-database/23/vecse/oracle-ai-vector-search-parameters.html

SYS@FREE> show parameter memory

NAME TYPE VALUE
------------------------------------ ----------- ------------------------------

...略...

inmemory_xmem_size big integer 0
memory_max_size big integer 0
memory_max_target big integer 0
memory_size big integer 0
memory_target big integer 0
optimizer_inmemory_aware boolean TRUE
shard_apply_max_memory_size integer 0
shared_memory_address integer 0
vector_memory_size big integer 0

SYS@FREE> show parameter sga

NAME TYPE VALUE
------------------------------------ ----------- ------------------------------
allow_group_access_to_sga boolean FALSE
lock_sga boolean FALSE
pre_page_sga boolean TRUE
sga_max_size big integer 1536M
sga_min_size big integer 0
sga_target big integer 1536M

 

Oracle Database 23ai / Oracle AI Vector Searchユーザーズ・ガイド / ベクトル・プールのサイズ設定

https://docs.oracle.com/cd/G11854_01/vecse/size-vector-pool.html

今回はギリギリですがw 512MBに設定して試します。

SYS@FREE> create pfile from spfile;

ファイルが作成されました。

経過: 00:00:00.01
SYS@FREE> ALTER SYSTEM SET vector_memory_size=512m SCOPE=spfile
SYS@FREE> shutdown immediate
データベースがクローズされました。
データベースがディスマウントされました。
ORACLEインスタンスがシャットダウンされました。
SYS@FREE> startup
ORACLEインスタンスが起動しました。

Total System Global Area 1603726344 bytes
Fixed Size 5360648 bytes
Variable Size 654311424 bytes
Database Buffers 402653184 bytes
Redo Buffers 4530176 bytes
Vector Memory Area 536870912 bytes
データベースがマウントされました。
データベースがオープンされました。
SYS@FREE>
SYS@FREE> show parameter vector_memory_size

NAME TYPE VALUE
------------------------------------ ----------- ------------------------------
vector_memory_size big integer 512M

 

vector_memory_sizeサイズの準備ができたので、Vector Index (HNSW) を作成します。

まずは、HNSWタイプののVector Indexを作成します。

SCOTT@localhost:1521/freepdb1> l
1 CREATE VECTOR INDEX search_data_hnsw_ix ON search_data ( vector_desc )
2 ORGANIZATION
3 INMEMORY NEIGHBOR GRAPH
4 IDISTANCE COSINE
5* WITH TARGET ACCURACY 90
SCOTT@localhost:1521/freepdb1> /

索引が作成されました。

経過: 00:00:36.13
SCOTT@localhost:1521/freepdb1> r
1 select index_name,table_name,index_type,index_subtype,uniqueness,ityp_name
2* from user_indexes where table_name = 'SEARCH_DATA'

INDEX_NAME TABLE_NAME INDEX_TYPE INDEX_SUBTYPE UNIQUENES ITYP_NAME
------------------------------ ------------------------------ --------------------------- ---------------------------- --------- --------------------
SYS_IL0000078074C00009$$ SEARCH_DATA LOB UNIQUE
SEARCH_DATA_HNSW_IX SEARCH_DATA VECTOR INMEMORY_NEIGHBOR_GRAPH_HNSW NONUNIQUE

経過: 00:00:00.00
SCOTT@localhost:1521/freepdb1> exec dbms_stats.gather_table_stats(ownname=>'SCOTT',tabname=>'SEARCH_DATA',no_invalidate=>false, cascade=>true);

PL/SQLプロシージャが正常に完了しました。

 

ちなみに、vector_memory_sizeに指定したメモリサイズでは小さすぎる場合、以下のエラーが発生します。vector_memory_sizeを増加するか、データ量を削減するかどちらかなんですよね。Oracle Database 23ai Freeってメモリサイズの制限も厳しいので。

CREATE VECTOR INDEX search_data_hnsw_ix ON search_data ( vector_desc )
*
行1でエラーが発生しました。:
ORA-51961: ベクトル・メモリー領域が不足しています。 ヘルプ:
https://docs.oracle.com/error-help/db/ora-51961/

 

VECTOR INDEX HNSW SCAN + TABLE ACCESS BY INDEX ROWIDになるか確認してみましょう.

索引(Hierarchical Navigable Small World (HNSW)またはInverted File Flat (IVF)ベクトル索引)を利用させるためには、以下のガイドラインに従うことが重要なので一読しておくことをおすすめしておきます。(知らないと簡単にハマってしまうので)


Oracle Database 23ai / Oracle AI Vector Searchユーザーズ・ガイド / 索引使用のガイドライン

https://docs.oracle.com/cd/G11854_01/vecse/guidelines-using-vector-indexes.html

 

シカゴの犯罪データから、"Incident in which someone may have been murdered" という条件で殺人事件に絡んでそうな事件を10件検索してみます。APPROXというキーワードも重要なので忘れずに。

SELECT
id
, description
, community
, location_desc
, district
, TO_NUMBER( v_distance ) AS v_distance
FROM
(
SELECT
id
, description
, community
, location_desc
, district
, VECTOR_DISTANCE
(
vector_desc
, VECTOR_EMBEDDING
(
all_minilm_l6 USING 'Incident in which someone may have been murdered' AS data
)
, COSINE
) v_distance
FROM
search_data
ORDER BY
v_distance
FETCH APPROX FIRST 10 ROWS ONLY
)
/

 

おおおおおお、索引使ってる:) ほっとした

ただ、これまでの INMEMORYとはことなり、INMEMORYにあるはずので索引アクセスのはずなのに、実行計画のOPERATIONには、INMEMORYというキーワードがないこと。ディクショナリービューの使い方もこれまでとは異なる傾向がり、なかなか分かりづらい部分も多い。今回気づいた違和感というか謎は要調査(2025/6/20追記)

SCOTT@localhost:1521/freepdb1> l
1 SELECT
2 id
3 , description
4 , community
5 , location_desc
6 , district
7 , TO_NUMBER( v_distance ) AS v_distance
8 FROM
9 (
10 SELECT
11 id
12 , description
13 , community
14 , location_desc
15 , district
16 , VECTOR_DISTANCE
17 (
18 vector_desc
19 , VECTOR_EMBEDDING
20 (
21 all_minilm_l6 USING 'Incident in which someone may have been murdered' AS data
22 )
23 , COSINE
24 ) v_distance
25 FROM
26 search_data
27 ORDER BY
28 v_distance
29 FETCH APPROX FIRST 10 ROWS ONLY
30* )
SCOTT@localhost:1521/freepdb1> /

ID DESCRIPTION COMMUNITY LOCATION_DESC DISTRICT V_DISTANCE
---------- ---------------------------------------- -------------------- ---------------------------------------- ---------- ----------
13405240 AGGRAVATED - OTHER DANGEROUS WEAPON AUSTIN APARTMENT 15TH .631286621
13405155 AGGRAVATED - OTHER DANGEROUS WEAPON AUSTIN HOSPITAL BUILDING / GROUNDS 15TH .631286621
13395521 AGGRAVATED - OTHER DANGEROUS WEAPON MORGAN PARK RESIDENCE 22ND .631286621
13404912 AGGRAVATED - OTHER DANGEROUS WEAPON ASHBURN SCHOOL - PUBLIC BUILDING 8TH .631286621
13405606 AGGRAVATED - OTHER DANGEROUS WEAPON AVALON PARK STREET 4TH .631286621
13509969 AGGRAVATED - OTHER DANGEROUS WEAPON AUBURN GRESHAM APARTMENT 6TH .631286621
13396440 AGGRAVATED - OTHER DANGEROUS WEAPON DUNNING STREET 16TH .631286621
13396497 AGGRAVATED - OTHER DANGEROUS WEAPON WOODLAWN APARTMENT 3RD .631286621
13405235 AGGRAVATED - OTHER DANGEROUS WEAPON NEAR WEST SIDE SIDEWALK 12TH .631286621
13396404 AGGRAVATED - OTHER DANGEROUS WEAPON NEAR NORTH SIDE CONVENIENCE STORE 18TH .631286621

10行が選択されました。

経過: 00:00:00.37
SCOTT@localhost:1521/freepdb1> set autot trace exp stat
SCOTT@localhost:1521/freepdb1> r
1 SELECT
2 id
3 , description
4 , community
5 , location_desc
6 , district
7 , TO_NUMBER( v_distance ) AS v_distance
8 FROM
9 (
10 SELECT
11 id
12 , description
13 , community
14 , location_desc
15 , district
16 , VECTOR_DISTANCE
17 (
18 vector_desc
19 , VECTOR_EMBEDDING
20 (
21 all_minilm_l6 USING 'Incident in which someone may have been murdered' AS data
22 )
23 , COSINE
24 ) v_distance
25 FROM
26 search_data
27 ORDER BY
28 v_distance
29 FETCH APPROX FIRST 10 ROWS ONLY
30* )


10行が選択されました。

経過: 00:00:00.32

実行計画
----------------------------------------------------------
Plan hash value: 2333665681

-------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10 | 1570 | 2 (50)| 00:00:01 |
| 1 | VIEW | | 10 | 1570 | 2 (50)| 00:00:01 |
|* 2 | COUNT STOPKEY | | | | | |
| 3 | VIEW | | 10 | 1570 | 2 (50)| 00:00:01 |
|* 4 | SORT ORDER BY STOPKEY | | 10 | 16400 | 2 (50)| 00:00:01 |
| 5 | TABLE ACCESS BY INDEX ROWID| SEARCH_DATA | 10 | 16400 | 1 (0)| 00:00:01 |
| 6 | VECTOR INDEX HNSW SCAN | SEARCH_DATA_HNSW_IX | 10 | 16400 | 1 (0)| 00:00:01 |
-------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

2 - filter(ROWNUM<=10)
4 - filter(ROWNUM<=10)


統計
----------------------------------------------------------
0 recursive calls
0 db block gets
10 consistent gets
0 physical reads
0 redo size
1621 bytes sent via SQL*Net to client
108 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
1 sorts (memory)
0 sorts (disk)
10 rows processed

 

ヒントで索引を利用できなくしてみます。新しいヒントが結構追加されていますが、それらについては、また、別の機会にでも。

Oracle Database 23ai / Oracle AI Vector Search User's Guide / Vector Index Hints

https://docs.oracle.com/en/database/oracle/oracle-database/23/vecse/vector-index-hints.html

SELECT
id
, description
, community
, location_desc
, district
, TO_NUMBER( v_distance ) AS v_distance
FROM
(
SELECT
/*+
NO_VECTOR_INDEX_SCAN(search_data)
*/
id
, description
, community
, location_desc
, district
, VECTOR_DISTANCE
(
vector_desc
, VECTOR_EMBEDDING
(
all_minilm_l6 USING 'Incident in which someone may have been murdered' AS data
)
, COSINE
) v_distance
FROM
search_data
ORDER BY
v_distance
FETCH APPROX FIRST 10 ROWS ONLY
)
/


10行が選択されました。

経過: 00:00:03.84

実行計画
----------------------------------------------------------
Plan hash value: 2441119222

------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time |
------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10 | 1570 | | 51357 (1)| 00:00:03 |
| 1 | VIEW | | 10 | 1570 | | 51357 (1)| 00:00:03 |
|* 2 | COUNT STOPKEY | | | | | | |
| 3 | VIEW | | 125K| 18M| | 51357 (1)| 00:00:03 |
|* 4 | SORT ORDER BY STOPKEY| | 125K| 195M| 244M| 51357 (1)| 00:00:03 |
| 5 | TABLE ACCESS FULL | SEARCH_DATA | 125K| 195M| | 8543 (1)| 00:00:01 |
------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

2 - filter(ROWNUM<=10)
4 - filter(ROWNUM<=10)


統計
----------------------------------------------------------
5635 recursive calls
0 db block gets
48201 consistent gets
46497 physical reads
0 redo size
1589 bytes sent via SQL*Net to client
108 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
119 sorts (memory)
0 sorts (disk)
10 rows processed

 

APPROXキーワードをEXACTに変えました。これもVECTOR INDEXを利用できなくなる条件の一つです。診てみましょう

SELECT
id
, description
, community
, location_desc
, district
, TO_NUMBER( v_distance ) AS v_distance
FROM
(
SELECT
id
, description
, community
, location_desc
, district
, VECTOR_DISTANCE
(
vector_desc
, VECTOR_EMBEDDING
(
all_minilm_l6 USING 'Incident in which someone may have been murdered' AS data
)
, COSINE
) v_distance
FROM
search_data
ORDER BY
v_distance
FETCH EXACT FIRST 10 ROWS ONLY
)
/

 

実行計画とは関係ないですが、APPROXとEXACTの問い合わせ結果って結構違うのね。。。

SCOTT@localhost:1521/freepdb1> /

ID DESCRIPTION COMMUNITY LOCATION_DESC DISTRICT V_DISTANCE
---------- ---------------------------------------- -------------------- ---------------------------------------- ---------- ----------
13325717 OTHER CRIME AGAINST PERSON BRIDGEPORT APARTMENT 9TH .382904768
13331403 OTHER CRIME AGAINST PERSON HEGEWISCH APARTMENT 4TH .382904768
13329763 OTHER CRIME AGAINST PERSON NEAR NORTH SIDE SMALL RETAIL STORE 18TH .382904768
13330138 OTHER CRIME AGAINST PERSON ROSELAND STREET 5TH .382904768
13329559 OTHER CRIME AGAINST PERSON CHICAGO LAWN APARTMENT 8TH .382904768
13329389 OTHER CRIME AGAINST PERSON BELMONT CRAGIN ALLEY 25TH .382904768
13328588 OTHER CRIME AGAINST PERSON LOOP APARTMENT 1ST .382904768
13328610 OTHER CRIME AGAINST PERSON SOUTH CHICAGO APARTMENT 4TH .382904768
13325825 OTHER CRIME AGAINST PERSON WEST RIDGE PARKING LOT / GARAGE (NON RESIDENTIAL) 24TH .382904768
13326308 OTHER CRIME AGAINST PERSON PULLMAN STREET 5TH .382904768



実行計画
----------------------------------------------------------
Plan hash value: 2441119222

------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time |
------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10 | 1570 | | 51357 (1)| 00:00:03 |
| 1 | VIEW | | 10 | 1570 | | 51357 (1)| 00:00:03 |
|* 2 | COUNT STOPKEY | | | | | | |
| 3 | VIEW | | 125K| 18M| | 51357 (1)| 00:00:03 |
|* 4 | SORT ORDER BY STOPKEY| | 125K| 195M| 244M| 51357 (1)| 00:00:03 |
| 5 | TABLE ACCESS FULL | SEARCH_DATA | 125K| 195M| | 8543 (1)| 00:00:01 |
------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

2 - filter(ROWNUM<=10)
4 - filter(ROWNUM<=10)

 

ということで、
実行計画は, SQL文のレントゲン写真だ! No.66 / AI Vector Search - VECTOR INDEX HNSW SCAN
はここまで。

今回は一部だけ試してますが更新関連なども含めいろいろな仕組みがあるようで。。。ちょっと追っかけてみないと見えてこないところも多いです。マニュアルも多いですし。。。斜め読みしていて、まじか。。。。となっているところ。

ところで、前回explain plan 文でVector Indexサイズの見積もりを取得していたのですが、覚えてますでしょうか? 実はちょっとクセがあるみたいなんですよね。*_SEGMENTSビューからは索引名では見つからないので。。。


あれ、 SEARCH_DATA_HNSW_IX というVECTOR索引って、索引名で検索しても、*_SEGMENTSビューにでないのか。別の名前になってるのか。。。 セグメントサイズ見たかったのに。。。という謎だけ投げつけて、宿題としておきますw(わざとらしいw) うううーーーーむ、いろいろと知っておく必要なる事項は多そうですねぇ。。。。。

SCOTT@localhost:1521/freepdb1> select index_name,index_type,table_name from user_indexes where table_name='SEARCH_DATA';

INDEX_NAME INDEX_TYPE TABLE_NAME
------------------------------ --------------------------- ------------------------------
SYS_IL0000078074C00009$$ LOB SEARCH_DATA
SEARCH_DATA_HNSW_IX VECTOR SEARCH_DATA

経過: 00:00:00.05
SCOTT@localhost:1521/freepdb1> select segment_name from user_segments where segment_name = 'SEARCH_DATA_HNSW_IX';

レコードが選択されませんでした。

SCOTT@localhost:1521/freepdb1> r
1* SELECT * FROM vecsys.vector$index

IDX_OBJN IDX_OBJD IDX_OWNER# IDX_NAME IDX_BASE_TABLE_OBJN IDX_BASE_TABLE_OWNER# IDX_PARAMS IDX_AUXILIARY_TABLES
---------- ---------- ---------- ------------------------------ ------------------- --------------------- ------------------------------ ------------------------------
78797 153 SEARCH_DATA_HNSW_IX 78074 153 {"type":"HNSW","num_neighbors" {"rowid_vid_map_objn":78798,"s
:32,"efConstruction":200,"dist hared_journal_transaction_comm
ance":"COSINE","accuracy":90," its_objn":78800,"shared_journa
vector_type":"FLOAT32","vector l_change_log_objn":78803,"rowi
_dimension":384,"degree_of_par d_vid_map_name":"VECTOR$SEARCH
allelism":1,"pdb_id":3,"indexe _DATA_HNSW_IX$78074_78797_0$HN
d_col":"VECTOR_DESC"} SW_ROWID_VID_MAP","shared_jour
nal_transaction_commits_name":
"VECTOR$SEARCH_DATA_HNSW_IX$78
074_78797_0$HNSW_SHARED_JOURNA
L_TRANSACTION_COMMITS","shared
_journal_change_log_name":"VEC
TOR$SEARCH_DATA_HNSW_IX$78074_
78797_0$HNSW_SHARED_JOURNAL_CH
ANGE_LOG"}

SCOTT@localhost:1521/freepdb1> r
1 SELECT
2 JSON_SERIALIZE(idx_params RETURNING VARCHAR2 PRETTY) AS "INDEX PARAM"
3 ,JSON_SERIALIZE(idx_auxiliary_tables RETURNING VARCHAR2 PRETTY) AS "INDEX AUX"
4* FROM vecsys.vector$index

INDEX PARAM INDEX AUX
-------------------------------------------------- --------------------------------------------------------------------------------------------------------------------------------------------
{ {
"type" : "HNSW", "rowid_vid_map_objn" : 78798,
"num_neighbors" : 32, "shared_journal_transaction_commits_objn" : 78800,
"efConstruction" : 200, "shared_journal_change_log_objn" : 78803,
"distance" : "COSINE", "rowid_vid_map_name" : "VECTOR$SEARCH_DATA_HNSW_IX$78074_78797_0$HNSW_ROWID_VID_MAP",
"accuracy" : 90, "shared_journal_transaction_commits_name" : "VECTOR$SEARCH_DATA_HNSW_IX$78074_78797_0$HNSW_SHARED_JOURNAL_TRANSACTION_COMMITS",
"vector_type" : "FLOAT32", "shared_journal_change_log_name" : "VECTOR$SEARCH_DATA_HNSW_IX$78074_78797_0$HNSW_SHARED_JOURNAL_CHANGE_LOG"
"vector_dimension" : 384, }
"degree_of_parallelism" : 1,
"pdb_id" : 3,
"indexed_col" : "VECTOR_DESC"
}

 

では、また!

Enjoin Execution Plans, SQLs, and AI Vector Search!

 


Related article on Mac De Oracle
実行計画は, SQL文のレントゲン写真だ!

実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 1 / TABLE FULL SCAN
実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 2 / INDEX UNIQUE SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 3 / INDEX RANGE SCAN, Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 4 / INDEX RANGE SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 5 / INDEX RANGE SCAN, INLIST ITERATOR
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 6 / INDEX FAST SCAN, Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 7 / INDEX FULL SCAN,Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 8 / INDEX SKIP SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 9 / TABLE ACCESS INMEMORY FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 10 / NESTED LOOP JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 11 / MERGE JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 12 / HASH JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 13 / HASH JOIN OUTER
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 14 / HASH JOIN FULL OUTER
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 15 / PX, TABLE ACCESS FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 16 / CONCATENATION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 17 / SORT UNIQUE, UNION-ALL = UNION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 18 / UNION-ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 19 / INTERSECTION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 20 / MINUS
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 21 / WINDOW NOSORT STOPKEY
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 22 / COUNT STOPKEY
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 23 / HASH JOIN - LEFT-DEEP JOIN vs RIGHT-DEEP JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 24 / CONNECT BY NO FILTERING WITH START-WITH
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 25 / UNION ALL (RECURSIVE WITH) DEPTH FIRST, RECURSIVE WITH PUMP
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#1 / STAR TRANSFORM, VECTOR TRANSFORM (DWH向け)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#2 / MERGE (UPSERT)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#3 / RDFView
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#4 / INDEX FULL SCAN (MIN/MAX) - Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! No.30 - LOAD TABLE CONVENTIONAL vs. LOAD AS SELECT
・実行計画は, SQL文のレントゲン写真だ! No.31 - TEMP TABLE TRANSFORMATION LOAD AS SELECT (CURSOR DURATION MEMORY)
・実行計画は, SQL文のレントゲン写真だ! No.32 - EXTERNAL TABLE ACCESS FULL / INMEMORY FULL
・実行計画は, SQL文のレントゲン写真だ! No.33 - BITMAP CONVERSION TO ROWIDS
・実行計画は, SQL文のレントゲン写真だ! No.34 - 似て非なるもの USE_CONCAT と OR_EXPAND ヒント と 手書きSQLのレントゲンの見分け方
・実行計画は, SQL文のレントゲン写真だ! No.35 - 似て非なるもの USE_CONCAT と OR_EXPANDヒントとパラレルクエリー
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 1 / No.36 / INTERSECT ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 2 / No.37 / MINUS ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 3 / No.38 / EXCEPT and EXCEPT ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 4 / No.39 / In-Memory Hybrid Scans
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 5 / No.40 / PIVOT and UNPIVOT
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 6 / No.41 / In-Memory Vectorized Join
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 7 / No.42 / INDEX RANGE SCAN (MULTI VALUE)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 8 / No.43 / TABLE ACCESS BY INDEX ROWID BATCHED
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 9 / No.44 / COLLECTION ITERATOR PICKLER FETCH
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 10 / No.45 / MAT_VIEW REWRITE ACCESS FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 11 / No.46 / GROUPING SETS, ROLLUP, CUBE
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 12 / No.47 / TEMP TABLE TRANSFORMATION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 13 / No.48 / MULTI-TABLE INSERT
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 14 / No.49 / the DUAL Table
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 15 / No.50 / REMOTE
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 16 / No.51 / Concurrent Execution of Union All and Union
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 17 / No.52 / Order by Elimination
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 18 / No.53 / Join Elimination
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 19 / No.54 / Group by Elimination
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 20 / No.55 / DISTINCT Elimination
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 21 / No.56 / INLIST ITERATOR と Sub Query と STATISTICS COLLECTOR
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 22 / No.57 / Subquery Unnesting
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 23 / No.58 / ANTI JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 24 / No.59 / SQL MACRO (19.7〜)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 25 / No.60 / ANSI JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 / No.60 / ANSI JOINのおまけ
実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 / No.61 / ANSI JOINのおまけのおまけ
実行計画は, SQL文のレントゲン写真だ! No.62 / ORDBMS機能であるコレクション型の列をアクセスする実行計画ってどうなるの?
実行計画は, SQL文のレントゲン写真だ! No.63 / Join Elimination (再び)その1
実行計画は, SQL文のレントゲン写真だ! No.63 / Join Elimination (再び)その2
実行計画は, SQL文のレントゲン写真だ! No.63 / Join Elimination (再び)その3
実行計画は, SQL文のレントゲン写真だ! No.64 / 先生、私のLEFT OUTER JOINが無いんです!!(Join Elimination番外編)
実行計画は, SQL文のレントゲン写真だ! No.65 / 忘れ去られたオプティマイザーヒントとTABLE ACCESS BY USER ROWID

 

AI Vector Search

Oracle Database 23ai freeで試すVector Search - ONNXモデル準備編
Oracle Database 23ai freeで試すVector Search - データ準備編

 

 

| | | コメント (0)

2024年8月30日 (金)

なぜ、主キー制約の追加時間に違いがでるのでしょうか? (東京都 ITエンジニア 男性)/ FAQ


え~〜っ、
十年ひと昔てぇなぁこと申しますけれど、十年経つと元へ戻るんですってねぇ。
あの問題でハマった〜ってぇ話題をおもいだしやして、ググってみたんでさぁ、ありましたよ、ありましたよ!
十年数年前。

落語調で入ってみましたww

 

さて、今日の患者さん、
何にハマってたのかぁってぇと、主キー制約を追加するのに〜、偉い時間がかかって、頭を抱えて相談に。
(いい加減、落語っぽい言葉遣いヤメぇ!w


この話題、10年以上前のOracle Forumでも話題になった有名な、FAQだよねー。
元々そういうリスクのあるテーブル定義だったり、各種ツールの使い方によっては遭遇することもあるので、この症状を覚えておくと対処もしやすいですよ。というお話をしたいと思います。

まずは、当時話題になったスレ。
Oracle Forum - SQL & PL/SQL "ALTER TABLE xxx add constraint primary key" takes long long time!

 

この症状、ご存知の方も多いと思うのですが、Forum外で言及しているエントリーって見たこともあまりないので、実はあまり知られてなかったりして。。w という心配もあり、メモがわりにブログに書いておきますね:)

ただし、この症状が顕著に現れるのは大規模な表の場合だけ(数十TBとか)なので、少量のデータだと気づかずというか影響していないことも多いので注意が必要なんです。(データが増加してから初めて気づいたりしてザワザワしたりするので)
巨大なデータを扱う場合、物凄い遅延に繋がってしまったりするので、舐めちゃいけません。


症状と原因をサクッとまとめると、

きっかけとなる主キー制約には以下の特徴があります。
・主キー制約では、キー値(複合キーの場合はキー全体で)が一意であり、NULL が含まれないでないことが保証されます。
NULLでないことが保証されるという点が今回のポイント。NULLと聞いてガタッとしたあなた!。いいリアクションです!w

 

主キー制約追加時には、主キーに含まれる列の値は NULL ではないことが検証されます。

主キー制約追加時には、主キーに含まれる列の値は NULL ではないことが検証されます。

主キー制約追加時には、主キーに含まれる列の値は NULL ではないことが検証されます。

(ここが大切なので3度書いておきましたw 太字でw

 

この NULL ではないことを保証するために、追加のIOと NULL でないことのチェックが塵も積もで、処理時間が伸びてしまう症状につながります。これは対象のデータ量に比例して伸びるので、データ量が少ないとほぼ気づきませんw
ただでさえ処理時間を要する大量データの場合のみ顕著な影響が現れます!!!!!

ただ、一般的には、主キー列として利用される列は NOT NULL となっている多いはずですが、
稀に、どうせ主キー制約でNOT NULLが保証されるから、それぞれの列には NOT NULL制約 (Oracle DatabaseではCHECK制約として実装されます) を付加しない! という方も見かけますが、それ手を抜かない方がいいですよ。
そんな横着していると、今回お見せするような症状を発症してしまうことがあります。
それ以外のケースでは、各種ツール(Oracle database純正のもを含む)の使い方次第では意図せず遭遇してしまうこともあります。(ex. impdp利用時のオブジェクトの扱いを制御するパラメータの使い方によるもの、とか)

症状と原因が理解できれば対処方法は簡単。そのような状態を避ければ良いだけですね! w
そんな大した話ではなくて、
・主キー制約に含める列は列定義レベルで、NOT NULL制約を付加しておきましょう!
・各種ツールの利用方法による副作用で、NULLABLEになってしまうようなケースでは、その状況を回避する利用手順なり、オプションを選ぶようにすることです。


これ覚えておくと、妙なところで時間溶かさなくて済むので、頭の片隅に置いておくと良いと思いますよ:)

 

 

 

では、簡単な例で、遅延の発生と、裏で何が起こっているのか10046トレースで、サクッと軽めに確認しておきましょう!

スクリプトの内容は本記事の後半に載せますので参考にしてみてください。

table_1/tabl_2を作成して、それぞれ、400MBほどになるようにデータを登録。(違いは、id2の列がNULLABLEかNOT NULLかだけです。この列は複合主キーに含まれます)


SCOTT@orclpdb1> @why_do_you_think_it_is_slow_adding_the_primary_constraint.sql

表が削除されました。

経過: 00:00:01.25

表が削除されました。

経過: 00:00:00.07
1 CREATE TABLE table_1
2 (
3 id1 NUMBER NOT NULL
4 ,id2 NUMBER
5 ,text VARCHAR2(10)
6* )

表が作成されました。

経過: 00:00:00.18
名前 NULL? 型
----------------------------------------- -------- ----------------------------
ID1 NOT NULL NUMBER
ID2 NUMBER
TEXT VARCHAR2(10)

1 CREATE TABLE table_2
2 (
3 id1 NUMBER NOT NULL
4 ,id2 NUMBER NOT NULL
5 ,text VARCHAR2(10)
6* )

表が作成されました。

経過: 00:00:00.02
名前 NULL? 型
----------------------------------------- -------- ----------------------------
ID1 NOT NULL NUMBER
ID2 NOT NULL NUMBER
TEXT VARCHAR2(10)

1 BEGIN
2 FOR i in 1..15000000 LOOP
3 INSERT ALL
4 INTO table_1 VALUES(i,i,i)
5 INTO table_2 VALUES(i,i,i)
6 SELECT null FROM dual
7 ;
8 IF MOD(i,1000) = 0
9 THEN
10 COMMIT;
11 END IF;
12 END LOOP;
13* END;

PL/SQLプロシージャが正常に完了しました。

経過: 00:11:42.62

 

ALTER TABLE ADD CONSTRAINT PRIMARY KEYで id1とid2の複合キーで主キー制約を追加。同時に索引も作成させる
今回の程度データ量だとびっくりするほどの差は見えずらいですが、id2がnullableであるtable_1の方が、約11秒ほど処理時間が長くなっています!
ちょっとわかりにくくなる原因は、制約追加と同時に索引も作成しているため索引作成の時間に埋もれやすく、索引作成の時間だと勘違いする方が多い影響もありますが、データ量が多くなればなるほどこの差は大きくなります。

  1* ALTER TABLE table_1 ADD CONSTRAINT pk_table_1 PRIMARY KEY (id1, id2) USING INDEX

表が変更されました。

経過: 00:00:48.31
1* ALTER TABLE table_2 ADD CONSTRAINT pk_table_2 PRIMARY KEY (id1, id2) USING INDEX

表が変更されました。

経過: 00:00:36.92

 

最初の例は、索引の作成時間も含まれいるため制約の追加のオーバーヘッドがわかりにくいので他の手順で確認してみると。。。。
ALTER TABLE ADD CONSTRAINT PRIMARY KEY USING [事前に作成しておいた一意索引名]部分(赤字)に注目。
制約の追加で、id2がNULLABLEかNOT NULLという部分だけの違いで、272倍の差になっています。table_2の制約追加時間はデータ量が増加してもほぼ一定なので、データ量が増加した場合の影響はどれぐらいかは想像してみてくださいね。
今回は、2列からなる複合主キーの1列だけですが、異なる条件では差は広がる可能性はあります。(環境の差も含め)

  1* ALTER TABLE table_1 DROP PRIMARY KEY DROP INDEX

表が変更されました。

経過: 00:00:00.93
1* ALTER TABLE table_2 DROP PRIMARY KEY DROP INDEX

表が変更されました。

経過: 00:00:00.10
1* CREATE UNIQUE INDEX pk_table_1 ON table_1(id1, id2)

索引が作成されました。

経過: 00:00:33.84
1* CREATE UNIQUE INDEX pk_table_2 ON table_2(id1, id2)

索引が作成されました。

経過: 00:00:36.13
1* ALTER TABLE table_1 ADD CONSTRAINT pk_table_1 PRIMARY KEY (id1, id2) USING INDEX pk_table_1

表が変更されました。

経過: 00:00:05.44
1* ALTER TABLE table_2 ADD CONSTRAINT pk_table_2 PRIMARY KEY (id1, id2) USING INDEX pk_table_2

表が変更されました。

経過: 00:00:00.02

PL/SQLプロシージャが正常に完了しました。

経過: 00:00:18.53

PL/SQLプロシージャが正常に完了しました。

経過: 00:00:15.50

SEGMENT_NAME MB
------------------------------ ----------
PK_TABLE_1 368
PK_TABLE_2 368
TABLE_1 416
TABLE_2 416

経過: 00:00:00.26

 

最後にNOT NULLにしておけば回避できることも確認
table_1のid2列をNOT NULLに変更して。。。

  1* ALTER TABLE table_1 DROP PRIMARY KEY DROP INDEX

表が変更されました。

経過: 00:00:00.41

名前 NULL? 型
----------------------------------------- -------- ----------------------------
ID1 NOT NULL NUMBER
ID2 NUMBER
TEXT VARCHAR2(10)

1* ALTER TABLE table_1 MODIFY id2 NUMBER NOT NULL

表が変更されました。

経過: 00:00:03.14

 

変更後のtabl_1 (table_2と同一定義になっています!)表の id2はNULLABLE から NOT NULL へ変更

 名前                                    NULL?    型
----------------------------------------- -------- ----------------------------
ID1 NOT NULL NUMBER
ID2 NOT NULL NUMBER
TEXT VARCHAR2(10)

 

table_2同様の処理時間となり大きく改善! 11秒のオーバーヘッドが消えてますね!

  1* ALTER TABLE table_1 ADD CONSTRAINT pk_table_1 PRIMARY KEY (id1, id2) USING INDEX

表が変更されました。

経過: 00:00:34.65

 

主キー制約の追加部分に絞って改善を確認してみましょう!
table_2同様の処理時間となり大きく改善! 0.02秒で完了 

  1* ALTER TABLE table_1 DROP PRIMARY KEY DROP INDEX

表が変更されました。

経過: 00:00:00.13
1* CREATE UNIQUE INDEX pk_table_1 ON table_1(id1, id2)

索引が作成されました。

経過: 00:00:32.22
1* ALTER TABLE table_1 ADD CONSTRAINT pk_table_1 PRIMARY KEY (id1, id2) USING INDEX pk_table_1

表が変更されました。

経過: 00:00:00.02

 

最後に、10046トレースで違い見る!
一目瞭然ですよね。 10046トレースでALTER文を拾ってみると、ID2列がNULLABLEdであるケースではexecuteで大量のqueryが発生していて、db file scattered readやdb file sequential readでオブジェクトを読みにに行っていることがわかります。これようするに該当列値に NULLが存在しないことを保証しなければならない主キー制約の宿命なわけですが、この作業を軽くしてくれているので、table_2のid2列にあるようなNOT NULL制約です。これがあればメタ情報をみるだけで NULL は存在しないことを確認できちゃいますからね。処理時間の差はそこだけです!

SCOTT@orclpdb1> @dive_in
DROP TABLE table_1 PURGE
*
行1でエラーが発生しました。:
ORA-00942: 表またはビューが存在しません。

経過: 00:00:00.01
DROP TABLE table_2 PURGE
*
行1でエラーが発生しました。:
ORA-00942: 表またはビューが存在しません。

経過: 00:00:00.01
1 CREATE TABLE table_1
2 (
3 id1 NUMBER NOT NULL
4 ,id2 NUMBER
5 ,text VARCHAR2(10)
6* )

表が作成されました。

名前 NULL? 型
----------------------------------------- -------- ----------------------------
ID1 NOT NULL NUMBER
ID2 NUMBER
TEXT VARCHAR2(10)

1 CREATE TABLE table_2
2 (
3 id1 NUMBER NOT NULL
4 ,id2 NUMBER NOT NULL
5 ,text VARCHAR2(10)v 6* )

表が作成されました。

名前 NULL? 型
----------------------------------------- -------- ----------------------------
ID1 NOT NULL NUMBER
ID2 NOT NULL NUMBER
TEXT VARCHAR2(10)

1 BEGIN
2 FOR i in 1..15000000 LOOP
3 INSERT ALL
4 INTO table_1 VALUES(i,i,i)
5 INTO table_2 VALUES(i,i,i)
6 SELECT null FROM dual
7 ;
8 IF MOD(i,1000) = 0
9 THEN
10 COMMIT;
11 END IF;
12 END LOOP;
13* END;

PL/SQLプロシージャが正常に完了しました。

1* CREATE UNIQUE INDEX pk_table_1 ON table_1(id1, id2)

索引が作成されました。

1* CREATE UNIQUE INDEX pk_table_2 ON table_2(id1, id2)

索引が作成されました。

Oracle Database 21c Enterprise Edition Release 21.0.0.0.0 - Production
Version 21.3.0.0.0との接続が切断されました。
接続されました。

セッションが変更されました。

...略...

経過: 00:00:00.02
1* ALTER TABLE table_1 ADD CONSTRAINT pk_table_1 PRIMARY KEY (id1, id2) USING INDEX pk_table_1

表が変更されました。

経過: 00:00:07.12

セッションが変更されました。

経過: 00:00:00.01
Oracle Database 21c Enterprise Edition Release 21.0.0.0.0 - Production
Version 21.3.0.0.0との接続が切断されました。
接続されました。

...略...

セッションが変更されました。

経過: 00:00:00.01
1* ALTER TABLE table_2 ADD CONSTRAINT pk_table_2 PRIMARY KEY (id1, id2) USING INDEX pk_table_2

表が変更されました。

経過: 00:00:00.05

セッションが変更されました。

経過: 00:00:00.00
Oracle Database 21c Enterprise Edition Release 21.0.0.0.0 - Production
Version 21.3.0.0.0との接続が切断されました。
接続されました。

表が削除されました。

表が削除されました。

[oracle@localhost ~]$ ls -lrt $ORACLE_BASE/diag/rdbms/orclcdb/ORCLCDB/trace/*table_1*
-rw-r-----. 1 oracle oinstall 48367 8月 28 20:11 /opt/oracle/diag/rdbms/orclcdb/ORCLCDB/trace/ORCLCDB_ora_7254_10046_table_1_slow.trm
-rw-r-----. 1 oracle oinstall 348949 8月 28 20:11 /opt/oracle/diag/rdbms/orclcdb/ORCLCDB/trace/ORCLCDB_ora_7254_10046_table_1_slow.trc
[oracle@localhost ~]$ tkprof /opt/oracle/diag20dbms/orclcdb/ORCLCDB/trace/ORCLCDB_ora_7254_10046_table_1_slow.trc ORCLCDB_ora_7254_10046_table_1_slow.txt sys=yes waits=yes

TKPROF: Release 21.0.0.0.0 - Development on 水 8月 28 20:14:16 2024

[oracle@localhost ~]$ view ORCLCDB_ora_7254_10046_table_1_slow.txt

...略...

SQL ID: 48v4hxnmpykdy Plan Hash: 0

ALTER TABLE table_1 ADD CONSTRAINT pk_table_1 PRIMARY KEY (id1, id2) USING
INDEX pk_table_1

call count cpu elapsed disk query current rows
------- ------ -------- ---------- ---------- ---------- ---------- ----------
Parse 1 0.01 0.02 2 4 0 0
Execute 1 4.15 6.85 48874 52229 2 0
Fetch 0 0.00 0.00 0 0 0 0
------- ------ -------- ---------- ---------- ---------- ---------- ----------
total 2 4.16 6.88 48876 52233 2 0

Misses in library cache during parse: 1
Optimizer mode: ALL_ROWS
Parsing user id: 109

Elapsed times include waiting on following events:
Event waited on Times Max. Wait Total Waited
---------------------------------------- Waited ---------- ------------
Disk file operations I/O 1 0.00 0.00
db file scattered read 1425 0.01 2.03
db file sequential read 54 0.00 0.04
Compression analysis 9 0.00 0.00
log file sync 1 0.00 0.00
PGA memory operation 1 0.00 0.00
SQL*Net message to client 1 0.00 0.00
SQL*Net message from client 1 0.00 0.00
********************************************************************************

...略...


[oracle@localhost ~]$ ls -lrt $ORACLE_BASE/diag/rdbms/orclcdb/ORCLCDB/trace/*table_2*
-rw-r-----. 1 oracle oinstall 15431 8月 28 20:11 /opt/oracle/diag/rdbms/orclcdb/ORCLCDB/trace/ORCLCDB_ora_7256_10046_table_2_fast.trm
-rw-r-----. 1 oracle oinstall 67970 8月 28 20:11 /opt/oracle/diag/rdbms/orclcdb/ORCLCDB/trace/ORCLCDB_ora_7256_10046_table_2_fast.trc
[oracle@localhost ~]$
[oracle@localhost ~]$ tkprof /opt/oracle/diag/rdbms/orclcdb/ORCLCDB/trace/ORCLCDB_ora_7256_10046_table_2_fast.trc ORCLCDB_ora_7256_10046_table_2_fast.txt sys=yes waits=yes

TKPROF: Release 21.0.0.0.0 - Development on 水 8月 28 20:17:33 2024

[oracle@localhost ~]$ view ORCLCDB_ora_7256_10046_table_2_fast.txt

...略...

SQL ID: 7x7hqgxcpkyky Plan Hash: 0

ALTER TABLE table_2 ADD CONSTRAINT pk_table_2 PRIMARY KEY (id1, id2) USING
INDEX pk_table_2

call count cpu elapsed disk query current rows
------- ------ -------- ---------- ---------- ---------- ---------- ----------
Parse 1 0.00 0.01 2 4 0 0
Execute 1 0.00 0.00 3 41 2 0
Fetch 0 0.00 0.00 0 0 0 0
------- ------ -------- ---------- ---------- ---------- ---------- ----------
total 2 0.01 0.01 5 45 2 0

Misses in library cache during parse: 1
Optimizer mode: ALL_ROWS
Parsing user id: 109

Elapsed times include waiting on following events:
Event waited on Times Max. Wait Total Waited
---------------------------------------- Waited ---------- ------------
Compression analysis 9 0.00 0.00
SQL*Net message to client 1 0.00 0.00
SQL*Net message from client 1 0.00 0.00
********************************************************************************

 

 

(参考)利用したスクリプト

[oracle@localhost ~]$ cat why_do_you_think_it_is_slow_adding_the_primary_constraint.sql
set line 80
DROP TABLE table_1 PURGE;
DROP TABLE table_2 PURGE;

CREATE TABLE table_1
(
id1 NUMBER NOT NULL
,id2 NUMBER
,text VARCHAR2(10)
)
.
l
/
desc table_1

CREATE TABLE table_2
(
id1 NUMBER NOT NULL
,id2 NUMBER NOT NULL
,text VARCHAR2(10)
)
.
l
/
desc table_2

set line 400

BEGIN
FOR i in 1..15000000 LOOP
INSERT ALL
INTO table_1 VALUES(i,i,i)
INTO table_2 VALUES(i,i,i)
SELECT null FROM dual
;
IF MOD(i,1000) = 0
THEN
COMMIT;
END IF;
END LOOP;
END;
.
l
/

ALTER TABLE table_1 ADD CONSTRAINT pk_table_1 PRIMARY KEY (id1, id2) USING INDEX
.
l
/

ALTER TABLE table_2 ADD CONSTRAINT pk_table_2 PRIMARY KEY (id1, id2) USING INDEX
.
l
/

ALTER TABLE table_1 DROP PRIMARY KEY DROP INDEX
.
l
/
ALTER TABLE table_2 DROP PRIMARY KEY DROP INDEX
.
l
/

CREATE UNIQUE INDEX pk_table_1 ON table_1(id1, id2)
.
l
/
CREATE UNIQUE INDEX pk_table_2 ON table_2(id1, id2)
.
l
/
ALTER TABLE table_1 ADD CONSTRAINT pk_table_1 PRIMARY KEY (id1, id2) USING INDEX pk_table_1
.
l
/
ALTER TABLE table_2 ADD CONSTRAINT pk_table_2 PRIMARY KEY (id1, id2) USING INDEX pk_table_2
.
l
/

@gather_tab_stats table_1
@gather_tab_stats table_2

col segment_name for a30
SELECT
SEGMENT_NAME
,BLOCKS * 8 / 1024 AS "MB"
FROM
USER_SEGMENTS
WHERE
SEGMENT_NAME IN (
'TABLE_1'
,'TABLE_2'
,'PK_TABLE_1'
,'PK_TABLE_2'
)
ORDER BY
SEGMENT_NAME
;

PROMPT **************** table_1のid2列をNOT NULLに変更 ******************
ALTER TABLE table_1 DROP PRIMARY KEY DROP INDEX
.
l
/

set line 80
PROMPT 変更前のtable_1
desc table_1
ALTER TABLE table_1 MODIFY id2 NUMBER NOT NULL
.
l
/
PROMPT 変更後のtabl_1 (table_2と同一定義になっています!) id2はNULLABLE から NOT NULL へ変更されました
desc table_1
set line 400

PROMPT ****** table_2同様の処理時間となり大きく改善! *******
ALTER TABLE table_1 ADD CONSTRAINT pk_table_1 PRIMARY KEY (id1, id2) USING INDEX
.
l
/

PROMPT ***** 主キー制約の追加部分に絞って改善を確認してみましょう!
ALTER TABLE table_1 DROP PRIMARY KEY DROP INDEX
.
l
/
CREATE UNIQUE INDEX pk_table_1 ON table_1(id1, id2)
.
l
/

PROMPT ****** table_2同様の処理時間となり大きく改善! *****
ALTER TABLE table_1 ADD CONSTRAINT pk_table_1 PRIMARY KEY (id1, id2) USING INDEX pk_table_1
.
l
/

 

(参考)内部で利用している統計情報取得スクリプト

[oracle@localhost ~]$ cat gather_tab_stats.sql
set verify on
eXEC DBMS_STATS.GATHER_TABLE_STATS(ownname=>'SCOTT',tabname=>upper('&1'),cascade=>true,no_invalidate=>false);
set verify off
undefine 1

 

(参考)10046トレース状況を覗くためのスクリプト

[oracle@localhost ~]$ cat dive_in.sql
set line 80
DROP TABLE table_1 PURGE;
DROP TABLE table_2 PURGE;

CREATE TABLE table_1
(
id1 NUMBER NOT NULL
,id2 NUMBER
,text VARCHAR2(10)
)
.
l
/
desc table_1

CREATE TABLE table_2
(
id1 NUMBER NOT NULL
,id2 NUMBER NOT NULL
,text VARCHAR2(10)
)
.
l
/
desc table_2

set line 400
BEGIN
FOR i in 1..15000000 LOOP
INSERT ALL
INTO table_1 VALUES(i,i,i)
INTO table_2 VALUES(i,i,i)
SELECT null FROM dual
;
IF MOD(i,1000) = 0
THEN
COMMIT;
END IF;
END LOOP;
END;
.
l
/

CREATE UNIQUE INDEX pk_table_1 ON table_1(id1, id2)
.
l
/
CREATE UNIQUE INDEX pk_table_2 ON table_2(id1, id2)
.
l
/

disconnect

connect scott/[パスワード]@orclpdb1
ALTER SESSION SET tracefile_identifier='10046_table_1_slow';
ALTER SESSION SET timed_statistics = true;
ALTER SESSION SET statistics_level=all;
ALTER SESSION SET max_dump_file_size = unlimited;
ALTER SESSION SET events '10046 trace name context forever,level 12';
ALTER TABLE table_1 ADD CONSTRAINT pk_table_1 PRIMARY KEY (id1, id2) USING INDEX pk_table_1
.
l
/
ALTER SESSION SET events '10046 trace name context off';
disconnect

connect scott/[パスワード]@orclpdb1
ALTER SESSION SET tracefile_identifier='10046_table_2_fast';
ALTER SESSION SET timed_statistics = true;
ALTER SESSION SET statistics_level=all;
ALTER SESSION SET max_dump_file_size = unlimited;
ALTER SESSION SET events '10046 trace name context forever,level 12';
ALTER TABLE table_2 ADD CONSTRAINT pk_table_2 PRIMARY KEY (id1, id2) USING INDEX pk_table_2
.
l
/
ALTER SESSION SET events '10046 trace name context off';
disconnect

connect scott/[パスワード]@orclpdb1
DROP TABLE table_1 PURGE;
DROP TABLE table_2 PURGE;

 


酷暑じゃなければ、台風起因の大雨の東京より。
みなさん、安全最優先で!

ではまた。

 

| | | コメント (0)

2016年3月27日 (日)

OracleのB*Tree索引にはNULLが含まれる場合があるんです! - その性質を使ってチューニングすることもあるよ:) その3



前回までのMac De Oracle


OracleのB*Tree索引にはNULLが含まれる場合があるんです! - その性質を使ってチューニングすることもあるよ:)

OracleのB*Tree索引にはNULLが含まれる場合があるんです! - その性質を使ってチューニングすることもあるよ:) その2



ということで、続きで〜〜すっ。

こんな表定義で

orcl@SCOTT> desc tab01
Name Null? Type
--------- -------- --------
FOO NUMBER
BAR NUMBER
HOGE NOT NULL CHAR(2)
ID NOT NULL NUMBER

こんな索引があって

****** Index column info : tab01 ******

INDEX_NAME COLUMN_NAME DESC
------------------------------ ------------------------------ ----
IX1_TAB01 FOO ASC

IX2_TAB01 BAR ASC
FOO ASC

IX3_TAB01 ID ASC
FOO ASC

IX4_TAB01 ID ASC
BAR ASC
FOO ASC

PK_TAB01 ID ASC


こんなデータで

orcl@SCOTT> set null [NULL]
orcl@SCOTT> select * from tab01

FOO BAR HO ID
---------- ---------- -- ----------
[NULL] [NULL] ** 1
1 [NULL] ** 2
[NULL] 1 ** 3
1 1 ** 4


2列の複合索引、どちらの列もNULLだと、やはり、NULLは索引に含まれないので IS NULL検索だと索引は利用されないですよねぇ〜。このような状態ではヒントで索引利用を強制利用させようとしても無理です。

orcl@SCOTT> r
1 select
2 /*+
3 gather_plan_statistics
4 index(tab01 ix2_tab01)
5 no_index(tab01 ix4_tab01)
6 no_index(tab01 ix3_tab01)
7 */
8 *
9 from
10 tab01
11 where
12 foo is null
13* and bar is null
     
FOO BAR HO ID
---------- ---------- -- ----------
[NULL] [NULL] ** 1

・・・略・・・
-------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 8 |
|* 1 | TABLE ACCESS FULL| TAB01 | 1 | 1 | 1 |00:00:00.01 | 8 |
-------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

1 - filter(("FOO" IS NULL AND "BAR" IS NULL))

でも、NOT NUL制約の列が1列でも含まれている索引であればNULLは索引に含まれます。(前回までの復習も兼ねた確認)
第1列がWHERE句で記述されていないので索引スキップスキャンになっていますが.....IS NULL検索を索引アクセスだけで行っているのがよく分かる例の一つです:)

orcl@SCOTT> r
1 select
2 /*+
3 gather_plan_statistics
4 */
5 *
6 from
7 tab01
8 where
9 foo is null
10* and bar is null

FOO BAR HO ID
---------- ---------- -- ----------
[NULL] [NULL] ** 1

・・・略・・・
--------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads |
--------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 3 | 1 |
| 1 | TABLE ACCESS BY INDEX ROWID BATCHED| TAB01 | 1 | 1 | 1 |00:00:00.01 | 3 | 1 |
|* 2 | INDEX SKIP SCAN | IX4_TAB01 | 1 | 1 | 1 |00:00:00.01 | 2 | 1 |
--------------------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

2 - access("BAR" IS NULL AND "FOO" IS NULL)
filter(("FOO" IS NULL AND "BAR" IS NULL))


最後にもう少しわかりやすい例を。

SQL文を少々書き換えてIndex Only Scanになるようにしました。索引にNULLが含まれていないと索引だけのアクセスで済むわけがないわけで、これ以上わかりやすい例はないと思います:)

まず、索引が利用できない例から。
FOO列とBAR列だけの複合索引をヒントで強制利用させようとしていますが、この索引は2列ともnullableなので2列をIS NULL検索しても索引が利用されません!
全ての列がNULLである場合、キーエントリーは索引に作成されない。単一列でも複合索引でも同じであることが確認できます。

orcl@SCOTT> r
1 select
2 /*+
3 gather_plan_statistics
4 index(tab01 ix2_tab01)
5 no_index(tab01 ix4_tab01)
6 no_index(tab01 ix3_tab01)
7 */
8 *
9 from
10 tab01
11 where
12 foo is null
13* and bar is null

FOO BAR HO ID
---------- ---------- -- ----------
[NULL] [NULL] ** 1

・・・略・・・
-------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 8 |
|* 1 | TABLE ACCESS FULL| TAB01 | 1 | 1 | 1 |00:00:00.01 | 8 |
-------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

1 - filter(("FOO" IS NULL AND "BAR" IS NULL))

おっと、
大切なのを忘れてました。

FOO列(nullable(、BAR列(nullable)の複合索引を IS NOT NULL AND IS NULLで検索した場合はどうなるか?
答えは以下の通り。IS NULL と IS NOT NULLの組み合わせでも、索引が利用されます。:)

BAR IS NULLで範囲検索しFOO IS NOT NULLでフィルタリングしています。 NULLが含まれていないと不可能な索引レンジスキャンと索引読み時のフィルタリング!

orcl@SCOTT> r
1 select
2 /*+
3 gather_plan_statistics
4 no_index(tab01 ix3_tab01)
5 no_index(tab01 ix4_tab01)
6 */
7 foo
8 , bar
9 from
10 tab01
11 where
12 foo is not null
13* and bar is null

FOO BAR
---------- ----------
1 [NULL]

・・・略・・・
-------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads |
-------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.02 | 1 | 1 |
|* 1 | INDEX RANGE SCAN| IX2_TAB01 | 1 | 1 | 1 |00:00:00.02 | 1 | 1 |
-------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------

1 - access("BAR" IS NULL)
filter("FOO" IS NOT NULL)

その逆も!

orcl@SCOTT> r
1 select
2 /*+
3 gather_plan_statistics
4 no_index(tab01 ix3_tab01)
5 no_index(tab01 ix4_tab01)
6 */
7 foo
8 , bar
9 from
10 tab01
11 where
12 foo is null
13* and bar is not null

FOO BAR
---------- ----------
[NULL] 1

・・・略・・・
----------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
----------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 2 |
|* 1 | INDEX SKIP SCAN | IX2_TAB01 | 1 | 1 | 1 |00:00:00.01 | 2 |
----------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

1 - access("FOO" IS NULL)
filter(("FOO" IS NULL AND "BAR" IS NOT NULL))


もういっちょ!
2列とものnullableな索引だとindex fast full scanにはできないけど、index full scanにはできるんですよ〜。

orcl@SCOTT7gt; r
1 select
2 /*+
3 gather_plan_statistics
4 no_index(tab01 ix3_tab01)
5 no_index(tab01 ix4_tab01)
6 */
7 foo
8 , bar
9 from
10 tab01
11 where
12 foo is not null
13* and bar is not null

FOO BAR
---------- ----------
1 1

・・・略・・・
----------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
----------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 2 |
|* 1 | INDEX FULL SCAN | IX2_TAB01 | 1 | 1 | 1 |00:00:00.01 | 2 |
----------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

1 - filter(("FOO" IS NOT NULL AND "BAR" IS NOT NULL))

Note
-----
- statistics feedback used for this statement

後に、NOT NULL列が含まれる索引なら index fast full scanでもできるはず!
独り言;index only scan+index fast full scanなんてのもセグメントサイズが小さければ物理読み込み量削減には効果があるんですよねぇ〜 ;-)

orcl@SCOTT> r
1 select
2 /*+
3 gather_plan_statistics
4 no_index(tab01 ix3_tab01)
5 index_ffs(tab01 ix4_tab01)
6 no_index(tab01 ix2_tab01)
7 */
8 foo
9 , bar
10 from
11 tab01
12 where
13 foo is not null
14* and bar is not null

FOO BAR
---------- ----------
1 1

・・・略・・・
-----------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | Reads |
-----------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.03 | 4 | 1 |
|* 1 | INDEX FAST FULL SCAN| IX4_TAB01 | 1 | 1 | 1 |00:00:00.03 | 4 | 1 |
-----------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

1 - filter(("FOO" IS NOT NULL AND "BAR" IS NOT NULL))


オラクルの索引にNULLは絶対含まれない、というのは都市伝説! 
IS NOT NULLは索引使えないとかIS NULLは索引使えないというというのも違うんですよね。 

OracleのB*Tree索引では、索引に含まれる全列がNULLの場合以外はNULLが含まれてまっす! というのが正しいですよね!?

この手の問題でピンチになったら、思い出してみてください ;)
USE THE INDEX ONLY SCAN, LUKE! w

| | | コメント (0) | トラックバック (0)

2016年3月21日 (月)

OracleのB*Tree索引にはNULLが含まれる場合があるんです! - その性質を使ってチューニングすることもあるよ:) その2

OracleのB*Tree索引にはNULLが含まれる場合があるんです! - その性質を使ってチューニングすることもあるよ:)
の続きです。

念のために、もう一つの主役NULL登場してもらいましょう。

IX1_TAB01索引はNullableなFOO列だけの索引なので、このタイプの索引ではNULLは索引に格納されることはありません。
いくらヒントで索引を指定しても索引にはNULLは格納されていないので全表走査になるはず。

注):索引が多いので意図した索引を利用するようにヒントで固定しています
例2)FOO IS NULL で検索

orcl@SCOTT> r
1 SELECT
2 /*+
3 gather_plan_statistics
4 index(tab01 ix1_tab01)
5 no_index(tab01 ix3_tab01)
6 no_index(tab01 ix4_tab01)
7 */
8 *
9 FROM
10 tab01
11 WHERE
12* foo IS NULL


FOO BAR HO ID
---------- ---------- -- ----------
[NULL] [NULL] ** 1
[NULL] 1 ** 3


ーーー中略ーーー
Plan hash value: 2044041692

-------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 2 |00:00:00.01 | 8 |
|* 1 | TABLE ACCESS FULL| TAB01 | 1 | 2 | 2 |00:00:00.01 | 8 |
-------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

1 - filter("FOO" IS NULL)

想定通り。NULLは格納されていないので索引が利用できず全表走査になっています。
索引からROWIDを取得できないので全表走査してフィルタリングしている箇所がポイントです。

OracleのB*Tree索引にはNULLが入らない都市伝説の始まりはここだったんじゃないか? とい言っている方がいたのですが、ここだけの話が広まってしまい

OracleのB*Tree索引にはNULLは絶対格納されない。

という都市伝説になってしまったのだろうと。。確かに入ってないですからねー。 (^^;;;

では、NULLが索引に含まれる一例を見てみましょう。

例3)ID列(NOT NULL)とFOO列(Nullable)で作成した複合索引を ID=1 AND FOO IS NULLで検索
注):索引が多いので意図した索引を利用するようにヒントで固定しています

Predicate Informationセクションのaccess predicateを見るとわかると思いますが、ID=1 AND FOO IS NULLで索引をアクセスしています。
NULLが索引に含まれていない場合はこのような状況にはなりません。 
複合索引では1列でもNOT NULL制約がり他の列がNullableである場合、NULLは格納されます。

NULLは格納されることもあるんです。マニュアル上の表現は少々わかりにくいとは思いますが、このような状況を指しています。

orcl@SCOTT> r
1 SELECT
2 /*+
3 gather_plan_statistics
4 no_index(tab01 ix1_tab01)
5 index(tab01 ix3_tab01)
6 no_index(tab01 ix4_tab01)
7 */
8 *
9 FROM
10 tab01
11 WHERE
12 id = 1
13* AND foo IS NULL

FOO BAR HO ID
---------- ---------- -- ----------
[NULL] [NULL] ** 1

ーーー中略ーーー
Plan hash value: 2558346564

-----------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-----------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 3 |
| 1 | TABLE ACCESS BY INDEX ROWID BATCHED| TAB01 | 1 | 1 | 1 |00:00:00.01 | 3 |
|* 2 | INDEX RANGE SCAN | IX3_TAB01 | 1 | 1 | 1 |00:00:00.01 | 2 |
-----------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

2 - access("ID"=1 AND "FOO" IS NULL)


少し寄り道。

そういえば、偶になんですが、IS NOT NULLだと索引使うことはないという都市伝説も聞いたことがあります。
それも都市伝説なんですよね。
次の例は、その都市伝説を覆す一例


例4)FOO IS NOT NULL で検索

IX1_TAB01索引は、NullableなFOO列だけの索引です。これまでの検証でこのタイプの索引にはNULLが格納されていないことは確認できると思います。
ようするに、NULLじゃないものは、まるっととズバッと索引に含まれているはずでっす!
なので、FOO IS NOT NULLという検索条件だと索引を使ってくれます。

このような性質を知っているとチューニングに役立つんですよ。
(この例のようなチューニング方法を使ったことは過去数度あるんですよ、苦肉の策でしたけどw NULLの数が非常に多い場合、この手の索引のセグメントサイズは非常に小さくなります(索引に格納されるエントリそのものがNULLを除くと非常に少ない場合)。その性質を利用したチューニング法もあるんです。)

余談NOTE:
INDEX FAST FULL SCANにならない理由:索引にNOT NULL制約の列が少なくとも1列あること、という前提条件を満たせていないからです。チューニングや設計時に必要な知識なので覚えておくと何かの時に役に立つと思います:)

注):索引が多いので意図した索引を利用するようにヒントで固定しています

orcl@SCOTT> r
1 SELECT
2 /*+
3 gather_plan_statistics
4 index(tab01 ix1_tab01)
5 no_index(tab01 ix3_tab01)
6 no_index(tab01 ix4_tab01)
7 */
8 *
9 FROM
10 tab01
11 WHERE
12* foo IS NOT NULL


FOO BAR HO ID
---------- ---------- -- ----------
1 [NULL] ** 2
1 1 ** 4

ーーー中略ーーー
Plan hash value: 2840602802

-----------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-----------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 2 |00:00:00.01 | 4 |
| 1 | TABLE ACCESS BY INDEX ROWID BATCHED| TAB01 | 1 | 2 | 2 |00:00:00.01 | 4 |
|* 2 | INDEX FULL SCAN | IX1_TAB01 | 1 | 2 | 2 |00:00:00.01 | 2 |
-----------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

2 - filter("FOO" IS NOT NULL)


恐竜博とか**博ってやつは、最後のショップがメインで博のほうはおまけなんじゃないかと最近思ってるw 子供達の目が活き活きしているのはショップに入ってからだ!w

次回へつづく。

| | | コメント (0) | トラックバック (0)

2016年3月20日 (日)

OracleのB*Tree索引にはNULLが含まれる場合があるんです! - その性質を使ってチューニングすることもあるよ:)

先日、OracleのB*Tree索引には絶対にNULLが含まれないって思い込んでる人が意外にいるよね〜とか。
OracleのB*Tree索引には絶対NULLが含まれないって都市伝説があるのはなんでだろう。
Oracle® Database概要12cリリース1 (12.1) - 一意索引と非一意索引

みたいなことが話題になって、
ある一人が、「だよね〜、ダンプ見れば含まれてるのわかります。」って言ってて、それ、ふつ〜の人は見ないからw

と思いつつ、私の周りには、やはり、変態が多いことに改めて気づいた次第です。:) はい。


で、
変態じゃない、ごく一般的なエンジニアの方々(ブロックダンプを華麗かつ自然にキメちゃわない方々)向けに、

OracleのB*Tree索引にNULLが含まれているか、NULLが含まれていないかの簡単な確認方法をお伝えしなければ!w

ということで、数回にわけて書いておこうかと思ってます。(予定は未定w)

(実は、このネタとほぼ同じことを1年前ぐらい前に、ローカルかつクローズドな勉強会?、でも使ってました。 最近、やってないみたいですけど)

環境は最近の定番 Oracle Database 12c R1 EE

orcl@SCOTT> select banner from v$version;

BANNER
--------------------------------------------------------------------------------
Oracle Database 12c Enterprise Edition Release 12.1.0.2.0 - 64bit Production
PL/SQL Release 12.1.0.2.0 - Production
CORE 12.1.0.2.0 Production
TNS for Linux: Version 12.1.0.2.0 - Production
NLSRTL Version 12.1.0.2.0 - Production


SCOTTスキーマに以下のような表と索引を作成し少ないですが、データを登録
今回の主役は索引とNULLなのでデータ量少なくても必要なパターンが登録できていれば十分です。
(データ量を増やせばチューニングのお題の元ネタにもなると思います。)

orcl@SCOTT> create table tab01 (foo number, bar number, hoge char(2) not null,id number not null);

Table created.

orcl@SCOTT> insert into tab01 values(null,null,'**',1);

1 row created.

orcl@SCOTT> insert into tab01 values(1,null,'**',2);

1 row created.

orcl@SCOTT> insert into tab01 values(null,1,'**',3);

1 row created.

orcl@SCOTT> insert into tab01 values(1,1,'**',4);

1 row created.

orcl@SCOTT> commit;

Commit complete.

orcl@SCOTT> alter table tab01 add constraint pk_tab01 primary key(id) using index;

Table altered.

orcl@SCOTT> exec dbms_stats.gather_table_stats(ownname=>'SCOTT',tabname=>'TAB01',cascade=>true,no_invalidate=>false);

PL/SQL procedure successfully completed.

orcl@SCOTT> create index ix1_tab01 on tab01 (foo);

Index created.

orcl@SCOTT> create index ix2_tab01 on tab01 (bar,foo);

Index created.

orcl@SCOTT> create index ix3_tab01 on tab01(id,foo);

Index created.

orcl@SCOTT> create index ix4_tab01 on tab01(id,bar,foo);

Index created.


FOOとBAR列はNullableにしています

orcl@SCOTT> desc tab01
Name Null? Type
----------------------------------------- -------- ----------------------------
FOO NUMBER
BAR NUMBER
HOGE NOT NULL CHAR(2)
ID NOT NULL NUMBER

主演の索引たち

NULLが索引でどう扱われるかを確認するため、事前作成の索引をたくさん用意してしました。後から作るの面倒なのでw
これだけ類似索引も含めて多数の索引があると確認時に意図した索引が利用されない可能性も高いため、検証では内容に合わせて利用する索引をヒントで指定することにします。

ちなみに、
普通の環境でこんなに索引があったら、アンチパターン:インデックスショットガンですからね。ご注意ください:) たまにこのような環境に遭遇することはありますが。。。

IX1_TAB01はnullableなFOO列だけの単一列索引
IX2_TAB01はnullableなBAR列とFOO列からなる複合索引
IX3_TAB01とTX4_TAB01は上記の列に加え主キー列のID列を第1キーとする複合索引

としてあります。

orcl@SCOTT> break on table_name on index_name skip 1
orcl@SCOTT> select table_name,index_name,column_name from user_ind_columns where table_name='TAB01' order by table_name,index_name,column_position;

TABLE_NAME INDEX_NAME COLUMN_NAME
------------------------------ ------------------------------ ------------------------------
TAB01 IX1_TAB01 FOO

IX2_TAB01 BAR
FOO

IX3_TAB01 ID
FOO

IX4_TAB01 ID
BAR
FOO

PK_TAB01 ID

登録したデータは以下の通り
NULLは索引とともに今回の主役なので、どこがNULLになっているかメモしておいてくださいませ。

orcl@SCOTT> set null [NULL]
orcl@SCOTT> select * from tab01 order by id;

FOO BAR HO ID
---------- ---------- -- ----------
[NULL] [NULL] ** 1
1 [NULL] ** 2
[NULL] 1 ** 3
1 1 ** 4

さて、索引にNULLが含まれているか、いないか、どうやって確認すると思います? ブロックダンプを華麗にキメきめる以外の方法でw

SQLチューニングをしたことがある方なら一般的に利用してい(と思ってる)機能で簡単に確認できちゃうんですよ。これが。

SQL*Plusのautotraceや、explain plan それに、DBMS_XPLAN.DISPLAY* なファンクションでも確認できます。
SQLclはautotraceにはまだ未対応となっているようですが、それ以外の方法ならはできるようです。
(今回はSQL*Plusを使い、dbms_xplan.display_cursor(format=>'ALLSTATS LAST'))で確認します)


どうやって、どの部分で確認するのか? 
autottraceやDBMS_XPLAN.DISPLAY*などでリストされるpredicate information部分のaccess/filter predicate部分で確認できるんです!


例1)FOO = 1 で検索した例

注):索引が多いので意図した索引を利用するようにヒントで固定しています

おわかりでしょうか?
Predicate Infomation部分から、Id=2のINDEX RANGE SCANで IX1_TAB01索引を FOO=1でアクセスしていることが読み取れますよね?
つまり、1 という値が索引に含まれている(含まれる)からaccess pathとしてIX1_TAB01索引を参照しているわけです。

orcl@SCOTT> r
1 SELECT
2 /*+
3 gather_plan_statistics
4 index(tab01 ix1_tab01)
5 */
6 *
7 FROM
8 tab01
9 WHERE
10* foo = 1

FOO BAR HO ID
---------- ---------- -- ----------
1 [NULL] ** 2
1 1 ** 4

orcl@SCOTT> select * from table(dbms_xplan.display_cursor(format=>'ALLSTATS LAST'));

ーーー中略ーーー

Plan hash value: 3795960549

-----------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-----------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 2 |00:00:00.01 | 4 |
| 1 | TABLE ACCESS BY INDEX ROWID BATCHED| TAB01 | 1 | 2 | 2 |00:00:00.01 | 4 |
|* 2 | INDEX RANGE SCAN | IX1_TAB01 | 1 | 2 | 2 |00:00:00.01 | 2 |
-----------------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

2 - access("FOO"=1)


ということで、今日は馬事公苑の散歩は気持ちいいよ〜。

次回へつづく。

| | | コメント (0) | トラックバック (0)