OceanBase 花三年时间整理出的向量数据库最佳实践

楔子

AI 时代,各种 AI Infra 都离不开向量数据的存储、检索。而做向量场景的 PoC,每次都要重新算一遍内存、选一遍索引、调一遍参数……

OceanBase 有两位不愿意透露姓名的大佬——序风、舸灏,在最近三年支持了无数 AI 场景的向量数据库 PoC(Proof of Concept)工作,有着究极丰富的向量数据库运维和调优经验。

这篇文章,是他们第一次把压箱底的向量数据库运维经验总结拿出来,在社区公众号为大家进行分享。文中包含了使用向量数据库时,需要考虑的方方面面。

(本文适合收藏,以备不时之需)

图片

欢迎大家关注 OceanBase 社区公众号”老纪的技术唠嗑局”。

图片

本文涵盖了:向量索引选型、内存与 CPU 规划、磁盘空间估算、分区设计、索引参数配置、混合查询调优、性能验证方法与实测数据、常见性能问题排查等等。适用于向量数据库 PoC 评估、向量索引类型选择、租户资源规划和查询性能调优。

本文的前置阅读材料是 《浅入了解向量数据库》

上篇:向量构建设计实践

【选型与规划】 这一部分会讲清楚:什么场景选什么索引、内存 CPU 磁盘怎么算、表怎么建、参数怎么配。

1. 索引选型

开篇就给结论:选型不靠经验,靠两条数据——数据规模和内存预算。

快速决策树如下:

图片

上图的决策逻辑简述:数据规模 < 1000 万不分区,1000 万 ~ 5 亿和 > 5 亿均做分区(单分区 1000 万)。< 5 亿且内存充裕选 HNSW 或 HNSW_SQ,内存有限选 HNSW_BQ;> 5 亿统一选 HNSW_BQ。内存极低时按维度选 IVF——维度 < 384 选 IVF_FLAT,≥ 384 选 IVF_PQ。

注意:HNSW_BQ 要求维度 ≥ 384, IVF_PQ 要求维度 ≥ 128。单分区 1000 万向量并不是强制要求,通常建议大数据量下单分区控制在 500 万~2000 万向量,单个分区的向量数不能超过 5000 万。

案例场景 推荐方案 详细章节
384 维,亿级,geohash 过滤 HNSW_SQ 或 HNSW_BQ,按 geohash 分区 第 4 章、第 6 章
1024 维,十亿级,多维过滤 HNSW_BQ,skill_id 一级分区,doc_id 二级分区 第 4 章
768 维,千万~亿级 HNSW_SQ 或 HNSW_BQ 第 1 章
任意维度,十亿级,只增不删 IVF_PQ 第 1 章、第 2 章

1.1. HNSW 系列

HNSW 系列索引是内存索引,查询时需要长驻内存,注意它不是缓存,无法像 KV Cache 那样临时换出。在重建时会有一段时间内存中存在新旧两份索引。

类型 内存(相对 HNSW) 召回率 查询性能 适用场景
HNSW_SQ 1/4 ~ 1/3 略低于 HNSW 最高 千万级首选,性能和内存的平衡点
HNSW_BQ 1/20 低于 SQ,需 refine 补偿 中高 亿级以上,内存有限时的唯一选择

如果内存成本足够,HNSW_SQ 是大多数场景下的最佳选择。HNSW_BQ 的极致量化(RapidQ)让索引本身极小,但查询时需要从磁盘捞原始向量做重排,因此磁盘性能对其性能有一定影响,TopK 越大影响越明显。

图片

1.2. IVF 系列

IVF 索引常驻磁盘,内存占用极低,适合内存预算极度有限的场景。

类型 查询性能 构建速度 召回率 适用场景
IVF_FLAT 较慢 内存紧张但维度不高(<384)
IVF_PQ 比 FLAT 快 略低 维度高(≥384)、内存极度紧张

如果大家要拿多种不同的向量数据库对比性能,则一定要注意区分索引类型:磁盘索引的 RT 通常比内存索引高数倍,这点各家是都一样的。不能单纯按照索引名称对比性能,而要按照实际的内存 / 磁盘索引类型来对比。

图片

注意:IVF 系列索引在 OceanBase 4.6.0 版本以前都不支持堆表的分区表。IVF_PQ 使用 l2 距离时需额外缓存预计算结果,大规模场景优先用 cosine 距离。IVF 系列索引,随表创建索引后写入数据相当于暴力搜索,要在写完数据后重建 IVF 索引。

2. 资源规划

