Postgres 18 于 2025 年 9 月 25 日发布,带来了多项性能增强和新功能。随着版本迭代,Postgres 在关键业务与非关键业务场景中均表现出更高的稳健性、可靠性和响应能力。
Postgres 18 包含多项实用增强特性,此前已被关注的异步 I/O(AIO)子系统便是重要性能优化之一。该特性能够提升顺序扫描、位图堆扫描和 VACUUM 操作期间的 I/O 吞吐量,可为多数 Postgres 用户带来性能提升。在 Linux 系统(借助 io_uring)上,通过将磁盘访问与处理过程重叠,可实现 2-3 倍的性能提升,更多细节可参考博客链接:
在众多更新中,增强的 RETURNING 子句与 Skip Scan 优化对实际应用场景尤为重要。这两项功能进一步提升查询性能、优化 SQL 编写体验,并降低应用侧的复杂度,无需进行 schema 调整或复杂调优。
RETURNING子句增强 :在INSERT、UPDATE、DELETE与MERGE语句中,可同时访问OLD与NEW行值,适用于审计、API 返回、ETL 等场景,有助于减少往返、提升原子性并保持 SQL 的自包含性。- Skip Scan 优化 :使查询在未过滤前导列时仍可高效利用多列 B-tree 索引,可显著提升分析型查询与报表查询的性能,无需额外创建索引。
两项功能体现了 Postgres 18 在智能性能与简化开发方面的设计理念。Skip Scan 功能由核心贡献者 Peter Geoghegan 开发,展示了社区对代码质量与审查流程的严格要求。
理解最左索引问题
B-tree Skip Scan 是 Postgres 18 最受关注的优化之一,用于解决多年来限制多列 B-tree 索引使用的"最左索引"问题。
在此前版本中,多列 B-tree 索引的最优使用依赖于查询必须包含前导列的过滤条件。索引结构按照前导列优先排序,再按第二列排序,以此类推。
例如,多列索引 (status, customer_id, order_date) 的叶子节点按字典序存储。
arduino
('active',101,'2024-01-01')
('active',101,'2024-01-15')
('active',102,'2024-01-03')
('pending',101,'2024-01-10')
('pending',103,'2024-01-20')
('shipped',101,'2024-01-05')
...
查询若包含 status = 'active' AND customer_id = 101,会触发连续范围扫描,效率极高。但若只过滤 customer_id = 101 而忽略 status,则索引中的匹配项会分散在不同的 status 值下,规划器通常会选择顺序扫描或使用其他索引,使该多列索引无法发挥作用。
这使得实际应用中常需要按不同列顺序创建多个索引,导致:
- 存储占用增加
- 写入性能降低
- 索引维护成本提升
Skip Scan 解决方案
Postgres 18 在 B-tree 索引中引入 Skip Scan 功能,使查询规划器能够在前导列缺少等值条件时仍然使用多列索引。该能力消除了索引因未过滤首列而被闲置的情况,使原本可用的索引得以重新发挥作用。
Skip Scan 优化的核心是让 Postgres 智能 "跳过" 索引的部分区域以查找相关数据。当查询索引中靠后的列而未指定前导列时,Postgres 可实现以下操作:
- 识别被省略前导列中的所有 distinct 值。
- 将查询逻辑有效转换为包含这些前导列匹配条件的等价形式。
- 利用既有索引基础设施,在扫描过程中跨前导列执行优化查找,跳过与查询条件不匹配的索引页。
该功能对于分析型与报表型工作负载尤为重要,因为此类场景经常需要基于不同字段组合执行查询,而无需始终指定索引的前导列。
Skip Scan 的底层工作原理
以下示例展示 skip scan 的典型应用场景。存在一张 orders 表,并创建了多列 B-tree 索引:
sql
CREATE TABLE orders (
order_id SERIAL PRIMARY KEY,
status VARCHAR(20),
customer_id INTEGER,
order_date DATE,
amount DECIMAL(10,2));
CREATE INDEX idx_orders ON orders(status,customer_id,order_date);
在 Postgres 18 以前,执行如下查询:
sql
SELECT * FROM orders
WHERE customer_id = 123
AND order_date > '2025-01-01';
由于谓词未包含索引的前导列 status,该索引通常无法被有效利用,执行计划往往退化为顺序扫描。
Postgres 18 引入 skip scan 后,多列索引在前导列缺失过滤条件的情况下仍可发挥作用。查询优化过程中,会对status 列的全部不同取值进行识别(如 pending、active、shipped),随后基于每个取值与 customer_id、order_date 的组合执行定向索引扫描。逻辑等价形式如下:
sql
SELECT * FROM orders WHERE status = 'pending' AND customer_id = 123 AND order_date > '2025-01-01'
UNION ALL
SELECT * FROM orders WHERE status = 'active' AND customer_id = 123 AND order_date > '2025-01-01'
UNION ALL
SELECT * FROM orders WHERE status = 'shipped' AND customer_id = 123 AND order_date > '2025-01-01';
当前导列的基数较低时,逐一扫描其不同取值的代价显著低于顺序扫描,因此 skip scan 在此类场景中能够实现更优的性能表现。查询优化器在执行计划生成阶段会自动评估此策略的收益,并选择最合适的执行方式。
Skip Scan 的适用场景
Skip scan 在以下场景中性能优势最为突出:
- 前导列低基数:当省略的前导列具有低基数时,优化效果最显著。例如,status 列仅包含 3--5 个不同取值时,skip scan 能够高效执行;若 distinct 值达到数千,则性能提升明显下降。
- 后续列等值条件:Skip scan 针对索引中后续列被等值引用的情况进行了优化,当前实现针对这些特定模式进行高效处理。
- 分析与报表型工作负载:在需要灵活组合不同索引列进行查询的分析场景中,skip scan 能显著提高性能。这类场景常见于商业智能工具及临时报表查询。
- 避免索引泛滥:无需为不同列顺序创建多个索引,可依靠单个设计合理的多列索引,通过 skip scan 实现高效查询。
重要限制与注意事项
Skip scan 功能虽强大,但存在以下当前限制:
- 仅支持 B-tree 索引 :Skip scan 目前仅适用于 B-tree 索引,这是最常用的索引类型。
- 性能依赖基数:随着被省略列的 distinct 值数量增加,性能提升会显著下降。对于高基数的前导列,仍可能需要专门索引以保证性能。
- 需等值条件:Skip scan 至少要求索引中后续列包含一个等值条件。对于任意范围或复杂谓词的后续列,不可期望该功能带来优化效果。
- 大数据集结果 :对于返回大量结果的查询,传统的位图扫描或顺序扫描计划可能仍然是更优选择。
实用示例与性能分析
通过一个更详细的示例说明 skip scan 的应用。创建一张 sales 表,数据分布贴近实际场景:
sql
-- Create the sales table
CREATE TABLE sales (
sale_id SERIAL PRIMARY KEY,
region VARCHAR(20),
product_category VARCHAR(50),
sale_date DATE,
amount DECIMAL(10,2)
);
-- Create multicolumn index
CREATE INDEX idx_sales_region_category_date
ON sales (region, product_category, sale_date);
-- Insert sample data
INSERT INTO sales (region, product_category, sale_date, amount)
SELECT
CASE (random() * 4)::int
WHEN 0 THEN 'North'
WHEN 1 THEN 'South'
WHEN 2 THEN 'East'
ELSE 'West'
END,
'Category_' || (random() * 20)::int,
'2024-01-01'::date + (random() * 365)::int,
(random() * 1000)::numeric(10,2)
FROM generate_series(1, 1000000);
ANALYZE sales;
在 Postgres 17 中,按 product_category 查询而未指定 region 列:
sql
EXPLAIN ANALYZE
testdb-# SELECT * FROM sales
testdb-# WHERE product_category = 'Category_5'
testdb-# AND sale_date > '2024-06-01';
QUERY PLAN
---------------------------------------------------------------------------------------------------------------------------
Gather (cost=1000.00..18244.90 rows=29289 width=30) (actual time=0.382..47.816 rows=29343 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Parallel Seq Scan on sales (cost=0.00..14316.00 rows=12204 width=30) (actual time=0.015..29.794 rows=9781 loops=3)
Filter: ((sale_date > '2024-06-01'::date) AND ((product_category)::text = 'Category_5'::text))
Rows Removed by Filter: 323552
Planning Time: 0.216 ms
Execution Time: 48.527 ms
(8 rows)
在 Postgres 17 中,由于未指定前导列 region,该查询会执行顺序扫描。Postgres 18 中,skip scan 可以高效利用索引,对 region 的四个不同值依次进行扫描,并执行定向查找。
同一查询在 Postgres 18 中执行如下:
ini
EXPLAIN ANALYZE
postgres-#SELECT *FROM sales
postgres-#WHERE product_category='Category_5'
postgres-#AND sale_date>'2024-06-01';
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------
Bitmap Heap Scan on sales(cost=457.63..8955.11rows=28832width=30)(actual time=2.671..11.931 rows=29202.00 loops=1)
RecheckCond(((product_category)::text='Category_5'::text)AND (sale_date>'2024-06-01'::date))
Heap Blocks: exact=7850 Buffers:shared hit=7917
->Bitmap Index Scan on idx_sales_region_category_date (cost=0.00..450.43rows=28832width=0)(actual time=1.916..1.917rows=29202.00loops=1)
Index Cond:(((product_category)::text = 'Category_5'::text)AND (sale_date>'2024-06-01'::date))
Index Searches:9
Buffers:sharedhit=67
Planning:
Buffers:shared hit=45 read=1
PlanningTime:0.189ms
ExecutionTime:12.801ms
(12rows)
执行计划显示 skip scan 正在发挥作用,相较顺序扫描,缓冲区读取显著减少,执行时间得到明显优化。
配置与调优
Postgres 18 将 skip scan 功能纳入查询规划器工具集。查询规划器会基于成本估算自动决定何时使用 skip scan。
与其他规划器优化类似,Postgres 提供通过配置启用或禁用 skip scan 的灵活性,但在正常运行中,应依赖统计信息和成本估算由规划器自动选择最优策略。
展望未来
Skip scan 功能在查询优化和索引利用方面迈出了重要一步,体现了社区在持续提升性能的同时,保持 Postgres 高可靠性和稳健性的承诺。
该功能解决了长期存在的索引使用痛点。通过实现多列索引的更灵活使用,skip scan 简化了数据库设计,降低了存储开销,并在广泛场景中提升查询性能。
随着 Postgres 的持续发展,skip scan 及其他查询优化能力预计将进一步增强。Postgres 18 打下的基础,有望在未来版本中扩展至更复杂的查询模式和更多类型的索引支持。
结论
Postgres 18 的 B-tree skip scan 功能解决了多列索引长期存在的可用性限制。在省略最左前缀列时,多列 B-tree 不再是"全有或全无"。对于特定工作负载------前导列基数低且后续列有等值条件------可以在无需创建额外索引的情况下充分发挥索引效能。
Postgres 社区在每一次版本迭代中持续提升数据库性能、可扩展性和企业级适用性。Skip scan 是 Postgres 18 中众多改进之一,共同增强了数据库对现代应用工作负载的支持能力。
在 18 版本之后,Postgres 将继续发展和优化,包括更多查询优化功能、更完善的分析型工作负载支持,以及持续关注性能与可扩展性。Skip scan 等功能体现了社区对用户需求的响应及对实际场景挑战的解决。
对于使用 Postgres 的数据库管理员和开发者,skip scan 简化了索引管理,并提升了查询性能。在规划升级至 Postgres 18 时,可审视现有多列索引,并识别可利用 skip scan 优化的查询,发现合并索引和提升整体数据库性能的机会。
原文链接:
作者:Ahsan Hadi