与没有函数包装器的查询相比,SQL 函数非常慢

Posted

技术标签:

【中文标题】与没有函数包装器的查询相比,SQL 函数非常慢【英文标题】:SQL function very slow compared to query without function wrapper 【发布时间】:2015-03-29 09:57:52 【问题描述】:

我有这个运行非常快(~12ms)的 PostgreSQL 9.4 查询:

SELECT 
  auth_web_events.id, 
  auth_web_events.time_stamp, 
  auth_web_events.description, 
  auth_web_events.origin,  
  auth_user.email, 
  customers.name,
  auth_web_events.client_ip
FROM 
  public.auth_web_events, 
  public.auth_user, 
  public.customers
WHERE 
  auth_web_events.user_id_fk = auth_user.id AND
  auth_user.customer_id_fk = customers.id AND
  auth_web_events.user_id_fk = 2
ORDER BY
  auth_web_events.id DESC;

但是如果我将它嵌入到一个函数中,查询在所有数据中运行的速度非常慢,似乎是在运行每条记录,我错过了什么?,我有大约 1M 的数据,我想简化我的数据库层存储对函数和视图的大型查询。

CREATE OR REPLACE FUNCTION get_web_events_by_userid(int) RETURNS TABLE(
    id int,
    time_stamp timestamp with time zone,
    description text,
    origin text,
    userlogin text,
    customer text,
    client_ip inet
     ) AS
$func$
SELECT 
  auth_web_events.id, 
  auth_web_events.time_stamp, 
  auth_web_events.description, 
  auth_web_events.origin,  
  auth_user.email AS user, 
  customers.name AS customer,
  auth_web_events.client_ip
FROM 
  public.auth_web_events, 
  public.auth_user, 
  public.customers
WHERE 
  auth_web_events.user_id_fk = auth_user.id AND
  auth_user.customer_id_fk = customers.id AND
  auth_web_events.user_id_fk = $1
ORDER BY
  auth_web_events.id DESC;
  $func$ LANGUAGE SQL;

查询计划是:

"Sort  (cost=20.94..20.94 rows=1 width=791) (actual time=61.905..61.906 rows=2 loops=1)"
"  Sort Key: auth_web_events.id"
"  Sort Method: quicksort  Memory: 25kB"
"  ->  Nested Loop  (cost=0.85..20.93 rows=1 width=791) (actual time=61.884..61.893 rows=2 loops=1)"
"        ->  Nested Loop  (cost=0.71..12.75 rows=1 width=577) (actual time=61.874..61.879 rows=2 loops=1)"
"              ->  Index Scan using auth_web_events_fk1 on auth_web_events  (cost=0.57..4.58 rows=1 width=61) (actual time=61.860..61.860 rows=2 loops=1)"
"                    Index Cond: (user_id_fk = 2)"
"              ->  Index Scan using auth_user_pkey on auth_user  (cost=0.14..8.16 rows=1 width=524) (actual time=0.005..0.005 rows=1 loops=2)"
"                    Index Cond: (id = 2)"
"        ->  Index Scan using customers_id_idx on customers  (cost=0.14..8.16 rows=1 width=222) (actual time=0.004..0.005 rows=1 loops=2)"
"              Index Cond: (id = auth_user.customer_id_fk)"
"Planning time: 0.369 ms"
"Execution time: 61.965 ms"

我是这样调用函数的:

SELECT * from get_web_events_by_userid(2)  

函数的查询计划:

"Function Scan on get_web_events_by_userid  (cost=0.25..10.25 rows=1000 width=172) (actual time=279107.142..279107.144 rows=2 loops=1)"
"Planning time: 0.038 ms"
"Execution time: 279107.175 ms"

编辑:我只是更改了参数,问题仍然存在。 EDIT2:欧文答案的查询计划:

