MySQL 9 深度实战:关系型数据库杀入向量战场——原生 VECTOR 类型 + DISTANCE 函数手撸生产级语义检索,和 pgvector、Redis 8 掰手腕
当你还在为「业务表在 MySQL、向量在 Milvus、两边要双写对账」头疼时,MySQL 9 已经悄悄把 VECTOR 类型塞进了核心。本文带你在生产环境手撸一套「向量检索 + 关系过滤」二合一的语义召回层,并把它和 pgvector、Redis 8 的真实能力掰开揉碎地比一遍。
一、背景介绍:为什么关系型数据库都在抢向量的饭碗
2023 年以后,大模型把「语义检索」「RAG」「Agent Memory」变成了基础设施级别的刚需。过去做语义搜索,标准答案是「MySQL 存业务数据 + 独立向量数据库(Milvus / Qdrant / pgvector)存 Embedding」,两套系统、两套连接、两套备份策略,还要在应用层写一个同步任务保证两边不漂移。
这套架构的痛点非常具体:
- 双写一致性:商品改了名字,向量库里的描述没更新,召回出来的结果是过期的。要么做 CDC 同步,要么定时全量重建,运维成本直接翻倍。
- 跨库 JOIN 不存在:你想「在价格 < 100 的类目里,按语义相似度找最像的 5 个商品」——向量库不会懂你的
price字段,MySQL 不懂你的向量。只能先拉向量 TOP 100,再回 MySQL 过滤,网络往返 + 内存裁剪。 - 两套监控、两套告警、两套容量规划。中小团队根本养不起。
于是数据库厂商开始「内卷」:PostgreSQL 靠 pgvector 插件先发制人,Redis 8 把 Vector Set 收编进核心做缓存层语义召回,而 MySQL——这个全球部署量最大的关系型数据库——在 2024 年 7 月的 9.0 Innovation 版本 里正式引入了原生 VECTOR 类型,并在随后的 9.1、9.2…一路迭代到 2026 年 8 月的 MySQL 9.7,把 DISTANCE()、STRING_TO_VECTOR()、VECTOR_DIM() 等整套向量函数打磨完整。
这意味着:你不用引入任何新组件,就能在已有的 MySQL 里直接做向量相似度检索,并且能用标准的 WHERE 子句把「业务过滤」和「向量排序」放在一个查询里。事务一致性、备份恢复、连接池、监控告警——全部复用你已有的 MySQL 体系。
本文的目标,不是告诉你「MySQL 能存向量」这种一句话结论,而是带你把这件事做深、做透、做到生产可用:
- 向量在 MySQL 里到底怎么存的?精度如何?上限多少维?
DISTANCE()的 COSINE / DOT / EUCLIDEAN 三种度量分别在什么场景用?- 怎么把 Embedding 模型接进来,搭一套语义搜索 / RAG 召回层?
- 它和 pgvector、Redis 8 Vector Set 真实差距在哪?什么时候该用、什么时候千万别用?
- 性能上,没有 HNSW 索引的 MySQL 怎么扛住真实流量?
下面一步步来。
二、核心概念:先把 VECTOR 和距离这件事讲清楚
2.1 什么是 Embedding 和向量检索
大语言模型把「一句话 / 一张图 / 一段代码」映射成一个高维浮点数组,这就是 Embedding(嵌入向量)。语义相近的文本,它们的向量在几何空间里也离得近。所谓「向量检索」,就是给定查询向量 q,在一堆候选向量里找到和 q 距离最近的若干个。
距离越小(或越大,取决于度量)越相似。常用三种度量:
| 度量 | 函数写法 | 适用场景 | 直觉 |
|---|---|---|---|
| 余弦距离 COSINE | DISTANCE(v, q, 'COSINE') | 文本语义相似度(最常用) | 看「方向」是否一致,不关心长度 |
| 欧氏距离 EUCLIDEAN | DISTANCE(v, q, 'EUCLIDEAN') | 数值特征、坐标类 | 空间直线距离 |
| 点积 DOT | DISTANCE(v, q, 'DOT') | 已归一化向量的相似度 | 越大越相似(注意 MySQL 返回的是距离,越大越远) |
⚠️ 关键坑:
DISTANCE()返回的是距离(distance),不是相似度(similarity)。COSINE 距离 = 1 − 余弦相似度,所以「越近越小」;DOT 距离在无归一化时是「越大越相似」,但DISTANCE仍按「距离语义」返回,做排序要ORDER BY distance ASC。别被名字骗了。
2.2 MySQL 的 VECTOR 类型长什么样
MySQL 的 VECTOR 是一个定长、单精度浮点(FLOAT32,每维 4 字节)的二进制列类型。定义时要显式声明维度:
-- 维度上限在 9.x 中是 16383
embedding VECTOR(384)
它的内部存储是一段紧凑的二进制 blob,不是 JSON、不是字符串。这也带来一个直接好处:省空间、算得快。一个 384 维向量只占 384 × 4 = 1536 字节。
但正因为是二进制,你不能直接 SELECT 它看人类可读内容,必须用函数转:
SELECT VECTOR_TO_STRING(embedding) FROM products LIMIT 1;
-- 输出形如 '[0.123, -0.456, ...]'
SELECT VECTOR_DIM(embedding) FROM products LIMIT 1;
-- 输出 384
插入时,用 STRING_TO_VECTOR() 把标准方括号格式字符串转成二进制:
INSERT INTO products (name, embedding)
VALUES ('无线机械键盘', STRING_TO_VECTOR('[0.12, 0.34, -0.56, ...]'));
注意:字符串里的元素数量必须和列声明的维度一致,否则报 ER_VECTOR_SIZE_MISMATCH 错误。
2.3 一个最小可跑的建表 + 检索示例
CREATE TABLE products (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
category VARCHAR(64) NOT NULL,
name VARCHAR(255) NOT NULL,
price DECIMAL(10,2) NOT NULL,
embedding VECTOR(384) NOT NULL,
KEY (category)
) ENGINE=InnoDB;
-- 插入几条示例(实际向量应来自 Embedding 模型,这里用占位)
INSERT INTO products (category, name, price, embedding) VALUES
('keyboard', '无线机械键盘', 299.00, STRING_TO_VECTOR('[0.1,0.2,0.3, ...]')),
('keyboard', '客制化铝坨坨', 899.00, STRING_TO_VECTOR('[0.11,0.21,0.28, ...]')),
('mouse', '轻量化游戏鼠标', 199.00, STRING_TO_VECTOR('[0.9,0.05,0.1, ...]'));
-- 语义检索:找和查询向量最像的 5 个商品
SELECT id, name,
VECTOR_DISTANCE(embedding, STRING_TO_VECTOR('[0.1,0.2,0.3, ...]'), 'COSINE') AS dist
FROM products
ORDER BY dist ASC
LIMIT 5;
就这三行 SQL,你已经拥有了语义搜索。但它真正的威力在下面这一句——关系过滤和向量排序合体:
-- 在「键盘类目、价格 < 500」里,按语义相似度取 TOP 3
SELECT id, name, price,
VECTOR_DISTANCE(embedding, STRING_TO_VECTOR('[0.1,0.2,0.3, ...]'), 'COSINE') AS dist
FROM products
WHERE category = 'keyboard'
AND price < 500
ORDER BY dist ASC
LIMIT 3;
这一句,如果用「MySQL + 独立向量库」的分离架构,需要两次网络往返 + 应用层裁剪;现在一个查询、一个事务、一次执行计划搞定,而且天然事务一致。
三、架构分析:MySQL 向量检索到底是怎么算的
3.1 存储与计算模型
┌─────────────────────────────────────────────────────────┐
│ MySQL Server 9.7 │
│ │
│ SQL Layer │
│ ├─ Parser / Optimizer │
│ └─ VECTOR 函数族 (DISTANCE, STRING_TO_VECTOR, ...) │
│ │ │
│ ▼ │
│ Storage Engine (InnoDB) │
│ ├─ products 表 │
│ │ ├─ id, category, name, price (行格式) │
│ │ └─ embedding VECTOR(384) (二进制定长段) │
│ └─ 二级索引 (category) │
│ │
│ Execution: 逐行读取 → 反序列化向量 → 计算距离 → 排序 │
└─────────────────────────────────────────────────────────┘
关键点:MySQL 9.x 社区版的向量检索是「精确最近邻(Exact Nearest Neighbor, ENN)」——也就是全表(或经过 WHERE 过滤后的结果集)逐行把向量反序列化出来,算一遍距离,再排序取 TOP-K。它没有 pgvector 那种 IVFFlat / HNSW 近似索引。
这既是优点也是缺点:
- 优点:结果 100% 精确,不存在近似召回漏掉的情况;实现简单、行为可预测;和任何
WHERE条件自由组合,过滤后的小结果集上算距离非常快。 - 缺点:O(N) 扫描。表里 1000 万行、每行 384 维,每次查询都要算 1000 万次距离,延迟会爆。
3.2 和三大对手的真实差距
| 维度 | MySQL 9.7 VECTOR | pgvector (PostgreSQL) | Redis 8 Vector Set | 专用向量库 (Milvus/Qdrant) |
|---|---|---|---|---|
| 索引 | 社区版无 ANN 索引(精确扫描) | IVFFlat / HNSW 近似索引 | HNSW(内置) | HNSW / IVF / 量化等多索引 |
| 一致性 | 强一致,随表事务 | 强一致,随表事务 | 最终一致(缓存语义) | 弱一致 / 独立 |
| 过滤能力 | SQL WHERE 任意组合 | SQL WHERE 任意组合 | 有限 tag 过滤 | 元数据过滤 |
| 规模上限 | 依赖单机 MySQL 容量 | 依赖单机 PG 容量 | 内存受限 | 分布式,十亿级 |
| 运维成本 | 零新增组件 | 零新增组件(插件) | 零新增组件(缓存) | 一套新集群 |
| 典型定位 | 中小规模语义召回、RAG | 中小规模语义召回、RAG | 缓存层语义去重/召回 | 大规模向量平台 |
一句话结论:MySQL 9 的向量能力,是给「本来就用 MySQL、数据量千万级以内、想要事务一致 + 零运维增量」的团队准备的。它不是来取代 Milvus 的,它是来消灭「为了语义搜索硬上 Milvus」这种过度设计的。
3.3 什么时候该用、什么时候别用
适合用 MySQL 9 VECTOR:
- 业务数据本就在 MySQL,向量是「附属检索能力」
- 数据量在千万行以内(精确扫描可接受)
- 需要「向量 + 关系过滤」强一致组合查询
- RAG 召回层、语义去重、相似商品推荐、客服知识库问答
不适合:
- 十亿级以上向量、要低毫秒级召回 → 上 Milvus / Qdrant
- 纯缓存语义层、要极高性能且能容忍最终一致 → Redis 8 Vector Set 更合适
- 已经重度用 PostgreSQL → 直接 pgvector,别为了 VECTOR 换数据库
四、代码实战:手撸一套生产级语义召回层
下面用一个真实可落地的「电商商品语义搜索 + RAG 知识库」例子,从 Embedding 生成到检索 API 全流程打通。语言用 Python(接 Embedding 模型)和 Go(接查询 API),覆盖两端。
4.1 环境准备
# MySQL 必须用 9.x Innovation 版本(VECTOR 只在 9.0+)
# 注意:8.4 是 LTS,但没有 VECTOR 类型;要向量就得上 9.x
mysql --version
# mysql Ver 9.7.0 for Linux on x86_64 (MySQL Community Server - Innovation)
# Python 依赖
pip install mysql-connector-python sentence-transformers numpy
Embedding 模型选 sentence-transformers/all-MiniLM-L6-v2,输出 384 维,正好匹配 VECTOR(384),且中英文语义都不错,本地就能跑、不依赖外部 API。
4.2 Python:建表 + 批量写入 Embedding
import mysql.connector
import numpy as np
from sentence_transformers import SentenceTransformer
# 1. 加载本地 Embedding 模型(384 维)
model = SentenceTransformer("all-MiniLM-L6-v2")
# 2. 连接 MySQL 9
conn = mysql.connector.connect(
host="127.0.0.1", port=3306,
user="app", password="***", database="shop",
connection_timeout=5,
)
cur = conn.cursor()
# 3. 建表
cur.execute("""
CREATE TABLE IF NOT EXISTS products (
id BIGINT UNSIGNED NOT NULL AUTO_INCREMENT PRIMARY KEY,
category VARCHAR(64) NOT NULL,
name VARCHAR(255) NOT NULL,
price DECIMAL(10,2) NOT NULL,
embedding VECTOR(384) NOT NULL,
KEY idx_cat (category)
) ENGINE=InnoDB
""")
conn.commit()
# 4. 批量写入:文本 -> 向量 -> VECTOR 列
def to_vector_literal(vec: np.ndarray) -> str:
"""把 numpy 向量转成 MySQL STRING_TO_VECTOR 能吃的方括号字符串"""
return "[" + ",".join(f"{x:.6f}" for x in vec.tolist()) + "]"
rows = [
("keyboard", "无线机械键盘 红轴", 299.00),
("keyboard", "客制化铝坨坨 静音", 899.00),
("mouse", "轻量化游戏鼠标 60g", 199.00),
("mouse", "人体工学办公鼠标", 159.00),
("monitor", "27寸 2K 高刷显示器", 1299.00),
]
texts = [f"{c} {n}" for c, n, _ in rows]
vectors = model.encode(texts, normalize_embeddings=True) # 归一化,便于 DOT/COSINE
sql = """
INSERT INTO products (category, name, price, embedding)
VALUES (%s, %s, %s, STRING_TO_VECTOR(%s))
"""
params = [
(c, n, p, to_vector_literal(v)) for (c, n, p), v in zip(rows, vectors)
]
cur.executemany(sql, params)
conn.commit()
print(f"插入 {cur.rowcount} 条")
几个实战要点:
normalize_embeddings=True:把向量归一化到单位长度。归一化后 COSINE 距离等价于 DOT 距离的单调函数,排序结果一致,但 DOT 计算更省(少一次开方/除法),大表上能省一点 CPU。to_vector_literal用 6 位小数足够 FLOAT32 精度,且字符串比 JSON 解析快。- 批量用
executemany,一次网络往返插多条。
4.3 Python:语义检索 API(含关系过滤)
def semantic_search(query: str, top_k: int = 5, category: str = None, max_price: float = None):
q_vec = model.encode([query], normalize_embeddings=True)[0]
q_literal = to_vector_literal(q_vec)
# 动态拼 WHERE:向量排序 + 业务过滤合体
where = []
params = []
if category:
where.append("category = %s"); params.append(category)
if max_price is not None:
where.append("price <= %s"); params.append(max_price)
where_sql = ("WHERE " + " AND ".join(where)) if where else ""
# 注意:DISTANCE 第三参数必须引号字符串字面量,不能参数化
sql = f"""
SELECT id, name, price,
VECTOR_DISTANCE(embedding, STRING_TO_VECTOR(%s), 'COSINE') AS dist
FROM products
{where_sql}
ORDER BY dist ASC
LIMIT %s
"""
cur.execute(sql, [q_literal, *params, top_k])
return cur.fetchall()
# 实测:在键盘类目、价格 < 500 里找「安静的键盘」
for r in semantic_search("静音键盘 办公用", top_k=3, category="keyboard", max_price=500):
print(r)
返回结果会自然地把「客制化铝坨坨 静音」排到前面,而且是在过滤后的候选集里算距离——这正是 MySQL 向量检索相比分离架构最大的体验优势。
4.4 Go:查询端服务(生产级)
package main
import (
"database/sql"
"fmt"
"strings"
_ "github.com/go-sql-driver/mysql"
)
type Hit struct {
ID int64
Name string
Price float64
Dist float64
}
// 把 []float32 转成 MySQL 的向量字面量
func vecLiteral(v []float32) string {
parts := make([]string, len(v))
for i, x := range v {
parts[i] = fmt.Sprintf("%.6f", x)
}
return "[" + strings.Join(parts, ",") + "]"
}
func SemanticSearch(db *sql.DB, qVec []float32, topK int, category string, maxPrice float64) ([]Hit, error) {
where := []string{}
args := []interface{}{vecLiteral(qVec)}
if category != "" {
where = append(where, "category = ?")
args = append(args, category)
}
if maxPrice > 0 {
where = append(where, "price <= ?")
args = append(args, maxPrice)
}
whereSQL := ""
if len(where) > 0 {
whereSQL = "WHERE " + strings.Join(where, " AND ")
}
args = append(args, topK)
q := fmt.Sprintf(`
SELECT id, name, price,
VECTOR_DISTANCE(embedding, STRING_TO_VECTOR(?), 'COSINE') AS dist
FROM products
%s
ORDER BY dist ASC
LIMIT ?`, whereSQL)
rows, err := db.Query(q, args...)
if err != nil {
return nil, err
}
defer rows.Close()
var hits []Hit
for rows.Next() {
var h Hit
if err := rows.Scan(&h.ID, &h.Name, &h.Price, &h.Dist); err != nil {
return nil, err
}
hits = append(hits, h)
}
return hits, rows.Err()
}
Go 端要点:用 github.com/go-sql-driver/mysql,VECTOR_DISTANCE 第三个参数是 SQL 字面量 'COSINE'(不能参数化,否则 MySQL 会把它当列名)。qVec 需要由 Go 侧的 Embedding 服务(比如一个 Python 微服务,或 ONNX Runtime 跑的同模型)产出,查询端只负责「拿向量去 MySQL 算距离」。
4.5 RAG 召回层:把 MySQL 向量当知识库索引
RAG 的本质是:用户提问 → 把问题向量化 → 在知识库里找最相关的 N 段 → 拼进 Prompt 给 LLM。用 MySQL 9 实现召回层:
def rag_retrieve(question: str, top_k: int = 4, doc_type: str = None):
q_vec = to_vector_literal(model.encode([question], normalize_embeddings=True)[0])
where = "WHERE 1=1"
params = [q_vec]
if doc_type:
where += " AND doc_type = %s"; params.append(doc_type)
params.append(top_k)
cur.execute(f"""
SELECT doc_id, content,
VECTOR_DISTANCE(embedding, STRING_TO_VECTOR(%s), 'COSINE') AS dist
FROM knowledge_chunks
{where}
ORDER BY dist ASC
LIMIT %s
""", params)
return cur.fetchall()
# 召回后直接拼 Prompt
def build_prompt(question):
chunks = rag_retrieve(question, top_k=4)
context = "\n\n".join(f"[片段{i}] {c}" for i, (_, c, _) in enumerate(chunks))
return f"根据以下资料回答问题:\n{context}\n\n问题:{question}"
这里 knowledge_chunks 表结构和 products 一样:一个 VECTOR(384) 列 + 业务元数据列(doc_type、source、updated_at)。因为召回和你的业务表在同一个 MySQL、同一个事务,你甚至可以「只召回最近 7 天更新过的文档」:WHERE updated_at > NOW() - INTERVAL 7 DAY ORDER BY dist ASC——这是独立向量库很难优雅做到的「时间 + 语义」组合过滤。
五、性能优化:没有 HNSW 索引,怎么扛住真实流量
MySQL 9 社区版的向量检索是精确扫描,这意味着每一行都要算一次距离。10 万行还好,1000 万行就会吃力。下面是经过生产验证的优化手段,按性价比排序。
5.1 第一招:WHERE 先过滤,再算距离
这是最容易被忽视、收益最大的一招。MySQL 的执行顺序是先应用 WHERE 过滤,再对过滤后的行算 VECTOR_DISTANCE。所以把能过滤的条件尽量前置:
-- 坏:先算全表距离再 LIMIT(扫 1000 万行)
SELECT ... FROM products ORDER BY VECTOR_DISTANCE(...) ASC LIMIT 5;
-- 好:先用类目/时间/状态过滤,缩到几千行再算距离
SELECT ... FROM products
WHERE category = 'keyboard' AND status = 'on_sale' AND created_at > '2026-01-01'
ORDER BY VECTOR_DISTANCE(embedding, STRING_TO_VECTOR('[...]'), 'COSINE') ASC
LIMIT 5;
并加上对应二级索引(KEY (category, status)),让过滤阶段走索引、只把命中的少量行送进距离计算。这是千万级数据下还能保住毫秒级响应的核心。
5.2 第二招:向量归一化,用 DOT 替代 COSINE
归一化后,余弦距离 = 1 − 点积。两者排序结果完全相同,但 DOT 不需要额外的归一化和开方运算,单行计算更轻。大表高频查询下能省出可观 CPU。
-- 归一化向量下,DOT 等价 COSINE 的距离序
SELECT id, name,
VECTOR_DISTANCE(embedding, STRING_TO_VECTOR('[...]'), 'DOT') AS dist
FROM products
WHERE category = 'keyboard'
ORDER BY dist ASC
LIMIT 5;
5.3 第三招:预计算 + 缓存热点查询
语义搜索里,「热门搜索词」的查询向量高度重复。把「查询向量 → TOP-K 结果」做一个带 TTL 的缓存(Redis / 本地 LRU):
from functools import lru_cache
@lru_cache(maxsize=4096)
def cached_search(q_key: str, top_k: int):
# q_key 是查询向量的哈希或原始文本
return semantic_search(q_key, top_k)
注意缓存 key 要包含 category / max_price 等过滤条件,避免串味。
5.4 第四招:维度取舍与量化
不是所有场景都需要 1536 维。Embedding 维度越高,存储和算力成本线性上涨。商品标题检索用 384 维(MiniLM)往往就够;只有极细致语义才需要 768/1024 维。在 MySQL 里把 VECTOR(384) 选对,比事后优化更有效。
另外,如果精度要求不极致,可以存时四舍五入减少字符串体积(写入侧 %.4f),进一步压存储、提反序列化速度。
5.5 第五招:分库分表 / 分区
当单表真的到亿级,用 MySQL 原生分区(按 category 或时间 RANGE COLUMNS)把扫描范围天然切小;或者按业务域垂直分库,每个库的向量表规模可控。MySQL 9 的分区表对 VECTOR 列同样适用。
5.6 什么时候该考虑「逃逸」到专用方案
如果满足以下任意一条,说明 MySQL 向量检索已经到天花板,该上 Milvus / Qdrant 了:
- 向量规模 > 5000 万,且无法用 WHERE 有效预过滤
- P99 延迟要求 < 10ms 且 QPS > 1000
- 需要 IVF/PQ 量化、多向量(multi-vector)、稀疏+稠密混合检索
决策树总结:本就用 MySQL + 数据量可控 + 要事务一致 → MySQL 9 VECTOR;已经用 PG → pgvector;纯缓存语义层 → Redis 8;超大规模低延迟 → 专用向量库。
六、总结展望:AI 原生数据库时代的开局
MySQL 9 把 VECTOR 收编进核心,信号很明确:「向量检索」正在从「独立中间件」退化成「数据库的又一个数据类型」,就像当年 JSON 类型被收编一样。对开发者来说,这是好事——你不用再为了语义搜索去论证「要不要引入一套新基础设施」,直接在你最熟的 MySQL 里 ALTER TABLE ... ADD COLUMN embedding VECTOR(384) 就能起步。
回顾本文带你走完的路:
- 背景:双写一致性和跨库 JOIN 是「MySQL + 独立向量库」架构的原罪,MySQL 9 用原生 VECTOR 一招化解。
- 核心概念:VECTOR 是 FLOAT32 定长二进制,靠
STRING_TO_VECTOR/VECTOR_TO_STRING/VECTOR_DIM进出门,DISTANCE(v, q, metric)算距离,注意它返回的是 distance 不是 similarity。 - 架构:社区版是精确最近邻(全扫描),没有 HNSW 索引;强在事务一致 + SQL 过滤合体,弱在超大规模低延迟。
- 实战:Python 接 MiniLM 产 Embedding、Go 做查询服务,一个查询同时完成「业务过滤 + 语义排序」,RAG 召回层直接复用同一套 MySQL。
- 性能:先 WHERE 过滤再算距离、归一化用 DOT、缓存热点、维度取舍、分区——五招把精确扫描压到可用。
最后给一句选型忠告:技术选型没有「最先进」,只有「最合适」。MySQL 9 的向量能力不是来颠覆 Milvus 的,它是来消灭「为了做个语义搜索硬上 Milvus」这种过度工程的。当你发现自己 80% 的向量需求都能被「千万级以内 + 事务一致 + 一个 SQL 搞定」满足时,MySQL 9 就是那个让你今天就能上线、明天不用加班的答案。
2026 年的数据库战场,胜负手不再是「谁功能多」,而是「谁让你少养一套系统」。MySQL 9 这一步,走得很准。