一个被忽视的容量问题

“MySQL 9.2 有原生向量搜索了”——这消息传开之后,很多团队的迁移冲动来了:

“我们之前用的 Milvus 太重了,把向量数据迁回 MySQL 吧,运维简单很多。”

然后开干:

  • 创建 embedding VECTOR(1536)
  • 把 100 万条文档的 embedding 灌进去
  • 跑了一次相似度查询
  • 应用层开始抖动
  • 查 MySQL 监控:Innodb_buffer_pool_wait_free 飙到 50+
  • 内存告警

MySQL 9.2 真的能扛百万级向量吗?能。但你必须算清楚内存。

先算账:1536 维 × 4 字节 = 6KB/行

向量的物理大小:

1
2
3
4
5
embedding VECTOR(1536)
= 1536 个 float32
= 1536 × 4 bytes
= 6144 bytes
= 6 KB

这只是纯向量数据。加上 InnoDB 的页结构(默认 16KB 一页)、行头(compact 格式约 27 字节)、主键索引、二级索引,每行实际占 7-8KB

向量规模 向量数据本身 含索引和行结构
10 万行 600 MB 800 MB
100 万行 6 GB 8 GB
500 万行 30 GB 40 GB
1000 万行 60 GB 80 GB

这只是HNSW 索引之外的基础存储。HNSW 索引本身还要再占一份。

HNSW 索引的内存占用

HNSW(Hierarchical Navigable Small World)是一个多层图结构,每个节点存 M 个邻居的引用。

公式(粗略):

1
HNSW 内存 ≈ 节点数 × M × 8 bytes(指针) × 层数因子

默认值 M=16ef_construction=100

向量规模 HNSW 索引内存(估算)
10 万行 200-400 MB
100 万行 2-4 GB
500 万行 10-20 GB
1000 万行 20-40 GB

所以百万级向量的总内存消耗:业务数据 8GB + HNSW 索引 4GB = 12GB。这只是向量相关的部分,业务数据本身的内存还没算。

InnoDB BufferPool 的真实压力

MySQL 的 BufferPool 缓存的是数据页。默认配置下:

1
2
SHOW VARIABLES LIKE 'innodb_buffer_pool_size';
-- 默认通常是 128MB

要扛住百万级向量 + 业务数据 + HNSW 索引,BufferPool 至少要:

1
2
3
BufferPool = (业务数据 + 向量数据 + 索引) × 1.5(留 50% 余量)
= (8 + 12) × 1.5
≈ 30 GB

配置:

1
2
3
4
# my.cnf
[mysqld]
innodb_buffer_pool_size = 30G
innodb_buffer_pool_instances = 8 # 多实例减少锁竞争

30GB 的 BufferPool 意味着你的 MySQL 服务器至少 64GB 物理内存(留出其他开销)。

量化压缩:内存优化的关键开关

HNSW 索引可以量化到 int8,内存减少 4 倍:

1
2
3
-- MySQL 9.2 暂未原生支持 int8 量化(参考 PolarDB-X 等实现)
-- 但可以用 MyVector 插件 + loose_hnsw_quantizer
SET SESSION loose_hnsw_quantizer = 'sq8';

或者用 SQ8 量化(Scalar Quantization 8-bit):

1
2
3
4
5
6
CREATE INDEX idx_vec ON documents(embedding) USING HNSW (
'algorithm' = 'hnsw',
'quantizer' = 'sq8', -- 8-bit 标量量化
'm' = '16',
'ef_construction' = '100'
);

效果:

  • 内存减少 4 倍(30GB → 7.5GB)
  • 召回率下降 1-3%
  • 查询速度提升 1.5-2 倍

生产环境强烈推荐开量化。除非你的召回率要求是 0.999+。

多租户场景:Scoped Vector Search 的内存意义

MySQL 9.2 支持带过滤条件的向量检索(Scoped Vector Search):

1
2
3
4
SELECT id, content FROM document_embeddings
WHERE user_id = 12345
ORDER BY VECTOR_DISTANCE(embedding, @query_vec, 'COSINE') ASC
LIMIT 10;

这看起来是 SQL 能力,但本质是”安全 + 内存”问题

  1. 安全:多租户 RAG 必须确保用户 A 搜不到用户 B 的文档
  2. 内存:如果每次查询都”全表扫 + 过滤”,1000 万行的总内存会频繁被访问

MySQL 9.2 的优化是:在 HNSW 搜索前先用 user_id 索引过滤,只在子集上做 ANN

但这要求:

1
2
3
CREATE INDEX idx_user ON document_embeddings(user_id);
-- + HNSW 索引
CREATE INDEX idx_vec ON document_embeddings(embedding) USING HNSW (...);

两个索引并存会进一步增加内存占用。所以:

  • 业务规模小(百万级):可以放心开
  • 业务规模大(千万级):考虑”按 user_id 分表”或”专门的租户隔离设计”

真实生产案例:100 万行 RAG 的内存规划

我自己在做的一个 RAG 知识库:

  • 数据量:100 万文档切片,每片 1536 维 embedding
  • 业务表:用户、权限、文档元数据
  • 查询模式:按 user_id 过滤 + 向量搜索 top 10

