【问题标题】:Postgresql not using index for queries using CURRENT_USERPostgresql 不使用索引进行使用 CURRENT_USER 的查询
【发布时间】:2021-09-30 11:43:13
【问题描述】:

我有一个 Postgresql 表:

  CREATE TABLE IF NOT EXISTS acls1k (
    pkey serial PRIMARY KEY,
    user_name VARCHAR(50),
    tenant_id VARCHAR(36),
    CONSTRAINT user_name_unique1k UNIQUE (user_name)
  );

user_name 列上有一个unique 索引。当我使用常量查询表时,index 用于查询:

explain analyze select * from acls1k where user_name = 'p1kuser1t1';
                                                         QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------
 Index Scan using user_name_unique1k on acls1k  (cost=0.28..8.29 rows=1 width=53) (actual time=0.071..0.073 rows=1 loops=1)
   Index Cond: ((user_name)::text = 'p1kuser1t1'::text)
 Planning Time: 0.240 ms
 Execution Time: 0.094 ms
(4 rows)

但是当我使用 current_user 变量时,会执行 顺序扫描 而不是索引扫描:

explain analyze select * from acls1k where user_name = current_user;
                                            QUERY PLAN
--------------------------------------------------------------------------------------------------
 Seq Scan on acls1k  (cost=0.00..59.00 rows=1 width=53) (actual time=0.162..0.845 rows=1 loops=1)
   Filter: ((user_name)::text = CURRENT_USER)
   Rows Removed by Filter: 999
 Planning Time: 0.097 ms
 Execution Time: 0.861 ms
(5 rows)

尝试铸造:

explain analyze select * from acls1k where user_name = CAST(current_user as varchar(50));
explain analyze select * from acls1k where user_name = CAST(current_user as text);
explain analyze select * from acls1k where user_name = current_user::text;

但是,仍然使用顺序扫描,不确定为什么我会看到这种行为,有什么方法可以让这个查询使用索引扫描?

编辑: 谁能回答为什么从name 数据类型到varchar 的内联转换失败?

@jimjonesbr 给出了如何使用prepared statements 进行索引扫描的答案。我推测问题在于查询规划器没有正确处理数据类型转换。

我尝试使用函数而不是准备好的语句,但无法运行解释分析来检查索引扫描是否执行。我能够注意到的是,将current_user 作为varchar 参数传递而不是内联使用它可能表明使用索引扫描时运行时更快。

create function get_acl(auser varchar(63))
returns varchar(36)
language plpgsql
as
$$
begin
        return (select tenant_id from acls1k where user_name = auser);
end;
$$;


create function get_acl_inline()
returns varchar(36)
language plpgsql
as
$$
begin
        return (select tenant_id from acls1k where user_name = current_user);
end;

并使用explain analyze 运行产生这个:

explain analyze select from get_acl(current_user::text);
                                              QUERY PLAN
------------------------------------------------------------------------------------------------------
 Function Scan on get_acl  (cost=0.26..0.27 rows=1 width=0) (actual time=0.435..0.436 rows=1 loops=1)
 Planning Time: 0.027 ms
 Execution Time: 0.456 ms
(3 rows)

explain analyze select from get_acl_inline();
                                                 QUERY PLAN
-------------------------------------------------------------------------------------------------------------
 Function Scan on get_acl_inline  (cost=0.25..0.26 rows=1 width=0) (actual time=1.833..1.834 rows=1 loops=1)
 Planning Time: 0.024 ms
 Execution Time: 1.850 ms

时间差可能表明索引扫描是在get_acls(current_user::text) 中进行的

【问题讨论】:

  • 如果不强制转换为textvarchar 类型,则需要进行seq 扫描,因为current_username 类型,具有不同的比较语义。使用演员表应该可以工作,如果表格是新填充的,请确保您已经分析过表格。
  • @DanielVérité 我仍在努力理解这一点。看来铸造是不够的:db-fiddle.com/f/8qX2tbtU3tNH1U6QsVTyaf/3我错过了什么?
  • @JimJones:您的示例非常不同,因为它插入同一用户的 100 万次,而 darc 对该列具有唯一约束。如果统计数据表明该常数非常频繁(在这种情况下索引会适得其反),则 postgres 可能不会使用索引来查找常数
  • @DanielVérité 我明白了,但我的意思是:为什么它使用准备好的语句来使用索引,尽管这是一个坏主意?我的意思是,这两个查询的数据是相同的。我有点困惑,很抱歉我打扰了:)
  • 像这样的简单函数最好定义为language sql stable - 然后您还可以看到完整的执行计划。

