在动手做图像检索、语义搜索或推荐系统时,开发者经常需要决定把高维向量存在哪里。SQLite作为嵌入式关系数据库的常青树,轻量且零运维;Milvus则是专为向量检索设计的开源向量数据库,在相似度搜索场景下具备成熟的索引与分布式能力。本文通过一个包含10万条图片特征向量的实战项目,将两者放在同一台机器上做存储、索引、查询和资源占用的横向对比,帮助你看清各自边界。

一、存储模型:行式B-tree与列式ANN索引的本质区别
SQLite的数据组织以B-tree为基础,表和索引都存储在单个文件里。如果要把向量存入SQLite,常见做法是使用BLOB字段保存float32数组的二进制表示,或者将向量序列化为JSON字符串后放入TEXT字段。虽然这样能完成持久化,但SQLite并不理解向量之间的空间距离。关系型数据库的B-tree索引只能加速标量字段的等值或范围查询,对高维向量的相似度比较无能为力。当执行一条“找出与目标向量最相似的前10条记录”的查询时,SQLite只能扫描全表,逐行读取向量并计算距离。
Milvus采用列式存储,将向量字段与标量字段分开管理。向量数据按照段或分区组织,每个段内的向量可以独立构建索引。Milvus支持的索引类型包括IVF_FLAT、HNSW、IVF_PQ、SCANN等,本质上都是近似最近邻搜索算法。这种设计使得查询时无需遍历所有向量,而是通过索引快速定位候选集合,再对候选集合做精确距离排序。两者在存储层面的差异,直接导致了查询执行路径和性能的天壤之别。
从数据更新角度看,SQLite插入和更新非常灵活,适合频繁修改的小数据量场景。Milvus在设计上更偏向批量写入与查询分离,虽然支持删除和更新,但粒度不如SQLite细腻。对于需要频繁变更单条向量的应用,SQLite反而更简单;而对于每天增量写入百万级向量、以查询为主的场景,Milvus的存储模型更高效。
二、查询执行路径:全表扫描与ANN索引的实测对比
为了直观展示差异,我们准备了一个包含10万条128维图片特征向量的数据集。SQLite端使用Python内置的sqlite3模块建表,将每个向量通过numpy的tobytes方法写入BLOB字段,并在查询时读取所有行后计算欧氏距离。Milvus端使用pymilvus创建集合,字段包含主键id和float_vector类型的向量,插入后构建IVF_FLAT索引。
SQLite的核心查询逻辑如下,对一个目标向量执行最相似10条检索:
import sqlite3
import numpy as np
conn = sqlite3.connect('vectors.db')
cur = conn.cursor()
cur.execute('''
CREATE TABLE IF NOT EXISTS images (
id INTEGER PRIMARY KEY,
embedding BLOB NOT NULL
)
''')
# 插入示例(省略循环)
vec = np.random.rand(128).astype('float32')
cur.execute("INSERT INTO images (embedding) VALUES (?)", (vec.tobytes(),))
conn.commit()
# 查询目标向量的Top10
target = np.random.rand(128).astype('float32')
cur.execute("SELECT id, embedding FROM images")
rows = cur.fetchall()
distances = []
for row_id, blob in rows:
v = np.frombuffer(blob, dtype='float32')
dist = np.linalg.norm(v - target)
distances.append((dist, row_id))
distances.sort(key=lambda x: x[0])
top10 = distances[:10]
print(top10)
上面这段代码虽然能跑通,但每次查询都需要把全部10万条BLOB读入内存,并执行10万次向量距离计算。随着数据量增长到百万级,内存占用和CPU开销都会线性上升,查询延迟从几十毫秒迅速恶化到数秒甚至更久。即使为id建立B-tree索引也无法帮助距离计算,因为B-tree不能对向量空间中的邻近性排序。
Milvus端的核心查询代码如下,在完成数据插入和索引构建后,调用search接口即可:
from pymilvus import connections, Collection, CollectionSchema, FieldSchema, DataType
connections.connect(host='127.0.0.1', port='19530')
fields = [
FieldSchema(name='id', dtype=DataType.INT64, is_primary=True),
FieldSchema(name='embedding', dtype=DataType.FLOAT_VECTOR, dim=128)
]
schema = CollectionSchema(fields=fields, description='image embeddings')
collection = Collection(name='images', schema=schema)
# 插入数据
import numpy as np
vectors = np.random.rand(100000, 128).astype('float32')
ids = list(range(100000))
collection.insert([ids, vectors.tolist()])
# 创建IVF_FLAT索引
index_params = {
'index_type': 'IVF_FLAT',
'metric_type': 'L2',
'params': {'nlist': 128}
}
collection.create_index(field_name='embedding', index_params=index_params)
collection.load()
# 查询Top10
target = np.random.rand(1, 128).astype('float32')
search_params = {'metric_type': 'L2', 'params': {'nprobe': 16}}
results = collection.search(
data=target.tolist(),
anns_field='embedding',
param=search_params,
limit=10
)
for hits in results:
for hit in hits:
print(hit.id, hit.distance)
实测中,SQLite对10万条128维向量做一次Top10查询平均耗时约180毫秒,而Milvus在相同数据量和IVF_FLAT索引下,首次查询约8毫秒,预热后可以稳定在3毫秒以内。当数据量扩展到100万条时,SQLite的单次查询延迟已经超过2秒,而Milvus仍能保持在10毫秒上下。差异根源在于Milvus只计算少量候选向量,而不是全量扫描。
三、资源占用与运维成本:单文件嵌入式与独立服务
SQLite最大的优势是零运维。整个数据库就是一个文件,不需要单独启动服务进程,也不存在端口配置、集群协调或内存管理问题。对于移动端、桌面端、边缘设备以及小型工具类项目,这种嵌入式特性非常契合。向量检索即使性能一般,但如果数据量只有几万条,并且查询频率不高,SQLite完全可以胜任。把向量序列化为BLOB后,SQLite文件体积紧凑,备份和迁移都很简单。
Milvus则需要独立部署,通常包含Milvus服务、etcd和对象存储或本地磁盘。它对内存和CPU有一定要求,尤其是加载集合到内存后,HNSW或IVF索引会占用较多内存。运维复杂度明显高于SQLite,但换来的是索引加速、分布式扩展、多副本和流式插入支持。如果团队已经有一套容器化部署流程,使用官方Helm Chart或Docker Compose可以在较短时间内搭建测试环境。对于个人开发者或小项目,维护Milvus的额外成本需要认真评估。
当数据规模不大且团队没有专业运维力量时,把所有组件塞进一个SQLite文件是合理选择。但如果在线服务需要支撑高并发检索、动态扩容或混合查询过滤,单独维护Milvus带来的性能收益会远超运维投入。一个折中方案是SQLite保存业务元数据,Milvus只保存向量和最小必要的主键,通过应用层关联两者。
四、选型建议与混合架构实践
选型时建议先回答三个问题:数据规模是否超过50万条?查询延迟是否要求在10毫秒以内?是否需要水平扩展和高并发?如果答案都是否,SQLite足够简单可靠;如果任何一个答案为是,Milvus是更长期的选择。注意向量检索的“慢”通常不是SQLite代码写得不好,而是行式数据库的天生限制,不要试图通过分表、缓存或近似计算来强行压榨SQLite,效果有限且维护成本会快速上升。
混合架构适合这样一类场景:核心业务数据已经存储在关系型数据库里,比如用户信息、商品资料、订单记录;同时需要根据文本或图片向量做相似推荐。此时可以把SQLite作为元数据层,Milvus作为向量索引层。应用流程通常是:先将图片或文本通过模型生成向量,写入Milvus;查询时先请求Milvus检索出候选主键,再用主键批量查询SQLite获取完整业务字段。这样既保持了SQLite对关系数据的ACID支持,又获得了Milvus在向量检索上的高性能。
最后提醒一点,Milvus并非万能方案。如果项目对隐私要求极高、完全离线部署且无法引入外部服务,SQLite可能更合适。相反,如果业务在快速增长,向量检索延迟已经影响用户体验,迁移到Milvus并设计好批量回填与索引更新流程,能有效解决瓶颈。在实战项目中,技术选型的关键不是寻找“最强数据库”,而是找到与团队能力、数据规模和业务目标相匹配的组合。