Machine Learning Mastery

pgvector vs. Dedicated Vector Databases: A Production Benchmark

7.1内容质量

TL;DR · AI 摘要

pgvector vs. Dedicated Vector Databases: A Production Benchmark - MachineLearningMastery.com pgvector vs. Dedicated Vect...

核心要点

  • 主题聚焦:pgvector vs. Dedicated Vector Databases: A Produ
  • 来源:Machine Learning Mastery,建议结合原文判断细节。
  • AI 分析暂不可用,本条为保底评分与摘要。
#AI#后端#云计算#产品#开源
打开原文

pgvector 与专用向量数据库:生产环境基准测试 - MachineLearningMastery.com

pgvector 与专用向量数据库:生产环境基准测试

By

Shittu Olumide

on

2026年10月20日

in

语言模型

0

分享

发布

在本文中,你将了解 pgvector 在实际测得的性能方面如何与专用向量数据库进行比较,并学习如何根据你的生产工作负载选择合适的技术方案。

我们将涵盖的主题包括:

  • pgvector 和专用向量数据库的定义,以及它们在架构上的差异。
  • 如何在自己的机器上设置并运行一个实际的基准测试,比较 pgvector 和 Qdrant 的性能。
  • 生产规模的基准测试数据如何真实反映吞吐量、延迟以及两种方案之间的权衡。

某个团队在周二下午将 pgvector 添加到现有的 Postgres 数据库中,嵌入向量和关系数据存储在同一个表中,单个查询即可完成过滤和相似性搜索。六个月后,一封来自供应商的推销邮件出现在他们的邮箱中,坚持要求他们迁移到“真正的”向量数据库,否则将面临瓶颈。无论哪一方,这封邮件都没有附带任何基准数据,只有自信的断言。这种差距——强烈的观点却缺乏数据支持——正是本文要填补的空白。

pgvector 的真实定位

pgvector 是一个开源的 Postgres 扩展,根据其官方 GitHub 仓库的信息,当前版本为 0.8.6,支持 Postgres 13 及以上版本。它为 Postgres 添加了原生的向量列类型,并在你可能已经在运行的数据库中直接支持精确和近似最近邻搜索。

实际上这意味着:向量嵌入与它描述的记录存储在同一个表中,如果需要,甚至可以存储在同一条记录中,使用你已经熟悉的 SQL 进行查询。无需部署额外的服务,无需维护第二个需要同步的系统,无需在关系数据和向量数据之间进行网络跳转。pgvector 支持单精度、半精度、二进制和稀疏向量类型,以及五种距离函数——L2、内积、余弦、L1 和汉明距离,覆盖了真实嵌入模型几乎所有的距离度量需求。

专用向量数据库的定义

专用向量数据库是一个独立的、专为向量搜索设计的系统,作为独立进程运行,拥有自己的存储引擎,从底层架构开始就围绕向量搜索进行设计,而不是在通用数据库上添加功能。这不仅是产品上的不同选择,更是架构上的根本差异,意味着需要部署、监控、备份并同步一个全新的系统,以保持与你实际数据源的一致性。

以下是五个最常被选用的数据库及其适用场景的简要说明:

| 数据库 | 最适合的场景 | |------------|------------------------------------------| | Pinecone | 全托管、零配置的云数据库,最适合低运维的大型企业级应用 | | Qdrant | 基于 Rust 的高性能数据库,最适合需要快速查询和复杂 JSON 负载过滤的场景 | | Weaviate | 开源的 AI 原生系统,最适合内置向量化和混合 BM25 搜索的场景 | | Milvus | 分布式、GPU 加速的数据库,最适合超大规模企业级工作负载 | | ChromaDB | 轻量级嵌入式数据库,最适合本地原型开发和 Python 开发 |

先决条件

本教程中的所有内容均可在您自己的计算机上运行,无需云账户。您需要安装 Docker 和 Docker Compose,Python 3.10 或更高版本,约 8GB 的空闲内存,并具备基本的 SQL 和 Python 阅读能力。下面的动手实践基准测试规模设计为可在笔记本电脑上舒适运行。

本地安装 pgvector

从一个已内置 pgvector 的 Postgres Docker Compose 文件开始:

docker-compose.yml services: postgres: image: pgvector/pgvector:pg17 ports: - "5432:5432" environment: - POSTGRES_PASSWORD=benchmark - POSTGRES_DB=vectorbench

1

2

3

4

5

6

7

8

9

docker-compose.yml

services

:

postgres

image

pgvector

/

pg17

ports

-

"5432:5432"

environment

POSTGRES_PASSWORD

=

benchmark

POSTGRES_DB

vectorbench

运行 docker compose up -d,然后连接并设置扩展和表:

