PostgreSQL JOIN limit 优化器 成本计算 改进 - mergejoin startup cost 优化

本文涉及的产品
RDS MySQL DuckDB 分析主实例,集群系列 4核8GB
简介: 标签PostgreSQL , join , limit , startup cost , cbo , 优化器改进背景PostgreSQL limit N的成本估算,是通过计算总成本A,以及估算得到的总记录数B得到:(N/B)*A 大概意思就是占比的方法计算 对于单表查询...

标签

PostgreSQL , join , limit , startup cost , cbo , 优化器改进


背景

PostgreSQL limit N的成本估算,是通过计算总成本A,以及估算得到的总记录数B得到:

(N/B)*A  
  
大概意思就是占比的方法计算  

对于单表查询,这种方法通常来说比较适用,但是如果数据分布有倾斜,实际上也并不一定适用,例如以下两种情况:

1、符合条件的数据占总记录数的50%,但是全部分布在表的末尾,那么limit 10000 条到底是走索引快还是走全表扫描快呢?

2、符合条件的数据占总记录数的50%,全部分布在表的头部,那么LIMIT 10000 条,肯定是全表扫描快了。

对于JOIN的情况,同样有类似的问题:

比如JOIN并且带条件时,LIMIT N,是走嵌套循环快,还是走MERGE 或 HASH JOIN快?

1、嵌套循环+where+LIMIT的成本计算方法,可以使用LIMIT占总估算记录数占比的方法得到,还算是比较合理。

2、MERGE JOIN+where+LIMIT的成本计算方法,必须考虑启动成本,例如WHERE条件在A表上(可以走索引直接从条件位置开始扫描),B表则需要从索引的开头开始扫描(到与A表的索引匹配时,也许需要扫描很多的索引ENTRY,这个启动成本可能会很高),启动成本,加上LIMIT条数在剩余的所有成本中的一个占比,得到的成本是一个比较合理的成本。

3、hash join+where+limit的成本计算方法,使用启动成本+LIMIT占总估算记录数占比的方法得到,优化器目前就是这么做的,比较合理。

然而,对于MERGE JOIN,目前在使用LIMIT时,PG没有加上这个启动成本,使得最后得到的执行计划可能不准确。

改进方法建议可以加入merge join启动成本。

PostgreSQL 例子

1、建表如下:

postgres=# create table test1(a int, b text, primary key(a));  
CREATE TABLE  
postgres=# create table test2(a int, b text, primary key(a));  
CREATE TABLE  
postgres=# alter table test1 add constraint testcheck foreign key(a) references test2(a);                                                                    
ALTER TABLE  
postgres=# insert into test2 select generate_series(1,1000000),'abcdefg';  
INSERT 0 1000000  
postgres=# insert into test1 select generate_series(1,1000000,2),'abcdefg';  
INSERT 0 500000  
  
analyze test1;  
analyze test2;  

2、查询SQL如下:

explain (analyze,verbose,timing,costs,buffers) select * from test2 left join test1 on test2.a = test1.a where test2.a > 500000 limit 10;  

该语句中表结构比较特殊,两个关联字段都是主键,并且存在外键约束关系,查询计划如下:

                                                                     QUERY PLAN                                                                       
----------------------------------------------------------------------------------------------------------------------------------------------------  
 Limit  (cost=0.73..0.89 rows=10 width=24) (actual time=54.729..54.739 rows=10 loops=1)  
   Output: test2.a, test2.b, test1.a, test1.b  
   Buffers: shared hit=2042  
   ->  Merge Left Join  (cost=0.73..7929.35 rows=498340 width=24) (actual time=54.728..54.735 rows=10 loops=1)  
         Output: test2.a, test2.b, test1.a, test1.b  
         Inner Unique: true  
         Merge Cond: (test2.a = test1.a)  
         Buffers: shared hit=2042  
         ->  Index Scan using test2_pkey on public.test2  (cost=0.37..3395.42 rows=498340 width=12) (actual time=0.017..0.020 rows=10 loops=1)  
               Output: test2.a, test2.b  
               Index Cond: (test2.a > 500000)  
               Buffers: shared hit=4  
         ->  Index Scan using test1_pkey on public.test1  (cost=0.37..2322.99 rows=500000 width=12) (actual time=0.006..34.120 rows=250006 loops=1)  
               Output: test1.a, test1.b  
               Buffers: shared hit=2038  
 Planning Time: 0.216 ms  
 Execution Time: 54.765 ms  
