花有重开日,人无再少年

本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

转载声明:转载请注明出处,本技术博客是本人原创文章

本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

原文链接:blog.ouyangsihai.cn >> 花有重开日,人无再少年

今天有位朋友,微信群中问eygle曾经讲过的一个SQL语句优化案例,这是2015年的一次Oracle会议上,eygle讲的一个主题,碰巧当时听了这场演讲,回来模拟了整个操作,记录在了博客中,于是搜出来回复了他,也引来了eygle的感慨,转眼过了这么些年了。

花有重开日,人无再少年

的确如此,人生就是这样,“花有重开日,人无再少年”,有些残酷,可能你会后悔某些决定,一旦选择,就再也不能回头,我们能做的就是把握每一天,时间对每个人都是公平的,就看你如何利用,缺憾也是一种美,只有积极面对,才能继续前进,当然说起来容易做起来难,只能一起共勉,一起加油了啊!

2015年我在CSDN和ITPub博客,记录了eygle这个主题的操作过程,如下链接,文章的标题是“复盘eygle在甲骨文大会上演讲中的示例,看看什么是大师的由点及面”,

https://blog.csdn.net/bisal/article/details/47137239
http://blog.itpub.net/7192724/viewspace-1755954/

eygle在这次甲骨文大会的演讲中,通过一个简单的UPDATE语句,为我们展示了什么叫由点及面的优化,什么叫由点及面的知识覆盖度,不在于这个案具体如何操作,更应关注或更值得我们借鉴的是这种学习态度和方法思路,大师是如何炼成的?我想这个案例可以带给我们一些启迪。

下面就复盘一下这个案例的整个过程,注:版权归eygle所有~

 

问题的标题是:“并行更新成为系统瓶颈”

UPDATE /*+ parallel(a, 8) */ tbl_a a  
SET name = (SELECT name FROM tbl_b WHERE id = a.id),  

        class = (SELECT class FROM tbl_b WHERE id = a.id)  WHERE a.id IN (SELECT /*+ parallel(b, 8) */ id FROM tbl_b b);

现象是这条SQL执行时间非常长,从介绍看是有2.5分钟。

优化过程:

  1. 为了以下可以更清楚地说明问题,对这个SQL做了简化处理,我们需要优化的是这条SQL:

UPDATE tbl_a a SET name = (SELECT name FROM tbl_b WHERE id = a.id),         class = (SELECT class FROM tbl_b WHERE id = a.id) WHERE a.id IN (SELECT id FROM tbl_b b);

我们创建两张模拟表:

create table tbl_a(           id number,           name varchar2(5),           class varchar2(5));

create table tbl_b(           id number,           name varchar2(5),           class varchar2(5));

create sequence seq_a cache 1000;

create sequence seq_b cache 1000;

插入一些随机数据:

begin

  for i in 1 .. 100000 loop

    insert into tbl_a values (seq_a.nextval, dbms_random.string('U', 5), dbms_random.string('U', 5));

  end loop;

  commit;

end;

/

select count(*) from tbl_a;

  COUNT(*)

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

     100000

begin

  for i in 1 .. 10000 loop

    insert into tbl_b values (seq_b.nextval, dbms_random.string('U', 5), dbms_random.string('U', 5));

  end loop;

  commit;

end;

/

select count(*) from tbl_b;

  COUNT(*)

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

      10000
  1. 执行原SQL语句

SQL set timing on

SQL UPDATE tbl_a a

          SET name = (SELECT name FROM tbl_b WHERE id = a.id),

                 class = (SELECT class FROM tbl_b WHERE id = a.id)

          WHERE a.id IN (SELECT id FROM tbl_b b);

10000 rows updated.

Elapsed: 00:00:07.42

需要7秒多的时间(虽然和示例中2.5分钟有差距,但仅为了说明优化的问题,时间上的差距可以忽略)。

  1. 第一次优化

我们从这个SQL中可以看到,更新TBL_A表的ID列,但TBL_B表的SELECT有三次,即三次的全表扫描,那么要是能减少TBL_B表检索的次数,执行时间肯定可以减少。

SQL UPDATE tbl_a a

          SET (name, class) = (SELECT name, class FROM tbl_b WHERE id = a.id)

          WHERE a.id IN (SELECT id FROM tbl_b b);

10000 rows updated.

Elapsed: 00:00:04.04

