PostgreSQL offset 原理,及使用注意事项
背景
使用PostgreSQL查询记录时,可以使用offset跳过一些记录,从跳跃点开始取后面的数据。
但是这里有一个问题是这样的:
postgres=# create or replace function f() returns void as $$
declare
begin
raise notice 'called';
end;
$$ language plpgsql strict volatile;
CREATE FUNCTION
虽然我在这里跳跃了3条记录,但是f()还是在offset的过程中被调用了。
理论上offset跳过的部分是不应该被调用的。
postgres=# select f(),* from ( values(1),(2),(3),(4),(5),(6) ) t(id) offset 3 limit 2;
NOTICE: called
NOTICE: called
NOTICE: called
NOTICE: called
NOTICE: called
f | id
---+----
| 4
| 5
(2 rows)
postgres=# alter function f() stable;
ALTER FUNCTION
postgres=# select f(),* from ( values(1),(2),(3),(4),(5),(6) ) t(id) offset 3 limit 2;
NOTICE: called
NOTICE: called
NOTICE: called
NOTICE: called
NOTICE: called
f | id
---+----
| 4
| 5
(2 rows)
如果使用where条件过滤的行,是绝对不会被调用的。
postgres=# select f(),* from ( values(1),(2),(3),(4),(5),(6) ) t(id) where id=1 limit 5;
NOTICE: called
f | id
---+----
| 1
(1 row)
另外把函数改成immutable的话,优化器会把这个函数当成一个常量来处理,在生成执行计划前就常量化掉了,所以怎么样都只会调用一次。
postgres=# alter function f() immutable;
ALTER FUNCTION
postgres=# select f(),* from ( values(1),(2),(3),(4),(5),(6) ) t(id) offset 3 limit 2;
NOTICE: called
f | id
---+----
| 4
| 5
(2 rows)
那么问题来了,offset 100000 limit 1这样的查询,会造成一个非常严重的问题,被offset的行,每次都会触发函数调用,非常的耗时。
这也给排查性能问题带来了一定的困扰。
这个问题已提交给社区,tom lane给出了回复,因为offset只是将执行结果的一部分过滤掉,不发送给客户端,实际上这些过滤掉的结果是实际发生了计算的。
No, it's not a bug. OFFSET only results in the skipped tuples not being
delivered to the client; it does not cause them not to be computed.
You could probably do something with a two-level select with the OFFSET
in the sub-select and the volatile function in the top level.
regards, tom lane
如果要把计算挪出来,防止多次调用function,可以用子查询。把function 放到最外层,把offset放到子查询里面,这样offset过滤的结果就只会被扫描,而不会被用于计算。 例如
postgres=# alter function f() volatile;
ALTER FUNCTION
postgres=# select f(),* from (select * from ( values(1),(2),(3),(4),(5),(6) ) t(id) offset 3 limit 2) t;
NOTICE: called
NOTICE: called
f | id
---+----
| 4
| 5
(2 rows)