前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
返回 AI 情报前线
All News · 全部资讯9712
  • Claude Opus 5.5 vs GPT-6 Sol:新一轮降价潮来了
  • TypeSafe AI Jev:不做生成、只做判断的极速模型
  • AI Agent 的失败根源不在推理,在于状态管理
  • Cloudflare Turnstile Spin:AI Agent自动修复网站安全配置
  • 微软 Copilot 超级应用:聊天、编程、Agent 三合一
  • Meta Muse为每位用户配备云端Ubuntu电脑
  • n8n推出新Agent类型:自然语言描述即可创建自动化代理
  • Microsoft 推出 Copilot 超级应用,整合聊天、编程和 Agent
  • OpenAI与Cursor对Agent协调器的设计分歧
  • Anthropic 联手 Google Cloud 在纽约办 Claude Code 实战工作坊
  • AI SRE智能处理值班警报:自动根因分析并提PR修复
  • AI生成应用的K8s生产就绪:四层架构避坑指南
  • AI Agent从概念到盈利:2026年实战总结
  • 远程GPU服务器监控:利用率与温度实战指南
  • stable-diffusion.cpp 新增通义图像模型 day-0 支持
  • 程序员最重要的技能:专业怀疑精神
  • NVIDIA 开源统一模型优化库
  • Hindsight:让 AI Agent 真正学习的记忆系统
  • Meta Muse macOS版被发现高危漏洞可劫持账户
  • Fastino开源340M决策模型GLiNER2.5-Decide:CPU可运行
  • Postgres 向量内联与分离表性能实测对比
  • 多 AI Agent 协作:实测最难攻克的工程难题
  • 自托管LLM总拥有成本对比指南
  • DeepSeek Harness桌面版发布:四种Agent模式开箱即用
  • AI 编程工具让软件开发更难了
  • GitHub谈何时不该用Chat:Canvas作为AI交互新范式
  • OpenAI Agent绕过安全拦截访问政府门户
  • ThinkingCap:减少37.2%思考token的Qwen微调版
  • MCP Nexus 1.0:MCP工具路由与发现层,一个端点代理数百工具
  • MCP网关安全:AI Agent生产部署的鉴权与限流实战
  • GitHub安全实验室:AI驱动的模糊测试实战
  • Meta Muse 被曝可被诱导泄露全文件系统
  • Black Forest Labs 发布开源机器人控制模型
  • Google Cloud API Gateway原生支持MCP协议
  • 3 万次 AI Agent 调用审计:可验证收据机制实践
  • Gemini 3.8 Live发布:实时Avatar交互能力
  • 用 AgentCore Gateway 和 MCP 构建跨账号 AI Agent 架构
  • AI 性能成本每年下降 13 倍,超越所有前代技术
  • Lovable年化收入超6000万美元,vibe coding进入主流
  • Agent失败的根本原因:从未定义"完成"的标准
  • Agent补丁审查策略:second process重放验证才能合并
  • 我用 Claude Code 九天给 47 个服务接入 OpenTelemetry 链路追踪
  • Google Gemini CLI新增文件修改确认机制
  • DeepSeek推理优化:内核补齐+通信重构,吞吐翻7倍
  • Hugging Face推出LFM2.5-VL-DSpark视觉语言模型加速方案
  • 管理 15 万 AI Agent 对数据库团队的挑战与变革
  • Cursor 收购 Firetiger 后一月推出代码变更生产追踪 Bot
  • OpenAI AI Agent 自主入侵澳大利亚政府网站
  • Claude Code 实际项目开发全流程复盘
  • Google Cloud Developer 插件让 AI 编码助手直连云端部署
  • OpenAI Agent 入侵澳大利亚 Medicare 统计门户
  • 已加载 51 / 9712
8.0
热点
AI SCORE
技术实践2026-09-25 12:29

Postgres 向量内联与分离表性能实测对比

Google AI · DEV#PostgreSQL#向量数据库#性能优化
Editor brief · 编辑速览

通过 3 万条产品数据实测,在 AlloyDB 上对比向量嵌入与主数据共表 vs 分离存储的性能差异,揭示 ANN 索引刷新时的 JOIN 开销。

文章思维导图
Knowledge map
拖拽缩放
Full translation

完整中文译文

在 Postgres 中向量存储:内联表 vs. 独立表——量化 Join 开销

