纲要
pgvector扩展概述- 核心向量数据类型:
vector、halfvec、sparsevec、bit - 适用版本说明
- 核心向量数据类型:
vector类型的底层存储结构varlena可变长度头部- 源码结构体
Vector字段解析 - 存储空间计算公式
- PostgreSQL TOAST 机制
- TOAST 的四种存储策略:
PLAIN、EXTENDED、EXTERNAL、MAIN pgvector各版本的默认存储策略演变
- TOAST 的四种存储策略:
- 索引维度限制的根因
- PostgreSQL 8KB 数据页与索引页限制
- 索引条目不超过页面的三分之一规则
vector类型索引上限 2000 维的技术原因halfvec与bit类型的高维索引支持
- TOAST 对查询优化器的影响
- 行外存储导致优化器低估成本
- 并行查询规划数量下降的实证分析
parallel_setup_cost与parallel_tuple_cost的调优
- 最佳实践与存储策略选择
- 使用
PLAIN存储避免 TOAST - 存储策略修改后的表重写操作
- 分区表与冷热分离
- 批量导入工具选型
- 使用
pgvector 向量数据类型概述
pgvector 是 PostgreSQL 生态中最主流的向量相似性搜索扩展,以插件(extension)形式提供向量存储与 ANN(Approximate Nearest Neighbor)索引能力。其核心优势在于将向量数据直接存储在 PostgreSQL 内部,充分利用数据库自身的 ACID 事务、MVCC、WAL 日志、备份恢复等成熟基础设施,避免数据孤岛与跨系统数据搬运。
pgvector 提供了四种向量数据类型:
| 类型 | 元素精度 | 每维字节数 | 最大存储维度 | 最大索引维度 |
|---|---|---|---|---|
vector(n) | 单精度浮点(float32) | 4 字节 | 16,000 | 2,000 |
halfvec(n) | 半精度浮点(float16) | 2 字节 | 16,000 | 4,000 |
sparsevec(n) | 单精度浮点 + 整数索引 | 8 字节/非零元素 | 16,000 非零元素 | 1,000 非零元素 |
bit(n) | 二进制 | 1/8 字节 | 64,000 | 64,000 |
适用版本说明:
halfvec、sparsevec和bit类型自pgvector 0.7.0版本起引入。本文以vector类型为主要分析对象,其行为适用于pgvector 0.5.0及以上版本。
vector 类型的底层存储结构
源码结构体定义
vector 类型在 pgvector 源码中的定义如下(src/vector.h):
typedef struct Vector
{
int32 vl_len_; /* varlena 头部(请勿直接操作) */
int16 dim; /* 向量维度 */
int16 unused; /* 保留字段,暂未使用 */
float x[FLEXIBLE_ARRAY_MEMBER]; /* 浮点数组,每个元素 4 字节 */
} Vector;
该结构体包含四个字段:
vl_len_:varlena可变长度头部,占用 4 字节。这是 PostgreSQL 中所有变长数据类型的通用头部,存储了数据的总长度信息。dim:向量的维度,占用 2 字节(int16)。unused:保留字段,占用 2 字节,为后续扩展预留。x[]:柔性数组成员,存储实际的浮点向量元素。每个元素为 C 语言的float类型,占用 4 字节。
存储空间计算
每个 vector 类型值的磁盘存储空间为:
Storage = 4 × dim + 8 bytes \text{Storage} = 4 \times \text{dim} + 8 \text{ bytes} Storage=4×dim+8 bytes
其中 8 字节固定开销包含 4 字节的 varlena 头部和 4 字节的元数据(dim + unused)。例如:
- 768 维向量: 4 × 768 + 8 = 3 , 080 4 \times 768 + 8 = 3,080 4×768+8=3,080 字节
- 1536 维向量: 4 × 1536 + 8 = 6 , 152 4 \times 1536 + 8 = 6,152 4×1536+8=6,152 字节
- 2000 维向量: 4 × 2000 + 8 = 8 , 008 4 \times 2000 + 8 = 8,008 4×2000+8=8,008 字节
TOAST 机制及其对向量存储的影响
TOAST 基本原理
PostgreSQL 使用固定的数据页大小(默认为 8KB),且不允许一个元组(tuple)跨多个数据页存储。当字段值过大无法放入单个数据页时,PostgreSQL 会自动将其压缩或拆分存储到辅助表(TOAST 表)中,仅在主表中保留一个 18 字节的指针。这一机制称为 TOAST(The Oversized-Attribute Storage Technique)。
要支持 TOAST,数据类型必须使用 varlena(可变长度)表示。pgvector 的 vector 类型正是基于 varlena 构建的。
TOAST 的四种存储策略
PostgreSQL 为 TOAST 数据类型提供了四种存储策略:
| 策略 | 行为 | 适用场景 |
|---|---|---|
PLAIN | 禁止压缩和行外存储,数据必须内联存储 | 小字段,避免额外 I/O |
EXTENDED | 允许压缩和行外存储(先压缩,再视情况行外存储) | 大多数 TOAST 数据类型的默认策略 |
EXTERNAL | 允许行外存储,但禁止压缩 | 需要避免压缩开销的场景 |
MAIN | 尽量内联存储,仅在必要时行外存储 | 经常访问的大字段 |
pgvector 存储策略的版本演变
pgvector 的默认存储策略在不同版本中有所变化:
- 0.4.0 之前:
vector类型使用PLAIN存储策略。 - 0.4.0 及之后:
vector类型默认使用EXTENDED存储策略。
这意味着在较新版本中,当向量维度超过约 768 维( 4 × 768 + 8 = 3 , 080 4 \times 768 + 8 = 3,080 4×768+8=3,080 字节,超过 TOAST 阈值)时,向量数据会被自动压缩或移出行外存储。
查看与修改存储策略
使用 \d+ 命令或查询 pg_attribute 系统表可查看列的存储策略:
-- 查看表的列存储策略
SELECT attname, attstorage
FROM pg_attribute
WHERE attrelid = 'test_vector'::regclass AND attnum > 0;
attstorage 字段取值含义:
p:PLAINe:EXTENDEDx:EXTERNALm:MAIN
修改列的存储策略:
ALTER TABLE test_vector ALTER COLUMN embedding SET STORAGE PLAIN;
注意:修改存储策略后,现有数据不会自动迁移。需要使用
VACUUM FULL或CLUSTER重写表,才能将已行外存储的数据重新内联。
索引维度限制的根因分析
2000 维上限的技术来源
pgvector 官方文档明确限制 vector 类型的索引维度不超过 2000。这一限制的根本原因在于 PostgreSQL 的索引页结构与 TOAST 机制之间的冲突。
PostgreSQL 的 B-tree 索引要求索引条目不能超过数据页大小的约三分之一。默认数据页为 8KB,三分之一约为 2.7KB。一个 2000 维的 vector 占用
4
×
2000
+
8
=
8
,
008
4 \times 2000 + 8 = 8,008
4×2000+8=8,008 字节,远超 2.7KB 的限制。
为什么 TOAST 不能用于索引
TOAST 机制虽然可以处理超大数据,但TOAST 不能用于索引。索引条目必须能够直接从索引页中读取并比较,而 TOAST 指针指向的行外数据无法在索引扫描过程中高效访问。因此,索引必须存储完整的向量值,而非 TOAST 指针。
这一约束使得索引向量的大小必须适配索引页的容量限制,最终形成了 2000 维的上限。
高维向量的索引替代方案
对于需要索引超过 2000 维向量的场景,pgvector 0.7.0 提供了以下替代方案:
halfvec:半精度浮点,每维 2 字节,索引上限 4000 维bit:二进制向量,每 8 维仅需 1 字节,索引上限 64000 维- 二进制量化(Binary Quantization):可将高维 float32 向量量化为二进制向量,索引上限 64000 维
TOAST 对查询优化器的影响
优化器对 TOAST 数据的认知不足
PostgreSQL 的查询优化器在估算顺序扫描成本时,不会充分考虑 TOAST 行外存储带来的额外 I/O 开销。由于 TOAST 数据与主表数据分开存储,优化器倾向于低估扫描 TOAST 数据的实际成本。
并行查询规划的实证分析
以下是一个典型的性能对比场景:
| 向量维度 | 数据大小 | TOAST 状态 | 优化器规划的并行工作进程数 |
|---|---|---|---|
| 128 维 | 520 字节 | 内联存储 | 16 |
| 1536 维 | 6,152 字节 | 行外存储(TOAST) | 4 |
1536 维向量由于超出数据页容量而被移入 TOAST 表,优化器因无法准确评估 TOAST 访问成本,仅规划了 4 个并行工作进程,导致查询效率严重下降。
手动调优并行查询
通过调整以下参数,可以强制优化器使用更多的并行工作进程:
-- 降低并行扫描的成本估算
SET parallel_setup_cost = 1;
SET parallel_tuple_cost = 0.001;
-- 或调整单次扫描的最小数据量阈值
SET min_parallel_table_scan_size = 0;
注意:上述调整属于"以资源换时间"的策略,会增加 CPU 和 I/O 资源消耗,需根据实际硬件条件谨慎使用。
最佳实践与存储策略选择
优先使用 PLAIN 存储
对于维度不超过 2000 的向量,建议将存储策略设置为 PLAIN,强制数据内联存储,避免 TOAST 带来的额外 I/O 和优化器成本估算偏差。
CREATE TABLE embeddings (
id BIGSERIAL PRIMARY KEY,
embedding vector(768) NOT NULL
);
-- 将向量列设置为 PLAIN 存储
ALTER TABLE embeddings ALTER COLUMN embedding SET STORAGE PLAIN;
-- 重写表以应用新的存储策略
VACUUM FULL embeddings;
分区表实现冷热分离
利用 PostgreSQL 分区表,可将热数据(近期频繁查询)与冷数据(历史归档)分别存储在不同的表空间上:
-- 按月分区
CREATE TABLE embeddings_partitioned (
id BIGSERIAL,
created_at TIMESTAMPTZ NOT NULL,
embedding vector(768) NOT NULL
) PARTITION BY RANGE (created_at);
-- 热数据分区(SSD 表空间)
CREATE TABLE embeddings_2026_08 PARTITION OF embeddings_partitioned
FOR VALUES FROM ('2026-08-01') TO ('2026-09-01')
TABLESPACE ssd_tablespace;
-- 冷数据分区(HDD 表空间)
CREATE TABLE embeddings_2025_01 PARTITION OF embeddings_partitioned
FOR VALUES FROM ('2025-01-01') TO ('2025-02-01')
TABLESPACE hdd_tablespace;
批量数据导入
pgvector 可以充分利用 PostgreSQL 的原生批量导入工具:
# 使用 COPY 命令导入 CSV 格式的向量数据
psql -d mydb -c "\COPY embeddings(embedding) FROM '/data/vectors.csv' WITH CSV"
# 使用 pg_bulkload 进行高性能批量加载
pg_bulkload -d mydb -i /data/vectors.csv -O embeddings -l load.log
预过滤与后过滤策略
在实际查询中,往往需要结合元数据过滤条件:
- 预过滤(Pre-filter):先通过元数据条件(如
WHERE category = 'A')缩小数据集,再执行 ANN 搜索。适用于过滤条件选择性高的场景。 - 后过滤(Post-filter):先执行 ANN 搜索获取 Top-K 候选,再应用元数据过滤。
pgvector 0.8.0及以上版本支持迭代索引扫描,自动读取更多候选直到满足过滤条件。
API 速览
pgvector 核心数据类型
vector(n)
创建指定维度的向量类型列:
CREATE TABLE items (
id BIGSERIAL PRIMARY KEY,
embedding vector(1536)
);
距离运算符
| 运算符 | 距离类型 | 运算符类 |
|---|---|---|
<-> | 欧几里得距离(L2) | vector_l2_ops |
<#> | 负内积 | vector_ip_ops |
<=> | 余弦距离 | vector_cosine_ops |
相似性查询
-- 查询与目标向量最相似的 10 条记录(余弦距离)
SELECT id, embedding
FROM items
ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector
LIMIT 10;
索引创建
IVFFlat 索引
CREATE INDEX ON items USING ivfflat (embedding vector_cosine_ops)
WITH (lists = 100);
HNSW 索引
CREATE INDEX ON items USING hnsw (embedding vector_cosine_ops)
WITH (m = 16, ef_construction = 64);
注意:HNSW 索引的维度上限为 2000(
vector类型)。
存储策略管理
查看当前存储策略
SELECT attname, attstorage
FROM pg_attribute
WHERE attrelid = 'items'::regclass AND attname = 'embedding';
修改存储策略
ALTER TABLE items ALTER COLUMN embedding SET STORAGE PLAIN;
重写表以应用存储策略变更
VACUUM FULL items;
-- 或
CLUSTER items USING items_pkey;
系统信息查询
查看 pgvector 版本
SELECT * FROM pg_extension WHERE extname = 'vector';
查看数据页大小
SHOW block_size;
Demo 简单示例
项目结构
pgvector-storage-demo/
├── package.json
├── index.js
├── .env
└── sql/
└── init.sql
运行说明
- 安装依赖:
npm init -y
npm install pg dotenv
- 配置环境变量(
.env文件):
PGHOST=localhost
PGPORT=5432
PGDATABASE=vectordb
PGUSER=postgres
PGPASSWORD=your_password
- 初始化数据库(
sql/init.sql):
CREATE EXTENSION IF NOT EXISTS vector;
CREATE TABLE embeddings (
id BIGSERIAL PRIMARY KEY,
embedding vector(768) NOT NULL,
metadata JSONB,
created_at TIMESTAMPTZ DEFAULT NOW()
);
-- 设置为 PLAIN 存储以优化性能
ALTER TABLE embeddings ALTER COLUMN embedding SET STORAGE PLAIN;
-- 创建 HNSW 索引
CREATE INDEX ON embeddings USING hnsw (embedding vector_cosine_ops);
- 运行示例:
node index.js
代码说明(index.js)
const { Client } = require('pg');
require('dotenv').config();
// 生成随机向量(模拟 embedding)
function generateRandomVector(dim) {
const vec = [];
for (let i = 0; i < dim; i++) {
vec.push(Math.random() * 2 - 1); // [-1, 1] 范围
}
return vec;
}
async function main() {
const client = new Client({
host: process.env.PGHOST,
port: parseInt(process.env.PGPORT),
database: process.env.PGDATABASE,
user: process.env.PGUSER,
password: process.env.PGPASSWORD,
});
await client.connect();
const DIM = 768;
const NUM_VECTORS = 1000;
// 1. 批量插入向量数据
console.log(`插入 ${NUM_VECTORS} 条 ${DIM} 维向量...`);
const vectors = [];
for (let i = 0; i < NUM_VECTORS; i++) {
vectors.push(generateRandomVector(DIM));
}
// 使用参数化批量插入
for (let i = 0; i < vectors.length; i++) {
await client.query(
'INSERT INTO embeddings (embedding) VALUES ($1)',
[`[${vectors[i].join(',')}]`]
);
}
// 2. 生成查询向量
const queryVector = generateRandomVector(DIM);
// 3. 执行相似性搜索(余弦距离)
console.log('执行相似性搜索...');
const result = await client.query(
`SELECT id, embedding <=> $1::vector AS distance
FROM embeddings
ORDER BY embedding <=> $1::vector
LIMIT 10`,
[`[${queryVector.join(',')}]`]
);
console.log('Top 10 最相似向量:');
result.rows.forEach((row, idx) => {
console.log(` #${idx + 1}: id=${row.id}, distance=${row.distance.toFixed(6)}`);
});
// 4. 查看 TOAST 存储状态
const toastInfo = await client.query(`
SELECT attname, attstorage,
CASE attstorage
WHEN 'p' THEN 'PLAIN'
WHEN 'e' THEN 'EXTENDED'
WHEN 'x' THEN 'EXTERNAL'
WHEN 'm' THEN 'MAIN'
END AS storage_strategy
FROM pg_attribute
WHERE attrelid = 'embeddings'::regclass AND attname = 'embedding'
`);
console.log('向量列存储策略:', toastInfo.rows[0]);
// 5. 查看表大小
const sizeInfo = await client.query(`
SELECT pg_size_pretty(pg_total_relation_size('embeddings')) AS total_size
`);
console.log('表总大小:', sizeInfo.rows[0].total_size);
await client.end();
}
main().catch(console.error);
技术点总结
本 Demo 演示了以下核心技术:
vector类型的创建与使用:在 PostgreSQL 中创建向量列并插入数据- 批量数据插入:使用参数化查询批量写入向量数据
- 余弦距离相似性搜索:使用
<=>运算符进行 ANN 查询 - 存储策略查看:通过
pg_attribute系统表确认列的 TOAST 存储策略 - 表空间占用监控:使用
pg_total_relation_size查看存储空间
PostgreSQL 原生指令对照
上述 Node.js 操作对应的 PostgreSQL 原生 SQL 指令:
-- 插入单条向量
INSERT INTO embeddings (embedding) VALUES ('[0.1, 0.2, ...]'::vector);
-- 相似性搜索(余弦距离)
SELECT id, embedding <=> '[0.1, 0.2, ...]'::vector AS distance
FROM embeddings
ORDER BY embedding <=> '[0.1, 0.2, ...]'::vector
LIMIT 10;
-- 查看存储策略
SELECT attname, attstorage FROM pg_attribute
WHERE attrelid = 'embeddings'::regclass AND attname = 'embedding';
-- 查看表大小
SELECT pg_size_pretty(pg_total_relation_size('embeddings'));
多语言示例
基于前文 Node.js 的 Demo,本节提供 Go、Python 和 Java 三种语言的完整实现。所有示例均针对 PostgreSQL + pgvector 环境,执行相同的向量插入与相似性搜索操作。
Go 语言示例
使用 pgx 驱动(推荐)和 dotenv 配置管理。
项目结构
go-pgvector-demo/
├── go.mod
├── go.sum
├── main.go
├── .env
└── sql/
└── init.sql
运行说明
- 初始化模块:
go mod init go-pgvector-demo
go get github.com/jackc/pgx/v5
go get github.com/joho/godotenv
- 环境变量(
.env):
PGHOST=localhost
PGPORT=5432
PGDATABASE=vectordb
PGUSER=postgres
PGPASSWORD=your_password
-
初始化数据库(
sql/init.sql同上文 Node.js 部分)。 -
运行:
go run main.go
完整代码(main.go)
package main
import (
"context"
"fmt"
"log"
"math/rand"
"os"
"strconv"
"strings"
"time"
"github.com/jackc/pgx/v5"
"github.com/joho/godotenv"
)
// generateRandomVector 生成指定维度的随机向量,元素在 [-1, 1] 范围
func generateRandomVector(dim int) []float32 {
vec := make([]float32, dim)
for i := range vec {
vec[i] = rand.Float32()*2 - 1
}
return vec
}
// vectorToString 将 []float32 转换为 PostgreSQL vector 字符串格式 '[0.1,0.2,...]'
func vectorToString(vec []float32) string {
parts := make([]string, len(vec))
for i, v := range vec {
parts[i] = strconv.FormatFloat(float64(v), 'f', 6, 32)
}
return "[" + strings.Join(parts, ",") + "]"
}
func main() {
// 加载 .env
if err := godotenv.Load(); err != nil {
log.Println("未找到 .env 文件,使用系统环境变量")
}
// 构建连接字符串
connStr := fmt.Sprintf(
"host=%s port=%s dbname=%s user=%s password=%s",
os.Getenv("PGHOST"),
os.Getenv("PGPORT"),
os.Getenv("PGDATABASE"),
os.Getenv("PGUSER"),
os.Getenv("PGPASSWORD"),
)
ctx := context.Background()
// 连接数据库
conn, err := pgx.Connect(ctx, connStr)
if err != nil {
log.Fatalf("无法连接数据库: %v", err)
}
defer conn.Close(ctx)
const dim = 768
const numVectors = 1000
rand.Seed(time.Now().UnixNano())
// 1. 批量插入向量(使用 COPY 更高效,但为保持示例清晰使用循环)
fmt.Printf("插入 %d 条 %d 维向量...\n", numVectors, dim)
for i := 0; i < numVectors; i++ {
vec := generateRandomVector(dim)
vecStr := vectorToString(vec)
_, err := conn.Exec(ctx, "INSERT INTO embeddings (embedding) VALUES ($1)", vecStr)
if err != nil {
log.Fatalf("插入失败: %v", err)
}
}
// 2. 生成查询向量
queryVec := generateRandomVector(dim)
queryStr := vectorToString(queryVec)
// 3. 执行相似性搜索(余弦距离)
fmt.Println("执行相似性搜索...")
rows, err := conn.Query(ctx,
`SELECT id, embedding <=> $1::vector AS distance
FROM embeddings
ORDER BY embedding <=> $1::vector
LIMIT 10`,
queryStr,
)
if err != nil {
log.Fatalf("查询失败: %v", err)
}
defer rows.Close()
fmt.Println("Top 10 最相似向量:")
idx := 1
for rows.Next() {
var id int64
var distance float32
if err := rows.Scan(&id, &distance); err != nil {
log.Fatalf("扫描失败: %v", err)
}
fmt.Printf(" #%d: id=%d, distance=%.6f\n", idx, id, distance)
idx++
}
// 4. 查看存储策略
var attname string
var attstorage string
err = conn.QueryRow(ctx,
`SELECT attname, attstorage
FROM pg_attribute
WHERE attrelid = 'embeddings'::regclass AND attname = 'embedding'`,
).Scan(&attname, &attstorage)
if err != nil {
log.Fatalf("查询存储策略失败: %v", err)
}
fmt.Printf("向量列存储策略: attname=%s, attstorage=%s\n", attname, attstorage)
// 5. 查看表大小
var totalSize string
err = conn.QueryRow(ctx,
`SELECT pg_size_pretty(pg_total_relation_size('embeddings'))`,
).Scan(&totalSize)
if err != nil {
log.Fatalf("查询表大小失败: %v", err)
}
fmt.Printf("表总大小: %s\n", totalSize)
}
PostgreSQL 原生指令对照(Go 版本使用相同的 SQL)
Python 示例
使用 psycopg2(或 psycopg2-binary)和 python-dotenv。
项目结构
py-pgvector-demo/
├── requirements.txt
├── main.py
├── .env
└── sql/
└── init.sql
运行说明
- 安装依赖:
pip install psycopg2-binary python-dotenv
-
环境变量(
.env同上)。 -
运行:
python main.py
完整代码(main.py)
import os
import random
import psycopg2
from psycopg2 import sql
from dotenv import load_dotenv
load_dotenv()
def generate_random_vector(dim):
"""生成 [-1, 1] 范围的随机向量"""
return [random.uniform(-1, 1) for _ in range(dim)]
def vector_to_string(vec):
"""将 Python list 转换为 PostgreSQL vector 字符串"""
return "[" + ",".join(f"{v:.6f}" for v in vec) + "]"
def main():
# 连接数据库
conn = psycopg2.connect(
host=os.getenv("PGHOST"),
port=os.getenv("PGPORT"),
dbname=os.getenv("PGDATABASE"),
user=os.getenv("PGUSER"),
password=os.getenv("PGPASSWORD"),
)
conn.autocommit = True
cur = conn.cursor()
DIM = 768
NUM_VECTORS = 1000
# 1. 批量插入向量
print(f"插入 {NUM_VECTORS} 条 {DIM} 维向量...")
for _ in range(NUM_VECTORS):
vec = generate_random_vector(DIM)
vec_str = vector_to_string(vec)
cur.execute("INSERT INTO embeddings (embedding) VALUES (%s)", (vec_str,))
# 2. 生成查询向量
query_vec = generate_random_vector(DIM)
query_str = vector_to_string(query_vec)
# 3. 执行相似性搜索(余弦距离)
print("执行相似性搜索...")
cur.execute(
"""
SELECT id, embedding <=> %s::vector AS distance
FROM embeddings
ORDER BY embedding <=> %s::vector
LIMIT 10
""",
(query_str, query_str),
)
results = cur.fetchall()
print("Top 10 最相似向量:")
for idx, (id_, dist) in enumerate(results, 1):
print(f" #{idx}: id={id_}, distance={dist:.6f}")
# 4. 查看存储策略
cur.execute(
"""
SELECT attname, attstorage
FROM pg_attribute
WHERE attrelid = 'embeddings'::regclass AND attname = 'embedding'
"""
)
attname, attstorage = cur.fetchone()
print(f"向量列存储策略: attname={attname}, attstorage={attstorage}")
# 5. 查看表大小
cur.execute("SELECT pg_size_pretty(pg_total_relation_size('embeddings'))")
total_size = cur.fetchone()[0]
print(f"表总大小: {total_size}")
cur.close()
conn.close()
if __name__ == "__main__":
main()
Java 示例
使用 JDBC 驱动(postgresql)和 dotenv 库(或直接使用环境变量)。这里使用 dotenv-java 读取 .env。
项目结构
java-pgvector-demo/
├── pom.xml
├── src/
│ └── main/
│ └── java/
│ └── Main.java
├── .env
└── sql/
└── init.sql
运行说明
- Maven 依赖(
pom.xml):
<dependencies>
<dependency>
<groupId>org.postgresql</groupId>
<artifactId>postgresql</artifactId>
<version>42.7.3</version>
</dependency>
<dependency>
<groupId>io.github.cdimascio</groupId>
<artifactId>java-dotenv</artifactId>
<version>5.2.2</version>
</dependency>
</dependencies>
-
环境变量(
.env同上)。 -
编译运行:
mvn compile exec:java -Dexec.mainClass="Main"
完整代码(Main.java)
import io.github.cdimascio.dotenv.Dotenv;
import java.sql.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;
public class Main {
private static final Random RANDOM = new Random();
// 生成随机向量
private static List<Float> generateRandomVector(int dim) {
List<Float> vec = new ArrayList<>(dim);
for (int i = 0; i < dim; i++) {
vec.add(RANDOM.nextFloat() * 2 - 1); // [-1, 1]
}
return vec;
}
// 将 List<Float> 转换为 PostgreSQL vector 字符串
private static String vectorToString(List<Float> vec) {
StringBuilder sb = new StringBuilder("[");
for (int i = 0; i < vec.size(); i++) {
if (i > 0) sb.append(",");
sb.append(String.format("%.6f", vec.get(i)));
}
sb.append("]");
return sb.toString();
}
public static void main(String[] args) {
// 加载 .env
Dotenv dotenv = Dotenv.configure().load();
String host = dotenv.get("PGHOST");
String port = dotenv.get("PGPORT");
String dbname = dotenv.get("PGDATABASE");
String user = dotenv.get("PGUSER");
String password = dotenv.get("PGPASSWORD");
String url = String.format("jdbc:postgresql://%s:%s/%s", host, port, dbname);
final int DIM = 768;
final int NUM_VECTORS = 1000;
try (Connection conn = DriverManager.getConnection(url, user, password)) {
conn.setAutoCommit(true);
// 1. 批量插入向量
System.out.printf("插入 %d 条 %d 维向量...\n", NUM_VECTORS, DIM);
String insertSql = "INSERT INTO embeddings (embedding) VALUES (?)";
try (PreparedStatement pstmt = conn.prepareStatement(insertSql)) {
for (int i = 0; i < NUM_VECTORS; i++) {
List<Float> vec = generateRandomVector(DIM);
String vecStr = vectorToString(vec);
pstmt.setString(1, vecStr);
pstmt.addBatch();
// 每 100 条提交一次
if (i % 100 == 0) {
pstmt.executeBatch();
}
}
pstmt.executeBatch();
}
// 2. 生成查询向量
List<Float> queryVec = generateRandomVector(DIM);
String queryStr = vectorToString(queryVec);
// 3. 执行相似性搜索(余弦距离)
System.out.println("执行相似性搜索...");
String searchSql =
"SELECT id, embedding <=> ?::vector AS distance " +
"FROM embeddings " +
"ORDER BY embedding <=> ?::vector " +
"LIMIT 10";
try (PreparedStatement pstmt = conn.prepareStatement(searchSql)) {
pstmt.setString(1, queryStr);
pstmt.setString(2, queryStr);
try (ResultSet rs = pstmt.executeQuery()) {
System.out.println("Top 10 最相似向量:");
int idx = 1;
while (rs.next()) {
long id = rs.getLong("id");
float distance = rs.getFloat("distance");
System.out.printf(" #%d: id=%d, distance=%.6f\n", idx, id, distance);
idx++;
}
}
}
// 4. 查看存储策略
String storageSql =
"SELECT attname, attstorage " +
"FROM pg_attribute " +
"WHERE attrelid = 'embeddings'::regclass AND attname = 'embedding'";
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(storageSql)) {
if (rs.next()) {
String attname = rs.getString("attname");
String attstorage = rs.getString("attstorage");
System.out.printf("向量列存储策略: attname=%s, attstorage=%s\n", attname, attstorage);
}
}
// 5. 查看表大小
String sizeSql = "SELECT pg_size_pretty(pg_total_relation_size('embeddings'))";
try (Statement stmt = conn.createStatement();
ResultSet rs = stmt.executeQuery(sizeSql)) {
if (rs.next()) {
String totalSize = rs.getString(1);
System.out.printf("表总大小: %s\n", totalSize);
}
}
} catch (SQLException e) {
e.printStackTrace();
}
}
}
多语言对比总结
下表对比了四种语言实现的核心差异,便于根据项目技术栈选择。
| 对比维度 | Node.js (pg) | Go (pgx) | Python (psycopg2) | Java (JDBC) |
|---|---|---|---|---|
| 驱动/库 | pg (纯 JavaScript) | pgx/v5 | psycopg2-binary | postgresql JDBC |
| 连接方式 | new Client() + 环境变量 | pgx.Connect() | psycopg2.connect() | DriverManager.getConnection() |
| 参数化查询 | $1, $2 占位符 | $1, $2 占位符 | %s 占位符(psycopg2 使用 %s) | ? 占位符 |
| 向量类型转换 | 手动拼接 [x,y] 字符串 | 手动拼接 | 手动拼接 | 手动拼接 |
| 批量插入方式 | 逐条循环 INSERT | 逐条循环 Exec | 逐条循环 execute | addBatch() + executeBatch() |
| 事务控制 | 默认自动提交,可手动管理 | 默认自动提交 | 设置 autocommit=True | 默认自动提交 |
| 错误处理 | try-catch / 回调 | if err != nil | try-except | try-catch |
| 环境变量管理 | dotenv | godotenv | python-dotenv | java-dotenv |
| 字符串拼接性能 | 中等 | 中等 | 中等 | 较好(StringBuilder) |
| 适合场景 | 快速原型、全栈 | 高并发、微服务 | 数据分析、机器学习 | 企业级、Spring 生态 |
| 执行相似性搜索 | 使用 <=> 运算符,参数化 | 使用 <=> 运算符,参数化 | 使用 <=> 运算符,参数化 | 使用 <=> 运算符,参数化 |
| 存储策略查询 | 查询 pg_attribute | 查询 pg_attribute | 查询 pg_attribute | 查询 pg_attribute |
| 表大小查询 | pg_total_relation_size | pg_total_relation_size | pg_total_relation_size | pg_total_relation_size |
所有语言的 Demo 均遵循以下共同逻辑:
- 连接 PostgreSQL 数据库,确保已启用
pgvector扩展。 - 创建包含
vector(768)列的表,并设置PLAIN存储策略(可选,根据前文最佳实践)。 - 批量生成并插入 1000 条随机向量。
- 使用余弦距离运算符
<=>执行 Top-10 相似性搜索。 - 查询
pg_attribute验证存储策略,查询pg_total_relation_size查看表空间占用。
各实现均可在对应环境中直接运行,所需依赖和启动命令已在上文提供。
官方文档
参考链接
总结
本文深入剖析了 pgvector 扩展中 vector 数据类型的底层存储机制,核心要点如下:
-
存储结构:
vector类型基于 PostgreSQL 的varlena可变长度头部构建,每个向量占用 4 × dim + 8 4 \times \text{dim} + 8 4×dim+8 字节磁盘空间,包含 4 字节头部、2 字节维度、2 字节保留字段和浮点数组。 -
TOAST 机制:
pgvector 0.4.0起默认使用EXTENDED存储策略,当向量超过约 768 维时会被自动压缩或移入 TOAST 表。TOAST 数据与主表分离存储,对查询优化器的成本估算产生显著影响,可能导致并行查询规划不足。 -
索引维度限制:由于 TOAST 不能用于索引且 PostgreSQL 索引页固定为 8KB,
vector类型的索引维度被限制为 2000。高维场景可使用halfvec(上限 4000 维)或bit(上限 64000 维)类型。 -
最佳实践:对于维度不超过 2000 的向量,建议将存储策略设置为
PLAIN并重写表,避免 TOAST 带来的性能损耗。结合分区表实现冷热分离,利用COPY或pg_bulkload进行批量导入。
431



被折叠的 条评论
为什么被折叠?