最终内存规划

1
2
3
4
5
6
7
8
9
10
11
12
13
[mysqld]
# 物理内存 128GB 的服务器
innodb_buffer_pool_size = 64G # 业务 + 向量 + 索引
innodb_buffer_pool_instances = 16 # 16 个实例减少锁竞争
max_connections = 200 # 应用连接池

# HNSW 索引参数
SET GLOBAL loose_hnsw_default_m = 16;
SET GLOBAL loose_hnsw_ef_construction = 100;
SET GLOBAL loose_hnsw_ef_search = 20;

# 量化压缩
SET GLOBAL loose_hnsw_quantizer = 'sq8';

实测数据

  • 向量数据 + HNSW 索引总占用:22GB(含量化)
  • 业务表 + 二级索引:8GB
  • BufferPool 命中率:98.5%
  • P99 延迟:85ms

百万级向量 + 简单业务场景,64GB BufferPool 配量化是合理配置

千万级怎么办?

如果向量规模到了 1000 万+,MySQL 9.2 的边界就到了

维度 MySQL 9.2 专用向量库(Milvus/Qdrant)
千万级查询延迟 500ms+ 50-100ms
内存成本 高(关系型 + 向量混合) 低(专门优化)
运维成本 低(已有 MySQL 团队) 高(新增组件)
召回率 0.93-0.95(量化后) 0.95+
多租户隔离 弱(靠 SQL 过滤) 强(分区、租户感知)

千万级是 MySQL 9.2 的真实边界。超过这个量,专用向量库的优势重新显现。

4 个容量规划的反模式

反模式 1:默认 BufferPool 配 128MB

部署完 MySQL 9.2 就直接灌百万级向量,BufferPool 还是默认的 128MB。结果是:

  • HNSW 索引频繁从磁盘加载
  • 查询延迟 1-3 秒
  • 磁盘 IO 100%

必须根据向量规模调大 BufferPool。

反模式 2:不开量化,强行用 float32

有些团队担心”量化降低召回率”,所以全用 float32。结果内存爆炸。

正确做法:默认开量化,只在召回率不达标时关闭。这是 1-3% 召回率换 4 倍内存的交易,绝大多数场景划算。

反模式 3:业务表和向量表用同一个表

1
2
3
4
5
6
7
8
9
-- 错误:业务字段和向量混在一起
CREATE TABLE documents (
id BIGINT,
title VARCHAR(255),
content TEXT,
user_id BIGINT,
embedding VECTOR(1536), -- 这列让行变成 8KB
INDEX idx_user (user_id)
);

问题

  • 按 user_id 查询时,每行 8KB 大小,BufferPool 命中率暴跌
  • 业务查询和向量查询互相挤占 BufferPool

正确做法

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
-- 业务表
CREATE TABLE documents (
id BIGINT PRIMARY KEY,
title VARCHAR(255),
content TEXT,
user_id BIGINT,
INDEX idx_user (user_id)
);

-- 向量表(独立)
CREATE TABLE document_vectors (
id BIGINT PRIMARY KEY,
document_id BIGINT,
chunk_index INT,
content_chunk TEXT,
embedding VECTOR(1536),
INDEX idx_doc (document_id)
);

两个表 BufferPool 隔离,互不干扰。

反模式 4:用 HNSW 索引在写密集场景

HNSW 索引的更新代价高:

  • 每条 INSERT 要更新 HNSW 图(ef_construction 决定搜索深度)
  • 频繁写入会让 HNSW 图频繁 rebalance

如果你的 RAG 场景是”写少读多”(典型的离线知识库)

  • HNSW 合适
  • 可以凌晨批量灌入,白天查询

如果是”写密集”(比如用户实时上传文档)

  • HNSW 不合适
  • 考虑 IVFFlat(更快构建、更低质量)或定期重建 HNSW

容量规划清单

部署 MySQL 9.2 做 RAG 前,必算

  1. 向量总行数 × 6KB = 向量数据大小
  2. HNSW 索引大小 ≈ 向量行数 × 50 bytes
  3. BufferPool = (业务表 + 向量表 + 索引) × 1.5
  4. 物理内存 ≥ BufferPool + 16GB(其他开销)
  5. 磁盘 ≥ (BufferPool × 2)(双缓冲)

百万级向量需要 64GB 内存 + SSD 磁盘
千万级向量需要 256GB 内存 + NVMe 磁盘 + 考虑专用向量库

意味着什么?

MySQL 9.2 的向量搜索”能用”和”用好”之间的距离很大:

  • 能用:建个 HNSW 索引、跑个 query,500ms 出结果
  • 用好:理解内存模型、配置 BufferPool、量化压缩、隔离表设计

没有容量规划的”MySQL 9.2 替代 Milvus”决策,一定会撞上内存墙

数据密集型 AI 后端的工程师,对内存模型的直觉是核心竞争力。不是”建索引能用就行”,是”上线前算清楚:100 万行 6GB、千万级 60GB、BufferPool 要多大”。

这套直觉需要在真实项目里练出来。文章是知识,踩坑是经验。

关联阅读