import psycopg2 conn = psycopg2.connect( host="localhost", port=5432, dbname="vectorbench", user="postgres", password="benchmark", ) conn.autocommit = True cur = conn.cursor() # Enables the vector type and index access methods for this database cur.execute("CREATE EXTENSION IF NOT EXISTS vector;") # 1536 dimensions matches OpenAI's text-embedding-3-small, a realistic # stand-in for a genuine production embedding model cur.execute(""" CREATE TABLE IF NOT EXISTS items ( id bigserial PRIMARY KEY, embedding vector(1536) ); """)

10

11

12

13

14

15

16

17

18

19

20

import

psycopg2

conn

.

connect

(

host

"localhost"

,

port

5432

dbname

"vectorbench"

user

"postgres"

password

"benchmark"

)

autocommit

True

cur

cursor

Enables the vector type and index access methods for this database

execute

"CREATE EXTENSION IF NOT EXISTS vector;"

1536 dimensions matches OpenAI's text-embedding-3-small, a realistic

stand-in for a genuine production embedding model

""

"

CREATE TABLE IF NOT EXISTS items (

id bigserial PRIMARY KEY,

embedding vector(1536)

);

pgvector/pgvector:pg17 是官方 Docker 镜像,将扩展与 Postgres 17 集成在一起,因此无需单独编译步骤。CREATE EXTENSION IF NOT EXISTS vector 是一次性操作,根据 pgvector 文档,在特定数据库中注册向量类型及其索引方法。

本地安装 Qdrant

为了比较的专用数据库部分,使用单个 Docker 命令运行 Qdrant:

docker run -d --name qdrant-bench -p 6333:6333 -p 6334:6334 qdrant/qdrant

docker

run

d

--

name

bench

p

6333

6334

然后从 Python 连接并创建匹配的集合:

from qdrant_client import QdrantClient from qdrant_client.models import Distance, VectorParams qdrant = QdrantClient(url="http://localhost:6333") qdrant.create_collection( collection_name="items", vectors_config=VectorParams(size=1536, distance=Distance.COSINE), )

from

qdrant_client

QdrantClient

models

Distance

VectorParams

url

"http://localhost:6333"

create_collection

collection_name

"items"

vectors_config

size

1536

COSINE

选择 Qdrant 进行此比较特别有意义,因为它是目前针对 pgvector 的最详细、方法最透明的公开基准测试对象,本文后续部分将直接引用该基准测试。VectorParams(size=1536, distance=Distance.COSINE) 与上方 pgvector 表使用的维度和距离度量一致,这一点很重要——公平比较需要两个系统测量相同的内容。

生成和加载真实测试数据集

在两个系统都运行后,生成一组逼真的合成向量并加载到每个系统中:

import numpy as np NUM_VECTORS = 100_000 DIM = 1536 rng = np.random.default_rng(seed=42) vectors = rng.normal(size=(NUM_VECTORS, DIM)).astype(np.float32) # 实际的嵌入模型会生成归一化的向量,因此将这些向量归一化 # 为长度1,以进行真实的余弦距离比较 vectors /= np.linalg.norm(vectors, axis=1, keepdims=True)

numpy

as

np

NUM_VECTORS

100_000

DIM

rng

random

default_rng

seed

42

vectors

normal

astype

float32

实际的嵌入模型会生成归一化的向量,因此将这些向量归一化

为长度1,以进行真实的余弦距离比较

/=

linalg

norm

axis

keepdims

通过 pgvector-python 的适配器加载到 pgvector 中,该适配器可以让 psycopg2 直接接受 NumPy 数组:

from pgvector.psycopg2 import register_vector from psycopg2.extras import execute_values register_vector(conn) data = [(v,) for v in vectors] execute_values(cur, "INSERT INTO items (embedding) VALUES %s", data, template="(%s)")

register_vector

extras

execute_values

data

[

v

for

]

"INSERT INTO items (embedding) VALUES %s"

template

"(%s)"

以批次方式加载相同向量到 Qdrant 中:

from qdrant_client.models import PointStruct batch_size = 1000 for start in range(0, NUM_VECTORS, batch_size): batch = vectors[start:start + batch_size] points = [ PointStruct(id=start + i, vector=batch[i].tolist()) for i in range(len(batch)) ] qdrant.upsert(collection_name="items", points=points)

PointStruct

batch_size

1000

start

range

batch

+

points

id

i

vector

tolist

len

upsert

register_vector(conn) 是 pgvector 端的关键代码行——它教会 psycopg2 如何将 NumPy 数组转换为 pgvector 期望的格式,这样 execute_values 就可以插入原始数组而无需手动字符串格式化。在 Qdrant 端,upsert 有意每次调用处理 1,000 个向量的批次,而不是一次发送大量请求,因为 Qdrant 的客户端(和大多数向量数据库客户端一样)在合理大小的批次上表现优于单个插入或一次性发送大量数据。

