不同用户的执行计划居然会不一样?

引言

今天线上和小伙伴共同处理了一个生产案例,基本确认为v11原生分区表的bug,十分值得分享。

问题现象

线上一位同事反馈,用user1普通用户 (非表的owner) 执行某条SQL,跑了6小时+都跑不出来,但是换成超级用户postgres,两分钟左右就跑出来了,用explain查看该SQL执行计划,发现同一个SQL在不同用户之下,执行计划居然不一样?!根据同事的反馈,唯一能够观察到的情况是,统计信息有一阵子没有更新了。

纳尼,以前从未遇到过此等问题,见鬼了。

问题分析

环境是v11.5,涉及到的查询语句中,包含了两个分区表,原生的PostgreSQL11范围分区表,非pg_pathman、pg_partman等创建的。

  • 出发点1:难道不同用户看到的统计信息是不一样的,所以选择了不同的执行计划?

  • 出发点2:表上是否有什么东西,超级用户有权限执行,而该普通用户没有权限?

  • 出发点3:是否有什么类似的规则存在,导致了查询重写?

  • 出发点4:从DDL触发,SQL触发了分区表的某个潜在bug?

首先第一点,仔细一想,假如不同用户看到的统计信息是不一样的,这样就乱套了,每个用户跑出来的SQL都可能不一样,岂不是还要看脸决定跑的孰快孰慢,所以,PostgreSQL应该不会存在这个问题。确认一下,于是乎,挨个从pg_stats、pg_stat_all_tables视图查起,发现pg_stats视图没有数据的,可能是这个问题?但转念一想,普通用户是看不到pg_statistic这个表的数据,pg_stats是基于pg_statistic的视图,pg_stat_all_tables这些视图普通用户是可以查询数据的,所以这个是正常现象,排除第一点。

postgres=# \c postgres u1 
You are now connected to database "postgres" as user "u1".
postgres=> select * from pg_stats where tablename = 'mytest';
schemaname | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs | histogram_bounds | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram
------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------+-------------+-------------------+------------------------+----------------------
(0 rows)
postgres=> select * from pg_statistic limit 1;
ERROR: permission denied for table pg_statistic

postgres=> \c postgres postgres
You are now connected to database "postgres" as user "postgres".
postgres=# select * from pg_stats where tablename = 'mytest';
schemaname | tablename | attname | inherited | null_frac | avg_width | n_distinct | most_common_vals | most_common_freqs |   histogram_bounds   | correlation | most_common_elems | most_common_elem_freqs | elem_count_histogram
------------+-----------+---------+-----------+-----------+-----------+------------+------------------+-------------------+------------------------+-------------+-------------------+------------------------+----------------------
public     | mytest   | id     | f         |         0 |         4 |         -1 |                 |                   | {1,2,3,4,5,6,7,8,9,10} |           1 |                   |                       |
(1 row)

第二点,是否表上有什么奇怪的东西导致的?诸如触发器、外键约束、规则等等,经过一番google,在PostgreSQL bug list里面发现了一个十分类似的问题,一个是Different query plans using different roles,不过遗憾的是没有人回复,另一个是PG 10 experience different user execute same sql get different access plan,Tom lane做了回复。也是不同的用户不同的执行计划,基于PostgreSQL10.3,Different user query with same SQL at same time. But get different access plan.,涉及到的表一个是分区表,一个是普通的小表。

PG 10.3 env.

User: ccfuser. This user is DB owner. It will get result within 3 second.

      The totally cost is : 99672

User: a_app. This is normal user . It will get result after take 15 minutes.

      The totally cost is : 52784
     

dcg.brick_base_ebr   This table is partition table. And have trigger define on the table.


dcg.brick_shipment   This is very simple and normal table.

然后Tom lane的回复中,提到了RLS行级策略,不过遗憾的是,经过确认之后,没有RLS,且Tom lane也没有后续回复了,相当于戛然而止了。

If you've got row-level security turned on for that table, it could explain results like this.  RLS limits the planner's ability to see statistics, which can easily result in a worse plan.

OK,自己的生产环境,多了几样检测的东西,触发器、外键约束、规则、RLS,仔细检查之后,发现都没有这些东西,就是一个原生的范围分区表,有主键和几个二级索引,以及24个子分区。不过这里get到了一个新的点, RLS limits the planner's ability to see statistics, which can easily result in a worse plan.行级策略可能会导致规划器走一个糟糕的执行计划,到此,第二第三这两个疑点也排除了。

最后一点,表是分区表,可能是分区表的问题吗?我们知道,PostgreSQL10引入了原生分区 (本质还是继承的方式),v11引入了哈希分区、默认分区、update自动跨分区移动等等,v12对分区进一步加强,允许外键引用、提升DML性能等,可以看到虽然分区演进了很多,看似很美好。生产的版本是v11,相对刚刚发布的v14 beta还是3年前的版本了,会不会是分区的某些隐藏bug?那就分析一下!