PoC 报方案前最怕被反问”内存够不够、机器买几台”——这一章给你一个最直观的算法。

图片

2.1. 内存估算与规划

OceanBase 自带了向量索引内存估算函数 dbms_vector.index_vector_memory_advisor,可根据索引类型、数据规模和参数计算所需向量内存:

1
2
3
4
5
6
-- 1 亿条 384 维向量,单分区最多 1000 万条
SELECT dbms_vector.index_vector_memory_advisor(
'HNSW_BQ', 100000000, 384, 'FLOAT32',
'M=32, TYPE=HNSW_BQ, ef_construction=400, distance=cosine, refine_type=SQ8',
10000000
);

参数依次是:索引类型、总数据量、维度、数据类型、索引参数、单分区最大行数。

1024 维(最常见,text-embedding-3-large、bge-large 等):

数据规模 推荐索引 分区 向量内存(单副本)
100 万 HNSW_SQ 不分区 2.7 GB
500 万 HNSW_SQ 不分区 14.2 GB
1000 万 HNSW_SQ 不分区 28.4 GB
3000 万 HNSW_BQ 3 分区 构建 34.4 GB,运行时 17.2 GB
1 亿 HNSW_BQ 10 分区 构建 74.6 GB,运行时 57.4 GB
10 亿 HNSW_BQ 100 分区 构建 591.2 GB,运行时 574.0 GB
10 亿 IVF_PQ 100 分区 构建 6.3 GB,运行时 1.9 GB

768 维(text-embedding-3-small、bge-base 等):

数据规模 推荐索引 分区 向量内存(单副本)
1000 万 HNSW_SQ 不分区 22.6 GB
1 亿 HNSW_BQ 10 分区 构建 67.8 GB,运行时 53.8 GB
10 亿 HNSW_BQ 100 分区 构建 552.2 GB,运行时 538.2 GB
10 亿 IVF_PQ 100 分区 构建 4.8 GB,运行时 1.4 GB

同样是 10 亿向量、单分区 1000 万:HNSW_BQ 构建峰值 591.2 GB,IVF_PQ 运行时 1.9 GB——内存差距约 300 倍。 这就是为什么内存预算决定了索引选型

图片

注意:向量内存估算函数计算的是单副本下的内存用量。租户内存 = 向量内存 ÷ ob_vector_memory_limit_percentage(默认 50%)。

HNSW 内存详解

索引类型 构建时内存 运行时常驻 说明
HNSW 76.3 GB 76.3 GB 全量常驻,不释放
HNSW_SQ 22.6 GB 22.6 GB 量化后常驻,约 HNSW 的 1/3
HNSW_BQ 22.6 GB 5.4 GB 构建时需要 SQ 缓存,完成后只剩 BQ 索引

IVF 内存详解

索引类型 索引参数 构建时内存 运行时常驻
IVF_FLAT nlist=3000 3.4 GB 13.2 MB
IVF_PQ (cosine) nlist=3000, m=384 3.4 GB 14.3 MB
IVF_PQ (l2) nlist=3000, m=384 5.0 GB 1.7 GB

l2 距离下 IVF_PQ 的常驻内存是 cosine 的 120 倍——因为 l2 要额外缓存预计算结果。

2.2. CPU 与 NUMA 对向量查询的影响

相比普通 SQL 查询,向量搜索的瓶颈主要在内存带宽,以及 CPU 支持的 SIMD 指令集。核数不是越多越好:特别是 64 核以后,每核分到的内存带宽减少、L3 cache 抢得厉害。核数不是越多越好。向量搜索的瓶颈是内存带宽和 SIMD 指令,超过 64 核之后,跨 NUMA 访问和 L3 cache 争抢会让性能不升反降。

2.3. 磁盘空间与查询性能

索引类型 磁盘估算
HNSW 约等于原始向量大小 × 1.2
HNSW_SQ 约等于原始向量大小 × 1.2 / 3
HNSW_BQ 约等于原始向量大小 × 1.2 / 20
IVF_FLAT 约等于原始向量大小
IVF_PQ 约等于原始向量大小 / 8

原始向量大小计算公式:行数 × 维度 × 4 字节,例如 1 亿 384 维 float32 = 144 GB。

总的说,几种索引算法受磁盘性能影响的程度:HNSW_SQ < HNSW_BQ < IVF/IVF_PQ。

3. 重要配置项说明

3 个参数,调与不调差距可能是 QPS 翻倍。

1
2
3
4
5
6
7
8
-- 1. 并行构建采样精度(千万级 5000,亿级 10000,十亿级以上 100000)
ALTER SYSTEM SET _px_object_sampling = 10000;

