PostgreSQL UDF(用户定义函数)开销

ash*_*ash 5 postgresql performance plpgsql functions postgresql-performance

免责声明

这项任务可能看起来很深奥,但我还是想创建某种 POC。

目标

我的目标是让 PostgreSQL 数据库(版本 10)向使用它的应用程序公开一个 API。

API 需要采用一组 UDF 的形式:所有函数都属于一个公共方案,这是应用程序唯一可以访问的。桌子和其他东西隐藏在私人计划中。几乎就像,您知道的,面向对象的数据库
这就是我试图让它发挥作用的原因:

  • 它将数据库与应用程序分离,因此您可以重构/优化/非规范化前者,而破坏后者的风险较小。您甚至可以将其维护委托给另一个团队或部门(哦,天哪)
  • API 将服务的要求形式化。数据库当然是一种服务,但称为迁移的传统机制并不能很好地用于弄清楚其中发生了什么。想想多年来收集的成百上千的迁移,其中一些已经损坏,再也不会工作了,并且

好吧,没关系。

问题

因此,当我尝试创建一些非常简单的函数(例如从表中获取所有记录)时,我提到它们总是比它包装的查询慢。虽然这本身是完全可以接受和理解的,但时间差异可能很大。因此,无法接受。

这个例子

我有一张这样的桌子。

CREATE TABLE notifications (
    id SERIAL PRIMARY KEY,
    source_type INTEGER NOT NULL,
    content JSONB,
    created TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
)
Run Code Online (Sandbox Code Playgroud)

并且其中有 >120k 条记录。
想象一下,我们想要获得所有这些。
在这里,我们通过一个简单的查询来完成。没有索引,JSONB 数据几乎每条记录 1kb。

EXPLAIN (ANALYZE,VERBOSE,BUFFERS) SELECT * FROM private.notifications;
                                                         QUERY PLAN                                                              
-------------------------------------------------------------------------------------------------------------------------------------
Seq Scan on private.notifications  (cost=0.00..16216.13 rows=120113 width=877) (actual time=0.015..496.473 rows=120113 loops=1)
  Output: id, source_type, content, created
  Buffers: shared hit=15015
Planning time: 0.063 ms
Execution time: 973.935 ms
Run Code Online (Sandbox Code Playgroud)

496 毫秒。
现在让我们尝试使用像这样的 pl/pgsql 函数:

CREATE OR REPLACE FUNCTION notifications_get()
RETURNS SETOF private.notifications AS
$$
BEGIN
    RETURN QUERY SELECT * from private.notifications;
END
$$
LANGUAGE 'plpgsql' 
SECURITY DEFINER;

EXPLAIN (ANALYZE,VERBOSE,BUFFERS) SELECT * FROM notifications_get();

                                                            QUERY PLAN                                                             
-----------------------------------------------------------------------------------------------------------------------------------
Function Scan on notifications_get  (cost=0.25..10.25 rows=1000 width=48) (actual time=99.561..589.129 rows=120113 loops=1)
  Output: id, source_type, content, created
  Function Call: notifications_get()
  Buffers: shared hit=15015
Planning time: 0.045 ms
Execution time: 1091.698 ms
Run Code Online (Sandbox Code Playgroud)

589 毫秒。
显然,函数和查询之间的区别在于获取第一条记录所花费的时间为 99.5 毫秒。
我尝试了进一步的优化(可能是天真):

  1. 调整行以使查询计划更加真实。假设120k。它产生相同的结果 (102.373..593.628)
  2. 使用 SQL 语言(很公平,查询很简单)。令人惊讶的是,相同的结果 (95.760..595.746)
  3. 使功能稳定。现在应该会好起来吧?不。相同的结果 (93.132..594.331)

问题

  1. 还有什么可以做来使函数性能更高(与简单查询相比)?
  2. 为什么这些技巧都没有效果?
  3. 这些前 100 毫秒究竟是什么?这些不是恒定的:当表中有 20k 行时,该函数会花费神秘的 18-20 毫秒尝试先做某事。很明显,它试图对表格的每一行进行处理。如何减少这种浪费或完全摆脱它?这甚至可能吗?

聚苯乙烯

我面临的另一个问题是通过 id 获取记录的函数。0.25 毫秒与 0.025 毫秒。十倍的差异,但我或多或少地了解它的来源。同样,上面列出的优化技巧没有任何区别(似乎不应该)。

Erw*_*ter 4

这(几乎)相当于问题中的函数,但执行起来就像一个简单的函数SELECT

CREATE OR REPLACE FUNCTION notifications_get_faster()
  RETURNS SETOF private.notifications AS
$func$
SELECT * FROM private.notifications
$func$  LANGUAGE sql STABLE;
Run Code Online (Sandbox Code Playgroud)

几乎,因为它不是SECURITY DEFINER,这会阻止预期的效果。

最值得注意的是,您将在查询计划中看到 aSeq Scan而不是 the 。Function Scan这就是造成大部分差异的原因。

为什么?

您的各种尝试都不满足内联表函数的所有条件。这个功能就可以了。尤其:

  • 函数是LANGUAGE SQL

  • 该函数不是SECURITY DEFINER

  • 该函数被声明STABLEIMMUTABLE

因此 Postgres 可以获取函数体并执行它,而无需函数开销(“函数内联”)。与普通的SELECT.

另外:不要引用语言名称。这是一个标识符。