(17 rows)  

从执行计划上可以看出,根据test2表先查询出满足条件的10条记录,然后和test1表采用mergejoin关联,由于在估算的时候没有考虑到limit的影响,估算的行数非常大,是498340行,

实际采用nestloop效果会更好(关闭掉seqscan和megejoin)

postgres=# set enable_seqscan =off;   
SET  
postgres=# set enable_mergejoin =off;   
SET  
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from test2 left join test1 on test2.a = test1.a where test2.a > 500000 limit 10;  
                                                                  QUERY PLAN                                                                     
-----------------------------------------------------------------------------------------------------------------------------------------------  
 Limit  (cost=0.73..4.53 rows=10 width=24) (actual time=0.040..0.060 rows=10 loops=1)  
   Output: test2.a, test2.b, test1.a, test1.b  
   Buffers: shared hit=39  
   ->  Nested Loop Left Join  (cost=0.73..189339.64 rows=498340 width=24) (actual time=0.039..0.057 rows=10 loops=1)  
         Output: test2.a, test2.b, test1.a, test1.b  
         Inner Unique: true  
         Buffers: shared hit=39  
         ->  Index Scan using test2_pkey on public.test2  (cost=0.37..3395.42 rows=498340 width=12) (actual time=0.025..0.027 rows=10 loops=1)  
               Output: test2.a, test2.b  
               Index Cond: (test2.a > 500000)  
               Buffers: shared hit=4  
         ->  Index Scan using test1_pkey on public.test1  (cost=0.37..0.37 rows=1 width=12) (actual time=0.002..0.002 rows=0 loops=10)  
               Output: test1.a, test1.b  
               Index Cond: (test2.a = test1.a)  
               Buffers: shared hit=35  
 Planning Time: 0.112 ms  
 Execution Time: 0.078 ms  
(17 rows)  

但是从评估的成本来看,merge join+limit 比 nestloop+limit更低,原因是nestloop的总成本更高(189339 比 7929)。所以优化器根据比例算法(未参照merge join的启动成本),认为在LIMIT的情况下,也是merge join成本更低。

实际情况是,MERGE JOIN的没带查询条件的B表,需要从索引的头部开始扫,而不是从指定位置开始扫。 因此实际情况是merge join是更慢的。

目前优化器使用hash join时,已经算上了startup cost,例子

postgres=# set enable_mergejoin =off;
SET
postgres=# set enable_seqscan =off;
SET
postgres=# set enable_nestloop =off;
SET
 
启动成本=3536.51
postgres=# explain (analyze,verbose,timing,costs,buffers) select * from test2 left join test1 on test2.a = test1.a where test2.a > 500000 limit 10;
                                                                        QUERY PLAN                                                                        