测试语义搜索、带过滤条件的查询以及多表连接在 AlloyDB 中的表现,衡量向量数据解耦后的真实性能成本。

文章配图

如果你正在使用向量嵌入,可能已经知道嵌入模型在不断演进,需要时不时地用新版本刷新嵌入。在上一篇文章中,我们讨论了嵌入刷新时表中出现的膨胀问题、TOAST 段以及相关索引。作为将嵌入与数据存储在同一张表中的替代方案,我提出了另一种布局——将嵌入放在专门的表中。在这篇文章中,我将展示与将嵌入存储在同一张表中相比,这种方式可能带来的性能影响。

为了测试查询,我需要带有真实嵌入的样本数据。之前文章中已经解释过原因——使用 ANN 索引时可能会对结果产生明显影响。我准备了一个包含 30k 行样本产品的数据集,基于产品描述构建嵌入,并将其填充到专门的嵌入表中。在所有测试中,我使用的是 AlloyDB Omni 数据库。它自带 AI 集成,开箱即用,这帮助我构建嵌入、向量索引,并通过将搜索短语转换为向量变量来生成搜索嵌入。

以下是测试中使用的表:

-- ecomm.products 表

demodb=# \d ecomm.products
                              Table "ecomm.products"
         Column | Type | Collation | Nullable | Default
------------------------+------------------------+-----------+----------+---------
 id | bigint | | not null |
 cost | numeric | | |
 category | character varying(255) | | |
 name | character varying(255) | | |
 brand | character varying(255) | | |
 retail_price | numeric | | |
 department | character varying(255) | | |
 sku | character varying(255) | | |
 distribution_center_id | bigint | | |
 product_description | text | | |
 product_image_uri | text | | |
 embedding | vector(768) | | |
Indexes:
    "products_pkey" PRIMARY KEY, btree (id)
    "fk_products_distribution_center_23" btree (distribution_center_id)
    "idx_products_brand" btree (brand)
    "idx_products_category" btree (category)
    "idx_products_retail_price" btree (retail_price)
    "idx_products_sku" btree (sku)
    "idx_products_vector" hnsw (embedding vector_cosine_ops)
Foreign-key constraints:
    "fk_products_distribution_center" FOREIGN KEY (distribution_center_id) REFERENCES ecomm.distribution_centers(id)
Referenced by:
    TABLE "ecomm.inventory_items" CONSTRAINT "fk_inventory_items_product" FOREIGN KEY (product_id) REFERENCES ecomm.products(id)
    TABLE "ecomm.order_items" CONSTRAINT "fk_order_items_product" FOREIGN KEY (product_id) REFERENCES ecomm.products(id)
    TABLE "ecomm.product_reviews" CONSTRAINT "fk_reviews_product" FOREIGN KEY (product_id) REFERENCES ecomm.products(id) ON DELETE CASCADE

-- ecomm.product_embeddings 表
demodb=# \d ecomm.product_embeddings
             Table "ecomm.product_embeddings"
   Column | Type | Collation | Nullable | Default
------------+-------------+-----------+----------+---------
 product_id | bigint | | not null |
 embedding | vector(768) | | not null |
Indexes:
    "product_embeddings_pkey" PRIMARY KEY, btree (product_id)
    "idx_product_embeddings_vector" hnsw (embedding vector_cosine_ops)

-- ecomm.inventory_items 表 
demodb=# \d ecomm.inventory_items
                                 Table "ecomm.inventory_items"
             Column | Type | Collation | Nullable | Default
--------------------------------+-----------------------------+-----------+----------+---------
 id | bigint | | not null |
 product_id | bigint | | |
 created_at | timestamp without time zone | | |
 sold_at | timestamp without time zone | | |
 cost | numeric | | |
 product_category | character varying(255) | | |
 product_name | character varying(255) | | |
 product_brand | character varying(255) | | |
 product_retail_price | numeric | | |
 product_department | character varying(255) | | |
 product_sku | character varying(255) | | |
 product_distribution_center_id | bigint | | |
Indexes:
    "inventory_items_pkey" PRIMARY KEY, btree (id)
    "fk_inventory_items_distribution_center_8" btree (product_distribution_center_id)
    "fk_inventory_items_product_7" btree (product_id)
Foreign-key constraints:
    "fk_inventory_items_distribution_center" FOREIGN KEY (product_distribution_center_id) REFERENCES ecomm.distribution_centers(id)
    "fk_inventory_items_product" FOREIGN KEY (product_id) REFERENCES ecomm.products(id)
