暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

标量子查询与filter

起鼓隆冬强 2024-02-07
191

1、标量子查询与filter

当一个查询在select和from之间,那么这种子查询就是标量子查询。实际应用中,很多人在写SQL时为了方便会写一堆标量子查询的SQL,在表数据不大时,一般并不会有什么影响,但当数据量较大时,往往会对性能造成巨大影响。

因为标量子查询类似于一个天然的嵌套循环,而且驱动表固定为主表。如下所示:

1

2

3

4

5

6

7

8

9

10

bill=# explain select empno,ename,sal,deptno,

bill-# (select d.dname from dept d where d.deptno = e.deptno) as dname

bill-# from emp e;

                          QUERY PLAN

--------------------------------------------------------------

 Seq Scan on emp e  (cost=0.00..15.84 rows=14 width=64)

   SubPlan 1

     ->  Seq Scan on dept d  (cost=0.00..1.05 rows=1 width=9)

           Filter: (deptno = e.deptno)

(4 rows)

对于上面的SQL,emp表每输出一行数据,都要去dept表中全表扫描一遍。

而我们都知道,嵌套循环的被驱动表的连接列必须包含在索引中,同理,标量子查询的表的连接列也必须包含在索引中。但是我们在实际写SQL时还是要避免使用标量子查询,否则主表返回大量数据时,子表得被多次遍历,从而对SQL性能产生巨大影响。

那么对于标量子查询的SQL我们该怎么优化呢?最常用的就是改写成外连接,这样对于PostgreSQL的优化器而言可以根据实际情况去选择表的连接方式。这里需要注意的是,不能将标量子查询改成内连接,我们前面的例子中也可以看到,标量子查询实际是一个传值的过程,当主表传值给子表时,如果没有相应的值则会显示NULL,而如果使用内连接的话这部分数据就丢失了。

因此,上面的标量子查询可以改写成:

可以看到,优化器根据实际情况选择了更合适的hash join。

1

2

3

4

5

6

7

8

9

10

11

bill=# explain select e.empno,e.ename,e.sal,e.deptno,d.dname

bill-# from emp e

bill-# left join dept d on (d.deptno = e.deptno);

                            QUERY PLAN

-------------------------------------------------------------------

 Hash Left Join  (cost=1.09..2.31 rows=14 width=27)

   Hash Cond: (e.deptno = d.deptno)

   ->  Seq Scan on emp e  (cost=0.00..1.14 rows=14 width=18)

   ->  Hash  (cost=1.04..1.04 rows=4 width=13)

         ->  Seq Scan on dept d  (cost=0.00..1.04 rows=4 width=13)

(5 rows)

当主表连接列是外键,而子表的连接列是主键时,使用内连接也可以,因为外键自然不会存在NULL值。

1

2

3

4

5

6

7

8

9

10

11

bill=# explain select e.empno,e.ename,e.sal,e.deptno,d.dname

bill-# from emp e

bill-# inner join dept d on (d.deptno = e.deptno);

                            QUERY PLAN

-------------------------------------------------------------------

 Hash Join  (cost=1.09..2.31 rows=14 width=27)

   Hash Cond: (e.deptno = d.deptno)

   ->  Seq Scan on emp e  (cost=0.00..1.14 rows=14 width=18)

   ->  Hash  (cost=1.04..1.04 rows=4 width=13)

         ->  Seq Scan on dept d  (cost=0.00..1.04 rows=4 width=13)

(5 rows)

除了标量子查询外,往往filter也会产生类似的情况,因为在filter中驱动表也会被固定住,那么优化器可能会选择低效的执行计划。而对于PostgreSQL而言本身也不支持hint功能,如果错误的执行计划被固定,那么往往只能去改写SQL。

这里说明下下filter,在PostgreSQL中filter主要有2种情况,一种是我们常见的where后面过滤数据的,这种一般不会产生什么性能问题,例如:

1

2

3

4

5

6

bill=# explain select * from t where id < 10;

                      QUERY PLAN

-------------------------------------------------------

 Seq Scan on t  (cost=0.00..16925.00 rows=100 width=4)

   Filter: (id < 10)

(2 rows)

而另一种就是filter中是一些表的连接条件,这种呢便是我们前面说的情况,往往需要去关注的,例如:

1

2

3

4

5

6

7

8

bill=# explain select  exists (select 1 from t where t.id=n.id) from n;

                         QUERY PLAN

-------------------------------------------------------------

 Seq Scan on n  (cost=0.00..169250145.00 rows=10000 width=1)

   SubPlan 1

     ->  Seq Scan on t  (cost=0.00..16925.00 rows=1 width=0)

           Filter: (id = n.id)

(4 rows)

那么哪些写法会容易产生filter呢?在PostgreSQL中当使用exists或者not exists时,或者子查询中有固话子查询的关键词,如union、union all、cube、rollup、limit等,那么执行计划往往容易产生filter。

因此上面的SQL我们用in去替换exists进行改写:

1

2

3

4

5

6

7

8

bill=# explain select id in (select id from t) from n;

                               QUERY PLAN

-------------------------------------------------------------------------

 Seq Scan on n  (cost=0.00..129160170.00 rows=10000 width=1)

   SubPlan 1

     ->  Materialize  (cost=0.00..23332.00 rows=1000000 width=4)

           ->  Seq Scan on t  (cost=0.00..14425.00 rows=1000000 width=4)

(4 rows)

除此之外,在PostgreSQL中我们更推荐使用= any的方式去改写该类SQL:

1

2

3

4

5

6

7

bill=# explain select id = any(array(select id from t)) from n;

                            QUERY PLAN

-------------------------------------------------------------------

 Seq Scan on n  (cost=14425.00..14695.00 rows=10000 width=1)

   InitPlan 1 (returns $0)

     ->  Seq Scan on t  (cost=0.00..14425.00 rows=1000000 width=4)

(3 rows)

复制

当然这并不是说in的写法就一定比exists要好,只是相较于exists更不容易产生filter。这是为什么呢?因为如果子查询中包含我们上面提到的固化关键字时,子查询会被固化为一个整体,当采用exists写法时,如果子查询中有主表的连接列,那么便只能是主表通过连接列给子查询中的表传值,因此会选择filter。而使用in的写法,即使子查询被固化,但如果没有主表连接列的字段,那么便不会选择filter。

「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论