----------------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=3536.51..3536.61 rows=10 width=24) (actual time=158.148..158.219 rows=10 loops=1)
   Output: test2.a, test2.b, test1.a, test1.b
   Buffers: shared hit=4079, temp written=1464
   ->  Hash Left Join  (cost=3536.51..8135.83 rows=494590 width=24) (actual time=158.147..158.215 rows=10 loops=1)
         Output: test2.a, test2.b, test1.a, test1.b
         Inner Unique: true
         Hash Cond: (test2.a = test1.a)
         Buffers: shared hit=4079, temp written=1464
         ->  Index Scan using test2_pkey on public.test2  (cost=0.37..3369.86 rows=494590 width=12) (actual time=0.023..0.027 rows=26 loops=1)
               Output: test2.a, test2.b
               Index Cond: (test2.a > 500000)
               Buffers: shared hit=4
         ->  Hash  (cost=2322.99..2322.99 rows=500000 width=12) (actual time=156.848..156.849 rows=500000 loops=1)
               Output: test1.a, test1.b
               Buckets: 262144  Batches: 4  Memory Usage: 7418kB
               Buffers: shared hit=4072, temp written=1464
               ->  Index Scan using test1_pkey on public.test1  (cost=0.37..2322.99 rows=500000 width=12) (actual time=0.011..72.506 rows=500000 loops=1)
                     Output: test1.a, test1.b
                     Buffers: shared hit=4072
 Planning Time: 0.141 ms
 Execution Time: 162.086 ms
(21 rows)

改进建议

针对test1表,需要估算a<500000有多少行,作为索引扫描的startup成本。

postgres=# explain select * from test1 where a<500000;  
                                   QUERY PLAN                                      
---------------------------------------------------------------------------------  
 Index Scan using test1_pkey on test1  (cost=0.37..1702.83 rows=249893 width=12)  
   Index Cond: (a < 500000)  
(2 rows)  
  
  
postgres=# explain select * from test1;  
                         QUERY PLAN                            
-------------------------------------------------------------  
 Seq Scan on test1  (cost=0.00..133.15 rows=500000 width=12)  
(1 row)  

所以,索引扫描test1(where a > 500000)的merge join启动成本应该有 1702,加上这个成本后,成本远大于NEST LOOP JOIN的成本,所以不会选择merge join。

Oracle 例子

create table test1(a int, b varchar2(4000), primary key(a));  
  
create table test2(a int, b varchar2(4000), primary key(a));  
  
alter table test1 add constraint testcheck foreign key(a) references test2(a);                                                                    
  
  
insert into test2 select rownum, 'abcdefg' from dual connect by level <=1000000;  
  
  
insert into test1 select * from (select rownum as rn, 'abcdefg' from dual connect by level <=1000000) t where mod(rn,2)=1;  
exec DBMS_STATS.GATHER_TABLE_STATS('DIGOAL', 'TEST1', method_opt => 'FOR COLUMNS (a, b)');   
exec DBMS_STATS.GATHER_TABLE_STATS('DIGOAL', 'TEST2', method_opt => 'FOR COLUMNS (a, b)');   

查询SQL如下:

set autotrace on  
set linesize 120  
set pagesize 200  
set wrap off  
  
select * from test2 left join test1 on test2.a = test1.a where test2.a > 500000 and rownum<=10;  
  
  
         A B  
---------- -------------------------------------------------------------------------------------------------------------  
    500001 abcdefg  
    500002 abcdefg  
    500003 abcdefg  
    500004 abcdefg  
    500005 abcdefg  
    500006 abcdefg  
    500007 abcdefg  
    500008 abcdefg  
    500009 abcdefg  
    500010 abcdefg  
  
10 rows selected.  
  
  
Execution Plan  
----------------------------------------------------------  
Plan hash value: 3391785554  
  
-----------------------------------------------------------------------------------------------  
| Id  | Operation                     | Name          | Rows  | Bytes | Cost (%CPU)| Time     |  
-----------------------------------------------------------------------------------------------  
|   0 | SELECT STATEMENT              |               |    10 |   500 |    15   (0)| 00:00:01 |  
|*  1 |  COUNT STOPKEY                |               |       |       |            |          |  
|   2 |   NESTED LOOPS OUTER          |               |    10 |   500 |    15   (0)| 00:00:01 |  
|   3 |    TABLE ACCESS BY INDEX ROWID| TEST2         |    10 |   250 |     4   (0)| 00:00:01 |  
|*  4 |     INDEX RANGE SCAN          | SYS_C00151146 |  9000 |       |     3   (0)| 00:00:01 |  
|   5 |    TABLE ACCESS BY INDEX ROWID| TEST1         |     1 |    25 |     2   (0)| 00:00:01 |  
|*  6 |     INDEX UNIQUE SCAN         | SYS_C00151145 |     1 |       |     1   (0)| 00:00:01 |  
-----------------------------------------------------------------------------------------------  
  
