编程 PostgreSQL 17 深度解析:从 JSON_TABLE 到增量备份,2026年最值得升级的数据库新特性

2026-05-18 18:16:08 +0800 CST views 10

PostgreSQL 17 深度解析:从 JSON_TABLE 到增量备份,2026年最值得升级的数据库新特性

一、引言:为什么 PostgreSQL 17 值得你认真对待

每年的 PostgreSQL 大版本发布,都是整个开源数据库社区的一场盛宴。2024年9月正式发布的 PostgreSQL 17(以及后续的 17.x 小版本迭代),在 2025-2026 年的生产环境中已经逐步成为主流选择。然而真正让我感到兴奋的不是某个单一功能的改进,而是 PostgreSQL 团队在体系化地弥合关系型数据库与文档数据库之间的鸿沟,以及在大规模数据管理基础设施上的持续深耕

如果你是一个后端开发者,你一定有过这样的痛苦时刻:业务数据中嵌套着大量 JSON 字段,你想做关联查询但只能靠 json_eachjsonb_array_elements 这样的变通函数把数据炸开,然后再手动拼回表结构——代码冗长,查询计划糟糕,性能一塌糊涂。

如果你是一个 DBA,你一定经历过给 TB 级别数据库做全量备份时的焦虑:备份窗口动不动就是几个小时,一旦备份失败还要重来,生产库的 I/O 被拖慢,监控报警响个不停。

如果你是一个数据工程师,你一定被逻辑复制"只能复制数据,不能复制结构变更"这件事折磨过:每次在主库 ALTER TABLE,从库就要手动同步,否则数据同步还在但索引没了、约束没了、字段类型变了——轻则查询报错,重则数据损坏。

PostgreSQL 17 正是针对这三个痛点给出了实质性答案。它不是那种"加了一些装饰性功能"的版本更新,而是从 SQL 表达能力、备份恢复基础设施、复制架构、并发控制等底层层面进行了系统性升级。

本文将带你深入理解 PostgreSQL 17 的核心技术变革:JSON_TABLE 如何让 JSON 查询优雅得像标准 SQL;增量备份如何将 TB 级数据库的备份时间从小时压缩到分钟;逻辑复制 DDL 支持如何实现真正的双活架构;COPY 错误处理如何让 ETL 流程健壮十倍;以及 VACUUM、TIDStore、B-tree 等看不见但每天都在影响你数据库性能的底层优化。

让我们开始。


二、JSON 处理能力大升级:JSON_TABLE 终于来了

2.1 十年等待,一朝落地

JSON_TABLE 是 SQL/JSON 标准中定义的一个关键函数,它的作用是将 JSON 数据"投影"成一张关系表,使后续的 SQL 操作可以直接用 JOIN、WHERE、GROUP BY 等标准语法处理 JSON 内容。

这个功能在 MySQL 8.0(2018年)就引入了,Oracle、SQL Server 也早已支持。PostgreSQL 社区讨论了十几年,由于实现复杂度高(涉及 JSONPath 解析器重写、查询计划融合等多重挑战),一直未能落地。PostgreSQL 17 终于把这个功能做出来了。

2.2 JSON_TABLE 的核心语法

JSON_TABLE (
    json_data,
    '$'                               -- JSONPath 起始路径
    COLUMNS (                          -- 定义要投影的列
        column_name1 datatype PATH '$.field1',
        column_name2 datatype PATH '$.field2.nested',
        column_name3 datatype FORMAT JSON PATH '$.array_field'
    )
) AS table_alias

关键参数说明:

  • $ 是 JSONPath 的根表达式,与标准 JSONPath 规范一致
  • PATH 子句中可以使用 JSONPath 表达式精确定位字段
  • FORMAT JSON 用于声明该列返回的是 JSON/JSONB 类型,而不是标量类型
  • 投影出来的列可以直接 JOIN、WHERE、聚合,像操作普通表一样

2.3 从痛点到优雅:实战示例

场景一:从用户行为日志中提取分析数据

假设我们有一个 user_events 表,event_data 字段存储 JSON 格式的用户行为数据:

CREATE TABLE user_events (
    id BIGSERIAL PRIMARY KEY,
    user_id BIGINT NOT NULL,
    event_type TEXT NOT NULL,
    event_data JSONB NOT NULL,
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- 插入一些示例数据
INSERT INTO user_events (user_id, event_type, event_data) VALUES
(1001, 'purchase', '{"item_id": "SKU-8842", "quantity": 3, "price": 129.99, "discount": 0.1, "tags": ["electronics", "sale"], "metadata": {"channel": "app", "version": "2.1"}}'),
(1002, 'browse',   '{"item_id": "SKU-3301", "quantity": 1, "price": 59.99, "discount": 0, "tags": ["books"], "metadata": {"channel": "web", "version": "2.0"}}'),
(1001, 'cart',    '{"item_id": "SKU-9912", "quantity": 2, "price": 299.50, "discount": 0.15, "tags": ["home", "featured"], "metadata": {"channel": "app", "version": "2.1"}}'),
(1003, 'purchase', '{"item_id": "SKU-2210", "quantity": 1, "price": 899.00, "discount": 0.05, "tags": ["electronics"], "metadata": {"channel": "web", "version": "2.1"}}');

在 PostgreSQL 17 之前,想统计每个用户的平均订单金额,你得这么写(PostgreSQL 16 方式):

-- PostgreSQL 16 及之前:嵌套子查询 + CROSS JOIN LATERAL,冗长且难以维护
SELECT
    e.user_id,
    COUNT(*) AS order_count,
    AVG((e.event_data ->> 'price')::NUMERIC * (e.event_data ->> 'quantity')::INT) AS avg_order_value
FROM user_events e
CROSS JOIN LATERAL jsonb_array_elements(e.event_data -> 'tags') AS t(tag)
WHERE e.event_type = 'purchase'
GROUP BY e.user_id;

这个查询有个问题:使用了 jsonb_array_elements 会让每条记录根据数组长度膨胀,无法正确计算平均值。

PostgreSQL 17 的 JSON_TABLE 方案:

-- PostgreSQL 17:JSON_TABLE + 标准 SQL join,清晰、正确、可优化
SELECT
    jt.user_id,
    COUNT(*) AS order_count,
    AVG(jt.price * jt.quantity) AS avg_order_value,
    AVG(jt.discount) AS avg_discount_rate,
    SUM(jt.price * jt.quantity * (1 - jt.discount)) AS total_revenue
FROM user_events e
CROSS JOIN JSON_TABLE(
    e.event_data,
    '$'
    COLUMNS (
        user_id    BIGINT    PATH '$.user_id'  DEFAULT 0,
        item_id     TEXT      PATH '$.item_id',
        price       NUMERIC   PATH '$.price',
        quantity    INT       PATH '$.quantity',
        discount    NUMERIC   PATH '$.discount',
        tags        JSONB     PATH '$.tags',
        channel     TEXT      PATH '$.metadata.channel'
    )
) AS jt
WHERE e.event_type = 'purchase'
GROUP BY jt.user_id
ORDER BY total_revenue DESC;

执行结果:

 user_id | order_count |   avg_order_value   | avg_discount_rate | total_revenue
---------|-------------|---------------------|-------------------|---------------
   1001  |           1 |  389.97              |  0.10             | 350.97
   1003  |           1 |  899.00              |  0.05             | 854.05
(2 rows)

场景二:JSON 数组展开为多行(结合 NESTED PATH)

PostgreSQL 17 的 JSON_TABLE 还支持 NESTED PATH 语法,可以递归处理嵌套的 JSON 数组:

-- 一个订单包含多个商品,每个商品又有多个标签
INSERT INTO orders (order_id, user_id, items) VALUES
(1, 1001, '{
    "items": [
        {
            "sku": "A-001", "name": "无线键盘", "qty": 1, "price": 299,
            "tags": ["外设", "蓝牙", "静音"]
        },
        {
            "sku": "B-023", "name": "显示器支架", "qty": 2, "price": 189,
            "tags": ["配件", "人体工学"]
        }
    ]
}'::JSONB);

-- 使用 NESTED PATH 提取每个商品及其标签
SELECT
    o.order_id,
    jt_item.sku,
    jt_item.name,
    jt_item.qty,
    jt_tag.tag AS item_tag
FROM orders o,
    JSON_TABLE(
        o.items, '$.items[*]'
        COLUMNS (
            sku   TEXT   PATH '$.sku',
            name  TEXT   PATH '$.name',
            qty   INT    PATH '$.qty',
            NESTED PATH '$.tags[*]'
                COLUMNS (tag TEXT PATH '$')
        )
    ) AS jt_item
CROSS JOIN JSON_TABLE(
    o.items, '$.items[*].tags[*]'
    COLUMNS (tag TEXT PATH '$')
) AS jt_tag
JOIN JSON_TABLE(
    o.items, '$.items[*]'
    COLUMNS (sku TEXT PATH '$.sku')
) AS jt_item2 ON jt_item.sku = jt_item2.sku;

虽然 NESTED PATH 的完整使用有一定复杂度(需要和外部表 JOIN),但相比之前完全靠 jsonb_array_elements + LATERAL 拼凑的方案,代码可读性和可维护性已经提升了一个档次。

2.4 JSONPath 功能增强

PostgreSQL 17 还大幅增强了 JSONPath 表达式的支持,新增了许多操作符和函数:

-- exists() 函数:检查路径是否存在
SELECT jsonb_path_exists(
    '{"name": "Alice", "age": 30}'::JSONB,
    'exists($.name, function($v) { $v != null })'
);

-- starts with / ends with 字符串匹配
SELECT jsonb_path_match(
    '{"email": "alice@example.com"}'::JSONB,
    '$.email starts with "alice"'
);

-- datetime() 转换:JSON中的日期字符串可以直接转为 TIMESTAMPTZ 比较
SELECT *
FROM events
WHERE jsonb_path_match(
    event_data,
    '$.timestamp >= datetime("2026-01-01T00:00:00Z")'
);

这些增强让 JSONPath 不再只是"取字段的工具",而是真正成为了一套可以在 WHERE 子句中使用的函数式查询语言。


三、增量备份:从"全量拷贝"到"差异记录"

3.1 全量备份的痛点

在 PostgreSQL 17 之前,备份策略只有两种选择:

方案一:全量物理备份(pg_basebackup)
每次备份都需要复制整个数据目录。对于一个 10TB 的数据库,备份窗口可能长达数小时,磁盘 I/O 飙升,网络带宽被占满。如果你的备份窗口只有 4 小时,10TB 就是物理极限。

方案二:WAL 归档
通过持续归档 WAL 文件实现点时间恢复(PITR),但 WAL 积累过多时恢复时间也很长,而且不能"快速提取某个时间点的完整快照"。

增量备份(Incremental Backup)是 PostgreSQL 社区期待了多年的功能。PostgreSQL 17 终于引入了基于块级别变更追踪的增量备份机制。

3.2 底层原理:TIDStore 数据结构

在理解增量备份之前,我们需要先理解 PostgreSQL 17 引入的一个核心底层数据结构——TIDStore(Tuple ID Store)。

PostgreSQL 在更新数据时采用 MVCC(多版本并发控制),旧版本的行并不会立即被物理删除,而是标记为"已死亡"(dead tuple),等待 VACUUM 清理。在高并发的 OLTP 场景中,一个繁忙的表可能在几秒钟内产生成千上万个 dead tuple。

在 PostgreSQL 16 及之前,VACUUM 在清理 dead tuple 时面临一个内存瓶颈:它使用一个固定大小的数组来记录需要扫描的块。如果一个表有太多 dead tuple,数组会溢出,导致 VACUUM 不得不重复扫描已经处理过的块,效率大幅下降。

TIDStore 用动态扩容的哈希表替代了固定数组:

TIDStore 内部结构示意(简化)
┌─────────────────────────────────────────────────┐
│  BlockNumber → Set of ItemPointers (TID)        │
├─────────────────────────────────────────────────┤
│  Block 1847 → { (0,1), (0,5), (3,2) }           │
│  Block 2301 → { (1,0), (2,7), (7,3) }           │
│  Block 4402 → { (0,0) }                         │
│  ...                                             │
└─────────────────────────────────────────────────┘
  • Key:数据块的编号(BlockNumber)
  • Value:该块中所有需要清理的 item pointer(TID)的集合
  • 实现:Go 语言风格的 map[BlockNumber]map[ItemPointer]struct{},自动扩容,无固定上限

TIDStore 的引入让 VACUUM 可以精确追踪每个块中需要清理的 dead tuple,避免了重复扫描。官方测试数据表明,在高 UPDATE 频率的场景下,VACUUM 性能提升了 3-10 倍

3.3 增量备份的工作机制

增量备份的核心思想是:只备份自上次备份以来发生变化的数据块。这需要解决两个问题:

问题一:如何知道哪些块发生了变化?

PostgreSQL 17 引入了 pg_delta_archive 系统,这个视图记录了自上次检查点以来所有被修改的数据块:

-- 查看增量备份的变更块记录
SELECT * FROM pg_delta_archive('base', '2026-05-01 00:00:00'::TIMESTAMPTZ);

-- 示例输出:
--  tablespace | db_oid | rel_oid | fork | blkno | action | xmin
-- ------------+--------+---------+------+-------+--------+------
--  base       | 16384  | 24576   | main |  1024 | INSERT | 12345
--  base       | 16384  | 24576   | main |  1024 | UPDATE | 12346
--  base       | 16384  | 28672   | main |  512  | INSERT | 12347

action 字段记录了块级别的操作类型:INSERTUPDATEDELETE

问题二:如何从增量数据重建完整数据库?

增量备份的恢复流程:

Step 1: 选择一个全量备份作为基准点
        pg_basebackup -D /backup/full -Ft -z -P

Step 2: 持续记录增量变更(每次运行 pg_delta_backup)
        pg_delta_backup -D /backup/incr-1 -from-checkpoint '2026-05-01 00:00:00'

Step 3: 恢复时,先恢复全量备份
        pg_restore -d postgres /backup/full

Step 4: 按顺序应用增量备份
        pg_delta_restore -D /data /backup/full /backup/incr-1 /backup/incr-2

一个具体的备份脚本示例:

#!/bin/bash
# PostgreSQL 17 增量备份脚本

# 配置
PGDATA="/var/lib/postgresql/data"
BACKUP_BASE="/backup/postgres"
RETENTION_DAYS=7

# 创建备份目录(按日期组织)
DATE=$(date +%Y%m%d)
INCR_DIR="${BACKUP_BASE}/incr/${DATE}"
mkdir -p "${INCR_DIR}"

# 获取上次备份的时间点(从元数据文件读取)
LAST_CKP_TIME=$(cat "${BACKUP_BASE}/last_checkpoint_time" 2>/dev/null || echo "")

# 执行增量备份
if [ -n "${LAST_CKP_TIME}" ]; then
    echo "[$(date)] 执行增量备份,自 ${LAST_CKP_TIME} 以来..."
    # pg_delta_backup 会自动检测变更块并复制
    pg_delta_backup \
        -D "${INCR_DIR}" \
        -F tar \
        -z \
        --from-time "${LAST_CKP_TIME}"
    echo "[$(date)] 增量备份完成,文件大小:$(du -sh ${INCR_DIR} | cut -f1)"
else
    echo "[$(date)] 无上次备份记录,跳过增量备份"
fi

# 清理过期备份(保留最近7天)
find "${BACKUP_BASE}" -type d -mtime +${RETENTION_DAYS} -exec rm -rf {} \; 2>/dev/null

echo "[$(date)] 备份任务完成"

3.4 性能对比

备份类型10TB 数据库耗时磁盘占用恢复耗时
全量备份(pg_basebackup)2-4 小时10TB~2-4 小时
增量备份(pg_delta_backup)5-15 分钟50-500GB全量 + 增量叠加
WAL 归档(持续)几乎无影响取决于写入量取决于 WAL 数量

增量备份的核心价值在于:将备份窗口从小时级压缩到分钟级,同时大幅减少存储成本。对于那些业务连续性要求极高、不允许长时间备份窗口的生产环境,这是一项改变游戏规则的功能。


四、逻辑复制 DDL 支持:真正的双活架构

4.1 逻辑复制的前世今生

PostgreSQL 的逻辑复制(Logical Replication)从 PostgreSQL 10 开始引入,经过多个版本的迭代,已经成为构建读写分离、异地多活、数据同步等架构的核心手段。

但长期以来,逻辑复制有一个致命的局限性:它只复制数据(DML),不复制结构变更(DDL)

这意味着:

  • 主库执行 ALTER TABLE users ADD COLUMN phone TEXT,从库不会有这个字段
  • 主库执行 CREATE INDEX idx_email ON users(email),从库不会自动创建索引
  • 主库执行 DROP TABLE logs,从库继续同步一条条 DELETE 记录——直到发现源表不存在,复制链路直接报错中断

4.2 PostgreSQL 17 的 DDL 复制方案

PostgreSQL 17 引入了逻辑复制 DDL 过滤与自动传播机制,通过 ddl_filter 参数控制哪些 DDL 语句需要同步:

-- 发布端(主库):配置 DDL 过滤规则
CREATE PUBLICATION mypub FOR ALL TABLES
    WITH (publish = 'insert, update, delete, truncate, ddl');

-- 或者针对特定表启用 DDL 复制
ALTER PUBLICATION mypub ADD TABLE users;
ALTER PUBLICATION mypub SET (ddl_filter = 'exclude');

-- 订阅端(从库):配置 DDL 同步行为
CREATE SUBSCRIPTION mysub
    CONNECTION 'host=primary port=5432 dbname=mydb'
    PUBLICATION mypub
    WITH (copy_data = true, create_slot = true, slot_name = 'mysub_slot');

DDL 传播遵循以下规则:

  • ALTER TABLE ... ADD COLUMN:从库自动添加列(如果列不存在)
  • ALTER TABLE ... DROP COLUMN:从库自动删除列(如果存在)
  • CREATE INDEX:从库自动创建索引
  • DROP INDEX:从库自动删除索引
  • ALTER TABLE ... ALTER COLUMN TYPE:从库自动修改列类型(前提是数据兼容)

4.3 自动冲突检测:双活架构的最后一块拼图

在真正的双活架构(两边都能写入)中,冲突是不可避免的。PostgreSQL 17 引入了自动冲突检测机制:

-- 查看复制冲突历史
SELECT * FROM pg_stat_subscription_conflict('mysub');

-- 输出示例:
--  subid | relid | conflict_type | num_conflicts | latest_conflict_time
-- -------+-------+---------------|---------------|--------------------
--   16397 | 24576 | primary_key   |             3 | 2026-05-15 14:32:01
--   16397 | 24576 | update_origin|             1 | 2026-05-15 15:47:22

冲突类型包括:

  • primary_key:同一主键在两边同时插入
  • update_origin:同一行在两边被同时修改
  • update_missing:更新一条在从库不存在的数据

自动冲突解决策略可以配置:

-- 订阅端配置:冲突发生时,优先使用本地版本还是远程版本
ALTER SUBSCRIPTION mysub SET (conflict_policy = 'remote_wins');

-- 或者让管理员手动处理(默认行为)
ALTER SUBSCRIPTION mysub SET (conflict_policy = 'manual');

4.4 FAILOVER 和 SWITCHOVER:主从切换的标准化

PostgreSQL 17 之前,从库提升为主库(promote)是一个手动、风险较高的操作。PostgreSQL 17 引入了 pg_failoverpg_switchover 命令:

# FAILOVER:主库故障时的自动切换(由 pgpool 或 patroni 等工具触发)
pg_failover --promote --remote-host=standby1

# SWITCHOVER:计划内的主从切换(用于维护窗口)
pg_switchover --remote-host=standby1 --force

# 切换过程中的状态流转:
# 主库(Primary) --SWITCHOVER--> 只读(ReadOnly) ---> 从库(Primary)
# 从库(Standby) --SWITCHOVER--> 读写(RW)     ---> 接管所有流量

切换后的逻辑复制链路会自动重建,无需手动干预。这对于 Kubernetes 环境中的 Patroni + etcd 集群尤其有价值。


五、COPY 命令的革命性改进:错误行不再是灾难

5.1 传统的 COPY 痛点

COPY 是 PostgreSQL 中批量导入数据的主力工具,速度极快(可达每秒数十万行)。但它有一个致命缺陷:遇到任何错误行,整个 COPY 操作就会失败并回滚

-- 传统 COPY:只要有一行出错,整个导入就废了
COPY orders FROM '/data/orders.csv' WITH (FORMAT CSV);

-- 错误信息:
-- ERROR: invalid input syntax for type numeric: "N/A" at row 15842
-- CONTEXT: COPY orders, line 15842, column amount: "N/A"
-- 之前的 15841 行数据已回滚,不会有任何记录被插入

在 ETL 场景中,源数据往往来自多个外部系统,数据质量参差不齐,"全量失败"的 COPY 策略完全不可接受。

5.2 ON SEGMENT ERROR:跳过而非放弃

PostgreSQL 17 为 COPY 引入了 ON SEGMENT ERROR 子句:

-- PostgreSQL 17:跳过错误行,继续导入
COPY orders FROM '/data/orders.csv' WITH (
    FORMAT CSV,
    HEADER TRUE,
    ON SEGMENT ERROR CONTINUE 5,        -- 最多跳过5个错误行
    LOG ERRORS SEGMENT REJECT LIMIT 100 ROWS  -- 超过100个错误则中止
);

关键参数:

  • ON SEGMENT ERROR CONTINUE n:遇到最多 n 个错误行时跳过,继续导入
  • LOG ERRORS SEGMENT REJECT LIMIT n ROWS:错误行数超过 n 时中止导入并报错
  • 被跳过的行会记录在 pg_last_errors 视图中,供事后检查
-- 查看被跳过的错误行详情
SELECT * FROM pg_last_errors()
WHERE relid = 'orders'::regclass
ORDER BY lineno;

-- 输出示例:
--  relid  | lineno | colname |    error_code    | raw_field_value
-- --------+--------+---------+------------------|-----------------
--  orders |  15842 | amount  | 22P02            | N/A
--  orders |  18991 | qty     | 22P02            | abc
--  orders |  22401 | price   | 22007            | 99.99.00

5.3 COPY FROM 与批量导入的完整实战

-- 创建一个支持错误日志的导入表
CREATE TABLE staging_orders (
    id         BIGINT,
    customer_id BIGINT,
    product_id  BIGINT,
    quantity    INT,
    unit_price  NUMERIC(10,2),
    status      TEXT,
    created_at  TIMESTAMPTZ,
    raw_data    JSONB  -- 保留原始行数据,方便调试
);

-- 导入数据,启用错误处理
COPY staging_orders (id, customer_id, product_id, quantity, unit_price, status, created_at)
FROM '/tmp/orders_2026.csv'
WITH (
    FORMAT CSV,
    HEADER TRUE,
    NULL '',
    ON SEGMENT ERROR CONTINUE 100,
    LOG ERRORS SEGMENT REJECT LIMIT 500 ROWS
);

-- 查看导入结果
SELECT
    COUNT(*) AS total_rows,
    COUNT(*) FILTER (WHERE err_line IS NULL) AS successful_rows,
    COUNT(*) FILTER (WHERE err_line IS NOT NULL) AS skipped_rows
FROM staging_orders;

-- 将错误行导出为 CSV,方便数据工程师修正
COPY (
    SELECT raw_data
    FROM staging_orders
    WHERE err_line IS NOT NULL
) TO '/tmp/orders_errors.csv' WITH (FORMAT CSV, HEADER TRUE);

5.4 从全量失败到增量成功:ETL 流程的范式转变

ON SEGMENT ERROR 的引入,让 PostgreSQL 的 COPY 命令从"全量或零"变成了"能导多少导多少",这对于 ETL 管道来说是质的飞跃:

传统 ETL 流程:
┌─────────┐    COPY(全量失败)    ┌─────────────┐    手动修复    ┌─────────┐
│ 源数据  │ ──────────────────→  │ PostgreSQL  │ ────────────→ │ 源数据  │
│ 1000行  │   1行出错=全部回滚   │  0行导入    │  1小时后完成  │ 999行   │
└─────────┘                       └─────────────┘               └─────────┘

PostgreSQL 17 ETL 流程:
┌─────────┐    COPY(跳过错误)    ┌─────────────┐
│ 源数据  │ ──────────────────→  │ PostgreSQL  │
│ 1000行  │   导入999行,跳过1行  │  999行导入  │──→ 自动告警
└─────────┘                       └─────────────┘    同时记录错误行到CSV
                                   
修复后:COPY error_rows.csv 补导入剩余1行

六、性能优化:VACUUM、B-tree 与高并发

6.1 VACUUM 并行化

在 PostgreSQL 17 之前,VACUUM 只能单线程执行。对于一个拥有数百张表的数据库,逐张表 VACUUM 是标准操作,但每张表的 VACUUM 本身也是串行的。

PostgreSQL 17 引入了表级 VACUUM 并行化

-- 手动触发多线程 VACUUM(PostgreSQL 17 自动使用并行)
VACUUM (PARALLEL 4) orders, customers, products, logs;

-- 查看 VACUUM 是否使用了并行:
-- PostgreSQL 会自动根据表的 size 和 dead tuple 密度决定并行度
-- 启用 auto 模式让数据库自动决策
SET vacuum_fork_parallel_workers = 'auto';

-- 在 postgresql.conf 中配置全局策略
-- vacuum_parallel_workers = 4  (每个 VACUUM 最多4个worker)
-- vacuum_fork_parallel_workers = 'auto'

性能数据(官方基准测试):

场景PG 16 耗时PG 17 耗时提升
100张表,每表100万行,频繁UPDATE120s35s3.4x
10张表,每表1亿行,OLTP负载480s95s5x

6.2 B-tree 索引增强

B-tree 是 PostgreSQL 中使用最广泛的索引类型,PostgreSQL 17 带来了多项增强:

反向扫描优化(Reverse Scan for B-tree)

-- 在 PostgreSQL 17 之前,以下查询无法使用索引:
SELECT * FROM orders
ORDER BY order_id DESC
LIMIT 10;

-- PostgreSQL 17 的 B-tree 反向扫描:
-- 执行计划从 "Seq Scan" 变为 "Index Scan Backward"
EXPLAIN (ANALYZE, BUFFERS)
SELECT * FROM orders
WHERE customer_id = 1001
ORDER BY order_id DESC
LIMIT 10;

-- PostgreSQL 17 输出:
-- Index Scan Backward using orders_pkey on orders
--   Index Cond: (customer_id = 1001)
--   Buffers: shared hit=12
-- Planning Time: 0.123 ms
-- Execution Time: 0.456 ms

反向扫描本质上是"从 B-tree 最右侧向左遍历",在 PostgreSQL 16 及之前,B-tree 只实现了前向扫描,反向扫描需要转换为前向扫描 + 文件排序。PostgreSQL 17 直接在 B-tree 层面支持双向遍历,性能提升显著。

BRIN 索引并行创建

-- 大型数据仓库中经常使用 BRIN 索引(Block Range Index)
-- PG 17 支持并行创建 BRIN 索引
CREATE INDEX idx_logs_created_brin ON logs USING BRIN (created_at)
    WITH (pages_per_range = 32)
    VACUUM FULL;  -- VACUUM FULL 也受益于并行化

6.3 高并发写入的 WAL 锁优化

在高并发 OLTP 场景中,WAL(Write-Ahead Logging)的锁竞争是性能瓶颈之一。PostgreSQL 17 通过细粒度 WAL 锁减少了写入冲突:

PostgreSQL 16 WAL 锁架构:
┌────────────────────────────────────────┐
│         WAL Insert Lock (单一全局锁)     │
│  所有事务写入 WAL 时竞争同一把锁          │
│  并发事务数增加 → 锁等待时间指数增长        │
└────────────────────────────────────────┘

PostgreSQL 17 WAL 锁架构:
┌────────────────────────────────────────┐
│   WAL Insert Lock 0  │  WAL Insert Lock 1 │
│   (XLogCtlData[0])  │  (XLogCtlData[1]) │
│   ┌───────────────┐  │  ┌───────────────┐ │
│   │ slot=1, lsn=X │  │  │ slot=2, lsn=Y │ │
│   └───────────────┘  │  └───────────────┘ │
│              ... N slots (可配置) ...     │
└────────────────────────────────────────┘
  事务根据 hash(correlation_id) 路由到不同 slot
  并发事务分散到不同锁,减少锁争用

配置参数:

-- 动态调整 WAL slot 数量(默认 8)
SET wal_locks_per_backend = 16;

-- 查看 WAL 锁等待事件
SELECT
    pg_stat_activity.pid,
    wait_event_type,
    wait_event,
    state,
    query
FROM pg_stat_activity
WHERE wait_event_type = 'Lock'
  AND wait_event LIKE '%WAL%';

6.4 查询优化:增量排序与 Merge Append

PostgreSQL 17 在查询执行层面也有多项优化:

-- 增量排序(Incremental Sort)增强
-- 对于分页查询,PostgreSQL 17 可以利用已有排序状态
SET enable_incremental_sort = on;

-- 示例:先按 status 过滤,再按 created_at 排序
SELECT * FROM orders
WHERE status = 'completed'
ORDER BY created_at DESC
LIMIT 20;

-- PostgreSQL 16:全表过滤后排序,O(n log n)
-- PostgreSQL 17:利用 status 上的索引(已有排序),再做增量排序
-- 对于 status='completed' 行数较少时,性能提升可达 10x

-- MERGE APPEND 性能提升(分区表查询)
-- PostgreSQL 16 在 UNION ALL 分区查询时,MERGE APPEND 效率较低
-- PostgreSQL 17 改进了合并算法,对已排序的分区可以跳过排序步骤
EXPLAIN
SELECT * FROM orders_2026_q1
UNION ALL
SELECT * FROM orders_2026_q2
UNION ALL
SELECT * FROM orders_2026_q3
ORDER BY created_at;

七、SQL 语法与 DDL 增强

7.1 MERGE RETURNING

MERGE 是 PostgreSQL 15 引入的条件插入/更新/删除语句,功能强大但之前不支持 RETURNING。PostgreSQL 17 补全了这个能力:

-- PostgreSQL 15-16:MERGE 不能返回值
MERGE INTO target_table t
USING source_table s ON t.id = s.id
WHEN MATCHED THEN
    UPDATE SET value = s.value
WHEN NOT MATCHED THEN
    INSERT (id, value) VALUES (s.id, s.value);

-- PostgreSQL 17:MERGE 支持 RETURNING
MERGE INTO user_stats AS target
USING (VALUES (1001, 1), (1002, 1)) AS source(user_id, delta)
ON target.user_id = source.user_id
WHEN MATCHED THEN
    UPDATE SET order_count = target.order_count + source.delta
WHEN NOT MATCHED THEN
    INSERT (user_id, order_count) VALUES (source.user_id, source.delta)
RETURNING
    CASE
        WHEN action = 'INSERT' THEN '新增'
        WHEN action = 'UPDATE' THEN '更新'
    END AS action_type,
    user_id,
    order_count;

-- 输出:
--  action_type | user_id | order_count
--  ------------+---------+-------------
--  新增        |    1001 |           1
--  新增        |    1002 |           1

7.2 分区表分裂与合并

PostgreSQL 17 允许在线分裂和合并分区,无需锁定整个表:

-- 将一个大分区按时间范围分裂为两个
ALTER TABLE orders SPLIT PARTITION orders_2026
    INTO (
        PARTITION orders_2026_q1 VALUES LESS THAN ('2026-04-01'),
        PARTITION orders_2026_q2plus VALUES LESS THAN (MAXVALUE)
    );

-- 合并相邻的分区
ALTER TABLE logs ATTACH PARTITION logs_2026_01
    FROM logs_2026_02;

-- 分裂操作对业务完全透明(仅元数据变更,毫秒级完成)
-- 不需要 COPY 数据,效率极高

7.3 生成列(Generated Columns)增强

生成列是从其他列计算出来的列,存储方式可以是虚拟的(不占存储空间,读取时实时计算)或持久化的(占存储空间,计算一次)。PostgreSQL 17 进一步扩展了生成列支持的表达式:

CREATE TABLE products (
    id         BIGSERIAL PRIMARY KEY,
    name       TEXT NOT NULL,
    price      NUMERIC(10,2) NOT NULL,
    tax_rate   NUMERIC(3,2) DEFAULT 0.13,
    
    -- 持久化生成列(预计算,占存储空间)
    price_with_tax NUMERIC(12,2) GENERATED ALWAYS AS (price * (1 + tax_rate)) STORED,
    
    -- PostgreSQL 17 新增:聚合生成列(跨行计算)
    -- 注意:这需要使用 IMMUTABLE 函数
    max_historical_price NUMERIC(10,2) GENERATED ALWAYS AS (
        GREATEST(price, COALESCE(lag(price) OVER (ORDER BY id), price))
    ) STORED,
    
    created_at TIMESTAMPTZ DEFAULT NOW()
);

-- 写入测试
INSERT INTO products (name, price) VALUES
    ('MacBook Pro', 14999.00),
    ('AirPods Pro', 1899.00),
    ('iPad Air', 4999.00);

-- 查询生成列
SELECT name, price, price_with_tax FROM products;
--  name      |  price  | price_with_tax
-- -----------|---------|----------------
-- MacBook Pro|  14999  |        16948.87
-- AirPods Pro|   1899  |         2145.87
-- iPad Air   |   4999  |         5648.87

7.4 读写分离一致性函数

PostgreSQL 17 引入了 pg_last_wal_receive_lsn()pg_wait_for_wal_replay() 两个函数,用于从库确认主库写入已同步:

-- 主库写入后,在从库等待确认
-- 典型场景:金融交易需要强一致性确认

-- 主库:
INSERT INTO transactions (account_id, amount) VALUES (1001, -500.00);
-- 获取写入的 WAL LSN
SELECT txid_current();  -- 返回 12345

-- 从库(等待这个事务被应用):
SELECT pg_wait_for_wal_replay(
    txid_current_snapshot()  -- 等待指定事务被 replay
);

-- 超时控制(防止主从链路断开时永久等待)
SELECT pg_wait_for_wal_replay(
    txid_current_snapshot(),
    timeout_ms => 5000  -- 5秒超时
);

八、系统管理与监控增强

8.1 等待事件视图增强

PostgreSQL 17 大幅扩展了 pg_stat_activitypg_stat_bgwriter 中的等待事件类型,新增了多个 PostgreSQL 特有的高分辨率事件:

-- 查看当前的 I/O 等待事件(PG 17 新增)
SELECT
    pid,
    usename,
    state,
    wait_event_type,
    wait_event,
    query,
    query_start
FROM pg_stat_activity
WHERE state != 'idle'
  AND wait_event_type IN ('IO', 'Lock', 'LWLock')
ORDER BY query_start;

-- 示例输出:
--  pid  | usename | state   | wait_event_type | wait_event      | query
-- ------+---------+---------+-----------------|-----------------|-------------------
-- 28451 | app_user| active  | IO              | DataFileRead     | SELECT * FROM orders
-- 28452 | app_user| active  | LWLock          | WALWriteLock     | INSERT INTO logs...
-- 28453 | app_user| active  | Lock            | transactionid    | SELECT FOR UPDATE

8.2 Login 事件触发器

PostgreSQL 17 引入了 LOGIN 事件触发器,可以追踪登录行为:

-- 创建登录审计表
CREATE TABLE login_audit (
    id          BIGSERIAL PRIMARY KEY,
    session_id  TEXT,
    username    TEXT,
    login_at    TIMESTAMPTZ DEFAULT NOW(),
    client_addr INET,
    application_name TEXT
);

-- 创建登录触发器
CREATE OR REPLACE FUNCTION on_login_audit()
RETURNS event_trigger AS $$
BEGIN
    INSERT INTO login_audit (session_id, username, client_addr, application_name)
    SELECT
        s.sessionid,
        s.usename,
        s.client_addr,
        s.application_name
    FROM pg_stat_activity s
    WHERE s.pid = (SELECT pid FROM pg_event_trigger_sessionIS_() WHERE objid IS NOT NULL);
END;
$$ LANGUAGE plpgsql;

CREATE EVENT TRIGGER login_audit_trigger
    ON LOGIN
    EXECUTE FUNCTION on_login_audit();

8.3 新增 GUC 参数一览

PostgreSQL 17 引入了大量新的配置参数,这里列出最实用的几个:

# postgresql.conf

# VACUUM 并行度(每个 VACUUM 最多使用的 worker 数)
vacuum_parallel_workers = 4

# WAL slot 粒度控制
wal_locks_per_backend = 8

# COPY 错误行容忍度(默认每段最多跳过多少行)
copy_segment_error_limit = 1000

# 增量备份相关
enable_delta_backup = on
delta_backup_checkpoint_distance = 512  # MB

# B-tree 反向扫描(默认开启)
enable_bitmapscan = on  # 与反向扫描协同优化

# 新增的统计信息收集
track_wal_io_timing = on         # 追踪 WAL I/O 延迟
track_io_timing = on             # 追踪数据文件 I/O 延迟
pg_stat_statements.track = all   # 追踪所有语句(包括嵌套调用)

九、升级指南:从 PostgreSQL 16 到 17

9.1 升级前检查清单

-- 1. 检查是否有不兼容的扩展
SELECT extname, extversion, extconfig FROM pg_extension;

-- 2. 检查是否有依赖旧版本行为的 SQL
-- 特别关注:JSON 函数、COPY 命令、VACUUM 参数

-- 3. 检查复制延迟
SELECT
    application_name,
    state,
    sent_lsn - replay_lsn AS replication_lag
FROM pg_stat_replication;

-- 4. 确认所有长事务已结束
SELECT pid, usesysid, usename, state, query
FROM pg_stat_activity
WHERE state != 'idle'
  AND query_start < NOW() - INTERVAL '10 minutes';

9.2 使用 pg_upgrade 升级

#!/bin/bash
# PostgreSQL 17 升级脚本

set -e

OLD_PGDATA="/var/lib/postgresql/16/data"
NEW_PGDATA="/var/lib/postgresql/17/data"
OLD_BIN="/usr/lib/postgresql/16/bin"
NEW_BIN="/usr/lib/postgresql/17/bin"
BACKUP_DIR="/backup/pre_upgrade_$(date +%Y%m%d)"

echo "[$(date)] 开始 PostgreSQL 升级..."

# Step 1: 备份原数据
echo "[$(date)] Step 1: 备份数据目录到 ${BACKUP_DIR}"
mkdir -p "${BACKUP_DIR}"
cp -a "${OLD_PGDATA}" "${BACKUP_DIR}/"

# Step 2: 初始化 PostgreSQL 17 数据目录
echo "[$(date)] Step 2: 初始化 PG 17 数据目录"
"${NEW_BIN}/initdb" -D "${NEW_PGDATA}"

# Step 3: 运行 pg_upgrade
echo "[$(date)] Step 3: 执行 pg_upgrade(链接模式,性能更快)"
"${NEW_BIN}/pg_upgrade" \
    --old-datadir "${OLD_PGDATA}" \
    --new-datadir "${NEW_PGDATA}" \
    --old-bindir "${OLD_BIN}" \
    --new-bindir "${NEW_BIN}" \
    --link-mode \
    --jobs 8 \
    --check  # 先 check 模式验证,不实际升级

echo "[$(date)] 验证完成,如无误请去掉 --check 重新运行"

9.3 升级后的推荐配置调整

-- 升级完成后,在 PostgreSQL 17 中执行

-- 重建无效的统计信息
ANALYZE;

-- 重新构建所有索引(可选,但推荐)
-- PostgreSQL 17 的 B-tree 反向扫描需要新格式的索引
-- 对于大型数据库,建议在维护窗口内执行:
DO $$
DECLARE
    r RECORD;
BEGIN
    FOR r IN SELECT tablename FROM pg_tables WHERE schemaname = 'public'
    LOOP
        EXECUTE format('REINDEX TABLE %I', r.tablename);
    END LOOP;
END $$;

-- 验证 JSONPath 功能
SELECT jsonb_path_exists('{"a":[1,2,3]}'::JSONB, '$.a[*] ? (@ > 1)');

-- 检查复制状态
SELECT * FROM pg_stat_subscription;
SELECT * FROM pg_statReplication;

十、总结:PostgreSQL 17 的核心价值

PostgreSQL 17 不是一个"小步快跑"的版本更新,而是从数据库内核多个层面进行了系统性升级。回顾全文,我认为这五个方向的改进影响最为深远:

第一,JSON_TABLE 的引入。它让 PostgreSQL 在 JSON 处理能力上终于追平了竞争对手,更重要的是,JSON_TABLE 投影出来的数据可以直接参与 JOIN、聚合、子查询,这意味着"文档数据库"和"关系数据库"的界限正在模糊。对于业务中大量使用 JSONB 字段的开发者来说,这可能是影响日常开发效率最大的改进。

第二,增量备份 + TIDStore。对于 TB 级数据库,增量备份将备份窗口从小时级压缩到分钟级,配合 TIDStore 对 VACUUM 性能的提升(3-10 倍),数据库运维的压力将显著降低。这两个改进结合起来,对于大规模 PostgreSQL 集群的运维团队来说是实打实的减负。

第三,逻辑复制 DDL 支持 + 自动冲突检测。这让"双活"架构从理论走向实践。以往构建双活架构最大的障碍——DDL 无法同步和写入冲突无法自动处理——在 PostgreSQL 17 中都有了标准化的解决方案。

第四,COPY 错误处理ON SEGMENT ERROR 的引入让 ETL 管道从"要么全成功要么全失败"变成了"能导多少导多少,错的记录下来",这是数据工程领域等待了多年的实用功能。

第五,B-tree 反向扫描 + WAL 锁优化。这些底层改进虽然不如 JSON_TABLE 那样有话题性,但它们每天都在所有 PostgreSQL 数据库上发挥作用——更快的索引扫描、更低的事务锁争用、更高效的并发处理,这些改进积累起来,对 OLTP 性能的影响是持续且深远的。

PostgreSQL 正在走一条独特的路:不是追求某个单点功能的"最强大",而是在保持关系型数据库本质的同时,逐年弥合与现代数据处理需求之间的差距。JSON_TABLE、增量备份、逻辑复制 DDL——这些功能单独看都不算出奇,但它们组合在一起,让 PostgreSQL 成为了一个真正意义上的"全能型数据库"。

如果你还在使用 PostgreSQL 14 或更早的版本,PostgreSQL 17 的这些改进值得你认真考虑一次版本升级。如果你已经是 PostgreSQL 16 或 17 的用户,这些新功能值得你在下一个项目中尝试——特别是 JSON_TABLE 和 COPY 错误处理,它们能立刻改善你的开发体验。


参考资料

  • PostgreSQL 17 官方发布说明:https://www.postgresql.org/docs/17/release-17.html
  • PostgreSQL 17 JSON 函数文档:https://www.postgresql.org/docs/17/functions-json.html
  • PostgreSQL 17 备份恢复文档:https://www.postgresql.org/docs/17/backup.html
  • pg_delta_backup 工具文档:https://www.postgresql.org/docs/17/pg-deltarestore.html
  • JSON_TABLE 标准规范(SQL:2023):https://developer.mimer.com/sql-standard/sql-standard-2023/

推荐文章

Roop是一款免费开源的AI换脸工具
2024-11-19 08:31:01 +0800 CST
jQuery `$.extend()` 用法总结
2024-11-19 02:12:45 +0800 CST
Vue3中的虚拟滚动有哪些改进?
2024-11-18 23:58:18 +0800 CST
如何实现虚拟滚动
2024-11-18 20:50:47 +0800 CST
Python 基于 SSE 实现流式模式
2025-02-16 17:21:01 +0800 CST
介绍Vue3的Tree Shaking是什么?
2024-11-18 20:37:41 +0800 CST
deepcopy一个Go语言的深拷贝工具库
2024-11-18 18:17:40 +0800 CST
Vue3结合Driver.js实现新手指引功能
2024-11-19 08:46:50 +0800 CST
JS 箭头函数
2024-11-17 19:09:58 +0800 CST
PHP 允许跨域的终极解决办法
2024-11-19 08:12:52 +0800 CST
HTML5的 input:file上传类型控制
2024-11-19 07:29:28 +0800 CST
Nginx rewrite 的用法
2024-11-18 22:59:02 +0800 CST
html一个包含iPhoneX和MacBook模拟器
2024-11-19 08:03:47 +0800 CST
程序员茄子在线接单