-- 2. 向量索引内存占比(默认自适应 50%,内存不够可手动调高到 60%)
ALTER SYSTEM SET ob_vector_memory_limit_percentage = 50;

-- 3. 查询策略(4.6.0 起默认 LATENCY_FIRST,老版本默认 RECALL_FIRST)
ALTER SYSTEM SET ob_vector_search_strategy = 'LATENCY_FIRST';

4. 表结构与分区设计

同时满足以下两条,建议分区:数据量千万级以上、查询条件里有明确的标量列能用来裁剪分区。目标单分区:500 万 ~ 2000 万行。结论:在满足 500-2000 万的前提下,分区越少越好。

总数据量 分区数 单分区量
5000 万 5-10 500-1000 万
1 亿 10-20 500-1000 万
4.5 亿 25-45 1000-1800 万
10 亿 50-100 1000-2000 万

二级分区(两个维度过滤,如 skill_id + doc_id):

1
2
3
4
5
6
CREATE TABLE iop_knowledge (
_id varchar(200), doc_id varchar(100), skill_id varchar(100),
search_vec vector(1024),
UNIQUE KEY idx_id (_id, skill_id, doc_id) LOCAL
) ORGANIZATION HEAP partition by key(skill_id) partitions 30
subpartition by key(doc_id) subpartitions 4;

“稀疏”的向量列

实测:主表 9 亿行,768 维列仅 2600 万行有值。大表上查 RT 21ms,拆到小表后降到 3ms 以内。9 亿行的主表查 21ms,拆出非空 2600 万行到独立小表后降到 3ms——延迟下降 7 倍。向量列稀疏时,大表分区扫描的隐藏开销远比想象高。

5. 索引创建与参数配置

强烈建议全量数据导入后再创建索引。并行度设租户 CPU 的 2 倍。

1
2
3
4
5
6
7
8
9
10
11
12
-- HNSW_BQ
CREATE /*+ PARALLEL(64) */ VECTOR INDEX idx_vec ON htl_image_recall(picturevector)
WITH (distance=cosine, type=hnsw_bq, m=32, ef_construction=400);

-- HNSW_SQ
CREATE /*+ PARALLEL(64) */ VECTOR INDEX idx_vec ON htl_image_recall(picturevector)
WITH (distance=cosine, type=hnsw_sq, m=32, ef_construction=400);

-- IVF_PQ
CREATE /*+ PARALLEL(64) */ VECTOR INDEX idx_vec ON htl_image_recall(picturevector)
WITH (distance=cosine, type=ivf_pq, lib=OB, m=192, nlist=3000, nbits=8,
sample_per_nlist=256) BLOCK_SIZE=1048576;

HNSW 系列参数

参数 默认值 范围 作用
distance 必填 l2 / cosine / inner_product 多数 embedding 用 cosine
type 必填 hnsw / hnsw_sq / hnsw_bq
m 16 [5, 64] 每节点最大邻居数
ef_construction 200 [5, 1000] 构建时候选集大小
ef_search 64 [1, 16000] 查询时候选集大小
refine_k 4.0 [1.0, 1000.0] 仅 BQ,重排比例
refine_type sq8 sq8 / fp32 仅 BQ

按规模的参数推荐

百万级:HNSW_SQ(m=16,ef_construction=200,ef_search=240);HNSW_BQ(m=16,ef_construction=200,ef_search=240,refine_k=4)

千万级:HNSW_SQ(m=32,ef_construction=400,ef_search=350);HNSW_BQ(m=32,ef_construction=400,ef_search=1000,refine_k=10);IVF_PQ(nlist=3000,m=dim/2,nbits=8,nprobes=20)

亿级(分区表):参数按单分区最大数据量来定。

增量与重建

索引创建后增量写入的向量立即可查,但增量部分不做量化压缩,会额外占内存。HNSW 系列增量达 20% 时自动触发后台重建,IVF 新增超 30% 后需手动 CALL dbms_vector.rebuild_index()

6. 查询与调优

不带标量过滤:

1
2
SELECT id, cosine_distance(embedding, @query_vector) AS distance
FROM htl_image_recall ORDER BY distance APPROXIMATE LIMIT 100;

APPROXIMATE 必须写(简写 APPROX 也行)。

混合查询(geohash + 向量):

1
2
3
4
SELECT id, picturename, cosine_distance(picturevector, @query_vector) AS distance
FROM htl_image_recall
WHERE geohash IN ('gcq2j', 'u10kk', 'wvkut')
ORDER BY distance APPROXIMATE LIMIT 100;