Predicate Information (identified by operation id):  
---------------------------------------------------  
  
   1 - filter(ROWNUM<=10)  
   4 - access("TEST2"."A">500000)  
   6 - access("TEST2"."A"="TEST1"."A"(+))  
       filter("TEST1"."A"(+)>500000)  
  
  
Statistics  
----------------------------------------------------------  
          0  recursive calls  
          0  db block gets  
         25  consistent gets  
          0  physical reads  
          0  redo size  
        937  bytes sent via SQL*Net to client  
        500  bytes received via SQL*Net from client  
          2  SQL*Net roundtrips to/from client  
          0  sorts (memory)  
          0  sorts (disk)  
         10  rows processed  

Oracle 选择了nestloop join。

使用HINT,让Oracle使用merge join,看看成本是多少,是否与修正PostgreSQL merge join启动成本接近。

select /*+ USE_MERGE(test2,test1) */ * from test2 left join test1 on test2.a = test1.a where test2.a > 500000 and rownum<=10;  

Execution Plan
----------------------------------------------------------
Plan hash value: 492577188

------------------------------------------------------------------------------------------------
| Id  | Operation                      | Name          | Rows  | Bytes | Cost (%CPU)| Time     |
------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT               |               |    10 |   750 |    29   (7)| 00:00:01 |
|*  1 |  COUNT STOPKEY                 |               |       |       |            |          |
|   2 |   MERGE JOIN OUTER             |               |    10 |   750 |    29   (7)| 00:00:01 |
|   3 |    TABLE ACCESS BY INDEX ROWID | TEST2         |    10 |   250 |     4   (0)| 00:00:01 |
|*  4 |     INDEX RANGE SCAN           | SYS_C00151146 |  9000 |       |     3   (0)| 00:00:01 |
|*  5 |    SORT JOIN                   |               | 25000 |   610K|    25   (8)| 00:00:01 |
|   6 |     TABLE ACCESS BY INDEX ROWID| TEST1         | 25000 |   610K|    23   (0)| 00:00:01 |
|*  7 |      INDEX RANGE SCAN          | SYS_C00151145 |  4500 |       |    11   (0)| 00:00:01 |
------------------------------------------------------------------------------------------------

Predicate Information (identified by operation id):
---------------------------------------------------

   1 - filter(ROWNUM<=10)
   4 - access("TEST2"."A">500000)
   5 - access("TEST2"."A"="TEST1"."A"(+))
       filter("TEST2"."A"="TEST1"."A"(+))
   7 - access("TEST1"."A"(+)>500000)


Statistics
----------------------------------------------------------
          1  recursive calls
          0  db block gets
       1099  consistent gets
          0  physical reads
          0  redo size
        937  bytes sent via SQL*Net to client
        500  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          1  sorts (memory)
          0  sorts (disk)
         10  rows processed

小结

1、PostgreSQL 在计算merge join+limit的成本时,优化器有优化的空间,可以考虑把启动成本算进来,提高优化器选择带limit输出的SQL的JOIN方法的正确性。

2、如果是inner join,通过query rewrite可以对merge join进行优化,跳过不符合条件的头部INDEX SCAN。

postgres=# explain (analyze,verbose,timing,costs,buffers) select * from test2 join test1 on test2.a = test1.a where test2.a > 500000 limit 10;
                                                                     QUERY PLAN                                                                     