"Sort  (cost=20.94..20.94 rows=1 width=791) (actual time=0.048..0.049 rows=2 loops=1)"
"  Sort Key: w.id"
"  Sort Method: quicksort  Memory: 25kB"
"  ->  Nested Loop  (cost=0.85..20.93 rows=1 width=791) (actual time=0.030..0.037 rows=2 loops=1)"
"        ->  Nested Loop  (cost=0.71..12.75 rows=1 width=577) (actual time=0.023..0.025 rows=2 loops=1)"
"              ->  Index Scan using auth_user_pkey on auth_user u  (cost=0.14..8.16 rows=1 width=524) (actual time=0.011..0.012 rows=1 loops=1)"
"                    Index Cond: (id = 2)"
"              ->  Index Scan using auth_web_events_fk1 on auth_web_events w  (cost=0.57..4.58 rows=1 width=61) (actual time=0.008..0.008 rows=2 loops=1)"
"                    Index Cond: (user_id_fk = 2)"
"        ->  Index Scan using customers_id_idx on customers c  (cost=0.14..8.16 rows=1 width=222) (actual time=0.003..0.004 rows=1 loops=2)"
"              Index Cond: (id = u.customer_id_fk)"
"Planning time: 0.541 ms"
"Execution time: 0.101 ms"

【问题讨论】:

第一个查询计划是什么?它使用索引吗? @jpmc26:我不同意你的建议。如果做得对,将大型查询放入函数中会非常有用。在数据库中维护函数通常要方便得多,这样更容易跟踪依赖关系。这种方式通常更快。应用程序不必为每个会话准备复杂的查询——除其他外,发送一个长查询字符串而不仅仅是一个简单的函数调用。最佳行动方案取决于整体情况。 我刚刚添加了查询计划... @jpmc26:您一直声称“增加了复杂性”,而我看到了降低复杂性的潜力。该应用程序不必准备(或更糟的是,连接)查询,只需调用存储过程。你最喜欢的标签是 python,你的论点反映了这项技能。我的主要专长是 Postgres,我有不同的看法。您是根据自己的观点概括声明,而不是根据(未知)用例的实际要求。这是一种常见的模式。 要检查的另一件事是,auth_web_events.user_id_fk 实际上是 INT 列吗? (听起来很奇怪,我知道,但值得确定。) 【参考方案1】:

user

在重写你的函数时,我意识到你在这里添加了列别名:

SELECT 
  ...
  auth_user.email AS user, 
  customers.name AS customer,

.. 不会做任何事情开始,因为这些别名在函数外部是不可见的,并且在函数内部不被引用。所以他们会被忽略。出于文档目的,最好使用注释。

但这也会使您的查询无效,因为user 完全是reserved word,除非双引号,否则不能用作列别名。

奇怪的是,在我的测试中,该函数似乎可以使用无效的别名。可能是因为它被忽略 (?)。但我不确定这不会有副作用。

您的函数已重写(否则等效):

CREATE OR REPLACE FUNCTION get_web_events_by_userid(int)
  RETURNS TABLE (
     id int
   , time_stamp timestamptz
   , description text
   , origin text
   , userlogin text
   , customer text
   , client_ip inet
  )
  LANGUAGE sql STABLE AS
$func$
SELECT w.id
     , w.time_stamp
     , w.description 
     , w.origin  
     , u.email     -- AS user   -- make this a comment!
     , c.name      -- AS customer
     , w.client_ip
FROM   public.auth_user       u
JOIN   public.auth_web_events w ON w.user_id_fk = u.id
JOIN   public.customers       c ON c.id = u.customer_id_fk 
WHERE  u.id = $1   -- reverted the logic here
ORDER  BY w.id DESC
$func$;

显然,STABLE 关键字改变了结果。 Function volatility 在您描述的测试情况下应该不是问题。该设置通常不会使单个独立的函数调用受益。阅读details in the manual. 此外,标准EXPLAIN 不显示inside 函数的查询计划。您可以为此使用附加模块 auto-explain

Postgres query plan of a UDF invocation written in pgpsql

你有一个非常奇怪的数据分布

auth_web_events表有100000000条记录,auth_user->2条记录,customers->1条记录

由于您没有另外定义,因此该函数假定要返回 1000 行 的估计值。但您的函数实际上只返回 2 行。如果您的所有调用仅返回(在附近)2 行,只需添加 ROWS 2 即可声明。也可能会更改VOLATILE 变体的查询计划(即使STABLE 在这里是正确的选择)。

【讨论】:

似乎问题仍然存在:“get_web_events_by_userid 上的功能扫描(成本=0.25..10.25 行=1000 宽度=172)(实际时间=250263.587..250263.587 行=2 循环=1)”“计划time: 0.036 ms" "Execution time: 250263.612 ms" auth_web_events表有100000000条记录,auth_user->2条记录,customers->1条记录 @Mmeyer:很好用。 STABLE 是此处的正确设置,并且可能有利于在更大查询的上下文中重复调用。但它不应该对您的孤立测试用例产生影响。我在答案中添加了一些内容。 @ErwinBrandstetter 嗯。在我发表评论后,通过在VOLATILESTABLE 之间切换,我能够用一个非常简单的功能重现EXPLAIN 的行为。我在 psql 中这样做了,SHOWauto_explain 的一些配置参数上给出了“无法识别”的错误。所以我很确定auto_explain 没有加载或启用。我在9.3。如果它很有趣,我可以发布一个问题。 还有一些奇怪的事情:当它没有显示函数的内部计划时,我得到rows=1000(与 OP 相同)。我的函数最多可以返回 5 行,并且当它显示内部计划时它正确估计了 5 行。我还看到运行时间比EXPLAIN ANALYZE 输出增加了大约 3 倍。 (查询是如此之快,但我不确定这是否重要。)规划器是否真的可以放弃函数调用而只是将查询内联为子查询?这可以解释很多。 @ErwinBrandstetter 仅供参考,但我在研究一个非常相似的问题时发现了这个问题和答案。我有一个运行大约 91 毫秒的查询,当我把它放到一个函数中时,它跳到了 4,900 多毫秒。添加STABLE 使其执行类似于原始SQL。【参考方案2】:

通过使该查询动态化并使用 plpgsql,您将获得更好的性能。

CREATE OR REPLACE FUNCTION get_web_events_by_userid(uid int) RETURNS TABLE(
    id int,
    time_stamp timestamp with time zone,
    description text,
    origin text,
    userlogin text,
    customer text,
    client_ip inet
     ) AS $$
BEGIN

RETURN QUERY EXECUTE
'SELECT 
  auth_web_events.id, 
  auth_web_events.time_stamp, 
  auth_web_events.description, 
  auth_web_events.origin,  
  auth_user.email AS user, 
  customers.name AS customer,
  auth_web_events.client_ip
FROM 
  public.auth_web_events, 
  public.auth_user, 
  public.customers
WHERE 
  auth_web_events.user_id_fk = auth_user.id AND
  auth_user.customer_id_fk = customers.id AND
  auth_web_events.user_id_fk = ' || uid ||
'ORDER BY
  auth_web_events.id DESC;'

END;
$$ LANGUAGE plpgsql;

【讨论】:

嗯,这实际上是RETURN 吗?您不必使用RETURN QUERY吗? 我想这可能会影响您的结果。我不知道它是否可以优化查询执行,但似乎您应该重新验证性能是否仍然更好。 我收到一个:错误:在“SELECT”第 13 行或附近出现语法错误:SELECT ^ ********** 错误 ********** 错误: “SELECT” SQL 状态或附近的语法错误:42601 字符:268 @pwnyexpress 见this question & answer,正如 rchang 指出的那样。从 9.2 及更高版本开始,无论查询是否在任何类型的函数中,都应重新计划查询。 这根本不需要。一个简单的 SQL 函数应该是您所需要的。 PL/pgSQL 在没有动态 SQL 的情况下可能有用,因为它将查询视为准备好的语句(重用查询计划),但这与手头的问题完全无关。简而言之:这个答案具有误导性,基本上是错误的。此外,在使用动态 SQL 时,最好使用 USING 子句传递值参数,而不是连接文本表示形式。

以上是关于与没有函数包装器的查询相比,SQL 函数非常慢的主要内容,如果未能解决你的问题,请参考以下文章

使用装饰器的类内的函数包装器

与 SQL 相比,Hibernate JPQL 查询非常慢

SWIG:numpy 包装器的意外结果?

与 XAMPP 相比,MariaDB Docker 容器中的 INSERT SQL 查询非常慢

使用没有数据库包装器的 PHP 记录 mysql 查询

自动生成函数的类型安全包装,然后仅使用 `__typename` 作为参数动态调用。打字稿