Postgres 18 于 2025 年 9 月 25 日发布,带来了多项性能增强和新功能。随着版本迭代,Postgres 在关键业务与非关键业务场景中均表现出更高的稳健性、可靠性和响应能力。
Postgres 18 包含多项实用增强特性,此前已被关注的异步 I/O(AIO)子系统便是重要性能优化之一。该特性能够提升顺序扫描、位图堆扫描和 VACUUM 操作期间的 I/O 吞吐量,可为多数 Postgres 用户带来性能提升。在 Linux 系统(借助 io_uring)上,通过将磁盘访问与处理过程重叠,可实现 2-3 倍的性能提升,更多细节可参考博客链接:
https://www.pgedge.com/blog/highlights-of-postgresql-18
在众多更新中,增强的 RETURNING 子句与 Skip Scan 优化对实际应用场景尤为重要。这两项功能进一步提升查询性能、优化 SQL 编写体验,并降低应用侧的复杂度,无需进行 schema 调整或复杂调优。
RETURNING 子句增强:在
INSERT、
UPDATE、
DELETE 与
MERGE 语句中,可同时访问
OLD 与
NEW 行值,适用于审计、API 返回、ETL 等场景,有助于减少往返、提升原子性并保持 SQL 的自包含性。两项功能体现了 Postgres 18 在智能性能与简化开发方面的设计理念。Skip Scan 功能由核心贡献者 Peter Geoghegan 开发,展示了社区对代码质量与审查流程的严格要求。
B-tree Skip Scan 是 Postgres 18 最受关注的优化之一,用于解决多年来限制多列 B-tree 索引使用的“最左索引”问题。
在此前版本中,多列 B-tree 索引的最优使用依赖于查询必须包含前导列的过滤条件。索引结构按照前导列优先排序,再按第二列排序,以此类推。
例如,多列索引 (
status,
customer_id,
order_date) 的叶子节点按字典序存储。
('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 值下,规划器通常会选择顺序扫描或使用其他索引,使该多列索引无法发挥作用。
这使得实际应用中常需要按不同列顺序创建多个索引,导致:
Postgres 18 在 B-tree 索引中引入 Skip Scan 功能,使查询规划器能够在前导列缺少等值条件时仍然使用多列索引。该能力消除了索引因未过滤首列而被闲置的情况,使原本可用的索引得以重新发挥作用。
Skip Scan 优化的核心是让 Postgres 智能 “跳过” 索引的部分区域以查找相关数据。当查询索引中靠后的列而未指定前导列时,Postgres 可实现以下操作:
该功能对于分析型与报表型工作负载尤为重要,因为此类场景经常需要基于不同字段组合执行查询,而无需始终指定索引的前导列。
以下示例展示 skip scan 的典型应用场景。存在一张 orders 表,并创建了多列 B-tree 索引:
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 以前,执行如下查询:
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 的组合执行定向索引扫描。逻辑等价形式如下:
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 功能虽强大,但存在以下当前限制:
通过一个更详细的示例说明 skip scan 的应用。创建一张
sales 表,数据分布贴近实际场景:
-- 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 列:
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 中执行如下:
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 优化的查询,发现合并索引和提升整体数据库性能的机会。
原文链接:
https://www.pgedge.com/blog/postgres-18-skip-scan-breaking-free-from-the-left-most-index-limitation
作者:Ahsan Hadi
来自 “ ITPUB博客 ” ,链接:https://blog.itpub.net/70043484/viewspace-3104367/ ,如需转载,请注明出处,否则将追究法律责任。