----------------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=0.77..1.09 rows=10 width=24) (actual time=54.626..54.638 rows=10 loops=1)
   Output: test2.a, test2.b, test1.a, test1.b
   Buffers: shared hit=2042
   ->  Merge Join  (cost=0.77..7895.19 rows=247295 width=24) (actual time=54.625..54.635 rows=10 loops=1)
         Output: test2.a, test2.b, test1.a, test1.b
         Inner Unique: true
         Merge Cond: (test2.a = test1.a)
         Buffers: shared hit=2042
         ->  Index Scan using test2_pkey on public.test2  (cost=0.37..3369.86 rows=494590 width=12) (actual time=0.017..0.020 rows=19 loops=1)
               Output: test2.a, test2.b
               Index Cond: (test2.a > 500000)
               Buffers: shared hit=4
         ->  Index Scan using test1_pkey on public.test1  (cost=0.37..2322.99 rows=500000 width=12) (actual time=0.008..34.009 rows=250010 loops=1)
               Output: test1.a, test1.b
               Buffers: shared hit=2038
 Planning Time: 0.244 ms
 Execution Time: 54.669 ms
(17 rows)
  
sql rewrite:
  
可以做到内核里面,这样就不需要改SQL了。效果如下,超好。

postgres=# explain (analyze,verbose,timing,costs,buffers) select * from test2 join test1 on test2.a = test1.a where test2.a > 500000 and test1.a > 500000limit 10;
                                                                  QUERY PLAN                                                                   
-----------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=0.75..1.30 rows=10 width=24) (actual time=0.035..0.048 rows=10 loops=1)
   Output: test2.a, test2.b, test1.a, test1.b
   Buffers: shared hit=8
   ->  Merge Join  (cost=0.75..6711.51 rows=123700 width=24) (actual time=0.034..0.044 rows=10 loops=1)
         Output: test2.a, test2.b, test1.a, test1.b
         Inner Unique: true
         Merge Cond: (test2.a = test1.a)
         Buffers: shared hit=8
         ->  Index Scan using test2_pkey on public.test2  (cost=0.37..3369.86 rows=494590 width=12) (actual time=0.015..0.019 rows=19 loops=1)
               Output: test2.a, test2.b
               Index Cond: (test2.a > 500000)
               Buffers: shared hit=4
         ->  Index Scan using test1_pkey on public.test1  (cost=0.37..1704.30 rows=250106 width=12) (actual time=0.015..0.017 rows=10 loops=1)
               Output: test1.a, test1.b
               Index Cond: (test1.a > 500000)
               Buffers: shared hit=4
 Planning Time: 0.276 ms
 Execution Time: 0.074 ms
(18 rows)

参考

《PostgreSQL 优化器案例之 - order by limit 索引选择问题》

src/backend/optimizer/path/costsize.c

