RAG 的向量存在哪?MySQL 普通表和专用向量库怎么选?
简化版
向量存储要解决两件事:把向量连同片段 ID、元数据存下来,以及按相似度快速找出最近的 K 条。数据量小、而且检索前能先按业务条件把候选收窄到几十条时,MySQL 普通表存向量、在内存里算余弦就够用,还省掉一个中间件;数据量大、要毫秒级响应、要把过滤和向量检索放在一次查询里完成时,就该用 PGVector 或专用向量库,靠近似最近邻索引加速。选型看四点:数据规模、候选能否收窄、延迟要求、运维成本。
详细版
三种常见方案:
| 方案 | 检索方式 | 适合的规模 | 优点 | 代价 |
|---|---|---|---|---|
| MySQL 普通表 + 内存计算 | 读出候选向量,在应用里逐条算余弦 | 候选能收窄到几十到几百条 | 不引入新数据库,和业务数据一起备份 | 候选一多,读库和计算开销直线上升 |
| PostgreSQL + PGVector | SQL 里用向量类型和距离运算符,走 IVFFlat / HNSW 索引 | 几万到几百万条 | 过滤条件和向量检索写在一条 SQL 里,有事务 | 多维护一个 PostgreSQL |
| 专用向量库(Milvus 等) | 分布式 ANN 索引,分片、副本 | 千万条以上、高并发 | 规模和吞吐上限最高 | 独立集群,运维和数据同步成本最高 |
判断顺序:
- 检索前能不能按业务维度收窄候选? 比如按知识库、租户、分类先过滤。收不窄就别考虑内存计算;
- 收窄之后还剩多少条? 几十、几百条时内存计算足够;上千条以后应该换带索引的方案;
- 有没有复杂过滤? 需要把元数据过滤和向量排序合在一次查询里时,PGVector 最顺手;
- 团队能不能多运维一套数据库? 能不加就不加,但不能为了少一个组件,让检索延迟失控。
完整版教学
一、向量存储要解决的两件事
RAG 的离线阶段把每个片段编码成一个向量,比如 1024 个浮点数。存储层要承担两个完全不同的任务:
存:片段 ID + 向量 + 元数据(知识库、文档、片段类型、模型名、维度)
查:给一个问题向量,找出最相似的 K 个片段,最好还能同时按元数据过滤
「存」对任何数据库都不难,难的是「查」。最朴素的查法是暴力检索:把候选向量逐个拿出来和问题向量算相似度,排序取前 K。专用的向量存储多做了一件事:建近似最近邻(ANN)索引,不用和每条向量都算一遍,就能以很高的概率找到最近的那几条。
所以「存在哪」的问题,本质是在问:你的候选规模,暴力检索扛不扛得住?
二、MySQL 存向量是怎么工作的
MySQL 没有向量类型,也没有向量距离运算,常见做法是把向量序列化后放进 text 或 blob 列,检索时读出来在应用里计算:
① 问题文本 → 用和入库时同一个模型编码成问题向量
② 按业务条件收窄候选:where kb_id = ? and status = 'READY'
③ 读出候选的向量文本,逐条解析成浮点数组
④ 逐条算余弦相似度:cos = (a · b) / (|a| × |b|)
⑤ 按相似度倒序取前 K 条
余弦的计算本身很简单,1024 维的向量算一次就是 1024 次乘加,再加两次求模。真正的成本不在计算,而在第③步:把向量从数据库读出来、再解析。
记忆钩子:MySQL 方案能不能用,不取决于算得快不快,而取决于每次要读出多少条向量。
三、算一笔账:暴力检索什么时候扛不住
以 1024 维向量为例。存成逗号分隔的文本时,每个数形如 0.593302,,大约 9~10 个字符,一条向量约 10 KB;即使存成二进制 float32,也要 1024 × 4 = 4 KB。
| 每次检索的候选条数 | 从库里读出的数据量(文本存储) | 计算量(乘加次数) | 可行性 |
|---|---|---|---|
| 50 | 约 0.5 MB | 约 5 万次 | 轻松 |
| 500 | 约 5 MB | 约 50 万次 | 可以,但每次检索都在搬数据 |
| 5000 | 约 50 MB | 约 500 万次 | 并发一上来,网络和内存都吃紧 |
| 100000 | 约 1 GB | 约 1 亿次 | 不可行 |
从表里能看出,计算量一直不是瓶颈,读库的数据量才是。候选收窄到几十条时,MySQL 方案成本很低;一旦要全库检索,或者单个知识库本身就有几万条片段,就必须换成有索引的方案。
四、向量数据库多做了什么
以 PGVector 为例,它给 PostgreSQL 加了三样东西:
-- 1. 向量类型:embedding 列是 vector(1024),按二进制紧凑存储
-- 2. 距离运算符:<=> 是余弦距离,<-> 是欧氏距离,<#> 是负内积
-- 3. ANN 索引:IVFFlat 或 HNSW,查询时只在索引圈定的少量候选里精算
select chunk_id, 1 - (embedding <=> :query_vector) as similarity
from knowledge_chunk_vector
where kb_id = :kb_id -- 元数据过滤和向量排序写在一条 SQL 里
order by embedding <=> :query_vector -- 按距离排序,能走向量索引
limit 5;
对比 MySQL 方案,差别在于:数据不用搬到应用里,计算在数据库内完成,而且有索引兜底。候选从几十条涨到几十万条,查询时间也不会跟着线性增长。
五、PGVector 和专用向量库怎么选
两者都有 ANN 索引,区别在规模上限和运维形态:
| 维度 | PostgreSQL + PGVector | 专用向量库(Milvus 等) |
|---|---|---|
| 部署形态 | 一个数据库扩展 | 独立服务或集群 |
| 和业务数据的关系 | 可以同库,也可以单独一个库 | 独立存储,靠同步保持一致 |
| 过滤能力 | 标准 SQL,任意组合 | 各家有自己的过滤语法 |
| 规模上限 | 单机百万级比较从容 | 千万、亿级,支持分片 |
| 运维成本 | 会 PostgreSQL 就能维护 | 需要专门维护 |
大多数企业知识库的片段量在几万到几十万条之间,PGVector 足够,还能用熟悉的 SQL 做过滤和联表;只有数据量和并发都到了单机扛不住的程度,专用向量库的分布式能力才值得它的运维成本。
六、收窄候选是 MySQL 方案成立的前提
MySQL 方案能跑通,靠的是「先用普通索引过滤,再在内存里精算」。要做到这一点,向量表的设计要配合:
- 在向量表里冗余业务维度,比如
kb_id,并建普通索引。检索时直接where kb_id = ?,不用先联片段表; - 只让可用的向量参与检索,比如加一个状态列,只取
READY的; - 片段 ID 建唯一索引,一个片段只有一条向量,重新生成时覆盖旧的。
如果业务上做不到收窄,比如用户每次都要在全部资料里搜,或者单个知识库本身就有上万条片段,那这套方案从一开始就不成立。
七、换存储方案时要注意什么
从 MySQL 迁到向量库,或者在两种向量库之间迁移,向量本身可以原样搬过去,但有几件事不能漏:
- 模型和维度必须一致。 迁移前后用的必须是同一个 Embedding 模型、同一个维度,否则要全部重新向量化;
- 片段 ID 的映射不能丢。 向量库只负责找到 ID,原文、标题、权限这些信息还要回业务库查;
- 删除要两边同步。 业务库删了片段,向量库里对应的向量也要删,否则检索会命中一条再也查不到出处的「孤儿向量」;
- 重建索引。 IVFFlat 这类需要聚类的索引,要在数据导入之后再建。
八、常见误区与追问
- 误区:做 RAG 就必须上向量数据库。 候选能收窄到几十条时,MySQL 普通表加内存余弦完全够用,还少维护一个组件;是否上向量库,由数据规模和延迟要求决定。
- 误区:MySQL 方案慢是因为算余弦慢。 1024 维算一次余弦只要一千多次乘加,真正的开销是把大量向量从库里读出来、再解析成浮点数组。
- 误区:专用向量库一定比 PGVector 好。 在几万到几十万条的规模上,两者速度差异不明显,PGVector 还能用标准 SQL 做过滤和联表,运维也更简单。
- 误区:向量库里存了原文,就不需要业务库了。 权限、文档标题、版本这些业务信息仍然在业务库里,向量库负责「找」,业务库负责「查全」。
- 追问:MySQL 存向量用 text 还是 blob? text 存逗号分隔的字符串便于查看和调试,blob 存二进制更省空间、解析更快;候选很少时差别不大,候选多时二进制明显更好。
- 追问:什么信号说明该从 MySQL 迁到向量库了? 单次检索的候选条数稳定超过几百条、检索延迟开始影响问答响应,或者出现了无法按业务维度收窄的全库检索需求。
九、加强记忆
向量存储要干两件事:存下向量和元数据,按相似度快速找出最近的 K 条。MySQL 没有向量类型和索引,只能读出候选在应用里算余弦,所以它成立的前提是先按业务维度把候选收窄到几十条;算一笔账,1024 维向量以文本存储约 10 KB 一条,候选 50 条只读 0.5 MB,候选 10 万条就要读 1 GB,瓶颈在读库而不在计算。PGVector 给 PostgreSQL 加了向量类型、距离运算符和 IVFFlat / HNSW 索引,过滤和排序写在一条 SQL 里,是大多数企业知识库的合适选择;专用向量库留给千万级数据和高并发。无论选哪种,都要保证入库和查询用同一个模型,片段 ID 能回查业务库,删除两边同步。
项目实战落地
项目里怎么做的
同一个「向量存哪」的问题,AI 实战库里有两种落地方式:
《AI Agent 智慧医院智能导诊就诊系统》把向量存在 MySQL 普通表 medical_chunk_vector 里:
vector_text是text列,存逗号分隔的 1024 维向量;chunk_id建唯一索引,一个片段只有一条向量,重新生成时覆盖;kb_id冗余一份并建普通索引,检索时先按知识库收窄;embedding_model、vector_dimension记录生成这条向量的模型和维度,embedding_status只让READY的向量参与检索。
检索固定三步:用同一个模型把问题向量化 → 按 kb_id 取出 READY 的候选向量 → 在内存里算余弦取前 K 条。
《AI企业知识库》则是双库:业务数据在 MySQL,向量在 PostgreSQL + PGVector,向量列是 vector(1024),建了余弦相似度的 IVFFlat 索引。
《AI Agent岗位匹配与求职规划系统》把向量和切片放在同一张 MySQL 表 knowledge_chunk 里,向量存在 longtext 列 embedding_text。检索时不建向量索引,也不先按业务维度收窄:SQL 取出全部启用的切片(所属知识必须启用且已向量化),读进内存逐条算余弦,按相似度倒序取前 topK 条。
《AI企业流程编排系统》连单独的向量表都没建:向量以 JSON 数组的形式存在知识切片表 knowledge_chunk 的 vector_json 列里,和标题、正文在同一行。向量在检索时才生成:知识检索节点取出所配知识库下启用的切片(最多 500 条),哪条还没有向量,就把它的标题和正文拼起来调一次向量模型,结果写回 vector_json,下次直接复用;相似度在 Java 里逐条算。面试文档写明了这个取舍:不引入 PGVector 这类专用向量库,完整的工业化 RAG 放在企业知识库项目里。
《AI Agent电商平台导购与运营增长系统》的向量单独放在 MySQL 表 product_knowledge_embedding 里,一条切片一行,向量以保留 6 位小数、逗号分隔的文本存在 vector_text,同时冗余切片所属的资料 ID 和商品 ID。检索时按商品 ID 取出状态为 READY 的向量,在内存里逐条算余弦,分数保留 4 位小数,排序后取前 topK。
为什么这样取舍
- 智慧医院是 Agent 项目,知识库检索只是 Agent 的一个工具。 检索前按
kb_id把候选收窄到几十条,内存计算绰绰有余;向量表跟着业务库一起建、一起备份,不需要再装一个 PostgreSQL。 - AI企业知识库是专门讲 RAG 的项目。 知识库、文档会持续增加,需要向量索引,也要为按知识库过滤、后续扩展检索策略留好空间,PGVector 更合适。
面试官还会追问
- 向量表为什么要冗余一份
kb_id,而不是检索时去联片段表? - 双库方案里,PGVector 的向量表是在什么时机建出来的?为什么不直接写进初始化 SQL?
- 向量明明已经存成文本了,为什么还要单独记一列模型名和维度?
学完《AI Agent 智慧医院智能导诊就诊系统》和《AI企业知识库》,上面这些追问你都会迎刃而解。