Referenced by:
    TABLE "ecomm.order_items" CONSTRAINT "fk_order_items_inventory_item" FOREIGN KEY (inventory_item_id) REFERENCES ecomm.inventory_items(id)

-- ecomm.product_reviews 表
demodb=# \d ecomm.product_reviews
                                Table "ecomm.product_reviews"
   Column | Type | Collation | Nullable | Default
-------------+--------------------------+-----------+----------+------------------------------
 id | bigint | | not null | generated always as identity
 user_id | bigint | | not null |
 product_id | bigint | | not null |
 rating | integer | | not null |
 review_text | text | | not null |
 created_at | timestamp with time zone | | not null | CURRENT_TIMESTAMP
Indexes:
    "product_reviews_pkey" PRIMARY KEY, btree (id)
    "idx_product_r

ecomm.products 和 ecomm.product_embeddings 通过主键关联,两张表都在嵌入向量上构建了 HNSW 索引。

测试产品语义搜索

我从一个简单的场景开始测试——仅使用 products 表进行余弦语义搜索,然后将其与 products 和 product_embeddings 表的连接进行比较。对于搜索,我将短语 'lightweight waterproof high quality jacket' 转换为向量嵌入,并将其作为 test_vec 变量传入。这样消除了模型响应时间不可预测的问题,使我能够更精确地比较查询执行时间。

-- 在 psql 中设置 test_vec 变量
SELECT (google_ml.embedding(
    model_id => 'text-embedding-005',
    content => 'lightweight waterproof high quality jacket'
)::public.vector(768))::text AS test_vec \gset

在同一个 psql 会话中,我运行了搜索并使用 EXPLAIN ANALYZE 查看执行计划和耗时。每个测试都重复多次。

-- 嵌入存储在 ecomm.products 中
EXPLAIN (ANALYZE, BUFFERS)
SELECT 
    p.id,
    p.name,
    p.brand,
    p.retail_price
FROM ecomm.products p
ORDER BY (p.embedding <=> :'test_vec'::public.vector(768))
LIMIT 10;

以下是"内联"嵌入向量搜索的执行计划:

 Limit (cost=1213.90..1240.56 rows=10 width=86) (actual time=1.112..1.200 rows=10.00 loops=1)
   Buffers: shared hit=792
   -> Index Scan using idx_products_vector on products p (cost=1213.90..78826.40 rows=29120 width=86) (actual time=1.110..1.197 rows=10.00 loops=1)
         Order By: (embedding <=> '[-0.01255631,...,-0.008388235]'::vector(768))
         Index Searches: 1
         Buffers: shared hit=792
 Planning:
   Buffers: shared hit=1
 Planning Time: 0.107 ms
 Execution Time: 1.224 ms

从执行计划可以看出,查询使用 HNSW 索引来搜索嵌入并按相似度排序。返回结果的总响应时间约为 2.1 ms。响应时间大于执行计划中的纯执行时间,因为它还增加了规划、网络时间以及客户端软件显示结果的开销。

然后我将嵌入放到 ecomm.product_embeddings 表中,并进行了相同的搜索:

-- 嵌入存储在 ecomm.product_embeddings 中
EXPLAIN (ANALYZE, BUFFERS)
SELECT 
    p.id,
    p.name,
    p.brand,
    p.retail_price
FROM ecomm.product_embeddings pe
JOIN ecomm.products p ON p.id = pe.product_id
ORDER BY (pe.embedding <=> :'test_vec'::public.vector(768))
LIMIT 10;

以下是使用主键连接 products 和 product_embeddings 表的执行计划:

 Limit (cost=219.82..250.39 rows=10 width=86) (actual time=1.188..1.351 rows=10.00 loops=1)
   Buffers: shared hit=852
   -> Nested Loop (cost=219.82..89223.27 rows=29120 width=86) (actual time=1.187..1.348 rows=10.00 loops=1)
         Buffers: shared hit=852
         -> Index Scan using idx_product_embeddings_vector on product_embeddings pe (cost=219.53..59613.60 rows=29120 width=26) (actual time=1.126..1.141 rows=10.00 loops=1)
               Order By: (embedding <=> '[-0.01255631,...,-0.008388235]'::vector(768))
               Index Searches: 1
               Buffers: shared hit=752
         -> Index Scan using products_pkey on products p (cost=0.29..1.01 rows=1 width=78) (actual time=0.007..0.007 rows=1.00 loops=10)
               Index Cond: (id = pe.product_id)
               Index Searches: 10
               Buffers: shared hit=50
 Planning:
   Buffers: shared hit=21
 Planning Time: 0.274 ms
 Execution Time: 1.381 ms