相关实践学习
使用PolarDB和ECS搭建门户网站
本场景主要介绍如何基于PolarDB和ECS实现搭建门户网站。
阿里云数据库产品家族及特性
阿里云智能数据库产品团队一直致力于不断健全产品体系,提升产品性能,打磨产品功能,从而帮助客户实现更加极致的弹性能力、具备更强的扩展能力、并利用云设施进一步降低企业成本。以云原生+分布式为核心技术抓手,打造以自研的在线事务型(OLTP)数据库Polar DB和在线分析型(OLAP)数据库Analytic DB为代表的新一代企业级云原生数据库产品体系, 结合NoSQL数据库、数据库生态工具、云原生智能化数据库管控平台,为阿里巴巴经济体以及各个行业的企业客户和开发者提供从公共云到混合云再到私有云的完整解决方案,提供基于云基础设施进行数据从处理、到存储、再到计算与分析的一体化解决方案。本节课带你了解阿里云数据库产品家族及特性。
目录
相关文章
|
6月前
|
SQL 缓存 关系型数据库
MySQL 慢查询是怎样优化的
本文深入解析了MySQL查询速度变慢的原因及优化策略,涵盖查询缓存、执行流程、SQL优化、执行计划分析(如EXPLAIN)、查询状态查看等内容,帮助开发者快速定位并解决慢查询问题。
278 0
|
4月前
|
缓存 关系型数据库 MySQL
降低MySQL高CPU使用率的优化策略。
通过上述方法不断地迭代改进,在实际操作中需要根据具体场景做出相对合理判断。每一步改进都需谨慎评估其变动可能导致其他方面问题,在做任何变动前建议先在测试环境验证其效果后再部署到生产环境中去。
233 6
|
10月前
|
SQL 关系型数据库 MySQL
MySQL进阶突击系列(07) 她气鼓鼓递来一条SQL | 怎么看执行计划、SQL怎么优化?
在日常研发工作当中,系统性能优化,从大的方面来看主要涉及基础平台优化、业务系统性能优化、数据库优化。面对数据库优化,除了DBA在集群性能、服务器调优需要投入精力,我们研发需要负责业务SQL执行优化。当业务数据量达到一定规模后,SQL执行效率可能就会出现瓶颈,影响系统业务响应。掌握如何判断SQL执行慢、以及如何分析SQL执行计划、优化SQL的技能,在工作中解决SQL性能问题显得非常关键。
|
5月前
|
存储 SQL 关系型数据库
MySQL 核心知识与索引优化全解析
本文系统梳理了 MySQL 的核心知识与索引优化策略。在基础概念部分,阐述了 char 与 varchar 在存储方式和性能上的差异,以及事务的 ACID 特性、并发事务问题及对应的隔离级别(MySQL 默认 REPEATABLE READ)。 索引基础部分,详解了 InnoDB 默认的 B+tree 索引结构(多路平衡树、叶子节点存数据、双向链表支持区间查询),区分了聚簇索引(数据与索引共存,唯一)和二级索引(数据与索引分离,多个),解释了回表查询的概念及优化方法,并分析了 B+tree 作为索引结构的优势(树高低、效率稳、支持区间查询)。 索引优化部分,列出了索引创建的六大原则
151 2
|
SQL 关系型数据库 MySQL
深入解析MySQL的EXPLAIN:指标详解与索引优化
MySQL 中的 `EXPLAIN` 语句用于分析和优化 SQL 查询,帮助你了解查询优化器的执行计划。本文详细介绍了 `EXPLAIN` 输出的各项指标,如 `id`、`select_type`、`table`、`type`、`key` 等,并提供了如何利用这些指标优化索引结构和 SQL 语句的具体方法。通过实战案例,展示了如何通过创建合适索引和调整查询语句来提升查询性能。
2611 10
|
5月前
|
存储 SQL 关系型数据库
MySQL 动态分区管理:自动化与优化实践
本文介绍了如何利用 MySQL 的存储过程与事件调度器实现动态分区管理,自动化应对数据增长,提升查询性能与数据管理效率,并详细解析了分区创建、冲突避免及实际应用中的关键注意事项。
239 0
|
7月前
|
存储 SQL 关系型数据库
京东面试:mysql深度分页 严重影响性能?根本原因是什么?如何优化?
京东面试:mysql深度分页 严重影响性能?根本原因是什么?如何优化?
京东面试:mysql深度分页 严重影响性能?根本原因是什么?如何优化?
|
9月前
|
存储 关系型数据库 MySQL
MySQL细节优化:关闭大小写敏感功能的方法。
通过这种方法,你就可以成功关闭 MySQL 的大小写敏感功能,让你的数据库操作更加便捷。
752 19
|
10月前
|
关系型数据库 MySQL 数据库
从MySQL优化到脑力健康:技术人与效率的双重提升
聊到效率这个事,大家应该都挺有感触的吧。 不管是技术优化还是个人状态调整,怎么能更快、更省力地完成事情,都是我们每天要琢磨的事。
288 23
|
10月前
|
SQL 关系型数据库 MySQL
基于SQL Server / MySQL进行百万条数据过滤优化方案
对百万级别数据进行高效过滤查询,需要综合使用索引、查询优化、表分区、统计信息和视图等技术手段。通过合理的数据库设计和查询优化,可以显著提升查询性能,确保系统的高效稳定运行。
517 9

相关产品

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

    更多