这样的调整是符合SQL语法的,执行时间变为了4秒多,效果显著。

 

  1. 第二次优化

虽然执行时间减少了接近一半,但SQL中还是对TBL_B执行了两次扫描,是否还可以减少一次?

SQL UPDATE (SELECT b.name b_name, b.class b_class, a.name, a.class

                      FROM tbl_a a, tbl_b b

                      WHERE a.id = b.id)

          SET name = b_name, class = b_class;

SET name = b_name, class = b_class

    *

ERROR at line 4:

ORA-01779: cannot modify a column which maps to a non key-preserved table

Elapsed: 00:00:00.01

这样就做到了只扫描一次TBL_B表,直接对子查询更新,但此时报了一个错误,ORA-01779,

这就引出了non key-preserved table的概念。非键值保存表,杨长老的博客(http://blog.itpub.net/4227/viewspace-195889/)中提到过这个错误:

“造成这个错误的原因是更新的列不是事实表的列,而是维度表的列。换句话说,如果两张表关联,其中一张表的关联列是主键,那么另一张表就是事实表,也就是说另一张表中的列就是可更新的;除非另一张表的关联列也是主键,否则这张表就是不可更新的,如果更新语句涉及到了这张表,就会出现ORA-01779错误。如果是两张表主键关联,那么无论更新哪个表的字段都可以。
其实这个限制的真正原因是Oracle要确保连接后更新的内容可以写到一张表中,而这就要求连接方式必须是1对N或者1对1的连接。这样才能确保连接后的结果集数量和事实表一致。从而使得Oracle对连接后子查询的更新可以顺利的更新到事实表中。”

补充:自己曾经碰见和key-preserved相关的问题,可以参考《》。

a.id=b.id,我们是用TBL_B的id列作为条件更新,需要确保这列只会对应到TBL_B表的一行记录,可以为表TBL_B的id列设置主键、唯一索引或唯一约束,三种操作,这里选择设置唯一约束:

SQL alter table tbl_b add constraint uq_b_id unique(id);

再次执行:

SQL UPDATE (SELECT b.name b_name, b.class b_class, a.name, a.class

                      FROM tbl_a a, tbl_b b

                      WHERE a.id = b.id)

          SET name = b_name, class = b_class;

10000 rows updated.

Elapsed: 00:00:00.12

执行时间一下仅为0.12秒。

上面如果TBL_A的ID列设置为主键,则为1对1的连接,如果仅是TBL_B的ID列为唯一约束,则为1对N的连接。

 

总结:

通过两次优化,执行时间从7秒降到了0.12秒,虽然这里的示例数据未必和实际情况一致,但成比例的缩放足以说明这个问题,从这个案例可以看出,优化的本质就是少做事,原始SQL执行三次全表扫描,那目标就是减少全表扫描的次数,第一次优化的操作可能相对容易想到,但第二次优化的操作,就需要知道可以有这种语法,而且出现了ORA-01779的错误,还需要知道这种错误的根本原因是什么,才能有可行的解决方法。

 

问题还没完,以上说明了SQL语句的优化,下面就是针对这条SQL展开的知识。

假设上面的TBL_A和TBL_B表是属于用户bisal的,此时新建一个用户phibisal,并授予最简单的权限:

SQL create user phibisal identified by phibisal;

SQL grant create session to phibisal;

bisal用户创建这两张表的public同义词:

SQL create public synonym tbl_a for bisal.tbl_a;

SQL create public synonym tbl_b for bisal.tbl_b;

然后授予phibisal用户对TBL_A表的读和更新权限:

SQL grant select, update on tbl_a to phibisal;

此时phibisal登录后执行:

sqlplus phibisal/phibisal

SQL UPDATE (SELECT b.name b_name, b.class b_class, a.name, a.class

                      FROM tbl_a a, tbl_b b

                      WHERE a.id = b.id)

          SET name = b_name, class = b_class;

                FROM tbl_a a, tbl_b b

                                    *

ERROR at line 2:

ORA-00942: table or view does not exist

会提示TBL_B不存在,因为用户没有该表的任何权限,(注:此处和eygle的示例中反馈不同,他提示的是ORA-01031: insufficient privileges)

如果授予phibisal对TBL_B表的读权限,

SQL grant select on tbl_b to phibisal;

此时可以完成更新:

sqlplus phibisal/phibisal

SQL UPDATE tbl_a a

          SET (name, class) = (SELECT name, class FROM tbl_b WHERE id = a.id)

          WHERE a.id IN (SELECT id FROM tbl_b b);

10000 rows updated.

但用如下SQL会提示权限错误:

UPDATE (SELECT b.name b_name, b.class b_class, a.name, a.class

                      FROM tbl_a a, tbl_b b

                      WHERE a.id = b.id)

          SET name = b_name, class = b_class;

                FROM tbl_a a, tbl_b b

                                    *

ERROR at line 2:

ORA-01031: insufficient privileges

即这种子查询更新会因没有TBL_B表的UPDATE权限报错。

但如果使用如下with语法,则可以正常执行:

SQL UPDATE

(WITH tmp AS (

              SELECT b.name b_name, b.class b_class, a.name, a.class

              FROM tbl_a a, tbl_b b

              WHERE a.id = b.id)

             )

SET name = b_name, class = b_class;

10000 rows updated.

做得更彻底一些:

SQL revoke update on tbl_a from phibisal;

撤消了phibisal用户对TBL_A的更新权限,按理说,phibisal用户不应该能再更新TBL_A表了。

使用上面两个调整后的SQL,确实如此:

sqlplus phibisal/phibisal

SQL UPDATE (SELECT b.name b_name, b.class b_class, a.name, a.class

                      FROM tbl_a a, tbl_b b

                      WHERE a.id = b.id)

          SET name = b_name, class = b_class;

                FROM tbl_a a, tbl_b b

                                    *

ERROR at line 2:

ORA-01031: insufficient privileges 

SQL UPDATE tbl_a a

          SET (name, class) = (SELECT name, class FROM tbl_b WHERE id = a.id)

          WHERE a.id IN (SELECT id FROM tbl_b b);

UPDATE tbl_a a

       *

ERROR at line 1:

ORA-01031: insufficient privileges

但是,奇怪的是如下SQL可以执行:

SQL UPDATE

(WITH tmp AS (

              SELECT b.name b_name, b.class b_class, a.name, a.class

              FROM tbl_a a, tbl_b b

              WHERE a.id = b.id)

              SELECT * FROM tmp

             )

SET name = b_name, class = b_class;

10000 rows updated.

这就从原理规则上,违背了权限控制,看下版本:

SQL select banner from v$version where rownum=1;

BANNER

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

Oracle Database 11g Enterprise Edition Release 11.2.0.1.0 - 64bit Production

这就是2014年7月提出的一个bug,在11.2.0.3、11.2.0.4、12.1等版本中都存在的一个问题,需要修正这个bug,相当于使用with语法,可以绕过用户权限,对没有权限的表进行DML操作。

总结:

精髓不在于这个bug,而是在于从一条简单的UPDATE语句,可以派生出如此丰富的知识,可谓举一反三,受益匪浅。一方面需要我们能够从原理上理解每一个概念,另一方面也要培养自己举一反三,知识点由点及面的想法,做到真正的触类旁通,这样才能逐渐向大师靠拢,向大师学习。

再次向大师致敬!

如果您觉得本文有帮助,欢迎关注转发:bisal的个人杂货铺,

花有重开日,人无再少年 花有重开日,人无再少年
本人花费半年的时间总结的《Java面试指南》已拿腾讯等大厂offer,已开源在github ,欢迎star!

转载声明:转载请注明出处,本技术博客是本人原创文章

本文GitHub https://github.com/OUYANGSIHAI/JavaInterview 已收录,这是我花了6个月总结的一线大厂Java面试总结,本人已拿大厂offer,欢迎star

原文链接:blog.ouyangsihai.cn >> 花有重开日,人无再少年


 上一篇
一生可能只有一次耐高,但优化可以不止一次 一生可能只有一次耐高,但优化可以不止一次
最近单位举办了开发者大会,有幸分享了一个话题,《曾经踩过的“坑”-一则应用夜维优化案例》,其实在之前OCMU的活动上,就讲过这个主题,简介可以参考《》中的主题三, 主题三:应用程序夜维优化案例简介:某核心系统生产环境,对于历史
下一篇 
under any table权限 under any table权限
这篇文章《》,介绍的知识点中,有一处细节问题,描述信息如下, SQL UPDATE (SELECT b.name b_name, b.class b_class, a.name, a.class