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)
中完成
老实说,我也不理解这种行为 - 尽管这可能有充分的理由。我会写下一些想法,我们可以开始讨论。
也许是从 name
到 text
的转换没有按我们预期的那样工作,无论 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
我有一个 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)
老实说,我也不理解这种行为 - 尽管这可能有充分的理由。我会写下一些想法,我们可以开始讨论。
也许是从 name
到 text
的转换没有按我们预期的那样工作,无论 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