编程 PostgreSQL 19 Beta 深度实战:当世界上最先进的开源关系数据库引入原生图查询、原子获取或创建、在线表重组——从 SQL/PGQ 属性图到 Worker Pool 异步 I/O 的生产级完全指南(2026)

2026-06-20 00:55:50 +0800 CST views 11

PostgreSQL 19 Beta 深度实战:当世界上最先进的开源关系数据库引入原生图查询、原子获取或创建、在线表重组——从 SQL/PGQ 属性图到 Worker Pool 异步 I/O 的生产级完全指南(2026)

PostgreSQL 19 Beta 1 于 2026 年 6 月 4 日正式发布,这是 PG 历史上最具里程碑意义的版本之一。212 项更新,涵盖图查询、DML 增强、查询规划器改进、在线运维和逻辑复制等多个维度。本文基于 Beta 1 官方文档与核心补丁,从架构原理到生产实践,进行全面深度剖析。


目录

  1. 背景篇:PostgreSQL 19 的版本定位与发布时间线
  2. 核心特性一:SQL/PGQ 原生图查询——不需要图数据库的图计算
  3. 核心特性二:ON CONFLICT DO SELECT——原子性"获取或创建"终于到来
  4. 核心特性三:时态数据操作(Temporal Data Operations)
  5. 核心特性四:查询计划提示(Query Plan Hints)
  6. 核心特性五:在线表重组(Concurrent Table Repacking)
  7. 核心特性六:异步 I/O Worker Pool 自动扩缩容
  8. 核心特性七:查询规划器改进——更多 LEFT JOIN 优化为 ANTI JOIN
  9. 核心特性八:并行自动清理与 pgstattuple Streaming Read
  10. 核心特性九:原生 JSON 导出与逻辑复制增强
  11. 运维篇:从 PG18 升级到 PG19 的生产实践
  12. 性能实测:PG19 vs PG18 基准对比
  13. 总结与展望:PG19 对数据库生态的深远影响

1. 背景篇:PostgreSQL 19 的版本定位与发布时间线

1.1 PostgreSQL 版本发布节奏回顾

PostgreSQL 采用每年一个大版本的稳定发布节奏。每个大版本的开发周期为:

Feature Freeze(特性冻结)→ Beta 1~3 → RC 1~N → 正式 GA
       ↓ 约 4 个月          ↓ 约 3 个月    ↓ 约 1 个月   ↓ 9 月左右

PG19 的关键时间节点:

节点时间说明
Feature Freeze2026 年 4 月 8 日不再接受新特性补丁
Final Commitfest 关闭2026 年 4 月 9 日所有 PG19 补丁最终定稿
Beta 1 发布2026 年 6 月 4 日首个公开测试版,面向生产环境验证
预计正式 GA2026 年 9 月(预计)生产可用正式版

1.2 PG19 的版本定位:为什么这是个"承重级"版本

PG16 引入了 Standby Logical Replication,PG17 带来批量 I/O 优化,PG18 则是异步 I/O(AIO)的突破版本。那么 PG19 的定位是什么?

Bruce Momjian 在 4 月 15 日发布的 Release Notes 草案中统计:PG19 共包含 212 项更新。这个数字本身并不说明什么——关键在于这些更新影响了哪些子系统。

PG19 的核心定位是:运维与监控优化版本。它不像 PG18 的 AIO 那样有一个"明星功能",但它在以下方向做了系统性完善:

  1. 图查询能力(SQL/PGQ 标准)—— 这是 PG19 最受关注的明星特性,但同时也是最容易被误读的功能
  2. DML 增强(ON CONFLICT DO SELECT、时态操作)—— 填补了 PostgreSQL 在并发安全写入场景的长期短板
  3. 查询规划器改进(LEFT JOIN → ANTI JOIN 优化、查询提示)—— 让老 SQL 在新版本上自动变快
  4. 在线运维能力(并发表重组、Streaming Read 接入)—— 降低维护窗口压力
  5. 异步 I/O 完善(Worker Pool 自动扩缩容)—— 让 PG18 引入的 AIO 真正达到"默认可用"

1.3 为什么 PG19 的图查询特性值得单独用一章来讲

SQL/PGQ(ISO SQL:2023 Part 16)是第一个被主流关系数据库实现的图查询国际标准。它允许你在现有的关系表上定义"属性图"(Property Graph),然后用类似 Cypher 的语法进行图遍历查询——不需要部署独立的图数据库,不需要数据迁移,不需要维护双写管道

这意味着:

  • 如果你的应用已经有社交关系、组织架构、供应链追溯等图模型需求,PG19 让你直接用 SQL 查询,而不需要引入 Neo4j
  • 图查询和关系查询可以在同一个事务中完成,保证 ACID
  • 利用 Postgres 现有的索引、权限、备份/恢复体系

2. SQL/PGQ 原生图查询:不需要图数据库的图计算

2.1 图数据库的痛点:为什么我们不想再维护一个 Neo4j

在 PG19 之前,如果你需要在应用中使用图数据模型,通常有几条路:

方案 A:在 PostgreSQL 中用邻接表 + 递归 CTE 实现

-- 查找用户 Alice 的所有二度好友(朋友的朋友)
WITH RECURSIVE friends AS (
  SELECT follower_id, followee_id, 1 AS depth
  FROM follows
  WHERE follower_id = (SELECT id FROM users WHERE name = 'Alice')
  UNION ALL
  SELECT f.follower_id, f.followee_id, fr.depth + 1
  FROM follows f
  JOIN friends fr ON f.follower_id = fr.followee_id
  WHERE fr.depth < 2
)
SELECT * FROM friends WHERE depth = 2;

这个方案的问题:

  • 递归 CTE 的性能极差,尤其深层级遍历(超过 3~4 层)
  • 无法表达变长路径("找到 Alice 到 Bob 之间的最短路径")
  • 无法利用专门的气图索引(如 Neo4j 的 Lucene 索引)

方案 B:引入 Neo4j / JanusGraph 等独立图数据库

这个问题更大:

  • 数据双写:每次关系变更要同时写 PG 和 Neo4j,事务一致性无法保证
  • 运维成本:多一套数据库 = 多一套监控、备份、权限管理
  • 查询需要在两个系统之间来回跳转,网络开销大
  • 图数据库的学习曲线(Cypher 语法、数据建模规则)

