首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >PG未来可能会支持图索引

PG未来可能会支持图索引

作者头像
用户4035096
发布2026-07-10 12:15:49
发布2026-07-10 12:15:49
580
举报

今天想聊一个 PostgreSQL 生态里很有意思的东西 —— pgGraph。简单说,它解决的是一个很具体的问题:怎么在不搬数据的情况下,让关系数据库也能高效地做图查询。

这个问题其实很普遍。很多业务系统最初设计的时候,根本没想过要用图数据库。用户、订单、设备、账号、公司、资金流,全在 PostgreSQL 的普通表里,关系藏在主键、外键、关联表里面。但上线以后,问题就来了——

想查一个客户 2 跳内关联了哪些账号、公司和设备;想解释两个主体为什么有关联、路径是什么;或者在 RAG 场景里,不只要找相似文本,还要沿实体关系补齐证据。这时候用递归 CTE 能做,但复杂 schema 下经常变成"每个问题写一段递归 SQL"。

换专用图数据库也能做,但代价是新的存储、新的运维、新的权限体系、新的同步机制,还要学新的查询语言。pgGraph 的取舍比较务实:PostgreSQL 表还是事实来源,扩展只是根据注册元数据构建一个可重建的 CSR 图索引,然后通过 SQL 函数做搜索、遍历和路径查询。

那它具体是怎么工作的呢?

底层用的是 CSR——Compressed Sparse Row,就是稀疏矩阵转置里面常见的那种结构。它把"找邻居"这件事,从反复的 join 和递归扩展,变成了一次数组切片访问。

举个例子。node i 的出边目标存在 targets 数组里,边界由 edge_offsets 数组定义。用公式说就是:

node i 的出边目标等于 targets 从 edge_offsets[i] 到 edge_offsets[i+1] 的切片;对应的边类型也在同一个下标范围的 type_ids 数组里;权重在 weights 数组的同样位置。

查询一个节点的邻居,不需要哈希表,不需要二分,不需要 join,只要两次数组读取得到切片边界,然后顺序扫描。Ligra 论文强调共享内存图处理要把算法组织成对顶点集合和边集合的高效遍历,pgGraph 虽然不是 Ligra 那种通用批量图计算框架,但采用了同样的思路:图算法的内层循环要尽量贴近连续内存访问,减少对象跳转和随机查找。

不过光有 CSR 还不够。pgGraph 还有几个关键设计值得注意。

第一个是 ResolutionIndex。用户调用 SQL 的时候不会知道 node_idx,只会给出表名和主键,比如 seed_table 等于 public.users,seed_id 等于 u1。ResolutionIndex 做的是把(table_oid, pk)这个业务坐标解析成内部节点编号。构建的时候累计条目,finalize 后转成排序的字节数组,查询时通过二分查找解析,加载 .pggraph 之后也可以直接在 mmap 字节段上做二分。

第二个是 FilterIndex。搜索和遍历过滤是两件不同的事。graph.search() 面向源表列,通过 PostgreSQL SQL 谓词找候选源行;而 graph.traverse 里的 filter 面向遍历过程中的候选节点,注册过的 filter column 可以预编码到 FilterIndex 里,支持 numeric、boolean、text、date、timestamptz、uuid 等类型。它会根据列的稀疏程度选择 dense 或 sparse 存储。

这里有个细节:FilterIndex 按"填充值低于节点数 15%"来判断是否稀疏。遍历热循环遇到候选节点时,可以直接在内部索引中判断 age >= 40、status in (...) 之类的谓词,不需要每个邻居都回源表。

第三个是多租户支持。NodeStore 里有 tenant_membership 位图,搜索的时候可以顺着租户边界剪掉不该看到的节点。这个在多租户 SaaS 系统里很有用。

说完了索引,再说说持久化。.pggraph 文件保存构建后的固定数组和索引段,结构挺讲究的——文件头有 magic、version、flags、node_count、edge_count、11 个 section offset 和 CRC。写入用临时文件、回填 header、fsync、rename 这套流程来保证原子性。加载时验证 magic、版本、CRC、section 边界、对齐、CSR offset 单调性、target 范围等,然后才把不可变数组 mmap 成只读视图。