通过对比两个用户下的不同执行计划,发现一个total cost 300W,一个 total cost是45W,45W也就对应着跑了2分钟左右的执行计划。通过仔细对比,发现在进行join的这一步中,一个total cost变成了300W,使用了Merge join,一个变成了44W,使用了Hash join。

OK,主要就是这两个步骤在作梗导致成本差异!于是对SQL进行拆分精简,只提取出了关键SQL,同时创建了一个测试用户,赋予了父表的查询权限,注意这里,只赋予了父表的查询权限。(这里为了安全,进行了脱敏,使用 mytest_202105 替代实际的生产中的子表,mytest 是父表)

对父表进行查询,select * from test1 left join mytest on xxx 发现走了Nested loop,总成本317万,大致如下:

                       QUERY PLAN                         
-----------------------------------------------------------
Nested Loop (cost=454.98..3178868.66 rows=2 width=150)
 -> Append (cost=454.56..3178737.66 rows=24,width=1366)
    Index Scan using xxxx on mytest_202105   <--进行了裁剪,扫描了一个子表
xxx

原SQL是进行了分区裁剪的,只扫描了某一个分区,于是将SQL进行改写,直接改成扫描这个特定分区 mytest_202105 ,发现居然没有权限,提示如下错误:(那岂不是新增的用户得挨个对所有子表进行授权?)

ERROR : 42501: permession denied for table mytest_202105

子表居然没有自动继承父表的权限??没办法,只能手动赋予一下子表的查询权限

grant select on mytest_202105 to user1;

再次进行查询,这一次指定了分区子表,select * from test1 left join mytest_202105 on xxx,奇怪的事情发生了,cost只有35W!并且走了Hash join,大致如下:

                       QUERY PLAN                         
-----------------------------------------------------------
Gather (cost=42783.98..350108.10 rows=64124 width=769)
Workers Planned : 2
  -> Parallel Hash Join (cost=41783.64..342695.70 rows=26718 width=769)
  Hash Cond
    -> Parallel Index Scan using xxx on mytest_202105 <--只扫描了一个子表

可以看到,显式指定了子表,理论上和指定父表进行裁剪的结果集是一样的,但是cost居然相差了10倍之多!然后这个时候再次进行查询父表,select * from test1 left join mytest on xxx 执行计划又变了!和最开始的nest loop差异很大,和指定子表查询的执行计划基本一样,但这里仅仅是做了一个子表的查询授权,居然导致了执行计划的变化,和上方查询指定子表的执行计划比较仅多了排除其他分区的步骤,也就是Subplans Removed : 23,大致如下,

                       QUERY PLAN                         
-----------------------------------------------------------
Gather (cost=43519.90..3203295.10 rows=64126 width=769)
Workers Planned : 2
  -> Parallel Hash Join (cost=42519.64..3195882.46 rows=26719 width=769)
    Hash Cond
   -> Parallel Append (cost=736.70..3153408.85 rows=263010 width=543)
      Subplans Removed : 23 <-- 差异的部分

      -> Parallel Index Scan using xxx on mytest_202105 <--进行了裁剪,扫描了一个子表

这里初现端倪,仅仅是一个子表的查询权限这么一个变更,导致了如此大的差异化,不仅导致查询父表的执行计划的变更,由nest loop变成了Hash join,这也直接导致了成本的不同。

于是为了验证想法,再次回收user1查询子表的权限,直接查询父表,同时设置超时参数statement_timeout 为10s,直接就报超时了,根本查不出来。当再次赋予了子表查询权限之后,不管是直接查询父表还是直接查询子表,都只需要6s!

至此,问题明了了,和子表的查询权限有关!你敢信?v11的原生分区还有这个坑,于是联系同事,让他挨个针对普通用户赋予了子表的查询权限

grant select on xxx to user1;

很不错,同事反馈瞬间通透了,SQL立马就跑出来了!真是一段神奇的踩坑之路。


小结

经过一阵摸索,复现和解决,基本可以确认,应该是PostgreSQL v11原生分区表的bug,查询的时候多达6小时还卡住,也没有报错,也没有提示权限不足,就一直卡在那里,相信那位童鞋在bug list里遇到的问题大概率和我此次遇到的问题一样,子表权限!不过到底是什么场景触发的还不得而知,还得继续摸索,希望遇到此类同样的问题的人 (虽然很少?),能够有所经验和分析的思路。

bug 链接:

https://www.postgresql-archive.org/Different-query-plans-using-different-roles-td5909631.html

https://www.postgresql-archive.org/PG-10-experience-different-user-execute-same-sql-get-different-access-plan-td6199561.html#a6199643 (这个还是国人遇到的)


请使用浏览器的分享功能分享到微信等