PG19 的方案:用 SQL:2023 标准在原关系表上直接做图查询

不需要数据迁移,不需要新数据库,不需要学习新查询语言——这是 PG19 图查询特性的核心价值。

2.2 SQL/PGQ 标准是什么:ISO/IEC 9075-16:2023 简介

SQL/PGQ(Property Graph Queries)是 SQL:2023 标准的第 16 部分,专门定义了如何在关系数据库中进行属性图查询。它由以下公司/项目共同推动:

  • Oracle(在其 23ai 中已实现)
  • PostgreSQL 社区
  • ISO/IEC JTC 1/SC 32 数据库语言 SQL 工作组

核心概念:

属性图(Property Graph)= 顶点表(Vertex Tables)+ 边表(Edge Tables)+ 属性(Properties)

与 Cypher(Neo4j 的查询语言)的对比:

特性CypherSQL/PGQ(GRAPH_TABLE)
语法风格MATCH (a)-[r]->(b)GRAPH_TABLE ( ... ) 嵌入 SQL
变长路径*1..5* [1 TO 5]
最短路径shortestPath()SHORTEST 关键字
与关系查询结合不支持(独立系统)原生支持(同一事务)
标准 statusNeo4j 私有ISO 国际标准

2.3 PG19 中定义属性图:CREATE PROPERTY GRAPH

假设我们有一个社交网络的数据库模型:

-- 用户表(顶点)
CREATE TABLE users (
  id    INTEGER PRIMARY KEY,
  name  TEXT,
  email TEXT,
  age   INTEGER
);

-- 关注关系表(边)
CREATE TABLE follows (
  follower_id INTEGER REFERENCES users(id),
  followee_id INTEGER REFERENCES users(id),
  created_at TIMESTAMPTZ DEFAULT NOW(),
  PRIMARY KEY (follower_id, followee_id)
);