创建索引

在 pgvector 中直接根据 pgvector 自己的索引文档创建 HNSW 索引:

cur.execute( "CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops) " "WITH (m = 16, ef_construction = 64);" )

"CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops) "

"WITH (m = 16, ef_construction = 64);"

在 Qdrant 中应用匹配的 HNSW 参数,使两个系统使用相同的底层算法和设置:

from qdrant_client.models import HnswConfigDiff qdrant.update_collection( collection_name="items", hnsw_config=HnswConfigDiff(m=16, ef_construct=64), )

HnswConfigDiff

update_collection

hnsw_config

m

ef_construct

64

m 控制 HNSW 图中每个节点的最大连接数,ef_construction 控制构建索引时搜索的彻底程度。这里两者分别设置为 16 和 64——即 pgvector 的默认值——在 Qdrant 端特意进行了镜像。这比看起来更重要:比较使用不同索引参数的两个系统并不是基准测试,而是两个无法比较的结果。匹配参数才是使下一节中的数字具有意义的关键。

import time import statistics query_vectors = rng.normal(size=(200, DIM)).astype(np.float32) query_vectors /= np.linalg.norm(query_vectors, axis=1, keepdims=True) def benchmark_pgvector(cur, query_vectors, ef_search=64, limit=10): cur.execute(f"SET hnsw.ef_search = {ef_search};") latencies = [] for qv in query_vectors: start = time.perf_counter() cur.execute( "SELECT id FROM items ORDER BY embedding <=> %s LIMIT %s", (qv, limit), ) cur.fetchall() latencies.append((time.perf_counter() - start) * 1000) # ms return latencies def benchmark_qdrant(client, query_vectors, limit=10): latencies = [] for qv in query_vectors: start = time.perf_counter() client.query_points(collection_name="items", query=qv.tolist(), limit=limit) latencies.append((time.perf_counter() - start) * 1000) return latencies def summarize(name, latencies, total_time): latencies_sorted = sorted(latencies) p50 = statistics.median(latencies_sorted) p95 = latencies_sorted[int(len(latencies_sorted) * 0.95)] p99 = latencies_sorted[int(len(latencies_sorted) * 0.99)] qps = len(latencies) / total_time print(f"{name}: p50={p50:.2f}ms p95={p95:.2f}ms p99={p99:.2f}ms QPS={qps:.1f}") start = time.perf_counter() pg_latencies = benchmark_pgvector(cur, query_vectors) summarize("pgvector", pg_latencies, time.perf_counter() - start) start = time.perf_counter() qdrant_latencies = benchmark_qdrant(qdrant, query_vectors) summarize("Qdrant", qdrant_latencies, time.perf_counter() - start)

21

22

23

24

25

26

27

28

29

30

31

32

33

34

35

36

37

38

39

40

41

time

statistics

query_vectors

200

def

benchmark_pgvector

ef_search

limit

f

"SET hnsw.ef_search = {ef_search};"

latencies

qv

perf_counter

"SELECT id FROM items ORDER BY embedding <=> %s LIMIT %s"

fetchall

append

*

ms

return

benchmark_qdrant

client

query_points

query

summarize

total_time

latencies_sorted

sorted

p50

median

p95

int

0.95

p99

0.99

qps

print

"{name}: p50={p50:.2f}ms p95={p95:.2f}ms p99={p99:.2f}ms QPS={qps:.1f}"

pg_latencies

"pgvector"

qdrant_latencies

"Qdrant"

这里有几个需要注意的地方。生成的200个查询向量是新生成的,而不是从已加载的10万个向量中提取的,因为如果使用索引中已存在的向量进行查询,会直接返回自身且距离为零,这会使基准测试失去意义。

SET hnsw.ef_search = 64 控制 pgvector 在查询时的搜索彻底性——更高的值会以牺牲速度为代价换取更好的召回率,显式设置该参数而非依赖默认值可以保证测试的可重复性。embedding <=> %s 是 pgvector 的余弦距离运算符,与 Qdrant 集合中使用的 Distance.COSINE 设置相匹配。client.query_points() 是 Qdrant 当前推荐的搜索方法,已取代客户端较新版本中旧的 search() 调用。summarize 函数会计算两个系统相同的三个百分位数(p50、p95、p99)以及每秒查询数,因此输出结果可以逐行直接对比。

一个简单的柱状图示例,显示 pgvector 和 Qdrant 的 p50、p95 和 p99 延迟(点击放大)

本地测试结果的实际含义