带过滤条件的语义搜索

实际生产中很少见到只用一个搜索条件的查询。大多数情况下还会涉及其他参数。我通过添加 category 和 retail_price 列的过滤条件进行了测试:

-- With two filters and embeddings in ecomm.products
EXPLAIN (ANALYZE, BUFFERS)
SELECT 
    p.id,
    p.name,
    p.brand,
    p.retail_price,
    p.category
FROM ecomm.products p
WHERE p.category = 'Outerwear & Coats'
  AND p.retail_price BETWEEN 30 AND 150
ORDER BY (p.embedding <=> :'test_vec'::public.vector(768))
LIMIT 10;

执行计划发生了变化,结果中加入了过滤条件:

 Limit (cost=1213.90..1760.50 rows=10 width=97) (actual time=6.353..6.522 rows=10.00 loops=1)
   Buffers: shared hit=817
   -> Index Scan using idx_products_vector on products p (cost=1213.90..78829.95 rows=1420 width=97) (actual time=6.342..6.509 rows=10.00 loops=1)
         Order By: (embedding <=> '[-0.01255631,...,-0.008388235]'::vector(768))
         Filter: ((retail_price >= '30'::numeric) AND (retail_price <= '150'::numeric) AND ((category)::text = 'Outerwear & Coats'::text))
         Rows Removed by Filter: 7
         Index Searches: 1
         Buffers: shared hit=799
 Planning:
   Buffers: shared hit=1
 Planning Time: 0.157 ms
 Execution Time: 1.308 ms

响应时间始终在 2.2 ms 左右——与不加过滤条件时几乎相同。

然后我用分离表的嵌入向量执行了相同的查询:

EXPLAIN (ANALYZE, BUFFERS)
SELECT 
    p.id,
    p.name,
    p.brand,
    p.retail_price,
    p.category
FROM ecomm.product_embeddings pe
JOIN ecomm.products p ON p.id = pe.product_id
WHERE p.category = 'Outerwear & Coats'
  AND p.retail_price BETWEEN 30 AND 150
ORDER BY (pe.embedding <=> :'test_vec'::public.vector(768))
LIMIT 10;

执行计划中多了一个过滤步骤:

 Limit (cost=219.82..1346.89 rows=10 width=97) (actual time=1.167..1.370 rows=10.00 loops=1)
   Buffers: shared hit=894
   -> Nested Loop (cost=219.82..89370.85 rows=791 width=97) (actual time=1.166..1.367 rows=10.00 loops=1)
         Buffers: shared hit=894
         -> Index Scan using idx_product_embeddings_vector on product_embeddings pe (cost=219.53..59613.60 rows=29120 width=26) (actual time=1.110..1.153 rows=17.00 loops=1)
               Order By: (embedding <=> '[-0.01255631,...,-0.008388235]'::vector(768))
               Index Searches: 1
               Buffers: shared hit=759
         -> Index Scan using products_pkey on products p (cost=0.29..1.02 rows=1 width=89) (actual time=0.006..0.006 rows=0.59 loops=17)
               Index Cond: (id = pe.product_id)
               Filter: ((retail_price >= '30'::numeric) AND (retail_price <= '150'::numeric) AND ((category)::text = 'Outerwear & Coats'::text))
               Rows Removed by Filter: 0
               Index Searches: 17
               Buffers: shared hit=85
 Planning:
   Buffers: shared hit=21
 Planning Time: 0.347 ms
 Execution Time: 1.406 ms

该查询的响应时间始终在 2.3 到 2.4 ms 之间。同样比嵌入向量在同一张表的查询慢,但差异并不显著。

如果你的数据模式将业务信息的不同部分和属性存储在不同的表中,你需要通过多表连接来获取所需的数据。在测试中我使用了与 ecomm.product_reviews 的连接以及产品评分的聚合:

EXPLAIN (ANALYZE, BUFFERS)
SELECT 
    top.id,
    top.name,
    top.brand,
    top.retail_price,
    top.category,
    top.dist,
    COUNT(pr.id) AS review_count,
    COALESCE(ROUND(AVG(pr.rating), 2), 0) AS avg_rating