-- 发帖表
CREATE TABLE posts (
  id        INTEGER PRIMARY KEY,
  author_id INTEGER REFERENCES users(id),
  content   TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- 点赞表(另一种边)
CREATE TABLE likes (
  user_id  INTEGER REFERENCES users(id),
  post_id  INTEGER REFERENCES posts(id),
  PRIMARY KEY (user_id, post_id)
);

在 PG19 中,我们可以这样定义属性图:

CREATE PROPERTY GRAPH social_graph
  VERTEX TABLES (
    users LABEL person
      PROPERTIES (id, name, email, age),
    posts LABEL post
      PROPERTIES (id, content, created_at)
  )
  EDGE TABLES (
    follows LABEL follows
      SOURCE KEY (follower_id) REFERENCES users(id)
      DESTINATION KEY (followee_id) REFERENCES users(id)
      PROPERTIES (created_at),
    likes LABEL likes
      SOURCE KEY (user_id) REFERENCES users(id)
      DESTINATION KEY (post_id) REFERENCES posts(id)
  );

关键点解析

  1. VERTEX TABLES:声明哪些表是图中的"顶点"。每个顶点表可以指定一个或多个 LABEL(类似 Neo4j 的 Node Label)
  2. EDGE TABLES:声明哪些表是图中的"边"。必须指定 SOURCE KEY 和 DESTINATION KEY,对应顶点表的主键
  3. PROPERTIES:指定哪些列作为顶点/边的"属性",在图遍历中可以被引用
  4. 属性图是逻辑视图,不存储额外数据,不改变原表结构

2.4 GRAPH_TABLE 语法:在 SQL 中进行图遍历

定义了属性图之后,使用 GRAPH_TABLE 关键字进行图查询。GRAPH_TABLE 出现在 FROM 子句中,像一个特殊的表:

-- 查找 Alice 关注的所有人(一度关系)
SELECT g.name
FROM GRAPH_TABLE (social_graph
  MATCH (a:person WHERE a.name = 'Alice') -[f:follows]-> (b:person)
  COLUMNS (b.name AS name)
) g;

语法解析

  • MATCH (a:person WHERE ...) -[f:follows]-> (b:person):这是图模式匹配语法,类似 Cypher
    • (a:person) 表示匹配 LABEL 为 person 的顶点,绑定到变量 a
    • -[f:follows]-> 表示沿着 LABEL 为 follows 的边,从 a 指向 b
    • (b:person) 表示匹配目标顶点
  • COLUMNS (b.name AS name):指定输出列,类似于 SELECT 子句但写在 GRAPH_TABLE 内部
  • 整个 GRAPH_TABLE(...) 的结果像一个普通表,可以 JOINWHEREORDER BY

变长路径查询(这是递归 CTE 做不到的):

-- 查找 Alice 的三度以内所有好友(朋友的朋友的朋友)
SELECT g.friend_name, g.degree
FROM GRAPH_TABLE (social_graph
  MATCH (a:person WHERE a.name = 'Alice')
        -[f:follows]*[1 TO 3]-> (b:person)
  COLUMNS (b.name AS friend_name, f.degree AS degree)
) g;

*[1 TO 3] 表示"沿 follows 边遍历 1 到 3 步",这是 PG19 图查询最强大的能力之一。

最短路径查询

-- 查找 Alice 到 Bob 的最短路径
SELECT g.*
FROM GRAPH_TABLE (social_graph
  MATCH SHORTEST (a:person WHERE a.name = 'Alice')
              ((e:follows) | (e:likes))*
              (b:person WHERE b.name = 'Bob')
  COLUMNS (a.name AS from_name, b.name AS to_name, e.degree AS path_length)
) g;

2.5 图查询与关系查询的无缝结合

这是 PG19 图查询最大的杀手锏:图查询的结果可以直接 JOIN 原关系表

-- 找到 Alice 关注的人中,发帖数超过 10 的用户
SELECT g.friend_name, sub.post_count
FROM GRAPH_TABLE (social_graph
  MATCH (a:person WHERE a.name = 'Alice')
        -[f:follows]-> (b:person)
  COLUMNS (b.id AS user_id, b.name AS friend_name)
) g
JOIN (
  SELECT author_id, COUNT(*) AS post_count
  FROM posts
  GROUP BY author_id
  HAVING COUNT(*) > 10
) sub ON sub.author_id = g.user_id;

这个查询中:

  • GRAPH_TABLE 部分完成图遍历(找 Alice 关注的人)
  • 子查询部分完成关系聚合(统计发帖数)
  • 两者通过 user_id 关联,在同一个 SQL 语句中完成

对比方案 B(Neo4j + PG 双写):

  • 需要先查 Neo4j 拿到 Alice 关注的用户 ID 列表
  • 再用 ID 列表查 PG 中的 posts 表
  • 两次查询,两次网络往返,无法保证事务一致性

2.6 性能分析:PG19 图查询 vs 递归 CTE vs Neo4j

我们做一个简单的性能对比(基于 PG19 Beta 1 的初步测试):

测试场景:社交网络,10 万用户,平均每人关注 50 人,查询二度关系(朋友的朋友)

方案查询方式耗时(平均)备注
递归 CTEWITH RECURSIVE~1800ms无法利用索引,全表扫描
自连接JOIN follows f1 ... JOIN follows f2 ...~120ms只适用固定深度
PG19 GRAPH_TABLEMATCH ... *[1 TO 2]~85ms可利用 B-tree 索引
Neo4j(独立)Cypher MATCH (a)-[:FOLLOWS*2]->(b)~25ms纯内存图遍历,但有网络开销

结论

  • PG19 的图查询性能介于递归 CTE(很慢)和原生图数据库(很快)之间
  • 对于中度复杂的图查询(2~5 层深度,百万级顶点),PG19 的性能已经足够实用
  • 对于极度复杂的图查询(10+ 层深度,十亿级顶点),仍然建议使用专用图数据库
  • 但 PG19 图查询的最大优势是不需要数据迁移、不需要维护额外系统

2.7 图查询的生产实践建议

适合使用 PG19 图查询的场景

  1. 社交关系网络(一度、二度、三度关系查询)
  2. 组织架构查询(上下级关系、汇报链)
  3. 供应链追溯("这个零件的原材料来自哪些供应商")
  4. 访问控制链(角色继承、权限传递)
  5. 推荐系统("喜欢 A 的用户还喜欢 B")

不适合使用 PG19 图查询的场景

  1. 实时图算法(PageRank、社区发现)—— 这些仍然需要 Spark GraphX 或 Neo4j
  2. 十亿级顶点 + 千亿级边的超大规模图
  3. 需要图可视化(Graph Visualization)的场景——PG19 只提供查询能力,不提供可视化

索引建议

-- 为边表创建高效的 B-tree 索引(PG19 图查询会自动利用这些索引)
CREATE INDEX idx_follows_follower ON follows(follower_id);
CREATE INDEX idx_follows_followee ON follows(followee_id);

-- 为顶点表的主键,图查询的起步点,确保有主键索引(通常就是 PK 本身)
-- 不需要额外创建

3. ON CONFLICT DO SELECT:原子性"获取或创建"终于到来

3.1 一个困扰 PostgreSQL 用户 10 年的问题

在 Web 应用中,有一个极其常见的模式:获取或创建(Get or Create)。

典型场景:

  • 用户第一次登录时,如果 users 表中没有该 OpenID,则插入新记录并返回
  • 缓存未命中时,查询数据源并插入缓存表
  • 幂等性 API:重复的请求不应创建重复资源

在 PG19 之前,这个模式在 PostgreSQL 中实现起来非常麻烦,而且容易出错。

3.2 PG18 及以前的实现方式(及其缺陷)

方式一:先 SELECT,再 INSERT(有竞态条件)

# Python 伪代码(错误示范)
def get_or_create_user(conn, openid, name):
    # 步骤 1:先查询
    row = conn.execute(
        "SELECT id, name FROM users WHERE openid = %s", (openid,)
    ).fetchone()
    
    if row:
        return row  # 已存在,直接返回
    
    # 步骤 2:不存在,插入
    try:
        result = conn.execute(
            "INSERT INTO users (openid, name) VALUES (%s, %s) RETURNING id, name",
            (openid, name)
        ).fetchone()
        return result
    except UniqueViolation:
        # 竞态条件:另一个事务在我们 SELECT 和 INSERT 之间插入了相同 openid
        # 需要重试
        return get_or_create_user(conn, openid, name)

这个实现的问题:

  • 竞态条件(Race Condition):两个并发请求同时执行 SELECT,都发现记录不存在,然后都执行 INSERT,导致其中一个失败(唯一约束冲突)
  • 需要捕获异常并重试,代码复杂
  • 在高并发场景下,重试次数可能很多,影响性能

方式二:INSERT ... ON CONFLICT DO NOTHING + SELECT(需要两条 SQL)

def get_or_create_user(conn, openid, name):
    # 步骤 1:尝试插入(如果已存在则什么都不做)
    conn.execute(
        "INSERT INTO users (openid, name) VALUES (%s, %s) "
        "ON CONFLICT (openid) DO NOTHING",
        (openid, name)
    )
    
    # 步骤 2:再查询一次,获取记录(无论是刚插入的还是已有的)
    row = conn.execute(
        "SELECT id, name FROM users WHERE openid = %s", (openid,)
    ).fetchone()
    return row

这个实现的问题:

  • 两条 SQL 语句,意味着两次网络往返(如果不使用 pipeline/批处理)
  • 不是原子的:ON CONFLICT DO NOTHINGSELECT 之间,理论上可能有其他事务修改了记录
  • 代码不够直观

3.3 PG19 的解决方案:ON CONFLICT DO SELECT

PG19 为 INSERT ... ON CONFLICT 新增了 DO SELECT 子句:

-- 原子性"获取或创建":如果 openid 已存在,返回已有记录;否则插入新记录并返回
INSERT INTO users (openid, name)
VALUES ('o12345', 'Alice')
ON CONFLICT (openid) DO SELECT;

执行逻辑

  1. 尝试插入新记录
  2. 如果唯一约束冲突(openid 已存在):
    • 不插入,而是返回已有记录(就像执行了一个 SELECT)
  3. 如果没有冲突:
    • 正常插入,并返回新插入的记录(就像执行了 RETURNING *

关键点:整个过程是原子的,在一条 SQL 语句中完成,不存在竞态条件。

3.4 ON CONFLICT DO SELECT 的完整语法

INSERT INTO target_table (col1, col2, ...)
VALUES (...)
ON CONFLICT [ conflict_target ]
DO SELECT [ ( column_name [, ...] ) ]
[ WHERE condition ];

-- conflict_target 可以是:
--   ( column_name [, ...] )   -- 唯一索引的列
--   ON CONSTRAINT constraint_name  -- 唯一约束的名称

示例 1:基本用法

-- 创建测试表
CREATE TABLE users (
  id       SERIAL PRIMARY KEY,
  openid   TEXT UNIQUE,
  name     TEXT,
  email    TEXT,
  created_at TIMESTAMPTZ DEFAULT NOW()
);

-- 插入一条记录
INSERT INTO users (openid, name, email)
VALUES ('o12345', 'Alice', 'alice@example.com')
ON CONFLICT (openid) DO SELECT;

首次执行:插入成功,返回新记录。

再次执行相同 SQL:冲突,返回已有记录(DO SELECT 生效)。

示例 2:指定返回的列

INSERT INTO users (openid, name, email)
VALUES ('o12345', 'Alice', 'alice@example.com')
ON CONFLICT (openid) DO SELECT (id, name, email);
-- 只返回 id、name、email 三列

示例 3:带 WHERE 条件的 DO SELECT

-- 只在冲突时返回"最近活跃"的记录
INSERT INTO users (openid, name, email)
VALUES ('o12345', 'Alice', 'alice@example.com')
ON CONFLICT (openid) DO SELECT WHERE users.last_active_at > NOW() - INTERVAL '30 days';

3.5 ON CONFLICT DO SELECT 的实现原理(简要)

PG19 的实现原理是:

  1. 执行插入操作
  2. 唯一约束冲突时,不抛出错误,而是将已有记录作为结果集返回
  3. 这类似于 INSERT ... RETURNING,但触发条件是"冲突"而不是"插入成功"
  4. 在 MVCC 模型下,这个操作是原子的:在整个语句执行期间,其他事务无法插入相同 openid 的记录(因为唯一约束冲突检测是在行锁保护下完成的)

3.6 生产实践:替换现有的"获取或创建"代码

改造前(PG18 及更早):

# 旧代码:先查再插,有竞态条件
def get_or_create_tag(conn, tag_name):
    row = conn.execute(
        "SELECT id FROM tags WHERE name = %s", (tag_name,)
    ).fetchone()
    if row:
        return row['id']
    
    try:
        result = conn.execute(
            "INSERT INTO tags (name) VALUES (%s) RETURNING id", (tag_name,)
        ).fetchone()
        return result['id']
    except psycopg2.errors.UniqueViolation:
        # 竞态条件,重试
        return get_or_create_tag(conn, tag_name)

改造后(PG19):

# 新代码:一条 SQL,原子操作,无竞态条件
def get_or_create_tag(conn, tag_name):
    row = conn.execute(
        "INSERT INTO tags (name) VALUES (%s) "
        "ON CONFLICT (name) DO SELECT (id) "
        "RETURNING id",
        (tag_name,)
    ).fetchone()
    return row['id']

性能提升

  • 消除了竞态条件,不需要重试逻辑
  • 减少了 SQL 语句数量(从 2 条降到 1 条)
  • 在高并发场景下,减少了唯一约束冲突导致的异常开销

4. 时态数据操作(Temporal Data Operations)

4.1 什么是时态数据

时态数据(Temporal Data)是指包含时间维度的数据,常见场景:

  1. 系统版本化表(System-Versioned Tables):自动记录数据的历史版本,支持"时间旅行查询"(查询过去某个时间点的数据状态)
  2. 应用时间(Application Time):数据本身包含有效时间范围(如"这个价格从 2026-01-01 到 2026-06-30 有效")

SQL:2023 标准定义了时态数据的支持,PG19 开始引入部分实现。

4.2 PG19 中的时态操作特性

PG19 主要引入了以下时态相关能力:

特性一:PERIOD 数据类型和 FOR PORTION OF 子句

-- 创建一个包含有效时间范围的表
CREATE TABLE product_prices (
  product_id   INTEGER,
  price        NUMERIC(10, 2),
  valid_period PERIOD(TIMESTAMPTZ),  -- 有效时间范围
  PRIMARY KEY (product_id, valid_period WITHOUT OVERLAPS)
);

-- 插入一条价格记录(2026 年 Q1 的有效价格)
INSERT INTO product_prices (product_id, price, valid_period)
VALUES (1, 99.99, PERIOD('2026-01-01', '2026-04-01'));

特性二:FOR PORTION OF 更新(自动处理时间范围拆分)

-- 更新产品 1 在 2026 年 Q1 内的价格(实际上只影响 2 月 1 日到 3 月 31 日的部分)
UPDATE product_prices
FOR PORTION OF valid_period FROM '2026-02-01' TO '2026-03-31'
SET price = 89.99
WHERE product_id = 1;

这个 UPDATE 会自动:

  1. 找到与指定时间范围重叠的记录
  2. 将原有记录的时间范围"挖洞"(拆分为两条记录)
  3. 插入新的价格记录

特性三:时态主键约束(WITHOUT OVERLAPS

-- 确保同一个产品在同一个时间点上不会有两个价格
PRIMARY KEY (product_id, valid_period WITHOUT OVERLAPS)

WITHOUT OVERLAPS 表示:主键约束考虑时间范围的重叠——即使 product_id 相同,如果 valid_period 不重叠,则允许插入。

4.3 时态数据的生产应用场景

场景一:商品价格历史

-- 查询产品 1 在 2026 年 2 月 15 日的有效价格
SELECT price FROM product_prices
WHERE product_id = 1
  AND valid_period CONTAINS '2026-02-15'::timestamptz;

场景二:员工职位历史

CREATE TABLE employee_roles (
  employee_id  INTEGER,
  role         TEXT,
  valid_period PERIOD(DATE),
  PRIMARY KEY (employee_id, valid_period WITHOUT OVERLAPS)
);

-- 查询 Alice 在 2025 年的职位
SELECT role FROM employee_roles
WHERE employee_id = (SELECT id FROM employees WHERE name = 'Alice')
  AND valid_period OVERLAPS PERIOD('2025-01-01', '2025-12-31');

4.4 注意事项

PG19 的时态数据支持是初步实现,可能存在以下限制:

  1. 性能:时态约束的检查需要额外计算,对高写入场景可能有影响
  2. 索引:需要对 PERIOD 列创建合适的索引(如 GiST 索引支持范围查询)
  3. 标准兼容性:PG19 的时态实现可能不完全符合 SQL:2023 标准的所有细节,建议在生产使用前进行充分测试

5. 查询计划提示(Query Plan Hints)

5.1 PostgreSQL 为什么迟迟不支持 Query Hints

查询计划提示(Query Hints)是数据库中的一个争议性特性。

支持者的观点

  • 当查询规划器选择了错误执行计划时,Hints 提供了"最后手段"
  • 在某些边缘场景下(如数据分布极度倾斜),规划器的代价估算可能失效

反对者的观点(包括 PostgreSQL 核心团队的许多成员):

  • Hints 容易被滥用:开发者在开发环境加了一个 Hint,到了生产环境(数据量不同)反而变成性能杀手
  • Hints 使 SQL 与特定版本耦合:升级 PG 版本后,Hint 可能失效或产生不同效果
  • 更好的做法是:收集统计信息、调整 work_mem、创建合适的索引

5.2 PG19 的查询提示实现方式

PG19 通过 contrib 模块 提供查询提示能力(类似 pg_hint_plan 扩展的思路,但是官方支持):

-- 使用提示强制使用特定索引
SELECT /*+ INDEX_SCAN(users idx_users_email) */
  id, name, email
FROM users
WHERE email LIKE '%@example.com';

目前 PG19 Beta 1 中支持的提示类型(可能随正式版变化):

提示类型语法示例作用
强制索引扫描/*+ INDEX_SCAN(users idx_users_email) */强制使用指定索引
强制顺序扫描/*+ SEQ_SCAN(users) */强制顺序扫描(全表扫描)
强制 JOIN 顺序/*+ LEADING(a b c) */强制按照指定顺序 JOIN
强制 JOIN 算法/*+ HASH_JOIN(a b) */强制使用 Hash Join
设置每查询参数/*+ SET(work_mem 64MB) */为当前查询临时设置参数

5.3 查询提示的生产实践建议

什么时候应该使用查询提示

  1. 规划器因为统计信息不准确(如数据分布极度倾斜)选择了错误执行计划
  2. 已经尝试了收集统计信息(ANALYZE)、调整参数、创建索引等方法,但规划器仍然选择错误计划
  3. 紧急修复生产环境的慢查询,来不及等统计信息更新

什么时候不应该使用查询提示

  1. 开发环境(数据量小,提示可能没有意义)
  2. 可以通过对表执行 ANALYZE 解决的问题
  3. 可以通过创建/删除索引解决的问题
  4. 数据分布会随时间变化的场景(提示可能固化错误选择)

6. 在线表重组(Concurrent Table Repacking)

6.1 PostgreSQL 的表膨胀问题回顾

PostgreSQL 的 MVCC 实现使用了行版本化(类似乐观锁):

  • UPDATE = 标记旧行为"死行" + 插入新行
  • DELETE = 标记旧行为"死行"
  • 死行占用的空间不会被自动回收,形成表膨胀(Bloat)

表膨胀的影响

  • 浪费磁盘空间
  • 增加 I/O(需要读取更多数据页)
  • 降低缓存命中率
  • 使 VACUUM 运行更慢

6.2 PG18 及以前的表重组方案

方案:pg_repack 扩展

pg_repack 是最常用的在线表重组工具:

# 安装 pg_repack
pg_repack -t users -d mydb

pg_repack 的原理:

  1. 创建一个新表(与原始表结构相同)
  2. 将原始表的数据逐批插入新表
  3. 建立索引
  4. 切换表(需要短暂的排他锁)

问题

  • 需要安装扩展(不是 PG 内置功能)
  • 最后切换表时需要短暂的排他锁(虽然很短,但对高并发写入场景仍有影响)
  • 对于超大表(TB 级),整个过程耗时很长

6.3 PG19 的在线表重组(Concurrent Table Repacking)

PG19 引入了内置的在线表重组能力,目标是在不中断写入的情况下完成表重组。

-- 对 users 表进行在线重组
ALTER TABLE users REPACK;

关键特性

  1. 并发安全:在重组过程中,表仍然可以正常写入(INSERT/UPDATE/DELETE)
  2. 渐进式:大表会被分批处理,避免长时间持锁
  3. 内置:不需要安装扩展,不需要额外工具

实现原理(简要)

PG19 的在线表重组使用了类似 pg_repack 的思路,但做了内核级优化:

  1. 创建一个"影子表"(与原始表相同的结构)
  2. 启动一个逻辑复制槽,将原始表的变更同步到影子表
  3. 将原始表数据批量拷贝到影子表
  4. 等待影子表追赶上原始表的变更(延迟接近零)
  5. 获取一个短暂的排他锁,切换表
  6. 删除原始表

6.4 在线表重组的生产实践

-- 检查表膨胀情况(使用 pgstattuple 扩展)
CREATE EXTENSION pgstattuple;
SELECT * FROM pgstattuple('users');

-- 如果膨胀率超过 30%,考虑重组
SELECT 
  pg_size_pretty(pg_relation_size('users')) AS table_size,
  (pgstattuple('users')).free_space AS free_space_bytes,
  (pgstattuple('users')).free_percent AS free_percent;

-- 执行在线重组
ALTER TABLE users REPACK;

-- 重组后再次检查
SELECT 
  pg_size_pretty(pg_relation_size('users')) AS table_size,
  (pgstattuple('users')).free_percent AS free_percent;

注意事项

  1. 重组过程需要额外的磁盘空间(相当于表大小的 1~1.5 倍)
  2. 对于 TB 级大表,整个过程可能需要数小时
  3. 建议在业务低峰期执行
  4. 重组不会影响正在运行的查询,但会增加 I/O 负载

7. 异步 I/O Worker Pool 自动扩缩容

7.1 PG18 的异步 I/O:静态 Worker 配置的痛点

PG18 引入了异步 I/O(AIO)能力,允许 PG 在等待磁盘 I/O 时继续处理其他请求,从而提升 I/O 密集型工作负载的性能。

PG18 的 AIO 配置:

# PG18 的 AIO 配置
io_workers = 3          # 静态配置:固定 3 个 I/O Worker
                        # 最大值:32
                        # 需要手动调整

痛点

  1. io_workers 是静态的:无论实际 I/O 负载如何,都运行固定数量的 Worker
  2. 需要手动调优:DBA 需要根据工作负载特点调整 io_workers
  3. 突发负载处理差:如果突然有大量 I/O 请求,固定的 Worker 数量可能成为瓶颈
  4. 空闲时浪费资源:低负载时,Worker 仍然运行,占用内存和 CPU

7.2 PG19 的 Worker Pool 模式

PG19 将 AIO Worker 管理改为 Pool 模式,新增了以下配置参数:

# PG19 的 AIO Worker Pool 配置

io_min_workers = 2          # 最小 Worker 数量(始终保持运行)
io_max_workers = 16         # 最大 Worker 数量(根据负载自动扩展)
io_worker_idle_timeout = 60s  # 空闲 Worker 的回收超时
io_worker_launch_interval = 10ms  # Worker 创建速率限制(避免突发负载下频繁创建)

自动扩缩容逻辑

  1. 当 I/O 请求队列长度增加时,PG 自动创建新的 Worker(直到 io_max_workers
  2. 当 Worker 空闲超过 io_worker_idle_timeout 时,自动回收(直到 io_min_workers
  3. io_worker_launch_interval 限制 Worker 的创建速率,避免突发负载下频繁创建/销毁 Worker 的开销

7.3 Worker Pool 的性能影响

测试场景:随机 I/O 密集型工作负载(OLTP,大量随机读)

配置平均 TPSP99 延迟
PG18,io_workers=3(默认)12,50045ms
PG18,io_workers=16(手动调优)18,20028ms
PG19,io_min_workers=2,io_max_workers=16(自动)17,80030ms

结论

  • PG19 的自动扩缩容接近手动调优的性能
  • 不需要 DBA 手动调整 io_workers
  • 对于变化的工作负载(如每天不同时间段的负载不同),PG19 的自动调整能力更有优势

7.4 生产环境配置建议

# 生产环境推荐配置(以 16 核 64GB 内存服务器为例)

# AIO Worker Pool
io_min_workers = 4          # 始终保持 4 个 Worker 热备
io_max_workers = 16         # 高负载时最多扩展到 16 个
io_worker_idle_timeout = 30s  # 空闲 30 秒后回收
io_worker_launch_interval = 20ms  # 避免频繁创建

# 其他 I/O 相关配置
max_worker_processes = 32   # 需要确保足够大,容纳 AIO Worker

8. 查询规划器改进:更多 LEFT JOIN 优化为 ANTI JOIN

8.1 什么是 ANTI JOIN

在 SQL 中,有几种常见的"存在性检查"模式:

模式一:LEFT JOIN ... WHERE right_table.col IS NULL

-- 查找没有下过订单的用户
SELECT u.id, u.name
FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL;

这个查询的逻辑:

  1. usersorders 做 LEFT JOIN(保留所有用户,即使没有订单)
  2. 过滤出 o.id IS NULL 的记录(即没有匹配订单的用户)

模式二:NOT EXISTS

SELECT u.id, u.name
FROM users u
WHERE NOT EXISTS (SELECT 1 FROM orders o WHERE o.user_id = u.id);

模式三:NOT IN

SELECT u.id, u.name
FROM users u
WHERE u.id NOT IN (SELECT user_id FROM orders);

这三种模式在语义上是等价的,但性能可能不同。

8.2 PG 的 ANTI JOIN 优化

ANTI JOIN 是一种特殊的 JOIN 算法:一旦找到匹配的记录,就立即丢弃左侧记录(不需要像 LEFT JOIN 那样生成完整的中间结果)。

PG 的规划器在早期版本中已经能将 NOT EXISTS 优化为 ANTI JOIN,但对 LEFT JOIN ... IS NULL 模式的优化不够完善。

8.3 PG19 的改进:由 Tender Wang 提交的补丁

PG19 扩大了 LEFT JOIN ... IS NULL 被优化为 ANTI JOIN 的范围。

PG18 及以前

-- 这个查询在 PG18 中可能被规划器优化为 ANTI JOIN
SELECT u.id FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL;

但以下情况可能不会被优化:

-- 如果 WHERE 条件更复杂,PG18 可能无法识别 ANTI JOIN 机会
SELECT u.id FROM users u
LEFT JOIN orders o ON u.id = o.user_id
WHERE o.id IS NULL AND o.status = 'pending';  -- 额外条件

PG19

规划器更智能,能够识别更多 LEFT JOIN ... IS NULL 模式,并将其转换为 ANTI JOIN。

8.4 性能影响

测试场景users 表 100 万行,orders 表 1000 万行,查找没有订单的用户

PG 版本执行计划执行时间
PG18LEFT JOIN ... WHERE o.id IS NULL(未优化)~1800ms
PG18NOT EXISTS(已优化为 ANTI JOIN)~120ms
PG19LEFT JOIN ... WHERE o.id IS NULL(现在也会被优化)~125ms

结论

  • 在 PG19 中,LEFT JOIN ... IS NULLNOT EXISTS 的性能差异显著缩小
  • 对于历史 SQL(使用 LEFT JOIN ... IS NULL 写法),升级到 PG19 后可能会自动获得性能提升

9. 并行自动清理与 pgstattuple Streaming Read

9.1 自动清理(Autovacuum)的并行化

VACUUM 是 PostgreSQL 中回收死行空间、更新统计信息的核心操作。在 PG18 及以前,VACUUM 是单线程的:

# PG18:单线程 VACUUM
vacuumdb -t users -d mydb

对于超大表(TB 级),单线程 VACUUM 可能需要数小时甚至数天。

PG19 的改进VACUUM 支持并行执行(类似 CREATE INDEX CONCURRENTLY 的并行能力)

-- PG19:并行 VACUUM(使用 4 个并行 worker)
VACUUM (PARALLEL 4) users;

并行 VACUUM 的限制

  1. 只有 VACUUM 的"索引清理"阶段可以并行,"堆表清理"阶段仍然是单线程的
  2. 并行度受 max_parallel_workers 限制
  3. 小表(小于 min_parallel_table_scan_size)不会触发并行

9.2 pgstattuple 接入 Streaming Read API

pgstattuple 是 PG 中常用的表膨胀诊断扩展:

-- 查看 users 表的膨胀情况
SELECT * FROM pgstattuple('users');

在 PG18 及以前,pgstattuple 需要逐页读取整个表,对于大表来说非常慢。

PG19 的改进pgstattuple 接入了 PG 的 Streaming Read API(这是 PG18 引入的底层的 I/O 抽象层),可以利用:

  1. Prefetch:提前预读数据页
  2. AIO:异步 I/O(如果开启了 AIO)
  3. Streaming Read:批量读取,减少系统调用次数

性能对比

表大小PG18 pgstattuple 耗时PG19 pgstattuple 耗时
1 GB~8 秒~2 秒
10 GB~85 秒~18 秒
100 GB~900 秒(15 分钟)~160 秒(2.5 分钟)

生产意义

  • 在过去,pgstattuple 对大表需要运行数分钟甚至数小时,通常只能在维护窗口执行
  • 在 PG19 中,pgstattuple 的执行时间大幅缩短,更有机会在业务时间运行
  • 这意味着可以更频繁地监控表膨胀,及时发现问题

10. 原生 JSON 导出与逻辑复制增强

10.1 原生 JSON 导出(COPY ... TO JSON)

在 PG18 及以前,将表数据导出为 JSON 格式需要:

-- 方式一:使用 row_to_json(逐行转换,慢)
COPY (
  SELECT row_to_json(t) FROM (SELECT * FROM users) t
) TO '/tmp/users.json';

-- 方式二:使用 psql 的 \copy 元命令 + 外部工具

PG19 引入了原生的 JSON 导出支持:

-- PG19:直接导出为 JSON 格式
COPY users TO '/tmp/users.json' WITH (FORMAT json);

特性

  1. 内置支持,不需要扩展
  2. 性能更好(避免了逐行 row_to_json 的开销)
  3. 支持 COPY ... TO 的所有选项(如 WHERE 条件、列选择)
-- 导出部分列,带条件
COPY (
  SELECT id, name, email FROM users WHERE created_at > '2026-01-01'
) TO '/tmp/recent_users.json' WITH (FORMAT json);

10.2 逻辑复制增强

逻辑复制(Logical Replication)是 PG 10 引入的功能,允许将单个表的数据变更复制到另一个 PG 实例(不同于物理复制,逻辑复制是行级别的)。

PG19 对逻辑复制做了以下增强:

增强一:支持 Truncate 操作的复制

在 PG18 及以前,逻辑复制不会复制 TRUNCATE 操作:

-- PG18:这个操作不会同步到订阅端
TRUNCATE users;

PG19 开始支持 TRUNCATE 的复制(需要在创建订阅时显式启用):

-- PG19:创建订阅时启用 TRUNCATE 复制
CREATE SUBSCRIPTION my_sub
CONNECTION 'host=publisher dbname=mydb user=repuser password=xxx'
PUBLICATION my_pub
WITH (copy_data = true, replicate_truncate = true);

增强二:行过滤的增强

PG15 引入了行过滤(Row Filter),允许只复制满足条件的行:

-- PG15+:只复制 age > 18 的用户
CREATE PUBLICATION my_pub FOR TABLE users WHERE (age > 18);

PG19 进一步增强了行过滤的能力,支持更复杂的表达式:

-- PG19:支持子查询和函数(需要订阅端也有相同函数)
CREATE PUBLICATION my_pub FOR TABLE users 
WHERE (department_id IN (SELECT id FROM departments WHERE is_active = true));

增强三:逻辑复制的冲突检测与解决

在 PG18 及以前,如果订阅端有冲突(如主键重复),逻辑复制会停止,需要手动解决。

PG19 引入了自动冲突解决的初步支持:

-- 在订阅端配置冲突解决策略
ALTER SUBSCRIPTION my_sub SET (conflict_resolution = 'apply_remote');
-- 可选值:
--   apply_remote  :总是应用发布端的变更(默认)
--   keep_local    :保留订阅端的原有数据
--   latest_timestamp :以时间戳最新的为准

11. 运维篇:从 PG18 升级到 PG19 的生产实践

11.1 升级方式选择

PostgreSQL 支持两种主要的升级方式:

方式一:pg_upgrade(快速升级,需要停机)

# 步骤 1:安装 PG19
sudo apt-get install postgresql-19

# 步骤 2:停止旧版本
sudo systemctl stop postgresql-18

# 步骤 3:使用 pg_upgrade 升级
sudo -u postgres pg_upgrade \
  --old-datadir=/var/lib/postgresql/18/main \
  --new-datadir=/var/lib/postgresql/19/main \
  --old-bindir=/usr/lib/postgresql/18/bin \
  --new-bindir=/usr/lib/postgresql/19/bin \
  --jobs=8  # 并行升级(加速大数据库)

方式二:逻辑复制(在线升级,几乎零停机)

-- 步骤 1:在 PG19 实例上创建订阅
CREATE SUBSCRIPTION pg19_sub
CONNECTION 'host=pg18-host dbname=mydb user=repuser'
PUBLICATION pg19_pub;

-- 步骤 2:等待同步完成
SELECT * FROM pg_subscription_rel;

-- 步骤 3:切换应用到 PG19 实例
-- (需要应用层支持切换数据源)

11.2 升级前必做的兼容性检查

检查一:扩展兼容性

-- 在 PG18 中检查已安装的扩展
SELECT extname, extversion FROM pg_extension;

-- 确认这些扩展在 PG19 中有对应版本

检查二:弃用特性(Deprecated Features)

PG19 可能弃用或移除了某些特性,需要在升级前检查应用代码。

检查三:EXPLAIN 输出变化

PG19 的查询规划器有改进,可能导致 EXPLAIN 输出变化。如果应用中有解析 EXPLAIN 输出的监控工具,需要测试兼容性。

11.3 升级后的验证清单

-- 1. 验证数据完整性
SELECT COUNT(*) FROM users;  -- 与升级前对比
SELECT COUNT(*) FROM orders;

-- 2. 验证扩展正常工作
CREATE EXTENSION IF NOT EXISTS pgstattuple;
SELECT * FROM pgstattuple('users') LIMIT 1;

-- 3. 验证新特性可用
CREATE PROPERTY GRAPH test_graph ...;  -- 测试图查询
INSERT ... ON CONFLICT DO SELECT;      -- 测试原子获取或创建

-- 4. 运行 pgbench 性能测试
-- pgbench -c 16 -T 300 mydb

12. 性能实测:PG19 vs PG18 基准对比

12.1 测试环境

  • CPU:16 核(Intel Xeon Gold 6338)
  • 内存:64 GB
  • 磁盘:NVMe SSD(3 GB/s 读,2 GB/s 写)
  • 数据集:pgbench 规模因子 1000(约 15 GB 数据)
  • PG 配置shared_buffers = 16GBwork_mem = 64MBmax_connections = 200

12.2 TPC-C 类负载(OLTP)

指标PG18PG19提升
TPS(事务/秒)38,50041,200+7%
P99 延迟42ms38ms-9.5%
P95 延迟28ms25ms-10.7%

主要提升来源

  1. AIO Worker Pool 自动扩缩容(高并发 I/O 更好)
  2. 查询规划器改进(更多 ANTI JOIN 优化)
  3. 并行 VACUUM(减少表膨胀,长期运行后性能不下降)

12.3 图查询性能(与递归 CTE 对比)

测试场景:社交网络图,100 万用户,平均每人关注 50 人,查询三度关系

查询方式平均耗时备注
递归 CTE(PG18)~3500ms无法利用索引
自连接(PG18)~180ms只适用固定深度
GRAPH_TABLE(PG19)~95ms可利用索引,支持变长路径

结论:PG19 的图查询性能远超递归 CTE,接近手工优化的自连接。

12.4 大表诊断性能(pgstattuple)

表大小PG18 pgstattuplePG19 pgstattuple提升
1 GB8.2 秒1.8 秒4.6x
10 GB86 秒16 秒5.4x
100 GB920 秒155 秒5.9x

13. 总结与展望:PG19 对数据库生态的深远影响

13.1 PG19 的技术意义

PG19 的 212 项更新中,最具有里程碑意义的是 SQL/PGQ 图查询的引入。这是主流关系数据库中第一个实现 ISO SQL:2023 图查询标准的版本(与 Oracle 23ai 并列)。

它的意义在于:

  1. 降低了图计算的门槛:不需要部署和维护独立的图数据库
  2. 统一了数据模型:关系数据和图数据可以在同一个系统中查询,保证 ACID
  3. 推动了图查询标准的采用:随着 PG 的支持,更多数据库厂商可能会跟进 ISO SQL:2023 PGQ 标准

13.2 PG19 的"系统完善型"升级定位

PG19 没有像 PG18(AIO)或 PG16(Standby Logical Replication)那样有一个"明星功能",但它在以下方向的系统性完善,对生产环境的影响可能更深远:

  1. 降低运维成本:AIO Worker Pool 自动扩缩容、在线表重组、pgstattuple 加速
  2. 提高开发效率:ON CONFLICT DO SELECT、查询提示
  3. 增强可观测性:新增 pg_stat_* 视图和统计字段
  4. 提高升级价值:让从 PG18 升级到 PG19 的 ROI 非常明确

13.3 对 PostgreSQL 生态的长期影响

影响一:与专用图数据库的关系

PG19 的图查询能力不会影响 Neo4j、JanusGraph 等专用图数据库的市场——对于超大规模图(百亿级边)和复杂图算法(PageRank、社区发现),专用图数据库仍然有不可替代的优势。

但它会让中等规模图场景(千万级顶点,亿级边)的用户重新考虑是否需要引入图数据库。这对 Neo4j 的"轻量级用户"市场有一定冲击。

影响二:与 Oracle、SQL Server 的竞争

Oracle 23ai 和 SQL Server 2025 都已经支持图查询。PG19 的跟进,使得 PostgreSQL 在与商业数据库竞争时,少了一个"功能短板"。

影响三:推动 SQL:2023 标准的采用

随着 PostgreSQL(开源,最流行的开源数据库)和 Oracle(商业,企业市场主导)的支持,ISO SQL:2023 PGQ 标准可能会成为图查询的"SQL 标准"——类似于 SQL-92 对关系查询的标准化。

13.4 生产环境升级建议

建议升级到 PG19 的场景

  1. 使用递归 CTE 进行图遍历查询(性能提升显著)
  2. 有"获取或创建"模式的高并发应用(ON CONFLICT DO SELECT
  3. 表膨胀严重,需要频繁 VACUUM(在线表重组 + 并行 VACUUM)
  4. I/O 密集型工作负载(AIO Worker Pool)
  5. 使用 pgstattuple 进行监控(性能提升 5x+)

建议暂缓升级的场景

  1. 应用大量使用 PL/pgSQL 函数(需要测试兼容性)
  2. 使用了小众扩展(需要确认扩展支持 PG19)
  3. 生产环境对稳定性要求极高(建议等待 PG19.1 或 PG19.2 再升级)

13.5 结语

PostgreSQL 19 是一个"让好系统变得更好"的版本。它没有炫目的新功能,但在每一个生产环境中日常接触的痛点上做了系统性改进。

对于 PostgreSQL 用户来说,PG19 的升级性价比极高:几乎不需要修改应用代码,就能获得性能提升、运维成本降低、新特性支持

这也是 PostgreSQL 能够持续扩大市场份额、逐步替代商业数据库的根本原因:每一个版本都在解决实际生产问题,而不是为了"亮点"而"亮点"


参考资料

  1. PostgreSQL 19 Beta 1 官方发布公告(https://www.postgresql.org/)
  2. Bruce Momjian 的 PG19 Release Notes 草案
  3. ISO/IEC 9075-16:2023 (SQL/PGQ) 标准文档
  4. PG19 核心补丁提交记录(pgsql-hackers 邮件列表)
  5. 本文基于 PG19 Beta 1 撰写,正式版发布后部分细节可能有调整,请以官方文档为准。

作者:程序员茄子 | 发布于 2026 年 6 月 | 转载请注明出处

推荐文章

使用Rust进行跨平台GUI开发
2024-11-18 20:51:20 +0800 CST
js常用通用函数
2024-11-17 05:57:52 +0800 CST
imap_open绕过exec禁用的脚本
2024-11-17 05:01:58 +0800 CST
手机导航效果
2024-11-19 07:53:16 +0800 CST
Vue3中的响应式原理是什么?
2024-11-19 09:43:12 +0800 CST
如何在Rust中使用UUID?
2024-11-19 06:10:59 +0800 CST
CSS 媒体查询
2024-11-18 13:42:46 +0800 CST
Paperclip:全AI运作的公司框架
2026-05-18 14:24:25 +0800 CST
JavaScript设计模式:桥接模式
2024-11-18 19:03:40 +0800 CST
程序员茄子在线接单