PostgreSQL 18: SQL 语言函数支持计划缓存

John Doe 九月 8, 2025

在数据库性能优化领域,计划缓存(Plan Cache)的高效利用是提升 SQL 执行效率的关键环节之一。

image

特性提交日志

将 SQL 语言函数改为使用计划缓存。

在 SQL 函数的历史实现中(若未被内联),我们会在外部查询内首次调用函数时,为函数包含的所有查询生成执行计划,之后在该外部查询的生命周期内复用这些计划,查询结束后则完全丢弃所有计划。这种方式存在明显不足,尤其是生成的计划无法根据函数参数的特定值进行定制优化。而我们的计划缓存(plan cache)基础设施现已足够成熟,可用于解决这一问题。这一改进不仅能解决无法生成定制化计划的问题,还能解决连续多个外部查询间无法共享执行计划的问题。

除了性能层面的优化,此次修改还修复了 SQL 函数长期存在的一个棘手问题:此前无法在 SQL 函数中编写能影响后续语句的 DDL(数据定义语言)。对于新风格的 SQL 函数,这一限制在很大程度上仍未改变,因为语法分析结果会嵌入到存储的查询树中(并受依赖记录保护);但对于老风格的 SQL 函数,现在其行为将与 PL/pgSQL 函数类似,我们会将每个查询的语法分析和计划生成延迟到即将执行该查询时进行,因此 DDL 对后续语句的影响可正常生效。

讨论:https://postgr.es/m/8216639.NyiUUSuA9g@aivenlaptop

示例

在 PostgreSQL 数据库的演进中,SQL 语言函数作为轻量级逻辑封装工具,被广泛用于简化重复查询、统一业务逻辑。然而,其历史实现中存在跨查询重复生成执行计划、DDL 语句协作困难等问题。

本次提交(将 SQL 函数接入计划缓存)彻底改变了这一现状,让我们通过实际用例来看看。

某电商平台需通过 SQL 函数get_user_orders(user_id int, limit_num int)查询指定用户的订单,用户订单量差异极大(如普通用户仅 10 条,VIP 用户超 10 万条)。

计划缓存支持根据user_id参数动态生成定制计划:

-- 创建SQL函数:查询指定用户的订单
CREATE OR REPLACE FUNCTION get_user_orders(
    p_user_id INT, 
    p_limit INT
) RETURNS TABLE (order_id INT, create_time TIMESTAMP) AS $$
    SELECT order_id, create_time 
    FROM orders 
    WHERE user_id = p_user_id  -- 参数化条件
    LIMIT p_limit;
$$ LANGUAGE SQL;

-- 首次调用(VIP用户,p_user_id=1001,订单量10万+)
-- 计划缓存生成“全表扫描”
SELECT * FROM get_user_orders(1001, 50);

-- 二次调用(普通用户,p_user_id=2002,订单量10条)
-- 计划缓存检测参数差异,生成“索引扫描(user_id索引)”
SELECT * FROM get_user_orders(2002, 50);

计划缓存跨外部查询复用,仅首次调用生成计划,后续调用既能直接复用,也能定制优化。

另外,在 PostgreSQL 之前的版本中,无法在 SQL 函数中编写能影响后续语句的 DDL。

-- 历史版本中报错的函数
CREATE OR REPLACE FUNCTION create_temp_report(p_date DATE) 
RETURNS TABLE (id INT, sales NUMERIC) AS $$
    -- 步骤1:创建临时表
    CREATE TEMP TABLE temp_sales (id INT, sales NUMERIC);

    -- 步骤2:插入数据(历史版本解析时,temp_sales尚未存在,报错)
    INSERT INTO temp_sales 
    SELECT id, SUM(amount) 
    FROM orders 
    WHERE order_date = p_date 
    GROUP BY id;

    -- 步骤3:返回结果
    SELECT * FROM temp_sales;
$$ LANGUAGE SQL;

在上面的示例中,由于 SQL 函数在执行前会解析所有语句,CREATE TEMP TABLE尚未执行时,INSERT语句已被解析,导致产生 “表不存在” 的错误:

SELECT * FROM create_temp_report('2025-04-01');
ERROR: relation "temp_sales" does not exist

在 PostgreSQL 新版本中,SQL 函数会延迟解析和生成计划,CREATE TEMP TABLE执行后,INSERT语句才解析,所以可以成功执行。

非常不错的体验,感谢所有参与的社区人员。

参考

提交日志:https://git.postgresql.org/pg/commitdiff/0dca5d68d7bebf2c1036fd84875533afef6df992