Vector Indexes no Oracle Database: Inverted File Flat (IVF) indexes – Parte 2
Na parte 1 da série de posts sobre os Inverted File Flat (IVF) indexes, falei um pouco sobre o funcionamento desse tipo de índice, inclusive demonstrando o porquê de uma menor precisão acontecer quando esse tipo de estrutura é utilizada na busca vetorial. Neste post, quero mostrar um cenário prático no Oracle Database 26ai, onde irei realizar a criação e a busca vetorial através de um índice IVF.
Para isso, vou utilizar o laboratório criado para o post Sobre RAG e Vector Stores na Prática com Autonomous Database. Se você ainda não o leu, sugiro que o leia antes de seguir aqui, para que as coisas façam mais sentido. Vou estar utilizando o Python com LangChain rodando em um Google Colab nessa desmontração, uma vez que fazer queries com vetores de grandes dimensões se torna mais fácil através do script.
Todo o código usado neste tutorial está disponível no GitHub (acesse clicando aqui).
Análise inicial
Para começar, vou inicializar o ambiente, e gerar uma embedding a partir de uma pergunta:

Na sequência vou usar o bloco abaixo para passar a embedding gerada (um vetor de 1536 dimensões) como uma variável bind para a consulta:

Relembrando a estrutura da tabela, a coluna “embedding” armazena os vetores dos documentos vetorizados:

Após executar a consulta, o SQL ID da query pode ser identificado com:
alter session set nls_date_format='dd/mm/yyyy hh:mi:ss'; SELECT sql_id, child_number, last_active_time FROM v$sql WHERE sql_text LIKE '%teste_IVF%' AND sql_text NOT LIKE '%v$sql%';

Em seguida, vou habilitar um SQL Patch para o SQL ID, adicionando as hints GATHER_PLAN_STATISTICS, para coletar as estatísticas de execução do plano, e NO_PARALLEL, para evitar a execução em paralelo da query e deixar a compreensão do plano de execução mais simples:
DECLARE
x varchar2(100);
BEGIN
x := dbms_sqldiag.create_sql_patch(sql_id=>'fju056hgyramn',
hint_text=>'GATHER_PLAN_STATISTICS NO_PARALLEL',
name=> 'patch_gather_stats');
END;
/
Depois de executar novamente a consulta, vamos à análise do plano de execução:
SELECT * FROM TABLE (dbms_xplan.display_cursor ('fju056hgyramn',0,'ADVANCED ALLSTATS LAST'));
SQL_ID fju056hgyramn, child number 0
-------------------------------------
SELECT /*teste_IVF*/ text, JSON_VALUE(metadata,
'$.source') AS source, vector_distance(embedding,
TO_VECTOR(:vec), EUCLIDEAN) as distance FROM "ORACLE_DOCS" ORDER BY
distance FETCH APPROX FIRST 5 ROWS ONLY
Plan hash value: 3342055627
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows |E-Bytes|E-Temp | Cost (%CPU)| E-Time | A-Rows | A-Time | Buffers | Reads | OMem | 1Mem | Used-Mem |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | | | 12636 (100)| | 5 |00:00:00.54 | 29283 | 20041 | | | |
|* 1 | COUNT STOPKEY | | 1 | | | | | | 5 |00:00:00.54 | 29283 | 20041 | | | |
| 2 | VIEW | | 1 | 9792 | 37M| | 12636 (1)| 00:00:01 | 5 |00:00:00.54 | 29283 | 20041 | | | |
|* 3 | SORT ORDER BY STOPKEY| | 1 | 9792 | 32M| 38M| 12636 (1)| 00:00:01 | 5 |00:00:00.54 | 29283 | 20041 | 142K| 142K| 126K (0)|
| 4 | TABLE ACCESS FULL | ORACLE_DOCS | 1 | 9792 | 32M| | 5503 (1)| 00:00:01 | 9792 |00:00:01.20 | 29283 | 20041 | | | |
-----------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Aqui é possível observar que um TABLE ACCESS FULL está sendo feito, ou seja, uma Exact Search está sendo executada, pois o vetor passado na bind está sendo comparado com todos os vetores armazenados na tabela. Vale lembrar que foi especificada a sintaxe FETCH APPROX FIRST 5 ROWS ONLY na query, mas devido à ausência de um índice vetorial, a Exact Search foi feita. Nesse caso, a consulta está retornando um número elevado de buffers e com um tempo de execução em torno de 540ms.
Antes de criar o índice vetorial, vamos analisar os objetos que existem para o schema que estamos usando para os testes:
select segment_name, segment_type, sum(bytes)/1024/1024 as TAM_MB from dba_segments where owner='VECTOR' group by segment_name, segment_type;