这里有个常见的误解:mmap 共享的是 OS 页缓存里的固定数组页,不是 PostgreSQL shared buffers,也不是跨后端共享 Rust 堆。反向 CSR、FilterIndex、edge type registry、同步 overlay 这些东西,仍然是每个后端本地的结构。

还有一个点要提醒:基础 CSR 是不可变结构。如果拓扑会变化,pgGraph 靠 trigger sync 先形成日志和覆盖层,然后通过 vacuum 或 maintenance 重建,把覆盖层合回基础 CSR。构建阶段用临时 spool 和排序流式生成 CSR,避免长期持有全部原始边,这个思路借鉴了稀疏矩阵 CSR 构建的思想。

效果怎么样呢?不要把"CSR 很快"理解成任何图查询都会快。它快在特定条件下——同一拓扑被反复查询,构建成本可以摊销;查询是有界遍历、路径解释或局部邻域扩展;起点可以快速解析,边类型和过滤列能在图内剪枝;结果需要返回源表坐标,必要时再 hydrate 少量行。

收益主要来自:邻接访问从 join 变成连续数组扫描;访问状态用 bitmap 或 RoaringBitmap 表达;边标签压缩成 u8,过滤列预编码;mmap 让固定图数组可通过 OS 页缓存被后端共享。

代价也明确:构建需要扫描注册表和边源表;基础 CSR 不可变,拓扑变更需要 overlay 或重建;vacuum/maintenance 当前是完整重建,有新旧 engine 共存的双内存窗口;搜索 graph.search() 仍走源表 SQL 谓词,不是全文索引或向量索引替代品;Edge type ID 用 u8,用户边标签上限是 254 个。

适合的场景包括:风控关系扩展、客服和 CRM 关系解释、告警根因辅助、知识库 RAG 关系扩展、以及现有 PostgreSQL 应用补图能力。

不适合的场景:高频拓扑写入并要求所有连接立即看到完整一致图、无深度限制的社交网络全图探索、跨数据库和跨集群的分布式图 join、PageRank 和连通分量全量批处理放在 OLTP 主库上跑、需要 Cypher 和 Gremlin 完整语义的应用、以及边标签高基数到接近业务 ID 级别的模型。

常见的坑也有几个。把派生图当事实来源是最容易犯的错——.pggraph 可删、可重建、可失效,源表才是事实来源,备份恢复、权限和数据审计都要围绕 PostgreSQL 表做。

第二个坑是不设查询断路器。图查询最容易出问题的不是单条边慢,而是高扇出扩张。必须设置深度、节点数、frontier 和分页参数。

第三个是把搜索列和过滤列混淆。columns 供 graph.search() 查源表;graph.add_filter_column() 才会进入遍历 FilterIndex。两者是不同的路径。

第四个是在主库高峰跑全图维护。maintenance 和 vacuum 是重建路径,大图应该放到只读副本、低峰窗口或专门调度里。

总结一下,pgGraph 的定位不是"替代所有图数据库",而是在 PostgreSQL 原生运维和专用图热路径之间找一个折中点。关系表保真度越重要、查询越偏有界邻域,pgGraph 越有价值;如果图本身就是事实来源,或者需要分布式全图算法,应该考虑其他系统。

留了几个思考题,比如:如果 PostgreSQL 未来 SQL/PGQ 可以表达图模式,哪些适合直接走关系优化器,哪些适合下推到 CSR 运行时?CSR 对读取友好但对原地更新不友好,若要支持高频在线拓扑变更,应该用 delta CSR 还是 LSM-like 边段?在 RAG 系统中,图关系扩展应该发生在向量召回前、召回后,还是 rerank 阶段?

这些问题都值得思考。如果你对图数据库或 pgGraph 有兴趣,可以去看看项目源码和论文。今天就聊到这儿,我是德哥,我们下次再见。

本文参与 腾讯云自媒体同步曝光计划,分享自微信公众号。
原始发表:2026-06-01,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 digoal德哥 微信公众号,前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档