标签: sql postgresql query-optimization


【解决方案1】:

老实说,我也不理解这种行为——尽管这可能是有充分理由的。我会记下一些想法,我们可以开始讨论。

也许是从nametext 的转换没有像我们预期的那样工作,无论name 参数是否来自会话信息函数。 :

测试 1:将 CURRENT_USER 转换为 text

EXPLAIN (ANALYSE,COSTS OFF) 
SELECT * FROM acls1k WHERE user_name = CURRENT_USER::text; 

Gather (actual time=252.261..252.312 rows=0 loops=1)
  Workers Planned: 2
  Workers Launched: 2
  ->  Parallel Seq Scan on acls1k (actual time=234.139..234.140 rows=0 loops=3)
        Filter: (user_name = (CURRENT_USER)::text)
        Rows Removed by Filter: 333333
Planning Time: 0.262 ms
Execution Time: 252.328 ms

测试 2:将 name 字符串转换为 WHERE 子句中的文本:

EXPLAIN (ANALYSE,COSTS OFF) 
SELECT * FROM acls1k WHERE user_name = 'myuser'::name::text; 

Gather (actual time=200.262..200.321 rows=0 loops=1)
  Workers Planned: 2
  Workers Launched: 2
  ->  Parallel Seq Scan on acls1k (actual time=180.093..180.094 rows=0 loops=3)
        Filter: (user_name = 'myuser'::text COLLATE "C")
        Rows Removed by Filter: 333333
Planning Time: 0.043 ms
Execution Time: 200.334 ms

测试 3:在 CTE 中从 name 转换为 text

EXPLAIN (ANALYSE,COSTS OFF) 
WITH u (uname) AS (SELECT CURRENT_USER::text)
SELECT * FROM acls1k a 
JOIN u ON a.user_name = u.uname; 

Gather (actual time=229.228..229.280 rows=0 loops=1)
  Workers Planned: 2
  Workers Launched: 2
  ->  Parallel Seq Scan on acls1k a (actual time=208.065..208.066 rows=0 loops=3)
        Filter: (user_name = (CURRENT_USER)::text)
        Rows Removed by Filter: 333333
Planning Time: 0.085 ms
Execution Time: 229.293 ms

但是,我们可以在查询之前对这种转换进行排序处理,以便刨床已经将参数视为text。另一种方法是使用PREPARED STATEMENT(或您建议的函数)。在下面的示例中,我们创建了一个具有 text 参数的语句,因此如果有强制转换,它应该发生在 "before" 查询运行:

PREPARE pu (text) AS SELECT * FROM acls1k WHERE user_name = $1;

EXPLAIN (ANALYSE,COSTS OFF) 
EXECUTE pu(CURRENT_USER);

                                QUERY PLAN                                
--------------------------------------------------------------------------
 Index Scan using idx on acls1k (actual time=0.078..0.079 rows=0 loops=1)
   Index Cond: (user_name = 'myuser'::text)
 Planning Time: 10.607 ms
 Execution Time: 0.095 ms
(4 rows)

演示:db<>fiddle

【讨论】:

  • 谢谢,@jimjonesbr 这实际上回答了如何使用索引扫描的问题,但没有回答为什么 current_user 的内联转换失败。您对使用准备好的语句的直觉是什么?实际上,我尝试在运行查询之前使用函数强制转换,但在尝试运行解释分析时卡住了。
  • @darc 我也在努力寻找这种行为的答案。我用更多的细节和测试编辑了我的答案,以便我们有更好的基础来寻找问题。我确实怀疑从nametext 的转换可能是这里的问题。我想我们需要阅读 postgresql 源代码才能确定
猜你喜欢
  • 2012-05-21
  • 2014-02-06
  • 1970-01-01
  • 1970-01-01
  • 2016-01-23
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多