MySQL 9.x VECTOR 实战:开源版没有向量索引也没有 DISTANCE,手把手把 MySQL 改造成 RAG 向量库
一句先说在前面:如果你是被「MySQL 现在也能做向量数据库了」这类标题党吸引来的,建议先泼盆冷水——截至 MySQL 9.7,标准发行版(Community / Enterprise Server)只给了你一个 VECTOR 存储类型,原生的向量索引和
DISTANCE()距离函数全都不在社区版里。本文会把这个「能」与「不能」的边界彻底掰开,并给你一套真正能跑、能上生产的 RAG 落地架构。
一、背景介绍:RAG 浪潮下, everyone 都在问「我的 MySQL 能不能也存向量」
过去两年,检索增强生成(RAG)把一个本来只属于算法工程师的问题,硬生生砸到了每一个后端开发面前:你总得有个地方存 embedding 向量,然后按相似度把最相关的片段捞出来喂给大模型。
于是选型清单上出现了三拨人:
- 激进派直接上专用向量数据库,Milvus、Qdrant、Weaviate、pgvector,一个比一个能打;
- 务实派觉得「我系统里已经有一套 MySQL 了,再引入一个数据库就是多一套运维、多一份数据同步的糟心事」;
- 跟风派看到 MySQL 9.0 发版笔记里赫然写着「新增 VECTOR 数据类型」,眼睛一亮:「搞定,以后一个 MySQL 全包了」。
第三拨人最容易踩坑。因为 VECTOR 这三个字母给人的错觉太强——它听起来就像「MySQL 原生支持向量检索」的通行证。但真相是:
- VECTOR 只是一个存储类型,解决的是「怎么把一串浮点数高效地塞进一行、并以二进制紧凑地落盘」;
- 它不提供任何检索能力——你没法建向量索引,也没法在 SQL 里直接算两个向量的距离;
- 官方唯一的距离函数
DISTANCE()(以及它的同义词VECTOR_DISTANCE())在文档里写得明明白白:只对 MySQL HeatWave on OCI 和 MySQL AI 用户开放,社区版和商业版(非 HeatWave)一概没有。
换句话说,如果你用的是开源的 MySQL Community Server(或者普通的 Enterprise Server,没上 HeatWave),那么「用 SQL 一句 ORDER BY DISTANCE(emb, :q) LIMIT 10 把最相似的文档排出来」这种写法,根本不存在。
那是不是说 MySQL 就完全不适合做向量检索了?也不是。只是你不能再把它当成一个「开箱即用的向量数据库」,而要把它当成一个**「事务一致的关系型存储 + 应用层相似度计算」的混合体**来设计。本文接下来的五千多字,就是把这件事讲透:边界在哪、架构怎么搭、代码怎么写、性能怎么榨。
二、核心概念:先把 VECTOR 这台机器的齿轮看清楚
2.1 VECTOR 到底是什么
MySQL 9.0(2024 年 7 月)作为第一个「创新版(Innovation Release)」引入了 VECTOR 类型。它的定义极简:
VECTOR(N)
- 每个元素是 4 字节单精度浮点数(single-precision float,即
float32); N是向量的维度,默认长度 2048,最大 16383;- 声明默认长度时直接写
VECTOR,不能写VECTOR()(空括号会报语法错)。
底层存储是紧凑的二进制:维度 N 个 float32 连成一串,没有任何多余封装。你可以用 HEX() 把它打出来看,也可以用 BIT_LENGTH() / LENGTH() 看它占多少字节(N * 4)。
-- 建一张最朴素的「文档 + 向量」表
CREATE TABLE doc_embedding (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
category VARCHAR(64) DEFAULT NULL,
embedding VECTOR(1536) NOT NULL, -- 假设用 1536 维的 embedding 模型
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
INDEX idx_category (category)
) ENGINE=InnoDB;
注意 embedding 列不能设为主键、外键、唯一键或分区键——这是 VECTOR 类型的硬性约束,后面会讲为什么这个约束很要命。
2.2 VECTOR 的「不能」清单(这才是重点)
官方文档把 VECTOR 的能力边界划得很死,我把它翻译成人话:
| 能力 | 是否支持 | 说明 |
|---|---|---|
与同类型做相等比较 = | ✅ | 仅此一种比较 |
| 与任意其他类型比较 | ❌ | 连类型转换都不行 |
| 作为主键/外键/唯一键/分区键 | ❌ | 因为它不可比较、不可排序 |
| 作为直方图源 | ❌ | 优化器无法为其建统计信息 |
| 用于数值/日期/全文/XML/位运算/JSON 函数 | ❌ | 一条都不行 |
| 作为聚合函数或窗口函数参数 | ❌ | 只有 COUNT(DISTINCT) 例外 |
用 CAST(... AS VECTOR) 转换 | ❌ | 反向的 CAST(vec AS BINARY) 可以 |
| 在 JavaScript 存储程序里使用 | ⚠️ | 仅 MySQL Enterprise 版支持 JS 存储过程,社区版没有 |
这张表读下来,结论很扎心:你在 SQL 里几乎没法对 VECTOR 列做任何「计算」。它就是一个被装进 BLOB 的二进制串,你只能整存整取,然后一次性 SELECT 回应用层。
这也就是为什么网上那些「用 MySQL 原生做向量检索」的教程,要么偷偷假设你用了 HeatWave(有 DISTANCE()),要么在应用代码里算距离——纯 SQL 这条路,对社区版基本是堵死的。
2.3 唯一的距离函数 DISTANCE(),但它是「别人家的」
MySQL 9.x 提供的距离函数长这样:
-- 仅在 MySQL HeatWave on OCI / MySQL AI 中可用
DISTANCE(vector_column, query_vector, 'COSINE')
DISTANCE(vector_column, query_vector, 'DOT')
DISTANCE(vector_column, query_vector, 'EUCLIDEAN')
-- VECTOR_DISTANCE() 是它的同义词
VECTOR_DISTANCE(...) -- 同上
三种度量:
COSINE:余弦距离(实际返回的是 1 - 余弦相似度,范围 0~2);DOT:点积(内积);EUCLIDEAN:欧氏距离(L2)。
如果你正好是 HeatWave 用户,那恭喜,你确实可以写出优雅的查询:
-- 仅 HeatWave 可用,社区版执行会报函数不存在
SELECT id, title,
DISTANCE(embedding, STRING_TO_VECTOR('[0.1, 0.2, ...]'), 'COSINE') AS d
FROM doc_embedding
ORDER BY d
LIMIT 10;
但本文面向的是绝大多数用社区版的读者,所以这条捷径直接划掉。我们后面所有方案都基于「函数不可用、索引不可用」这个前提来设计。
2.4 三个距离度量,以及为什么归一化是灵魂
既然要自己在应用层算,就得先搞清楚三个度量的数学含义,否则写出来的检索会「看起来能跑,但结果一塌糊涂」。
点积(Dot Product):
dot(a, b) = Σ aᵢ · bᵢ
对已经 L2 归一化的向量(模长 = 1),点积等价于余弦相似度。这是工业界最常用 trick:embedding 模型输出后先归一化,存进去,检索时直接点积排序,连模长都不用管。
余弦相似度(Cosine Similarity):
cos(a, b) = dot(a, b) / (||a|| · ||b||)
衡量方向一致性,范围 [-1, 1]。文本语义检索几乎都用它,因为它对向量的绝对长度不敏感,只关心方向。
欧氏距离(Euclidean / L2):
l2(a, b) = sqrt( Σ (aᵢ - bᵢ)² )
衡量几何距离,对向量尺度敏感。图像特征、某些推荐场景更常用。
关键结论:如果你在写入时就把所有向量归一化成单位长度(大多数 sentence embedding 模型都支持 normalize_embeddings=True),那么「余弦相似度」就退化成「点积」,而你只需要存一个向量、算一次点积,排序即可。这一步归一化,直接决定了你后面代码的复杂度。
三、架构分析:为什么 MySQL 社区版做不了 ANN,以及我们该怎么做
3.1 ANN 是什么,为什么它这么重要
向量检索分两种:
- 精确检索(Exact / Brute Force):把查询向量和库里每一个向量都算一遍距离,排序取 top-k。结果 100% 准确,但复杂度 O(N·D),N 是数据量、D 是维度。
- 近似检索(ANN, Approximate Nearest Neighbor):用 HNSW、IVF、PQ 等算法建索引,牺牲一点点召回率换几百上千倍的查询速度,复杂度降到 O(log N) 级别。
专用向量数据库(Milvus、Qdrant)和 pgvector(IVFFlat / HNSW)强就强在 ANN 索引。而 MySQL 社区版既没有向量索引类型,也没有 DISTANCE(),所以你唯一能用的就是精确检索 + 应用层算距离。
这意味着一个残酷的事实:
用 MySQL 社区版做向量检索,查询成本与数据量成线性关系。 1 万条时毫无压力,100 万条时一次查询要把 100 万个 1536 维向量拉到内存里算一遍——这通常要好几百毫秒到几秒,且无法靠加索引解决。
所以架构设计的第一性原则是:要么把 N 控制得住,要么把候选集先过滤小。
3.2 两条可行架构路线
路线 A:纯应用层暴力检索(App-side Brute Force)
查询请求 → 应用层把 query 文本转成 embedding
→ SELECT id, embedding, content FROM doc_embedding [WHERE 过滤]
→ 应用层用 NumPy 把 query 和所有候选向量算点积/余弦
→ argsort 取 top-k → 返回
优点:实现极简,零额外组件,事务一致(向量和元数据在同一张表、同一事务里)。
缺点:候选集大时查询慢,且每次都要把向量全量拉出来。
路线 B:元数据预过滤 + 分区暴力(Filtered Brute Force)
这是路线 A 的工程化升级,也是真正能在生产里扛住中等规模(十万~百万级)的关键。
查询请求 → 应用层解析出「过滤条件」(如 category=技术, 时间>某值)
→ SELECT ... FROM doc_embedding WHERE category=? -- 先用 B+Tree 索引把候选集从 100万 砍到 5000
→ 应用层只对这 5000 条算相似度
→ 返回 top-k
核心思想:用关系型数据库最擅长的 B+Tree 索引做「粗筛」,把向量计算的 N 从百万降到几千,剩下的交给应用层。这恰好是 MySQL 的强项(它管结构化过滤一流)补上了它向量检索的弱项(ANN 没有)。
我把这套打法叫做 「SQL 管过滤,NumPy 管相似」的 hybrid 模式。在绝大多数真实业务里(电商商品检索、知识库问答、工单相似推荐),查询天然带过滤条件,这个模式几乎白捡性能。
3.3 决策矩阵:什么时候该用 MySQL 存向量,什么时候该上专用库
| 场景 | 推荐方案 |
|---|---|
| 数据量 < 50 万,且查询常带结构化过滤 | MySQL VECTOR + 应用层,零额外运维 |
| 数据量 < 50 万,无过滤纯语义检索 | MySQL 也能跑,但专用库更省心 |
| 数据量 50 万 ~ 500 万,且强过滤 | MySQL hybrid 模式,配合分区/分表 |
| 数据量 > 500 万,或要求亚毫秒级检索 | 直接上 Milvus / Qdrant / pgvector(HNSW) |
| 已有 MySQL,只想「顺手」存 embedding 做轻量 RAG | MySQL VECTOR,千万别为了 RAG 硬引一个数据库 |
| 需要多租户隔离、标量过滤与向量混合查询的一体化 | 专用向量库(它们这块更成熟) |
一句话总结:MySQL 做向量检索的定位是「够用就好、省事优先」,而不是「性能极致」。
四、代码实战:从建表到可上生产的检索服务
下面用 Python + mysql-connector-python + sentence-transformers 完整走一遍。为了让你能不依赖下载大模型权重也能跑通,我会先给一个确定性的合成 embedding 生成器做演示,再给生产用的真实模型代码。
4.1 依赖与连接
import mysql.connector
import numpy as np
# 连接(社区版 MySQL 8.4 LTS 或 9.x Innovation 均可,只要服务端支持 VECTOR)
conn = mysql.connector.connect(
host="127.0.0.1",
port=3306,
user="rag_user",
password="change_me",
database="rag_demo",
connection_timeout=10,
)
conn.autocommit = False
版本提醒:VECTOR 类型需要 MySQL 9.0 及以上。如果你用的是 8.0 / 8.4 LTS,服务端不认
VECTOR(...)这个类型,本文方案失效,请升级或用 JSON 数组兜底(后文 4.5 会讲)。
4.2 建表(注意 VECTOR 列的坑)
CREATE TABLE IF NOT EXISTS doc_embedding (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
content TEXT NOT NULL,
category VARCHAR(64) NOT NULL DEFAULT 'default',
embedding VECTOR(1536) NOT NULL,
emb_norm FLOAT NOT NULL, -- 预存 L2 模长,供余弦计算优化
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id),
INDEX idx_category (category),
INDEX idx_created (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
这里我额外加了一个 emb_norm FLOAT 列——它不是 VECTOR,就是普通浮点,存这个向量的 L2 模长。为什么要存?因为后面算余弦相似度时 cos = dot / (||a||·||b||),查询向量的模长我们已知,库里每个向量的模长如果每次现算就得把整个向量在应用层遍历一遍(其实我们本来就要遍历,所以这列更多是「语义清晰 + 可选」)。真正有大用的是下一节的分区/分桶思路,这里先埋个伏笔。
4.3 写入:把文本变成向量再落库
生产环境用真实 embedding 模型:
from sentence_transformers import SentenceTransformer
model = SentenceTransformer("bge-base-zh-v1.5") # 中文场景常用
# 关键:写入前归一化,检索时直接点积即余弦
def embed(texts):
return model.encode(texts, normalize_embeddings=True, convert_to_numpy=True)
def insert_docs(rows):
"""rows: list of (title, content, category)"""
texts = [f"{t}. {c}" for t, c, _ in rows]
vecs = embed(texts) # shape: (N, 1536), 已归一化
norms = np.linalg.norm(vecs, axis=1) # 全为 ~1.0
sql = """
INSERT INTO doc_embedding (title, content, category, embedding, emb_norm)
VALUES (%s, %s, %s, STRING_TO_VECTOR(%s), %s)
"""
data = []
for (title, content, category), vec, norm in zip(rows, vecs, norms):
# 把 numpy float32 数组序列化成 '[x,y,z,...]' 字符串,交给 STRING_TO_VECTOR
vec_str = "[" + ",".join(f"{x:.8g}" for x in vec.tolist()) + "]"
data.append((title, content, category, vec_str, float(norm)))
cur = conn.cursor()
cur.executemany(sql, data)
conn.commit()
cur.close()
# 用法
docs = [
("MySQL 索引原理", "B+Tree 如何通过聚簇索引减少随机 IO……", "数据库"),
("Rust 所有权", "Move 语义如何在编译期消除数据竞争……", "编程语言"),
# ... 任意数量
]
insert_docs(docs)
几个值得停下来品的点:
STRING_TO_VECTOR()(同义词TO_VECTOR()) 是社区版唯一能用的「字符串→向量」转换函数。它接受'[x,y,z,...]'格式的字符串,元素是十进制或科学计数法浮点。- 写入前必须归一化(
normalize_embeddings=True)。这样emb_norm ≈ 1,检索时余弦退化为点积,省掉除法。 - 用
executemany做批量插入,比单条execute快一个数量级。百万级数据请配合分批(每批 500~2000 条)+ 关掉每次的autocommit。
可独立运行的最小演示(不依赖模型下载):
def fake_embed(texts, dim=8):
"""确定性合成向量,仅用于跑通流程;生产请替换为真实模型。"""
rng = np.random.default_rng(42)
# 用词频做 hash,保证相同文本得到相同向量
out = []
for t in texts:
h = abs(hash(t)) % (2**31)
rng = np.random.default_rng(h)
v = rng.standard_normal(dim).astype(np.float32)
v /= np.linalg.norm(v) # 归一化
out.append(v)
return np.stack(out)
# 用 fake_embed 替换上面的 embed() 即可零依赖跑通整条链路
4.4 检索:应用层暴力 + 元数据预过滤(核心)
这是全文最值钱的一段。我们实现两个函数:无过滤的全局检索,和带结构化过滤的混合检索。
def retrieve(query_text, top_k=5, category=None, model=model, embed=embed):
q = embed([query_text])[0] # 查询向量,已归一化,shape (1536,)
q_norm = float(np.linalg.norm(q))
# 1) 先用 SQL 把候选集尽量缩小(B+Tree 索引生效)
if category is not None:
sql = """
SELECT id, title, content, embedding, emb_norm
FROM doc_embedding
WHERE category = %s
"""
params = (category,)
else:
sql = "SELECT id, title, content, embedding, emb_norm FROM doc_embedding"
params = ()
cur = conn.cursor()
cur.execute(sql, params)
rows = cur.fetchall() # 每行: (id, title, content, embedding_blob, emb_norm)
cur.close()
if not rows:
return []
ids, titles, contents, emb_blob, norms = zip(*rows)
# 2) 把 MySQL 返回的二进制向量解码回 numpy
# VECTOR 二进制布局: 4 字节维度(uint32, little-endian) + N*4 字节 float32
def decode(vec_blob):
dim = int.from_bytes(vec_blob[:4], "little")
return np.frombuffer(vec_blob[4:], dtype="<f4", count=dim)
matrix = np.stack([decode(b) for b in emb_blob]) # (candidate_count, 1536)
# 3) 余弦相似度 = dot / (||q|| * ||doc||);因都归一化,dot 即余弦
dots = matrix @ q # (candidate_count,)
sims = dots / (q_norm * np.array(norms, dtype=np.float32))
# 4) 取 top-k
top_idx = np.argsort(-sims)[:top_k]
return [
{"id": ids[i], "title": titles[i], "content": contents[i], "score": float(sims[i])}
for i in top_idx
]
# 混合检索:只搜「数据库」分类下最像的 3 条
results = retrieve("MySQL 怎么建索引最快", top_k=3, category="数据库")
for r in results:
print(f"{r['score']:.4f} {r['title']}")
为什么 @ 矩阵乘法是性能命脉:一次查询要算 N 个向量和查询向量的点积。如果用 Python for 循环逐个算,N=10 万时轻松卡死;用 np.stack 把候选集拼成矩阵,再 matrix @ q 走 BLAS 高度优化的底层实现,10 万次点积在毫秒级完成。这一行 matrix @ q,就是「能上生产」和「玩具 demo」的分水岭。
4.5 关于「纯 SQL 能不能算」的诚实交代
很多人会问:既然 VECTOR 能 CAST(... AS BINARY),那我能不能在 SQL 里把二进制拆开算点积?答案是理论上能,工程上别这么干。原因有三:
- VECTOR 不能用在任何数值函数里,你必须
CAST(vec AS BINARY)拿到原始字节,然后用一大串SUBSTRING+CAST(... AS FLOAT)手动按 4 字节偏移量解析每个维度——写一个 1536 维的点积 SQL 会长得反人类,且完全没法利用索引; - 这种写法每条记录都要在 MySQL 内部做上千次字符串截取和类型转换,性能比应用层 NumPy 差几个数量级;
- 你已经把向量
SELECT出来了,何必再绕回 SQL 算?应用层一行@就解决了。
所以正路只有一条:向量整取回应用,相似度在 NumPy 里算。如果你连 MySQL 都不想碰向量类型,也可以把 embedding 直接存成 JSON 数组或 BLOB——但那样你就失去了 VECTOR 类型的紧凑存储和 STRING_TO_VECTOR 的便捷写入,属于退而求其次。VECTOR 类型在「存储紧凑 + 写入方便」这一点上依然有价值,值得用。
4.6 如果你真有 HeatWave:那就优雅多了
公平起见,给 HeatWave / MySQL AI 用户留个正解,免得被说我标题党:
-- 仅 HeatWave / MySQL AI
SELECT id, title,
DISTANCE(embedding, STRING_TO_VECTOR(:q), 'COSINE') AS dist
FROM doc_embedding
WHERE category = :cat
ORDER BY dist
LIMIT 10;
而且 HeatWave 还支持在 VECTOR 列上建原生向量索引(VECTOR INDEX),实现真正的 ANN。但代价是你得把数据库搬到 Oracle Cloud 的 HeatWave 服务上——这对大多数「就想用本地 MySQL」的团队来说,门槛不低。本文的主线,依然是社区版下的务实方案。
五、性能优化:把线性扫描的极限榨出来
既然没有 ANN 索引,我们就只能和 O(N) 死磕。下面是按「投入产出比」排序的优化手段。
5.1 第一招:元数据预过滤(收益最大,必做)
这是 3.2 路线 B 的工程落地,几乎是免费的午餐。只要你的查询带任何结构化条件(分类、时间、租户、状态),先 WHERE 过滤再算向量。一个 B+Tree 索引就能把候选集从百万砍到几千。
-- 给高频过滤字段建索引,MySQL 的强项
ALTER TABLE doc_embedding ADD INDEX idx_tenant_cat (tenant_id, category);
经验值:候选集控制在 1 万条以内,应用层 matrix @ q 基本是亚 10 毫秒;控制在 5 万条以内,通常在 50 毫秒上下;超过 50 万条,就该考虑分表或迁专用库了。
5.2 第二招:分表/分区,把 N 物理切小
当单表数据注定很大,但查询总有明确分区键(如 tenant_id、月份)时,用 MySQL 的分区表或应用层分表,让一次查询只扫一个分区:
-- 按租户哈希分区,单租户查询只落在一个分区
ALTER TABLE doc_embedding
PARTITION BY HASH(tenant_id) PARTITIONS 16;
配合 5.1 的过滤,等价于把 N 又除以了分区数。
5.3 第三招:降维与量化,省内存带宽
- 降维:如果业务允许,把 1536 维降到 384 维(很多模型支持不同尺寸,或用 PCA)。维度减半,点积的计算量和内存带宽直接减半,相似度质量损失往往很小。
- 精度:VECTOR 固定是 float32。如果你愿意牺牲一点精度换吞吐,应用层可用 float16 做矩阵乘法(GPU 上收益明显)。
5.4 第四招:结果缓存 + 查询去重
RAG 场景里,用户常问高度重复的问题(「怎么重置密码」「支持哪些支付方式」)。用查询文本(或 query embedding 的量化指纹)做缓存 key,命中直接返回,跳过整轮向量计算。这是性价比极高的优化。
from functools import lru_cache
@lru_cache(maxsize=2048)
def cached_retrieve(query_text, category=None):
return retrieve(query_text, category=category)
生产环境把缓存换成 Redis,支持多实例共享和 TTL。
5.5 第五招:批量查询的矩阵化
如果要同时算多个 query 的 top-k(比如一次给 10 个问题做检索),别写 10 次循环,把 query 也堆成矩阵:
Q = np.stack([embed([q])[0] for q in queries]) # (num_q, 1536)
sims = matrix @ Q.T # (candidate, num_q),一次 BLAS 搞定
一次矩阵乘替代 N 次循环,吞吐提升看得见。
5.6 什么时候该「认输」迁库
诚实的工程判断比硬撑更重要。出现以下信号,就该认真评估迁移到专用向量库了:
- 单表向量数稳定超过 500 万,且查询无法有效预过滤;
- P99 检索延迟卡在几百毫秒以上,业务方开始投诉;
- 需要「向量 + 标量」的混合查询特别复杂(多向量、重排序、元数据联合索引);
- 团队开始花大量时间手搓分表、缓存、降级逻辑。
迁移时不必推倒重来:把向量和 ANN 检索职责移交给 Milvus/Qdrant/pgvector,MySQL 继续当「元数据 + 业务事务」的主库,两者之间靠主键关联即可。这种「MySQL 管业务,向量库管检索」的分工,反而是很多中大型系统的终极形态。
六、总结与展望
回到开头的那盆冷水。MySQL 9.x 的 VECTOR 类型,既不是某些标题党说的「MySQL 终于能取代向量数据库了」,也不是彻底没用。它的真实定位是:
一个紧凑、事务一致、与业务数据同库的 embedding 存储容器;检索能力需要你在应用层用 NumPy 自己补上。
把它用对的姿势总结成三句话:
- 能存、不能查:VECTOR 解决了「怎么放」,没解决「怎么找」。社区版没有向量索引、没有
DISTANCE(),相似度只能应用层算。 - SQL 管过滤、NumPy 管相似:用 MySQL 最擅长的关系过滤把候选集砍小,再用一行
matrix @ q完成相似度排序。这是社区版下唯一能上生产的打法。 - 够用就好、省事优先:数据量在百万级以内、查询带过滤、不想多运维一套数据库——MySQL VECTOR 是绝佳的轻量 RAG 方案;一旦规模或延迟突破阈值,果断让专业向量库接管。
展望一下:MySQL 的向量能力目前明显是「半成品」状态,真正的 ANN 和距离计算被锁在 HeatWave 里。以 Oracle 近年对 AI 的投入节奏,社区版未来补上原生向量索引(或其轻量形态)并非不可能。但在那一天到来之前,掌握「应用层暴力检索 + 元数据预过滤」这套组合拳,能让你在现有的 MySQL 上就把 RAG 稳稳跑起来——不必为了一个 embedding,去说服老板再引入一整套新基础设施。
最后留个行动清单,照着做就能落地:
- 确认 MySQL ≥ 9.0(否则升级或改用 JSON 存向量);
- 建表时 VECTOR 列 + 高频过滤字段的 B+Tree 索引;
- 写入前归一化 embedding,
executemany批量入库; - 检索走「SQL 预过滤 + NumPy
matrix @ q」; - 加查询缓存,候选集超 5 万就评估分表/迁库。
把这几步做完,你的 MySQL 就不是「差点意思的向量数据库」,而是一个务实、可控、零额外运维的生产级 RAG 向量库。