在普通笔记本电脑上运行时,两个系统报告的延迟都在低个位数到数十毫秒的范围内,具体数值高度依赖于你的CPU、可用内存以及当时机器上运行的其他程序。这种可变性正是本节要说明的重点:在笔记本电脑上使用10万个向量是学习两个系统工作原理和验证配置是否正确的良好方式,但规模实在太小,无法得出真实的生产环境结论。在这个规模下,两个系统的速度都足够快,它们之间的差异目前几乎可以忽略不计。真正的问题——当数据和查询负载实际增长时,哪个系统表现更稳定——需要在真实生产规模下收集数据,而不是依赖笔记本电脑的基准测试。

在生产规模下的表现

这就是Tiger Data发布的基准测试真正有价值的地方,因为它是目前最详细、方法论最透明的公开对比。他们使用5000万个真实Cohere嵌入向量(768维),在匹配的AWS r6id.4xlarge硬件(16个vCPU,128GB内存)上对两个系统进行了测试,使用的是行业标准ANN-Benchmarks工具的修改版分支。

在数据出现前需要强调一个准确性问题:这个特定的基准测试使用了Postgres配合pgvector和pgvectorscale——Timescale自己的附加扩展,它添加了一种名为StreamingDiskANN的不同索引类型——而不是单独使用pgvector。这个区别在很多关于该基准测试的二次报道中被模糊处理了,但确实很重要,因为下面的数字反映了这种组合设置的表现。

| 指标 | pgvector + pgvectorscale | Qdrant | |------|--------------------------|--------| | 99%召回率下的p50延迟 | 31.07 ms | 30.75 ms | | 99%召回率下的p95延迟 | 60.42 ms | 36.73 ms | | 99%召回率下的p99延迟 | 74.60 ms | 38.71 ms | | 99%召回率下的吞吐量 | 471.57 QPS | 41.47 QPS | | 90%召回率下的p50延迟 | 9.54 ms | 4.74 ms | | 90%召回率下的吞吐量 | 1,589.79 QPS | 360.81 QPS | | 索引构建时间(5000万向量) | 11.1 小时 | 3.3 小时 |

出现的模式是双向的,而不是某个系统明显占优。在99%召回率下,使用pgvectorscale的Postgres实现了Qdrant 11.4倍的查询吞吐量——471.57 QPS对比41.47 QPS。但反过来,Qdrant在尾部延迟方面确实有可衡量的优势:p99延迟提升了48%(38.71ms对比74.60ms),这意味着即使其整体容量较低,最慢的查询依然明显更快。Qdrant构建索引的速度也快了约3.4倍。两个系统在5000万向量数据集上,99%召回率下所有测量的百分位数都保持在100毫秒以下,这一点本身就很值得关注——因为它直接推翻了通用数据库无法在真实规模下跟上的假设。

pgvector适用的场景

在合理硬件上,规模低于约500万到1000万向量时,pgvector的表现非常稳定。如果你需要在关系型数据和其嵌入向量之间保持事务一致性——例如在同一个原子事务中更新文档及其向量——这是pgvector独有的结构性优势,单独的数据库无法提供。如果团队已经具备Postgres的使用、监控和备份经验,那么添加向量搜索只需扩展一个插件和一个索引,而不是引入全新系统。同时,中等查询并发量(而非每秒数千个并发查询)能让单个调优良好的Postgres实例轻松应对。

何时选择专用向量数据库

线的另一侧同样具体而明确。真正的大规模场景——数亿到数十亿向量的处理——正是专为特定用途设计的分布式架构展现其运营成本优势的领域。在高并发过滤搜索场景下,严格的尾延迟SLA(如p99超过25到40毫秒即构成实际问题)这一需求,恰好凸显了Qdrant已验证的尾延迟性能优势。而基于GPU加速的索引构建则明确指向Milvus——这是列表中唯一从底层架构起就围绕该特性构建的系统。对于组织层面真正偏好零运维托管基础设施的场景(团队中没有数据库专家,也没有运行和调优任何组件的意愿),Pinecone这类全托管服务的存在目的正是彻底消除这类负担。

结论

本文的基准测试数据并未支持人们通常倾向于选择的两个极端结论——"pgvector始终表现良好"或"专用数据库总是胜出"。测试显示,在匹配且透明的条件下,两种系统确实展现出截然不同的权衡:一个在吞吐量上具有决定性优势,另一个在尾延迟上占据绝对优势,而两者都能轻松满足大多数实际应用的真实需求。对于"应该选择哪一个"这个问题,诚实的答案从未可能是单一选项。答案取决于你的实际工作负载落在权衡曲线的哪一侧——而不是任何厂商的宣传话术。

更多相关内容

  • 在PostgreSQL中构建向量相似性搜索…
  • 5个专注于机器学习的免费YouTube频道…
  • 用三个难度级别解释向量数据库
  • 向量数据库与图RAG在智能体记忆中的对比:…
  • 高性能LLM应用的五大向量数据库
  • 机器学习领域向量数据库完整指南

/.entry