图片

召回与延迟的权衡

ef_search(HNSW 系列)和 nprobes(IVF)是核心旋钮。

HNSW 系列(768 维,千万级,目标 Recall ≈ 0.95):Top10 ef_search=100; Top100(HNSW_SQ) ef_search=350; Top100(HNSW_BQ) ef_search=1000, refine_k=10

IVF 单分区(千万级):Top10 nprobes=1; Top100 nprobes=20; Top1000 nprobes=90

7. 性能验证

四个核心指标:QPS、平均 RT、P95/P99 RT、召回率。

图片

召回率测试:准备 100 个以上查询向量,分别跑精确搜索和近似搜索,对比结果:

1
2
3
4
5
6
7
-- 精确搜索(ground truth)
SELECT /*+ PARALLEL(32) */ id, cosine_distance(picturevector, @query_vector) AS distance
FROM htl_image_recall WHERE geohash IN ('gcq2j', 'u10kk') ORDER BY distance LIMIT 100;

-- 近似搜索
SELECT id, cosine_distance(picturevector, @query_vector) AS distance
FROM htl_image_recall WHERE geohash IN ('gcq2j', 'u10kk') ORDER BY distance APPROXIMATE LIMIT 100;

压测时最好进行合并(major freeze)和预热,减小回表和读盘对查询性能的影响。

实测性能参考

百万级 768 维(m=16, ef_construction=200, Top100, ef_search=240):

索引类型 QPS 召回率 内存
HNSW 3475 0.9499 7.3 GB
HNSW_SQ 5599 0.9468 2.1 GB
HNSW_BQ (refine_k=4) 3113 0.9278 0.4 GB

千万级 768 维(m=32, ef_construction=400, Top100):

索引类型 QPS 召回率 ef_search
HNSW 2637 0.9574 350
HNSW_BQ (refine_k=10) 857 0.9531 1000

4.5 亿 384 维混合查询(HNSW_SQ, m=32, 45 分区,20 并发):

geohash 过滤个数 QPS 平均 RT
10 810 24ms
20 717 28ms
40 488 40ms

8. 向量查询性能问题排查手册

出问题时不要乱调参数,按这张表的”现象 → 原因 → 动作”走,90% 的问题能在 10 分钟内定位。

图片

现象 最可能的原因 动作
延迟秒级 没做分区裁剪 EXPLAIN 看 partitions 字段
延迟秒级 没加 APPROXIMATE EXPLAIN 确认是否走了向量索引
P99 >> P50 部分分区索引没加载到内存 查 GV$OB_VECTOR_MEMORY
RT 偏高 向量列大量 NULL,大表扫描开销 拆非空行到小表,实测 RT 从 21ms 降至 3ms
召回低 ef_search / nprobes 太小 逐步调大 ef_search 或 nprobes
混合查询慢 标量字段没索引 建标量索引
混合查询慢 自动策略没选对 hint 手动指定

9. 内存相关

OceanBase 4.3.5 BP3 之后可用 GV$OB_VECTOR_MEMORY 视图:

1
2
3
4
5
6
7
SELECT b.zone, a.svr_ip, a.svr_port, a.tenant_id,
ROUND(a.vector_mem_hold/1024/1024/1024,2) AS hold_gb,
ROUND(a.vector_mem_used/1024/1024/1024,2) AS used_gb,
ROUND(a.vector_mem_limit/1024/1024/1024,2) AS limit_gb
FROM GV$OB_VECTOR_MEMORY a JOIN gv$ob_units b
ON a.tenant_id = b.tenant_id AND a.svr_ip = b.svr_ip AND a.svr_port = b.svr_port
ORDER BY b.zone, used_gb DESC;

10. 向量索引创建

通过 __all_virtual_ddl_diagnose_info 确认索引创建状态,gv$session_longops 查看创建中的索引。通过 real_parallelism 关键字确认创建向量索引的并行度。

收集 traceid 示例:

1
obdiag gather log --from='2026-03-16 21:00:00' --to='2026-03-17 17:00:00' --scope=all --grep='YB420A80D369-000649E8EDEED23D-0-0'

写在最后

这份指南来自多个真实 PoC 项目的经验总结。如果这份指南对你有帮助,请转发给同样需要”向量检索”的同事和朋友~

图片

最后附上 PoC 经验总结前两篇文章:

图片

图片

添加 OB 社区小助手,进入技术交流群

图片

生成的二维码