PL/pgSQL分支函数能否采用合理查询计划?PostgreSQL技术咨询
我正尝试实现带可选参数的PostgreSQL函数,当前方案是先实现带必填参数的STABLE类型SQL函数,再通过简单的IF/ELSE分支逻辑调用对应函数。
初始化代码
CREATE TABLE IF NOT EXISTS test_tab1( field1 int PRIMARY KEY, field2 int ); CREATE TABLE IF NOT EXISTS test_tab2( field1 int PRIMARY KEY, field2 int ); INSERT INTO test_tab1 SELECT FLOOR(RANDOM()*10000), FLOOR(RANDOM()*10000) FROM GENERATE_SERIES(1,10); INSERT INTO test_tab2 SELECT * FROM test_tab1; CREATE OR REPLACE FUNCTION test_tab1_func() RETURNS SETOF test_tab1 LANGUAGE sql STABLE AS $$ SELECT * FROM test_tab1; $$; CREATE OR REPLACE FUNCTION test_tab2_func() RETURNS SETOF test_tab1 LANGUAGE sql STABLE AS $$ SELECT * FROM test_tab2; $$;
待验证的目标函数
CREATE OR REPLACE FUNCTION test_func(foo int DEFAULT null) RETURNS SETOF test_tab1 LANGUAGE plpgsql STABLE AS $$ BEGIN IF (foo IS null) THEN RETURN QUERY SELECT * FROM test_tab1_func(); ELSE RETURN QUERY SELECT * FROM test_tab2_func(); END IF; END; $$; EXPLAIN SELECT * FROM test_func();
执行上述EXPLAIN语句返回:
Function Scan on test_func (cost=0.25..10.25 rows=1000 width=8)
我的复杂查询实验显示,该Function Scan估算不够精准,无论函数体复杂度如何,结果基本一致。由于EXPLAIN输出缺乏有效信息,特咨询:
- 既然数据库能为
test_tab1_func和test_tab2_func生成查询计划,当foo为null时,test_func是否会采用test_tab1_func的常规查询计划?当foo非null时,是否会采用test_tab2_func的常规查询计划? - 若不能,有没有更优的分支函数实现方式,让PostgreSQL更容易生成合理查询计划?
关于查询计划的复用问题
不会。PL/pgSQL函数是黑盒执行的,外层查询优化器无法穿透函数内部逻辑去复用内部SQL函数的查询计划。调用test_func时,优化器只能识别这是一个函数调用,会使用默认的函数扫描成本估算(也就是你看到的固定cost=0.25..10.25 rows=1000),不会分析内部分支里调用的具体函数的执行计划。
内部的test_tab1_func和test_tab2_func确实会各自生成独立的查询计划,但这些计划是在test_func执行到对应分支时才会生成并执行,外层优化器完全感知不到。
更优的实现方式
要让优化器生成精准的查询计划,需要让分支逻辑对优化器可见,推荐两种方案:
1. 使用SQL函数+条件分支
把分支逻辑直接写在SQL函数里,优化器可以直接解析整个逻辑,根据参数值生成对应最优计划:
CREATE OR REPLACE FUNCTION test_func(foo int DEFAULT null) RETURNS SETOF test_tab1 LANGUAGE sql STABLE AS $$ SELECT * FROM test_tab1 WHERE foo IS NULL UNION ALL SELECT * FROM test_tab2 WHERE foo IS NOT NULL; $$;
也可以用CASE表达式实现:
CREATE OR REPLACE FUNCTION test_func(foo int DEFAULT null) RETURNS SETOF test_tab1 LANGUAGE sql STABLE AS $$ SELECT * FROM CASE WHEN foo IS NULL THEN test_tab1 ELSE test_tab2 END; $$;
这种方式下,调用test_func()或test_func(1)时,优化器会根据参数值直接消除无效分支,生成和直接调用test_tab1_func/test_tab2_func完全一致的查询计划,成本估算也会精准匹配实际数据量。
2. 使用函数重载
为不同参数情况定义独立函数,PostgreSQL会根据调用时的参数自动匹配对应函数:
-- 无参数(对应foo为null的情况) CREATE OR REPLACE FUNCTION test_func() RETURNS SETOF test_tab1 LANGUAGE sql STABLE AS $$ SELECT * FROM test_tab1; $$; -- 带参数(对应foo非null的情况) CREATE OR REPLACE FUNCTION test_func(foo int) RETURNS SETOF test_tab1 LANGUAGE sql STABLE AS $$ SELECT * FROM test_tab2; $$;
这种方式下,每个函数的查询计划都是独立优化的,调用时直接匹配对应函数,完全避免分支逻辑带来的黑盒问题,成本估算精准。
内容的提问来源于stack exchange,提问作者Him

