由 John Doe 十二月 1, 2025
PostgreSQL 一直都能够内联简单的 SQL 函数。如果你编写CREATE FUNCTION ... LANGUAGE SQL AS 'SELECT ...'这样的语句,规划器通常会拆解函数调用,并将原始 SQL 直接嵌入到主查询中。它意味着WHERE子句可以被下推,索引能够被正常使用,查询性能也因此表现优异。

但一旦超出简单 SQL 函数的范畴(例如使用 C 扩展或复杂动态 SQL),查询往往会陷入黑盒困境:规划器只能识别到函数本身,会盲目执行函数,之后才对结果应用筛选条件。
特性提交日志
添加 SupportRequestInlineInFrom 规划器支持请求。
该请求允许支持函数将出现在 FROM 子句中的函数调用(通常是返回集合的函数)替换为等效的 SELECT 子查询。该子查询随后将接受规划器常规的优化处理,从而有可能生成更优的执行计划。尽管规划器长期以来已能对简单的 SQL 语言函数自动执行此操作,但现在扩展也可对该类别之外的函数执行此优化。值得注意的是,对于目前通过生成并执行 SQL 查询来实现的 PL/pgSQL 函数而言,此功能可能会发挥重要作用。
讨论:https://postgr.es/m/09de6afa-c33d-4d94-a5cb-afc6cea0d2bb@illuminatedcomputing.com
示例
假设你有一个返回大量数据的函数,但你实际只需要其中几行数据,例如执行下面的查询:
SELECT * FROM my_complex_function() WHERE id = 42;
在 PostgreSQL 19 之前,如果my_complex_function不是简单的 SQL 函数,数据库会按以下步骤执行:
- 完整执行
my_complex_function()(例如生成 100 万行数据); - 对结果进行筛选,找到
id = 42的行。
这种方式效率极低:你本希望使用索引扫描,最终却不得不先完整执行函数,再对结果进行筛选。
借助本次提交的特性,PostgreSQL 现在允许函数开发者(尤其是 C 扩展开发者)提供一个支持函数,并通过该函数告知规划器:在 FROM 子句中被调用到的时候,这是能代表该函数内部逻辑的原始 SQL 查询树。
之后,规划器就可以像处理标准 SQL 函数一样,对这个查询树进行内联操作。
这一特性将为以下场景带来革命性改变:
- 扩展:许多扩展提供类表接口(如外部数据包装器、集合返回函数),现在它们可以向规划器暴露内部逻辑;
- 动态 SQL:生成 SQL 字符串并执行的函数,如今也有可能参与查询优化。
测试场景:创建一个 PL/pgSQL 函数foo_from_bar,该函数对包含 100 万行数据的text_tbl表执行动态查询。
首先准备测试数据与函数:
CREATE TABLE text_tbl (f1 text);
INSERT INTO text_tbl SELECT 'row_' || generate_series(1, 1000000);
INSERT INTO text_tbl SELECT 'common_row' FROM generate_series(1, 100);
CREATE INDEX text_tbl_idx ON text_tbl(f1);
ANALYZE text_tbl;
以下是用于测试的 PL/pgSQL 函数(设计目标为返回 100 行数据)。注意,该函数使用动态 SQL(规划器通常无法“看透”动态 SQL),并通过ALTER FUNCTION语句将其与 C 支持函数test_inline_in_from_support_func关联:
CREATE OR REPLACE FUNCTION foo_from_bar(colname TEXT, tablename TEXT, filter TEXT)
RETURNS SETOF TEXT
LANGUAGE plpgsql
AS $function$
DECLARE
sql TEXT;
BEGIN
sql := format('SELECT %I::text FROM %I', colname, tablename);
IF filter IS NOT NULL THEN
sql := CONCAT(sql, format(' WHERE %I::text = $1', colname));
END IF;
RETURN QUERY EXECUTE sql USING filter;
END;
$function$ STABLE;
ALTER FUNCTION foo_from_bar(TEXT, TEXT, TEXT)
SUPPORT test_inline_in_from_support_func;
执行的测试查询如下:
SELECT * FROM foo_from_bar('f1', 'text_tbl', 'common_row');
PostgreSQL 18 中的执行计划如下:
-> Function Scan on public.foo_from_bar (cost=0.25..10.25 rows=1000 width=32)
Output: foo_from_bar
Function Call: foo_from_bar('f1'::text, 'text_tbl'::text, 'common_row'::text)
Buffers: shared hit=32 read=4
Execution Time: 0.353 ms
- 函数被视为黑盒;
- 规划器默认估计返回 1000 行数据;
- 需扫描函数的完整输出后再筛选。
PostgreSQL 19 中的执行计划如下:
-> Index Only Scan using text_tbl_idx on public.text_tbl (cost=0.42..8.44 rows=1 width=10)
Output: text_tbl.f1
Index Cond: (text_tbl.f1 = 'common_row'::text)
Buffers: shared hit=1 read=3
Execution Time: 0.064 ms
- 规划器理解了函数内部逻辑,识别出其本质是对
text_tbl表的查询; - 直接使用索引进行扫描;
- 执行时间大幅缩短,效率提升约 5.5 倍。
接下来,测试向函数传递NULL(使其返回全部 100 万行数据),并在外部添加WHERE筛选条件:
SELECT * FROM foo_from_bar('f1', 'text_tbl', NULL) WHERE foo_from_bar = 'common_row';
PostgreSQL 18 中的执行计划如下:
-> Function Scan on public.foo_from_bar (cost=0.25..12.75 rows=5 width=32)
Output: foo_from_bar
Function Call: foo_from_bar('f1'::text, 'text_tbl'::text, NULL::text)
Filter: (foo_from_bar.foo_from_bar = 'common_row'::text)
Rows Removed by Filter: 1000000
Execution Time: 136.267 ms
数据库先扫描函数返回的全部 100 万行数据,再筛选出 100 行目标结果。
PostgreSQL 19 中的执行计划如下:
-> Index Only Scan using text_tbl_idx on public.text_tbl (cost=0.42..8.44 rows=1 width=10)
Output: text_tbl.f1
Index Cond: (text_tbl.f1 = 'common_row'::text)
Execution Time: 0.055 ms
规划器将外部的WHERE子句下推到了函数内部逻辑中,直接触发了索引查找,效率提升约 2400 倍!
本示例中使用的 C 支持函数(test_inline_in_from_support_func)定义在 PostgreSQL 源码的src/test/regress/regress.c文件中,该文件属于回归测试模块。如需了解更多关于支持函数的信息,可参考 PostgreSQL文档中的函数优化信息部分。
非常不错的新特性,感谢社区的所有相关人员。
参考
提交日志:https://git.postgresql.org/pg/commitdiff/b140c8d7a3f3a5de4e4cc4a0b52909aa13060d4c