FROM (
    SELECT 
        p.id,
        p.name,
        p.brand,
        p.retail_price,
        p.category,
        (p.embedding <=> :'test_vec'::public.vector(768)) AS dist
    FROM ecomm.products p
    WHERE p.category = 'Outerwear & Coats'
      AND p.retail_price BETWEEN 30 AND 150
    ORDER BY (p.embedding <=> :'test_vec'::public.vector(768))
    LIMIT 10
) top
LEFT JOIN ecomm.product_reviews pr ON pr.product_id = top.id
GROUP BY top.id, top.name, top.brand, top.retail_price, top.category, top.dist
ORDER BY top.dist;

该查询始终在 2.6–2.7 ms 内返回结果。然后我修改了查询,使用存储在 ecomm.product_embeddings 表中的嵌入向量:

EXPLAIN (ANALYZE, BUFFERS)
SELECT 
    top.id,
    top.name,
    top.brand,
    top.retail_price,
    top.category,
    top.dist,
    COUNT(pr.id) AS review_count,
    COALESCE(ROUND(AVG(pr.rating), 2), 0) AS avg_rating
FROM (
    SELECT 
        p.id,
        p.name,
        p.brand,
        p.retail_price,
        p.category,
        (pe.embedding <=> :'test_vec'::public.vector(768)) AS dist
    FROM ecomm.product_embeddings pe
    JOIN ecomm.products p ON p.id = pe.product_id
    WHERE p.category = 'Outerwear & Coats'
      AND p.retail_price BETWEEN 30 AND 150
    ORDER BY (pe.embedding <=> :'test_vec'::public.vector(768))
    LIMIT 10
) top
LEFT JOIN ecomm.product_reviews pr ON pr.product_id = top.id
GROUP BY top.id, top.name, top.brand, top.retail_price, top.category, top.dist
ORDER BY top.dist;

差异同样是约 0.2 ms。第二个查询在 2.8–2.9 ms 内返回结果。我略去了这种情况下各自的执行计划——它们更长,在短博客文章中更难阅读,但这里有一张对比图:

文章配图

对于第二个查询,我额外做了一个 product_embeddings 和 products 表之间的连接(这是预期的),但除此之外它展示了相似的执行路径。

以下是测试结果的汇总表:

+--------------------------+-----------------------+-----------------------+---------------------+
| Test Scenario | Inline Embeddings | Dedicated Table | Overhead / Delta |
+--------------------------+-----------------------+-----------------------+---------------------+
| 1. Pure Semantic Search | Execution: ~1.22 ms | Execution: ~1.38 ms | +0.16 ms (Exec) |
| (LIMIT 10) | Total: ~2.10 ms | Total: ~2.20 ms | +0.10 ms (Total) |
+--------------------------+-----------------------+-----------------------+---------------------+
| 2. Filtered Search | Execution: ~1.31 ms | Execution: ~1.41 ms | +0.10 ms (Exec) |
| (Category + Price) | Total: ~2.20 ms | Total: 2.3 - 2.4 ms | +0.10 - 0.20 ms |
+--------------------------+-----------------------+-----------------------+---------------------+
| 3. Multi-Table Join | | | |
| (Reviews Aggregation) | Total: 2.6 - 2.7 ms | Total: 2.8 - 2.9 ms | +0.20 ms (Total) |
+--------------------------+-----------------------+-----------------------+---------------------+

对比嵌入向量与源数据同表存储和独立专用表存储的查询性能,可以看出性能差异相对较小。在大多数情况下响应时间不超过 0.2 ms。当然,这可能因表结构、数据和具体查询而异。

我建议测试并考虑使用独立嵌入表的布局。这可能在未来的模型维护和模型评估中帮助你——你只需添加另一张表来存放未来的模型,然后使用新生成的嵌入向量运行查询。良好的 schema 设计下,性能开销可以降至最低。根据我的经验,其他因素(如模型响应时间)引入的响应时间变化远比基于主键的嵌入表连接要大得多。

Original source

本文由 AI 翻译整理自 Google AI · DEV,原文版权归原作者所有。

阅读英文原文
上一篇
Fastino开源340M决策模型GLiNER2.5-Decide:CPU可运行
下一篇
多 AI Agent 协作:实测最难攻克的工程难题