Temos a tabela, o índice da PK da tabela, e três LOBs (e seus LOBINDEX), referentes às outras três colunas da tabela ORACLE_DOCS:
SELECT owner, table_name, column_name, segment_name, index_name, tablespace_name FROM dba_lobs WHERE table_name = 'ORACLE_DOCS';

Criação do índice vetorial
Agora vamos criar um índice IVF para a coluna embeddings, com uma precisão alvo de 90%. Para a criação desse tipo de index é utilizada a cláusula ORGANIZATION NEIGHBOR PARTITIONS:
CREATE VECTOR INDEX vector.idx_ivf_oracle_docs_01 ON vector.oracle_docs (embedding) ORGANIZATION NEIGHBOR PARTITIONS DISTANCE EUCLIDEAN WITH TARGET ACCURACY 90;
Agora com o índice criado, e após reexecutar a consulta, vamos analisar o plano de execução:
SELECT * FROM TABLE (dbms_xplan.display_cursor ('fju056hgyramn',0,'ADVANCED ALLSTATS LAST'));
SQL_ID fju056hgyramn, child number 0
-------------------------------------
SELECT /*teste_IVF*/ text, JSON_VALUE(metadata,
'$.source') AS source, vector_distance(embedding,
TO_VECTOR(:vec), EUCLIDEAN) as distance FROM "ORACLE_DOCS" ORDER BY
distance FETCH APPROX FIRST 5 ROWS ONLY
Plan hash value: 4242188579
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows |E-Bytes| Cost (%CPU)| E-Time | Pstart| Pstop | A-Rows | A-Time | Buffers | Reads | OMem | 1Mem | Used-Mem |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | | 350 (100)| | | | 5 |00:00:00.07 | 1220 | 909 | | | |
| 1 | VIEW | | 1 | 5 | 20050 | 350 (1)| 00:00:01 | | | 5 |00:00:00.07 | 1220 | 909 | | | |
| 2 | NESTED LOOPS | | 1 | 5 | 48525 | 350 (1)| 00:00:01 | | | 5 |00:00:00.07 | 1220 | 909 | | | |
| 3 | VIEW | VW_IVPSR_11E7D7DE | 1 | 5 | 80 | 345 (1)| 00:00:01 | | | 5 |00:00:00.07 | 1210 | 909 | | | |
|* 4 | COUNT STOPKEY | | 1 | | | | | | | 5 |00:00:00.07 | 1210 | 909 | | | |
| 5 | VIEW | VW_IVPSJ_578B79F1 | 1 | 670 | 12060 | 345 (1)| 00:00:01 | | | 5 |00:00:00.07 | 1210 | 909 | | | |
|* 6 | SORT ORDER BY STOPKEY | | 1 | 670 | 16080 | 345 (1)| 00:00:01 | | | 5 |00:00:00.07 | 1210 | 909 | 2048 | 2048 | 2048 (0)|
|* 7 | HASH JOIN | | 1 | 670 | 16080 | 344 (1)| 00:00:01 | | | 808 |00:00:00.01 | 1210 | 909 | 2546K| 2546K| 1232K (0)|
| 8 | PART JOIN FILTER CREATE | :BF0000 | 1 | 13 | 52 | 69 (2)| 00:00:01 | | | 13 |00:00:00.01 | 235 | 0 | | | |
| 9 | VIEW | VW_IVCR_B5B87E67 | 1 | 13 | 52 | 69 (2)| 00:00:01 | | | 13 |00:00:00.01 | 235 | 0 | | | |
|* 10 | COUNT STOPKEY | | 1 | | | | | | | 13 |00:00:00.01 | 235 | 0 | | | |
| 11 | VIEW | VW_IVCN_9A1D2119 | 1 | 190 | 2470 | 69 (2)| 00:00:01 | | | 13 |00:00:00.01 | 235 | 0 | | | |
|* 12 | SORT ORDER BY STOPKEY | | 1 | 190 | 1900 | 69 (2)| 00:00:01 | | | 13 |00:00:00.01 | 235 | 0 | 2048 | 2048 | 2048 (0)|
| 13 | TABLE ACCESS FULL | VECTOR$IDX_IVF_ORACLE_DOCS_01$126314_134514_0$IVF_FLAT_CENTROIDS | 1 | 190 | 1900 | 68 (0)| 00:00:01 | | | 190 |00:00:00.01 | 235 | 0 | | | |
| 14 | PARTITION LIST JOIN-FILTER| | 1 | 9792 | 191K| 21 (0)| 00:00:01 |:BF0000|:BF0000| 808 |00:00:00.01 | 975 | 909 | | | |
| 15 | TABLE ACCESS FULL | VECTOR$IDX_IVF_ORACLE_DOCS_01$126314_134514_0$IVF_FLAT_CENTROID_PARTITIONS | 13 | 9792 | 191K| 21 (0)| 00:00:01 |:BF0000|:BF0000| 808 |00:00:00.03 | 975 | 909 | | | |
| 16 | TABLE ACCESS BY USER ROWID | ORACLE_DOCS | 5 | 1 | 9689 | 1 (0)| 00:00:01 | | | 5 |00:00:00.01 | 10 | 0 | | | |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Query Block Name / Object Alias (identified by operation id):
-------------------------------------------------------------
1 - SEL$94C0F189 / "from$_subquery$_002"@"SEL$2"
2 - SEL$94C0F189
3 - SEL$E731354C / "VW_IVPSR_11E7D7DE"@"SEL$1"
4 - SEL$E731354C
5 - SEL$0C00A749 / "VW_IVPSJ_578B79F1"@"SEL$E731354C"
6 - SEL$0C00A749
9 - SEL$700CE8F1 / "VW_IVCR_B5B87E67"@"SEL$0C00A749"
10 - SEL$700CE8F1
11 - SEL$E5326247 / "VW_IVCN_9A1D2119"@"SEL$700CE8F1"
12 - SEL$E5326247
13 - SEL$E5326247 / "VTIX_CENTRD"@"SEL$E5326247"
15 - SEL$0C00A749 / "VTIX_CNPART"@"SEL$0C00A749"
16 - SEL$94C0F189 / "ORACLE_DOCS"@"SEL$1"
Outline Data
-------------
/*+
BEGIN_OUTLINE_DATA
IGNORE_OPTIM_EMBEDDED_HINTS
OPTIMIZER_FEATURES_ENABLE('23.1.0')
DB_VERSION('23.1.0')
OPT_PARAM('_fix_control' '20648883:0 26552730:1 27175987:0 29972495:0 22387320:0 30195773:0 31945701:1 32108311:1 33659818:3 34092979:1 35495824:1 33792497:1 36554842:1 36283175:1 31720959:1 36004220:1 36635255:1 36675198:1 36868551:1 37400112:1
37346200:0 37626161:1')
ALL_ROWS
FORCE_XML_QUERY_REWRITE
FORCE_JSON_TABLE_TRANSFORM
XML_DML_RWT_STMT
XMLINDEX_REWRITE
XMLINDEX_REWRITE_IN_SELECT
NO_COST_XML_QUERY_REWRITE
OUTLINE_LEAF(@"SEL$E5326247")
OUTLINE_LEAF(@"SEL$700CE8F1")
OUTLINE_LEAF(@"SEL$0C00A749")
OUTLINE_LEAF(@"SEL$E731354C")
OUTLINE_LEAF(@"SEL$94C0F189")
VECTOR_INDEX_TRANSFORM(@"SEL$1" "ORACLE_DOCS"@"SEL$1" "IDX_IVF_ORACLE_DOCS_01" PRE_FILTER_WITHOUT_JOIN_BACK)
OUTLINE_LEAF(@"SEL$2")
OUTLINE(@"SEL$1")
NO_ACCESS(@"SEL$2" "from$_subquery$_002"@"SEL$2")
NO_ACCESS(@"SEL$94C0F189" "VW_IVPSR_11E7D7DE"@"SEL$1")
ROWID(@"SEL$94C0F189" "ORACLE_DOCS"@"SEL$1")
LEADING(@"SEL$94C0F189" "VW_IVPSR_11E7D7DE"@"SEL$1" "ORACLE_DOCS"@"SEL$1")
USE_NL(@"SEL$94C0F189" "ORACLE_DOCS"@"SEL$1")
NO_ACCESS(@"SEL$E731354C" "VW_IVPSJ_578B79F1"@"SEL$E731354C")
NO_ACCESS(@"SEL$0C00A749" "VW_IVCR_B5B87E67"@"SEL$0C00A749")
FULL(@"SEL$0C00A749" "VTIX_CNPART"@"SEL$0C00A749")
LEADING(@"SEL$0C00A749" "VW_IVCR_B5B87E67"@"SEL$0C00A749" "VTIX_CNPART"@"SEL$0C00A749")
USE_HASH(@"SEL$0C00A749" "VTIX_CNPART"@"SEL$0C00A749")
NO_ACCESS(@"SEL$700CE8F1" "VW_IVCN_9A1D2119"@"SEL$700CE8F1")
FULL(@"SEL$E5326247" "VTIX_CENTRD"@"SEL$E5326247")
END_OUTLINE_DATA
*/
Predicate Information (identified by operation id):
---------------------------------------------------
4 - filter(ROWNUM<=5)
6 - filter(ROWNUM<=5)
7 - access("VW_IVCR_B5B87E67"."CENTROID_ID"="VTIX_CNPART"."CENTROID_ID")
10 - filter(ROWNUM<=13)
12 - filter(ROWNUM<=13)
Column Projection Information (identified by operation id):
-----------------------------------------------------------
1 - "from$_subquery$_002"."TEXT"[LOB,4000], "from$_subquery$_002"."SOURCE"[VARCHAR2,4000], "from$_subquery$_002"."DISTANCE"[BINARY_DOUBLE,8]
2 - "VW_IVPSR_11E7D7DE"."VEC_DIST"[BINARY_DOUBLE,8], "ORACLE_DOCS"."TEXT"[LOB,4000], "METADATA" /*+ LOB_BY_VALUE */ [JSON,8200]
3 - "BASE_TABLE_ROWID"[ROWID,10], "VW_IVPSR_11E7D7DE"."VEC_DIST"[BINARY_DOUBLE,8]
4 - "VW_IVPSJ_578B79F1"."BASE_TABLE_ROWID"[ROWID,10], "VW_IVPSJ_578B79F1"."VEC_DIST"[BINARY_DOUBLE,8]
5 - "VW_IVPSJ_578B79F1"."BASE_TABLE_ROWID"[ROWID,10], "VW_IVPSJ_578B79F1"."VEC_DIST"[BINARY_DOUBLE,8]
6 - (#keys=1) VECTOR_DISTANCE("VTIX_CNPART"."DATA_VECTOR" /*+ LOB_BY_VALUE */ /*+ LOB_INLINE */ , VECTOR(:VEC, *, *, * /*+ USEBLOBPCW_QVCGMD */ ), EUCLIDEAN)[8], "VTIX_CNPART"."BASE_TABLE_ROWID"[ROWID,10]
7 - (#keys=1) "VTIX_CNPART"."BASE_TABLE_ROWID"[ROWID,10], VECTOR_DISTANCE("VTIX_CNPART"."DATA_VECTOR" /*+ LOB_BY_VALUE */ /*+ LOB_INLINE */ , VECTOR(:VEC, *, *, * /*+ USEBLOBPCW_QVCGMD */ ), EUCLIDEAN)[8]
8 - "VW_IVCR_B5B87E67"."CENTROID_ID"[NUMBER,22], "VW_IVCR_B5B87E67"."CENTROID_ID"[NUMBER,22]
9 - "CENTROID_ID"[NUMBER,22]
10 - "VW_IVCN_9A1D2119"."CENTROID_ID"[NUMBER,22]
11 - "VW_IVCN_9A1D2119"."CENTROID_ID"[NUMBER,22]
12 - (#keys=2) VECTOR_DISTANCE("VECTOR$IDX_IVF_ORACLE_DOCS_01$126314_134514_0$IVF_FLAT_CENTROIDS"."CENTROID_VECTOR" /*+ LOB_BY_VALUE */ /*+ LOB_INLINE */ , VECTOR(:VEC, *, *, * /*+ USEBLOBPCW_QVCGMD */ ), EUCLIDEAN)[8],
"VTIX_CENTRD"."CENTROID_ID"[NUMBER,22]
13 - "VTIX_CENTRD"."CENTROID_ID"[NUMBER,22], VECTOR_DISTANCE("VECTOR$IDX_IVF_ORACLE_DOCS_01$126314_134514_0$IVF_FLAT_CENTROIDS"."CENTROID_VECTOR" /*+ LOB_BY_VALUE */ /*+ LOB_INLINE */ , VECTOR(:VEC, *, *, * /*+ USEBLOBPCW_QVCGMD */ ),
EUCLIDEAN)[8]
14 - "VTIX_CNPART"."BASE_TABLE_ROWID"[ROWID,10], "VTIX_CNPART"."CENTROID_ID"[NUMBER,22], VECTOR_DISTANCE("VTIX_CNPART"."DATA_VECTOR" /*+ LOB_BY_VALUE */ /*+ LOB_INLINE */ , VECTOR(:VEC, *, *, * /*+ USEBLOBPCW_QVCGMD */ ), EUCLIDEAN)[8]
15 - "VTIX_CNPART"."BASE_TABLE_ROWID"[ROWID,10], "VTIX_CNPART"."CENTROID_ID"[NUMBER,22], VECTOR_DISTANCE("VTIX_CNPART"."DATA_VECTOR" /*+ LOB_BY_VALUE */ /*+ LOB_INLINE */ , VECTOR(:VEC, *, *, * /*+ USEBLOBPCW_QVCGMD */ ), EUCLIDEAN)[8]
16 - "ORACLE_DOCS"."TEXT"[LOB,4000], "METADATA" /*+ LOB_BY_VALUE */ [JSON,8200]
Hint Report (identified by operation id / Query Block Name / Object Alias):
Total hints for statement: 1
---------------------------------------------------------------------------
0 - STATEMENT
- NO_PARALLEL
Note
-----
- Degree of Parallelism is 1 because of hint
- SQL patch "patch_gather_stats" used for this statement
Query Block Registry:
---------------------
SEL$1 (PARSER)
SEL$94C0F189 (IVF VECTOR INDEX PRE-FILTER WITHOUT JOIN-BACK SEL$1) [FINAL]
SEL$E731354C (IVF VECTOR INDEX NO-FILTER JOIN ROWNUM VIEW SEL$94C0F189) [FINAL]
SEL$0C00A749 (IVF VECTOR INDEX NO-FILTER JOIN VIEW SEL$E731354C) [FINAL]
SEL$700CE8F1 (IVF VECTOR INDEX CENTROID ROWNUM VIEW SEL$0C00A749) [FINAL]
SEL$E5326247 (IVF VECTOR INDEX CENTROID VIEW SEL$700CE8F1) [FINAL]
SEL$2 (PARSER) [FINAL]
Bom, diferente do esperado (pelo menos do que eu esperava quando vi esse plano pela primeira vez), não temos um simples INDEX RANGE SCAN com um TABLE ACCESS BY ROWID acontecendo.
Agora vamos reexecutar a consulta para ver os objetos existentes no schema:
select segment_name, segment_type, sum(bytes)/1024/1024 as TAM_MB from dba_segments where owner='VECTOR' group by segment_name, segment_type;

Aqui podemos ver o plot twist: na realidade, um índice IVF não é um índice. Ele é implementado através da criação de uma estrutura com duas tabelas, uma que armazena os centróides (a VECTOR$IDX_IVF_ORACLE_DOCS_01$126314_134514_0$IVF_FLAT_CENTROIDS) e outra que armazena os vetores de cada partição associada aos centróides (a VECTOR$IDX_IVF_ORACLE_DOCS_01$126314_134514_0$IVF_FLAT_CENTROID_PARTITIONS).
Se você não entendeu o que são esses centróides, leia a parte 1 dessa série de posts.
Análise do plano de execução
Vamos analisar o plano gerado com a utilização do índice vetorial. Abaixo coloquei apenas as informações mais importantes para entendermos o que está acontecendo no plano:
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows |E-Bytes| Cost (%CPU)| E-Time | Pstart| Pstop | A-Rows | A-Time | Buffers | Reads | OMem | 1Mem | Used-Mem |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | | 350 (100)| | | | 5 |00:00:00.07 | 1220 | 909 | | | |
| 1 | VIEW | | 1 | 5 | 20050 | 350 (1)| 00:00:01 | | | 5 |00:00:00.07 | 1220 | 909 | | | |
| 2 | NESTED LOOPS | | 1 | 5 | 48525 | 350 (1)| 00:00:01 | | | 5 |00:00:00.07 | 1220 | 909 | | | |
| 3 | VIEW | VW_IVPSR_11E7D7DE | 1 | 5 | 80 | 345 (1)| 00:00:01 | | | 5 |00:00:00.07 | 1210 | 909 | | | |
|* 4 | COUNT STOPKEY | | 1 | | | | | | | 5 |00:00:00.07 | 1210 | 909 | | | |
| 5 | VIEW | VW_IVPSJ_578B79F1 | 1 | 670 | 12060 | 345 (1)| 00:00:01 | | | 5 |00:00:00.07 | 1210 | 909 | | | |
|* 6 | SORT ORDER BY STOPKEY | | 1 | 670 | 16080 | 345 (1)| 00:00:01 | | | 5 |00:00:00.07 | 1210 | 909 | 2048 | 2048 | 2048 (0)|
|* 7 | HASH JOIN | | 1 | 670 | 16080 | 344 (1)| 00:00:01 | | | 808 |00:00:00.01 | 1210 | 909 | 2546K| 2546K| 1232K (0)|
| 8 | PART JOIN FILTER CREATE | :BF0000 | 1 | 13 | 52 | 69 (2)| 00:00:01 | | | 13 |00:00:00.01 | 235 | 0 | | | |
| 9 | VIEW | VW_IVCR_B5B87E67 | 1 | 13 | 52 | 69 (2)| 00:00:01 | | | 13 |00:00:00.01 | 235 | 0 | | | |
|* 10 | COUNT STOPKEY | | 1 | | | | | | | 13 |00:00:00.01 | 235 | 0 | | | |
| 11 | VIEW | VW_IVCN_9A1D2119 | 1 | 190 | 2470 | 69 (2)| 00:00:01 | | | 13 |00:00:00.01 | 235 | 0 | | | |
|* 12 | SORT ORDER BY STOPKEY | | 1 | 190 | 1900 | 69 (2)| 00:00:01 | | | 13 |00:00:00.01 | 235 | 0 | 2048 | 2048 | 2048 (0)|
| 13 | TABLE ACCESS FULL | VECTOR$IDX_IVF_ORACLE_DOCS_01$126314_134514_0$IVF_FLAT_CENTROIDS | 1 | 190 | 1900 | 68 (0)| 00:00:01 | | | 190 |00:00:00.01 | 235 | 0 | | | |
| 14 | PARTITION LIST JOIN-FILTER| | 1 | 9792 | 191K| 21 (0)| 00:00:01 |:BF0000|:BF0000| 808 |00:00:00.01 | 975 | 909 | | | |
| 15 | TABLE ACCESS FULL | VECTOR$IDX_IVF_ORACLE_DOCS_01$126314_134514_0$IVF_FLAT_CENTROID_PARTITIONS | 13 | 9792 | 191K| 21 (0)| 00:00:01 |:BF0000|:BF0000| 808 |00:00:00.03 | 975 | 909 | | | |
| 16 | TABLE ACCESS BY USER ROWID | ORACLE_DOCS | 5 | 1 | 9689 | 1 (0)| 00:00:01 | | | 5 |00:00:00.01 | 10 | 0 | | | |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
4 - filter(ROWNUM<=5)
6 - filter(ROWNUM<=5)
7 - access("VW_IVCR_B5B87E67"."CENTROID_ID"="VTIX_CNPART"."CENTROID_ID")
10 - filter(ROWNUM<=13)
12 - filter(ROWNUM<=13)
Column Projection Information (identified by operation id):
-----------------------------------------------------------
1 - "from$_subquery$_002"."TEXT"[LOB,4000], "from$_subquery$_002"."SOURCE"[VARCHAR2,4000], "from$_subquery$_002"."DISTANCE"[BINARY_DOUBLE,8]
2 - "VW_IVPSR_11E7D7DE"."VEC_DIST"[BINARY_DOUBLE,8], "ORACLE_DOCS"."TEXT"[LOB,4000], "METADATA" /*+ LOB_BY_VALUE */ [JSON,8200]
3 - "BASE_TABLE_ROWID"[ROWID,10], "VW_IVPSR_11E7D7DE"."VEC_DIST"[BINARY_DOUBLE,8]
4 - "VW_IVPSJ_578B79F1"."BASE_TABLE_ROWID"[ROWID,10], "VW_IVPSJ_578B79F1"."VEC_DIST"[BINARY_DOUBLE,8]
5 - "VW_IVPSJ_578B79F1"."BASE_TABLE_ROWID"[ROWID,10], "VW_IVPSJ_578B79F1"."VEC_DIST"[BINARY_DOUBLE,8]
6 - (#keys=1) VECTOR_DISTANCE("VTIX_CNPART"."DATA_VECTOR" /*+ LOB_BY_VALUE */ /*+ LOB_INLINE */ , VECTOR(:VEC, *, *, * /*+ USEBLOBPCW_QVCGMD */ ), EUCLIDEAN)[8], "VTIX_CNPART"."BASE_TABLE_ROWID"[ROWID,10]
7 - (#keys=1) "VTIX_CNPART"."BASE_TABLE_ROWID"[ROWID,10], VECTOR_DISTANCE("VTIX_CNPART"."DATA_VECTOR" /*+ LOB_BY_VALUE */ /*+ LOB_INLINE */ , VECTOR(:VEC, *, *, * /*+ USEBLOBPCW_QVCGMD */ ), EUCLIDEAN)[8]
8 - "VW_IVCR_B5B87E67"."CENTROID_ID"[NUMBER,22], "VW_IVCR_B5B87E67"."CENTROID_ID"[NUMBER,22]
9 - "CENTROID_ID"[NUMBER,22]
10 - "VW_IVCN_9A1D2119"."CENTROID_ID"[NUMBER,22]
11 - "VW_IVCN_9A1D2119"."CENTROID_ID"[NUMBER,22]
12 - (#keys=2) VECTOR_DISTANCE("VECTOR$IDX_IVF_ORACLE_DOCS_01$126314_134514_0$IVF_FLAT_CENTROIDS"."CENTROID_VECTOR" /*+ LOB_BY_VALUE */ /*+ LOB_INLINE */ , VECTOR(:VEC, *, *, * /*+ USEBLOBPCW_QVCGMD */ ), EUCLIDEAN)[8],
"VTIX_CENTRD"."CENTROID_ID"[NUMBER,22]
13 - "VTIX_CENTRD"."CENTROID_ID"[NUMBER,22], VECTOR_DISTANCE("VECTOR$IDX_IVF_ORACLE_DOCS_01$126314_134514_0$IVF_FLAT_CENTROIDS"."CENTROID_VECTOR" /*+ LOB_BY_VALUE */ /*+ LOB_INLINE */ , VECTOR(:VEC, *, *, * /*+ USEBLOBPCW_QVCGMD */ ),
EUCLIDEAN)[8]
14 - "VTIX_CNPART"."BASE_TABLE_ROWID"[ROWID,10], "VTIX_CNPART"."CENTROID_ID"[NUMBER,22], VECTOR_DISTANCE("VTIX_CNPART"."DATA_VECTOR" /*+ LOB_BY_VALUE */ /*+ LOB_INLINE */ , VECTOR(:VEC, *, *, * /*+ USEBLOBPCW_QVCGMD */ ), EUCLIDEAN)[8]
15 - "VTIX_CNPART"."BASE_TABLE_ROWID"[ROWID,10], "VTIX_CNPART"."CENTROID_ID"[NUMBER,22], VECTOR_DISTANCE("VTIX_CNPART"."DATA_VECTOR" /*+ LOB_BY_VALUE */ /*+ LOB_INLINE */ , VECTOR(:VEC, *, *, * /*+ USEBLOBPCW_QVCGMD */ ), EUCLIDEAN)[8]
16 - "ORACLE_DOCS"."TEXT"[LOB,4000], "METADATA" /*+ LOB_BY_VALUE */ [JSON,8200]
A primeira operação acontece na linha 13 do plano, um acesso na tabela de centróides. Podemos ver na seção de projection do plano, que uma função de distância vetorial euclideana está sendo utilizada entre a coluna CENTROID_VECTOR da tabela e o vetor passado na bind da consulta. Com essas distâncias calculadas, na linha 12 são ordenados os centróides e na linha 10 são retornadas apenas os centróides mais próximos cujas partições serão consideradas na consulta. Nesse caso são retornadas duas partições, pois como falei no artigo anterior, o número de partições consideradas é raiz quadrada de k, arredondada para baixo (nesse caso, raiz de 5, arredondado para baixo é 2). Na sequência um Bloom Filter é criado na linha 8 para otimizar o Hash Join da linha 7.
Antes do Hash Join, nas linhas 14 e 15, são realizados cálculos de distância vetorial entre o vetor da variável bind com a coluna DATA_VECTOR da tabela que armazena os vetores de cada partição associada aos centróides. Assim o Hash Join retorna apenas as linhas cujas distâncias estejam associadas com os centróides mais próximos retornados nos passos anteriores. Já na linha 15, é possível identificar na seção de projection, que a coluna BASE_TABLE_ROWID é retornada. Ou seja, essa tabela que armazena os vetores de cada partição faz referência ao ROWID da linha da tabela ORACLE_DOCS onde este vetor está armazenado, e será utilizado posteriormente no plano.
Agora com os dados dos vetores mais próximos, na linha 6, ocorre a ordenação da distância vetorial calculada, e na linha 4, apenas os top-k vetores mais próximos são retornados. Com esses top-k mais próximos, agora o ROWID armazenado na tabela de partição do vetor é usado para o join da linha 2 do plano, com a tabela base ORACLE_DOCS, a fim de buscar os demais dados solicitados na projeção da consulta.
Depois dessa viagem, a pergunta é: mesmo com todo esse trabalho, a consulta fica melhor usando o índice? Bom, o plano fala por si só: houve uma reducão de buffers significativa e o tempo de execução caiu para 70ms.
Conclusão
Bom, então vale sempre a pena criar um índice numa coluna vetorial? A resposta é: nem sempre. Lembre-se que você está trocando precisão por velocidade. No próximo post dessa série vou demonstrar como calcular a precisão do índice criado.
Antes de fechar, sugiro ler o artigo do Connor McDonald: 23ai – where your vector indexes are stored. Neste artigo ele mostra que esses índices vetoriais atualmente criam as tabelas base dentro da tablespace default do database, mesmo quando uma tablespace diferente é especificada. Ou seja, para que o índice vetorial seja criado em uma tablespace específica, é necessário alterar a tablespace default do banco antes de executar o comando de criação (este comportamento pode ser alterado em releases futuras).
Publicar comentário