Postgresql 不使用索引进行使用 CURRENT_USER 的查询

Postgresql not using index for queries using CURRENT_USER

我有一个 Postgresql table:

  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 索引。当我使用常量查询 table 时,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)

中完成

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

也许是从 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 = ;

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