MySQL 9.x 深度实战:原生向量检索 + JSON Duality + 容器化,一个关系型数据库如何长出 AI 与文档数据库的翅膀(2026)
当所有人都在讨论 pgvector、Milvus、MongoDB 时,MySQL 这个"老古董"在 9.x 系列里悄悄完成了一次基因突变:它既能存向量做语义检索,又能把关系表直接投影成可读写的 JSON 文档,还能在 Kubernetes 里自动感知 CPU/内存限制。本文不堆参数,只讲一个务实工程师真正用得上什么、怎么用、以及坑在哪。
一、背景介绍:为什么 2026 年我们还要关心 MySQL 的新版本
如果你是一名做了五六年业务的后端,听到"数据库又发新版了"大概率会无感——MySQL 8.0 一用就是七八年,稳定、熟悉、生态全。但 2026 年这个时间点有点不一样:AI 应用落地的主力矛盾,已经从"模型够不够聪明"变成了"数据怎么喂给模型"。
RAG(检索增强生成)是当下最主流的落地形态,而 RAG 的检索层,本质就是一个向量相似度搜索。过去两年行业的标准答案是:业务数据放 MySQL/PostgreSQL,向量单独搬去 Pinecone、Milvus 或 pgvector。这套架构跑得起来,但代价是双写一致性、两套运维、两份账单、两套备份策略——对于一个日活几万的普通应用来说,这个复杂度是严重过剩的。
MySQL 9.x 想解决的,就是把这个复杂度收回到一张库里:
- 9.0(2024-07):引入原生
VECTOR类型,意味着你终于可以在一张 InnoDB 表里同时存订单记录和它的向量,不用再维护一个外置向量库。 - 9.6(2026-01):JSON Duality Views 正式可用,让同一份关系数据既能用 SQL 查、又能以 MongoDB 式 JSON 文档读写;同时
container_aware启动参数让它在容器里"懂事"了;GTID 结构升级、InnoDB 多项改进。 - 9.7:
DISTANCE()距离函数补齐 EUCLIDEAN(欧氏距离),和 COSINE、DOT 凑齐三件套。
这篇文章的核心观点很明确:MySQL 9.x 不是要取代专用向量库或文档库,而是给"还没到那个规模、但已经被多套存储折磨"的团队一个"先别拆"的理由。 下面我们用代码说话。
注:MySQL 采用"Innovation"创新版与 LTS 长期支持版双轨。9.x 属于创新版,迭代快、特性新,生产环境请评估稳定性后再上。本文所有示例基于 9.6 / 9.7 语法。
二、核心概念:三块新拼图到底长什么样
2.1 VECTOR 类型:不是"加了个字段类型"那么简单
VECTOR(N) 表示一个由 N 个 4 字节单精度浮点(FLOAT)组成的定长数组。关键点:
- 每个元素固定 4 字节,默认维度 2048,最大 16383。维度在定义时固定,例如
VECTOR(1536)对应 OpenAItext-embedding-3-small的维度。 - 底层以二进制字符串存储,不是 JSON、不是 TEXT。你看不到
[0.1, 0.2]这种明文。 - 它不能做主键、外键、唯一键、分区键——向量本身不是个好的"身份标识"。
- 它只能和另一个 VECTOR 做相等比较,不能和数值、字符串比较,也不能直接
ORDER BY embedding(必须借助距离函数)。
四个核心函数:
| 函数 | 作用 |
|---|---|
STRING_TO_VECTOR()(别名 TO_VECTOR()) | 把 '[1.0, 2.0]' 这种字符串转成二进制向量 |
VECTOR_TO_STRING()(别名 FROM_VECTOR()) | 反向,把二进制向量转回字符串便于查看 |
VECTOR_DIM() | 返回向量维度 |
DISTANCE() / VECTOR_DISTANCE() | 计算两向量距离,支持 COSINE、DOT,9.7 起加 EUCLIDEAN |
一个最朴素的例子:
-- 建一张带向量列的 InnoDB 表
CREATE TABLE product_embeddings (
id BIGINT PRIMARY KEY,
name VARCHAR(200),
embedding VECTOR(3) -- 为了演示用 3 维,真实场景是 1536/768 等
);
INSERT INTO product_embeddings VALUES
(1, '无线耳机', TO_VECTOR('[0.9, 0.1, 0.2]')),
(2, '机械键盘', TO_VECTOR('[0.2, 0.8, 0.1]')),
(3, '蓝牙音箱', TO_VECTOR('[0.85, 0.15, 0.25]'));
-- 以余弦距离为度量,找出和查询向量最像的 2 个
SELECT
name,
VECTOR_DIM(embedding) AS dim,
DISTANCE(embedding,
TO_VECTOR('[0.88, 0.12, 0.22]'),
'COSINE') AS cosine_dist
FROM product_embeddings
ORDER BY cosine_dist
LIMIT 2;
DISTANCE() 返回的是距离,越小越相似;如果你想要"相似度分数",用 1 - DISTANCE(..., 'COSINE')(余弦距离 = 1 - 余弦相似度)即可。
2.2 JSON Duality Views:关系表和 JSON 文档的"双向翻译官"
这是 9.x 里最容易被低估、但对业务开发最友好的特性。一句话解释:它让你用一条 SQL 视图定义,把多张关系表"拼"成一个 JSON 文档,而且这个文档不仅能读,还能被 UPDATE/INSERT/DELETE 回写进原来的关系表里。
传统 ETL 或 ORM 里,"关系→文档"通常是单向的(读的时候拼),写文档再拆回关系表得自己写一堆逻辑。JSON Duality 在数据库内核层把这个双向映射做掉了。
语法核心是 CREATE JSON DUALITY VIEW,用 JSON { ... } 描述文档结构,_id 作为文档主键:
-- 先准备关系表
CREATE TABLE customers (
customer_id INT PRIMARY KEY,
name VARCHAR(100),
email VARCHAR(200)
);
CREATE TABLE orders (
order_id INT PRIMARY KEY,
customer_id INT,
product VARCHAR(100),
amount DECIMAL(10,2),
FOREIGN KEY (customer_id) REFERENCES customers(customer_id)
);
INSERT INTO customers VALUES (1, 'Alice', 'alice@demo.com'), (2, 'Bob', 'bob@demo.com');
INSERT INTO orders VALUES
(1, 1, 'Milk', 10), (2, 1, 'Curd', 5), (3, 2, 'Flour', 20);
-- 定义一个"客户 + 其订单数组"的 JSON 文档视图
CREATE OR REPLACE JSON DUALITY VIEW customer_doc AS
SELECT JSON {
'_id': c.customer_id,
'name': c.name,
'email': c.email,
'orders': [
SELECT JSON {
'orderId': o.order_id,
'product': o.product,
'amount': o.amount
}
FROM orders o
WHERE o.customer_id = c.customer_id
]
}
FROM customers c;
读出来就是标准 JSON 文档:
SELECT c.* FROM customer_doc c;
-- 输出大致是:
-- {"_id": 1, "name": "Alice", "email": "alice@demo.com",
-- "orders": [{"orderId":1,"product":"Milk","amount":10}, ...]}
而关键的能力——反向写回——意味着你的应用可以像操作 MongoDB 文档一样 UPDATE 这个视图,数据库会自动把改动映射回 customers 和 orders 两张表。对前端/Node 同学来说,这相当于免费获得了一个"关系型底、文档型面"的存储层。
视图还有 WITH { INSERT | UPDATE | DELETE } 以及各种投影修饰符(EAGER/LAZY/INCLUDE)来控制嵌套数据的加载与同步策略,后面性能章节会展开。
2.3 9.6 的"运维向"硬货:容器、GTID、InnoDB、安全
这才是很多团队真正会拍大腿的部分,因为它们直接关乎"能不能安稳跑在生产里":
container_aware启动参数:MySQL 以前在容器里常常读不准宿主的 CPU/内存(尤其被 cgroup 限制后),导致缓冲池算错、线程数失控。--container-aware=ON让它主动读取 cgroup 的 limit,自动按容器实际配额调整资源使用,在 K8s 里部署稳定性直接上一个台阶。- GTID 集合结构升级:全新的 GTID 集合数据结构,处理全局事务 ID 更高效,复制场景下性能与可维护性提升,事务集的分组识别更直观。
- InnoDB 改进:唯一
rowid生成效率提升;事务恢复机制优化(避免无效事务残留);Redo 日志错误信息增强,现在会带上 LSN 与容量信息,排障时能直接定位到"是哪个文件、写到哪了"。 - 审计日志组件化:拆成可插拔组件,用
AUDIT_ADMIN权限精细管理输出位置、格式、缓冲区;传统MD5()、SHA1()被挪进classic_hashing组件,默认不装,符合现代安全合规(SHA-1 已被攻破)。
# my.cnf 中启用容器感知(K8s 部署强烈建议)
[mysqld]
container-aware=ON
# 9.6 起还可按需加载经典哈希组件(不推荐用于新系统)
# INSTALL COMPONENT 'file://component_classic_hashing';
三、架构分析:这些特性在引擎层是怎么落地的
3.1 向量在 InnoDB 里到底是什么
VECTOR 列在存储层就是一个定长二进制大对象,每个元素 4 字节紧凑排列。它不走传统的 B+Tree 索引路径——事实上 9.0 ~ 9.6 没有为向量建立 ANN(近似最近邻)索引的能力。这意味着:
一次
ORDER BY DISTANCE LIMIT k的语义检索,本质是全表扫描 + 逐行算距离 + 排序。
这听起来吓人,但在实践中完全可控,原因有两点:
- 向量维度虽然高(1536),但每一行的向量就那 1536×4≈6KB,顺序扫描的内存带宽压力远小于"随机读一堆小行"。
- 向量检索几乎总是带业务过滤条件的("找和这个最像、且 category=耳机、且 status=on_sale 的商品")。你可以先用传统的 B+Tree 索引把候选集从百万级砍到千级,再在这千行里算距离。
架构上,推荐的形态是:向量列 + 若干普通索引列共存于同一张 InnoDB 表,检索时 WHERE 业务条件 AND ... ORDER BY DISTANCE LIMIT k。这正是 MySQL 9.x 相比"外置向量库"的最大架构优势——过滤和向量检索在同一个查询计划里完成,没有跨系统 join。
3.2 JSON Duality 的双向映射原理
Duality View 不是一个物化视图(默认不冗余存储文档),它是一层查询重写 + 写操作分解的逻辑。读时,它把 JSON 结构展开成对底层关系表的 JOIN/子查询;写时,它把对 JSON 文档的 UPDATE/INSERT 翻译成对 customers、orders 等基础表的事务化改写,并通过 _id 维护文档与主表的对应关系。
这就意味着:你完全没有数据冗余,文档和关系表是同一份数据的两种投影。这跟 MongoDB 的文档模型、以及 PostgreSQL 的 jsonb 有本质区别——那些是"数据本身就是文档";而 MySQL Duality 是"数据还是关系表,只是多了一副文档的面具"。
3.3 复制与高可用
GTID 结构升级后,基于 GTID 的主从复制、以及组复制(MGR)在事务集合的表示与比对上更高效。对于向量 + 关系混合的库,复制链路和普通 InnoDB 表没有区别——向量列就是普通的列,跟着 binlog 走,不存在"向量库要单独同步"的麻烦。
四、代码实战:从零搭一个 MySQL 9.x 的语义检索 + 文档读写服务
下面是一套能真正跑起来的最小实践。我们用 Python 演示两个场景:① 用 MySQL 9.x 向量列做商品语义搜索;② 用 JSON Duality 做文档式读写。
4.1 环境与建表
# 拉取 9.x 创新版(演示用 9.6+)
docker run -d --name mysql96 \
-e MYSQL_ROOT_PASSWORD=secret \
-e MYSQL_DATABASE=demo \
-p 3306:3306 \
mysql:9.6 --container-aware=ON
-- 真实语义搜索:用 1536 维(OpenAI text-embedding-3-small)
CREATE TABLE products (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
title VARCHAR(255) NOT NULL,
category VARCHAR(64) NOT NULL,
on_sale TINYINT NOT NULL DEFAULT 1,
embedding VECTOR(1536) NULL,
KEY idx_cat (category), -- 业务过滤靠它
KEY idx_sale (on_sale)
);
4.2 Python:把文本变成向量,存进 MySQL,再搜回来
# pip install pymysql sentence-transformers numpy
import pymysql
import numpy as np
from sentence_transformers import SentenceTransformer
# 1) 加载本地嵌入模型(离线、免费、1536 维可选模型)
model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2") # 384 维
DIM = model.get_sentence_embedding_dimension()
# 2) 连接 MySQL 9.x
conn = pymysql.connect(
host="127.0.0.1", port=3306,
user="root", password="secret", database="demo",
charset="utf8mb4",
)
# 3) 动态建一张匹配维度的表
with conn.cursor() as cur:
cur.execute(f"CREATE TABLE IF NOT EXISTS docs ("
f"id BIGINT PRIMARY KEY AUTO_INCREMENT, "
f"text VARCHAR(512), emb VECTOR({DIM}), "
f"KEY idx_len (CHAR_LENGTH(text)))")
conn.commit()
def embed_and_insert(texts):
vecs = model.encode(texts, normalize_embeddings=True) # 归一化,余弦=点积
with conn.cursor() as cur:
for t, v in zip(texts, vecs):
# 把 numpy 数组拼成 '[a,b,c]' 字符串交给 TO_VECTOR
arr = "[" + ",".join(f"{x:.6f}" for x in v.tolist()) + "]"
cur.execute("INSERT INTO docs(text, emb) VALUES (%s, TO_VECTOR(%s))",
(t, arr))
conn.commit()
def search(query, top_k=3, category_filter=None):
q = model.encode([query], normalize_embeddings=True)[0]
q_arr = "[" + ",".join(f"{x:.6f}" for x in q.tolist()) + "]"
sql = """
SELECT text,
1 - DISTANCE(emb, TO_VECTOR(%s), 'COSINE') AS score
FROM docs
{where}
ORDER BY score DESC
LIMIT %s
"""
where = ""
params = [q_arr]
if category_filter:
where = "WHERE CHAR_LENGTH(text) > 0" # 占位,可替换为真实过滤列
# 真实业务里这里会是:WHERE category = %s
params.append(top_k)
with conn.cursor() as cur:
cur.execute(sql.format(where=where), params)
return cur.fetchall()
# 4) 灌入一点数据
embed_and_insert([
"如何给 Kubernetes 集群做资源限制和弹性伸缩",
"MySQL 9.x 原生向量类型做语义搜索的完整教程",
"用 Rust 写一个高性能的 gRPC 微服务框架",
"前端构建工具从 webpack 迁移到 Vite 的踩坑记录",
])
# 5) 搜一下
print(search("数据库怎么做 AI 检索"))
# => 会命中 'MySQL 9.x 原生向量类型...' 且 score 最高
这段代码里有几个工程上很重要的细节,很多教程会漏掉:
- 向量先做归一化(
normalize_embeddings=True):归一化后余弦相似度 = 点积,而DOT距离计算比COSINE更省事(省了除以模长的步骤)。如果你归一化了,用DISTANCE(emb, q, 'DOT')再取负更直接。 - 维度必须和模型对齐:
all-MiniLM-L6-v2是 384 维,OpenAI 是 1536,本地bge-small是 512。建表维度写死后不能改,所以建表前先model.get_sentence_embedding_dimension()确认。 - 字符串 → 向量用
TO_VECTOR('[...]'):注意浮点数精度,保留 6 位小数足够,别用str(float)默认格式(可能出1e-05这种科学计数法,部分版本解析要测一下)。 - 过滤先行:真实业务里务必把
category/on_sale这类带索引的条件写进WHERE,让 MySQL 先用 B+Tree 把行数砍下来,再算距离。百万级数据全表扫向量会慢,但"先过滤到几千行"后速度完全可接受。
4.3 JSON Duality:像操作文档一样读写关系数据
-- 创建一个"用户档案"文档视图:基础信息 + 嵌套的标签数组
CREATE OR REPLACE JSON DUALITY VIEW user_profile AS
SELECT JSON {
'_id': u.id,
'nickname': u.nickname,
'bio': u.bio,
'tags': [
SELECT JSON {'tag': t.name}
FROM user_tags t
WHERE t.user_id = u.id
]
} WITH INSERT UPDATE DELETE
FROM users u;
应用层(Node/Python 任意)就可以直接拿 JSON 文档来用,读是标准 JSON,写回库时数据库自动拆回 users 和 user_tags:
-- 像改文档一样改关系数据:把 Alice 的 bio 更新,同时追加一个标签
UPDATE user_profile
SET doc = JSON_SET(doc, '$.bio', '热爱云原生与数据库内核')
WHERE doc->>'$.nickname' = 'Alice';
对前端同学来说,这等于省掉了一整个"把关系结果拼成前端想要的 JSON"的胶水层,而且对写操作有事务保证——不会出现"文档更新了但某个子表没跟上"的部分失败。
4.4 把检索封装成一个生产可用的 HTTP 接口(FastAPI)
上面的代码还停留在脚本层面。真正落地时,语义搜索是要被业务服务调用的。下面用 FastAPI 包一层,注意几个生产细节:连接池、超时、对空结果的处理、以及把向量计算放到后台模型、查询走 MySQL 连接池。
# pip install fastapi uvicorn pymysql sentence-transformers dbutils
import pymysql, pymysql.cursors
from fastapi import FastAPI
from pydantic import BaseModel
from sentence_transformers import SentenceTransformer
from dbutils.pooled_db import PooledDB # pip install dbutils
model = SentenceTransformer("sentence-transformers/all-MiniLM-L6-v2")
DIM = model.get_sentence_embedding_dimension()
pool = PooledDB(
creator=pymysql, maxconnections=10, mincached=2, maxcached=5,
host="127.0.0.1", port=3306,
user="app", password="apppwd", database="demo",
charset="utf8mb4", cursorclass=pymysql.cursors.DictCursor,
)
app = FastAPI()
class SearchReq(BaseModel):
query: str
top_k: int = 5
category: str | None = None
@app.post("/search")
def search(req: SearchReq):
# 1) 编码查询(已归一化,余弦=点积)
q = model.encode([req.query], normalize_embeddings=True)[0]
q_arr = "[" + ",".join(f"{x:.6f}" for x in q.tolist()) + "]"
# 2) 拼 SQL:过滤条件 + 向量距离,务必参数化防注入
where = "WHERE 1=1"
params: list = [q_arr]
if req.category:
where += " AND category = %s"
params.append(req.category)
where += " ORDER BY 1 - DISTANCE(embedding, TO_VECTOR(%s), 'COSINE') DESC LIMIT %s"
params.append(req.top_k)
with pool.connection() as conn, conn.cursor() as cur:
cur.execute(
f"SELECT id, title, 1 - DISTANCE(embedding, TO_VECTOR(%s), 'COSINE') AS score FROM products {where}",
params,
)
rows = cur.fetchall()
if not rows:
return {"hits": [], "note": "无匹配,可能是该分类下无数据或模型维度不匹配"}
return {"hits": rows}
几个上线前必须想清楚的要点:
- 模型维度漂移是头号事故源:你
products.embedding是 384 维,结果哪天换了 1536 维的模型,灌进来的向量维度对不上,查询直接报错或返回垃圾。务必在应用启动和写入两条路径上统一维度来源,最好把维度写进一张meta表做校验。 - TO_VECTOR 的参数是字符串,必须走参数化:别用 f-string 把向量拼进 SQL,既不安全也可能因格式问题注入失败。上面把
TO_VECTOR(%s)当占位符传参是正确的。 - 连接池 + 只读副本:语义检索是读多写少,建议查向量走只读副本连接池,写入走主库,避免大查询拖慢主库写入。
五、性能优化:把 MySQL 9.x 用到"刚好够快"
5.1 向量检索的三条提速路径
由于 9.x 暂无原生 ANN 索引,检索性能完全靠"减少参与距离计算的行数":
- 强过滤 + 索引列:把
category、status、created_at等高频过滤条件建 B+Tree 索引,查询时先WHERE砍行。这是性价比最高的一招。 - 分区表:如果数据天然可分桶(按业务线、按地区),用
PARTITION BY把向量表按某个枚举列分区,查询时只扫相关分区。注意VECTOR列本身不能做分区键,但可以用其它列分区。 - 维度权衡:嵌入模型维度越高越准,但每行体积和算距成本线性上升。业务搜索用 384(
all-MiniLM-L6-v2)往往够用,别无脑上 1536。实测中 384 维在百万行"先过滤后检索"场景下,P99 延迟通常在百毫秒级。
-- 典型的"过滤 + 向量"混合查询模板
SELECT id, title,
1 - DISTANCE(embedding, TO_VECTOR(%s), 'COSINE') AS score
FROM products
WHERE category = '耳机' AND on_sale = 1 -- 先用索引砍行
ORDER BY score DESC
LIMIT 10; -- 再在候选集里排序取 top10
5.2 JSON Duality 的投影策略
Duality View 支持在嵌套结构上加修饰符控制加载行为:
EAGER:查询文档时立即把嵌套数据 JOIN 出来(适合小数据集、强一致读)。LAZY:访问时才懒加载(适合大数组、避免每次都拖一大坨)。INCLUDE:只读投影,不回写。
经验法则:顶层字段用 EAGER,超长的子数组(如订单历史、评论流)用 LAZY,否则一次读文档就把整张子表拉爆。
5.3 InnoDB 与容器化调优
- 开了
--container-aware=ON后,不要再硬编码innodb_buffer_pool_size为宿主物理内存的 80%——让 MySQL 自己读 cgroup limit 来定,否则在 2G 限制的容器里硬配 12G 缓冲池会 OOM 被杀。 - 9.6 的 Redo 日志错误带 LSN/容量信息后,建议把
innodb_print_all_deadlocks=ON配上,排障时能直接拿到"卡在哪段日志"的关键线索。 - 向量列不参与事务回滚的额外代价很低(就是普通列),但批量灌向量时请关掉
autocommit、每 1000~5000 行commit一次,比单行提交快一个数量级。
5.4 排障与踩坑清单(血泪向)
把这套东西跑进生产,有几个坑是文档不会主动告诉你的:
VECTOR列不能直接放进GROUP BY/DISTINCT/ 聚合函数。想对向量去重?先VECTOR_TO_STRING()转成字符串再操作,但那样会丢失精度且很慢——正确做法是向量列只做距离比较,其它聚合走普通列。DISTANCE()的第三个参数是字符串且必须加引号:'COSINE'、'DOT'、'EUCLIDEAN'(9.7+)。写成DISTANCE(a, b, COSINE)(没引号)会被当成列名/标识符而报错。- JSON Duality 写回失败,多半是
_id映射歧义:视图里必须有一个能唯一定位主表行的_id,如果嵌套子查询的关联条件写错,UPDATE 文档时数据库不知道改哪行,会报映射错误。开发时小步快跑、先验证单文档读写。 - 创新版的版本号陷阱:
mysql:9这种 tag 会随创新版滚动到 9.6、9.7……你的DISTANCE用了EUCLIDEAN就得确保镜像 ≥9.7,否则函数不存在。生产镜像请钉死具体小版本,例如mysql:9.6.0。 container-aware=ON不会自动调大缓冲池,它只是让 MySQL 读 cgroup limit。如果你在 4G 容器里手动设了innodb_buffer_pool_size=20G,依然会 OOM。正确姿势是让它自动,或显式设一个小于 cgroup limit 的值。- 向量全表扫的延迟随行数线性增长:没有 ANN 索引时,从 10 万行扫到 1000 万行,延迟大约翻 100 倍。所以"先过滤后检索"不是优化技巧,是必选项。
六、总结与展望:MySQL 9.x 到底该不该上
回到开头那个核心判断:MySQL 9.x 的价值不在于"它比 Milvus 快"或"它比 MongoDB 灵活",而在于它把三种工作负载收敛到了一套存储、一套备份、一套运维里。
适合用 MySQL 9.x 统一收敛的场景:
- 已有 MySQL 业务,想加一点语义搜索/RAG,数据量在千万行以内,不想引入新组件;
- 前端苦于"关系表拼 JSON"的胶水代码,想要文档式读写但底层仍是关系模型、要事务保证;
- 团队小、运维人力紧张,多一套专用存储的边际成本高于它带来的性能收益。
暂时不适合、该继续用专用方案的场景:
- 向量规模上亿、需要真正的 HNSW/IVF 近似索引和亚毫秒检索——这时候 Milvus / pgvector(带 ivfflat/hnsw)仍是更专业的选择;
- 文档结构极度动态、完全无模式(schema-less)需求强烈的——MongoDB 的文档原生模型更顺手;
- 对版本稳定性要求极高、不敢碰创新版的生产核心库——等 9.x 出 LTS 或升到对应 LTS 再迁移。
一个务实的建议:不要为了"追新"把核心交易库升级到 9.x 创新版;但完全可以在一条只读副本 / 新业务线上先试点向量检索和 JSON Duality,用最低的试错成本验证"少一套存储"是否真的省事。等尝到甜头、踩完坑,再决定是否往核心迁移。
为了更直观地判断"该不该用 MySQL 9.x",给出一张取舍表,按数据规模和团队阶段对号入座:
| 你的现状 | 推荐方案 | 理由 |
|---|---|---|
| 已有 MySQL,想加语义搜索,数据 < 千万行 | MySQL 9.x 向量列 | 零新组件,过滤+向量同库完成 |
| 已有 MySQL,前端苦于拼 JSON | JSON Duality Views | 关系底+文档面,双向同步有事务 |
| 向量上亿、要亚毫秒 ANN | Milvus / pgvector(hnsw) | 需要真正的近似最近邻索引 |
| 文档完全无模式、结构天天变 | MongoDB | 原生文档模型更顺手 |
| 核心交易库、稳定性压倒一切 | 维持 8.0 LTS,另起 9.x 副本试点 | 创新版不适合直接碰核心 |
最后给一条迁移路径建议,按风险从低到高:
- 影子读:在主库挂一个 9.x 只读副本,把语义搜索流量先打到副本,验证召回质量与延迟;
- 双写验证:新业务写入时同时落 9.x 向量列和老方案,跑一致性对账;
- 切读:确认无误后,把读路径从老方案切到 9.x;
- 收老:观察一个周期无问题,再下线外置向量/文档存储。
展望未来,MySQL 9.x 的路线图里最值得期待的,是向量索引——一旦官方补上 ANN 索引(类 HNSW),"过滤 + 向量"的混合检索就能从"先扫后算"进化到"索引直查",那才是它真正叫板专用向量库的时刻。在那之前,它是一个"足够好、且足够省心"的折中方案,而折中,恰恰是绝大多数工程团队真正需要的那把尺子。
参考要点(便于溯源):原生 VECTOR 类型与 TO_VECTOR/VECTOR_DIM/DISTANCE(COSINE/DOT,9.7 加 EUCLIDEAN)出自 MySQL 9.0+ 官方手册;JSON Duality Views 语法与双向映射见 MySQL 9.6 Reference Manual 第 27.7 节;container_aware、GTID 结构升级、InnoDB rowid/恢复/Redo 日志增强、审计日志组件化与 classic_hashing 出自 MySQL 9.6 发布说明。生产使用前请以你目标版本的官方文档为准。