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

SQL优化 -- 利用Rownum条件Count Stop特性优化SQL的一个案例

9513

Rownum 条件用于限制返回结果集数据量。对于两表连接的场景,如果没有rownum限制,则需要对两张表进行完整地关联,返回所有的行。是否有Rownum,对于mergejoin影响不大(hashjoin则视hash table的大小而定),而对于采用nestloop的场景影响是非常大的(没有必要等待所有连接结果返回后再根据取rownum取部分数据)。 如果rownum能与 nestloop 或索引结合,可以有效地提升SQL效率。对于同时包含order by 的场景,可以以含有排序列的表为驱动表,先进行排序,再与其他表进行关联。本例场景是客户现场一个POC案例,涉及多张表关联,排序列也涉及多表的列。语句看起来有点复杂,但实际是同一个优化思路。

PS:由于用户信息保密需要,本例只是模拟现场的语句逻辑及数据量,构造了相应的案例。同时,简化案例也为了让语句更容易读懂。

一、构造测试案例数据

create table t1(t1id1 integer, t1id2 integer, t1name text);
create table t2(t2id1 integer, t2id2 integer, t2name text);
create table t3(t3id1 integer, t3id2 integer, t3name text);
insert into t1 select id,mod(id,500),repeat('a',1000) from generate_series(1,10000) id;

begin
  for i in 1..9 loop
    insert into t1 select * from t1;
  end loop;
end;

insert into t2 select id,mod(id,1000),repeat('a',1000) from generate_series(1,10000) id;
insert into t3 select id,mod(id,100),repeat('a',1000) from generate_series(1,10000) id;
create index idx_t1_t1id1 on t1(t1id1);

其中 t1 表数据量较大。

二、优化前SQL执行计划

select * from (
select t1id2,t2id2,t3id2 from t1,t2,t3
where t1id1=t2id1 and t2id1=t3id1 order by t2id2 desc nulls last,t3id2 asc nulls first 
) limit 100

SQL 涉及3张表的关联,排序的列涉及到t2 与 t3 两张表。

优化前的执行计划如下:可以看到SQL先进行3表关联,最后再排序取100条。

                                                                QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=1602047.36..1602048.61 rows=100 width=12) (actual time=11369.060..11369.088 rows=100 loops=1)
   ->  Sort  (cost=1602047.36..1614847.37 rows=5120003 width=12) (actual time=11369.059..11369.075 rows=100 loops=1)
         Sort Key: t2.t2id2 DESC NULLS LAST, t3.t3id2 NULLS FIRST
         Sort Method: top-N heapsort  Memory: 34kB
         ->  Hash Join  (cost=3445.50..856474.57 rows=5120003 width=12) (actual time=14.213..10325.444 rows=5120000 loops=1)
               Hash Cond: (t1.t1id1 = t2.t2id1)
               ->  Seq Scan on t1  (cost=0.00..782629.03 rows=5120003 width=8) (actual time=0.010..7558.438 rows=5120000 loops=1)
               ->  Hash  (cost=3320.50..3320.50 rows=10000 width=16) (actual time=14.186..14.192 rows=10000 loops=1)
                     Buckets: 16384  Batches: 1  Memory Usage: 597kB
                     ->  Hash Join  (cost=1654.00..3320.50 rows=10000 width=16) (actual time=5.182..11.919 rows=10000 loops=1)
                           Hash Cond: (t2.t2id1 = t3.t3id1)
                           ->  Seq Scan on t2  (cost=0.00..1529.00 rows=10000 width=8) (actual time=0.004..2.726 rows=10000 loops=1)
                           ->  Hash  (cost=1529.00..1529.00 rows=10000 width=8) (actual time=5.159..5.160 rows=10000 loops=1)
                                 Buckets: 16384  Batches: 1  Memory Usage: 519kB
                                 ->  Seq Scan on t3  (cost=0.00..1529.00 rows=10000 width=8) (actual time=0.005..3.184 rows=10000 loops=1)
 Planning Time: 0.494 ms
 Execution Time: 11369.173 ms

从执行计划可以看到,实际语句最耗时的部分是t1表的访问,优化的方向也集中在了如何避免t1表的全表扫描上。由于语句只需要返回100条记录,自然就会考虑到用索引。由于排序列不涉及t1表,因此,可以先对t2, t3 表连接结果进行排序,再以排序的结果集为驱动表,与t1进行nestloop,这样就可以避免对t1 表的全表访问。可问题在于,本例的排序列涉及到两个表,靠优化器还无法这么智能地按以上思路执行,只能通过人为改写实现。

以下是同样SQL在oracle环境的执行情况


三、优化后的SQL及执行计划

基于思路,我们通过CTE语法进行改写,改写后的SQL如下:

with tmp_query1 as (select t2id1,t2id2,t3id2 from t2,t3 where t2id1=t3id1 order by t2id2 desc nulls last,t3id2 asc nulls first)
select t1id2,t2id2,t3id2 from t1, tmp_query1 where t1id1=t2id1 order by t2id2 desc nulls last,t3id2 asc nulls first limit 100

tmp_query1 内部先对 t2, t3 的连接结果进行排序,然后再以 tmp_query1 为驱动表与 t1 进行nestloop 连接。修改后的 SQL 有两个order by(优化器实际会忽略外层的order by , 没必要),目的是为了保证语句结果的一致性,实际并不会执行两次排序。

执行计划如下:

                                                             QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=3985.32..4132.93 rows=100 width=12) (actual time=11.379..11.464 rows=100 loops=1)
   ->  Nested Loop  (cost=3985.32..7561910.61 rows=5120003 width=12) (actual time=11.378..11.458 rows=100 loops=1)
         ->  Sort  (cost=3984.89..4009.89 rows=10000 width=12) (actual time=11.349..11.351 rows=1 loops=1)
               Sort Key: t2.t2id2 DESC NULLS LAST, t3.t3id2 NULLS FIRST
               Sort Method: quicksort  Memory: 853kB
               ->  Hash Join  (cost=1654.00..3320.50 rows=10000 width=12) (actual time=4.995..8.964 rows=10000 loops=1)
                     Hash Cond: (t2.t2id1 = t3.t3id1)
                     ->  Seq Scan on t2  (cost=0.00..1529.00 rows=10000 width=8) (actual time=0.011..1.718 rows=10000 loops=1)
                     ->  Hash  (cost=1529.00..1529.00 rows=10000 width=8) (actual time=4.960..4.961 rows=10000 loops=1)
                           Buckets: 16384  Batches: 1  Memory Usage: 519kB
                           ->  Seq Scan on t3  (cost=0.00..1529.00 rows=10000 width=8) (actual time=0.004..2.692 rows=10000 loops=1)
         ->  Index Scan using idx_t1_t1id1 on t1  (cost=0.43..750.65 rows=513 width=8) (actual time=0.023..0.090 rows=100 loops=1)
               Index Cond: (t1id1 = t2.t2id1)
 Planning Time: 0.168 ms
 Execution Time: 11.498 ms

可以看到,执行计划如我们所设想的。整个执行效率因为减少了排序的数据量,避免了t1表的全表访问,效率得到了非常大的提升。

四、总结

Count Stop + nestloop 是常见的优化方法,DBA 在遇到含有 rownum 的SQL时,需特别关注。


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

评论