使用用户定义的函数和左加入 oracle 10g 的分页速度很慢

Posted

技术标签:

【中文标题】使用用户定义的函数和左加入 oracle 10g 的分页速度很慢【英文标题】:pagination slow with userdefined function and left join oracle 10g 【发布时间】:2018-06-06 09:52:51 【问题描述】:

我正在尝试通过使用分页和每页显示 60 条记录来加速查询(大约 60k 行的用户列表) 这是我的查询(oracle 10g)

select * 
  from ( select 
  a.*, ROWNUM rnum 
      from ( select u.username,u.userfullname,u.usercomment,u.isInner,u.isTeacher,u.isEmployer,u.deleted, 
     ps_fio(u.personid, 1) teachername, 
  ps_fio(sr.personid, 1) studentname,  
                case when u.isInner=1 then '' end Inn,  
                case when u.isStudent=1 then '' end Stud,  
                case when u.isTeacher=1 then '' end Teach from AD_Users u  
                left join fc_studentrecords sr on sr.recordid=u.studentid 
    order by u.username) a 
      where ROWNUM <= 120) 
where rnum  > 60; 

问题是,一旦我将左联接放入 fc_studentrecords 表并尝试选择 ps_fio(sr.personid, 1),使用我的查询执行时间为 8.5 秒,而仅使用左联接和 sr 则为 0.6 秒.personid。

我确实在 ad_users ps_fio(u.personid, 1) 和 fc_studentrecords ps_fio(sr.personid, 1) 上放置了 2 个基于函数的索引

fc_studentrecords 表有大约 80K 行

这是 ps_fio 函数的代码(基本上它显示给定 personid 的人的全名)

  create or replace FUNCTION PS_FIO(PerId PS_PERSONS.PERSONID%Type, FioType NUMBER) RETURN VARCHAR2 DETERMINISTIC IS 
    result VARCHAR2(70);
    gender_int NUMBER ;


    BEGIN
    select decode(m.message_id,'SEX_MALE',1,0) into gender_int
    from ps_persons p
    join rb_messages m on m.message_value=p.sex
    where personid=PerId;  

    BEGIN
    select case  
    when fiotype=0 then trim(familyname)||' '||trim(firstname)||'    '||trim(secondname)
      when fiotype=1 then trim(familyname)||' '||substr(firstname, 1, 1)||'. '||substr(secondname, 1, 1)||'.'
      else '' end into result
     from ps_persons
      where personid=PerId;  
     EXCEPTION
      WHEN no_data_found then result:= '';
     END;
    return result;
    END PS_FIO;


here are the create table scripts for ad_users  and fc_studenrecords table

  CREATE TABLE "COPYREAL"."AD_USERS" 
   (    "USERNAME" VARCHAR2(25 CHAR), 
    "USERFULLNAME" VARCHAR2(100 CHAR) NOT NULL ENABLE, 
    "USERCOMMENT" VARCHAR2(200 CHAR), 
    "PWD" RAW(16), 
    "PERSONID" NUMBER(10,0), 
    "ISINNER" NUMBER(1,0) DEFAULT 1 NOT NULL ENABLE, 
    "ISTEACHER" NUMBER(1,0) DEFAULT 0 NOT NULL ENABLE, 
    "ISSTUDENT" NUMBER(1,0) DEFAULT 0 NOT NULL ENABLE, 
    "STUDENTID" NUMBER(10,0), 
    "DELETED" NUMBER(1,0) DEFAULT 0 NOT NULL ENABLE, 
    "ISEMPLOYER" NUMBER(1,0) DEFAULT 0 NOT NULL ENABLE, 
    "USERCODE" NUMBER(5,0), 
    "MOBILE_NUMBER" NUMBER(10,0), 
     CONSTRAINT "PK_AD_USERS" PRIMARY KEY ("USERNAME")
  USING INDEX PCTFREE 10 INITRANS 2 MAXTRANS 255 COMPUTE STATISTICS 
  STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
  PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT)
  TABLESPACE "SYSTEM"  ENABLE, 
     CONSTRAINT "CH_USER_PERSON" CHECK (personid is not null or isTeacher=0) ENABLE, 
     CONSTRAINT "CH_USER_STUDENT" CHECK (studentid is not null or isStudent=0) ENABLE, 
     CONSTRAINT "UK_USERCODE" UNIQUE ("USERCODE")
  USING INDEX PCTFREE 10 INITRANS 2 MAXTRANS 255 COMPUTE STATISTICS 
  STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
  PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT)
  TABLESPACE "USERS"  ENABLE, 
     CONSTRAINT "FK_USER_PERSON" FOREIGN KEY ("PERSONID")
      REFERENCES "COPYREAL"."PS_PERSONS" ("PERSONID") ENABLE, 
     CONSTRAINT "FK_USER_STUDENT" FOREIGN KEY ("STUDENTID")
      REFERENCES "COPYREAL"."FC_STUDENTRECORDS" ("RECORDID") ENABLE
   ) PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 NOCOMPRESS LOGGING
  STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
  PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT)
  TABLESPACE "SYSTEM" ;

  CREATE INDEX "COPYREAL"."AD_USERS_FIO_IDX" ON "COPYREAL"."AD_USERS" ("COPYREAL"."PS_FIO"("PERSONID",1)) 
  PCTFREE 10 INITRANS 2 MAXTRANS 167 COMPUTE STATISTICS 
  STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
  PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT)
  TABLESPACE "USERS" ;


  CREATE INDEX "COPYREAL"."IX_FK_USER_PERSON" ON "COPYREAL"."AD_USERS" ("PERSONID") 
  PCTFREE 10 INITRANS 2 MAXTRANS 255 COMPUTE STATISTICS 
  STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
  PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT)
  TABLESPACE "USERS" ;


  CREATE INDEX "COPYREAL"."IX_FK_USER_STUDENT" ON "COPYREAL"."AD_USERS" ("STUDENTID") 
  PCTFREE 10 INITRANS 2 MAXTRANS 255 COMPUTE STATISTICS 
  STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
  PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT)
  TABLESPACE "USERS" ;






 CREATE TABLE "COPYREAL"."FC_STUDENTRECORDS" 
   (    "RECORDID" NUMBER(10,0), 
    "PERSONID" NUMBER(10,0) NOT NULL ENABLE, 
    "BOOKNO" VARCHAR2(15 CHAR), 
    "BOOKDATE" DATE, 
    "DIPLOMANO" VARCHAR2(15 CHAR), 
    "PLANID" NUMBER(10,0) NOT NULL ENABLE, 
    "DOCDATE" DATE, 
    "DOCNO" VARCHAR2(15 CHAR), 
    "DOCSUM" NUMBER(15,2), 
    "BRANCHID" NUMBER(4,0) DEFAULT 1 NOT NULL ENABLE, 
    "STATEMENTID" NUMBER(10,0), 
    "DOCPRIVTYPE" NUMBER(2,0), 
    "DOCPRIVPER" NUMBER(3,0), 
    "PREVDOC" VARCHAR2(30 CHAR), 
    "ENTERED" VARCHAR2(30 CHAR), 
    "STUDYFORMID" NUMBER(1,0), 
    "BUDGET" NUMBER(1,0) DEFAULT 0 NOT NULL ENABLE, 
    "PLACE" VARCHAR2(40 CHAR), 
    "PAID_SUM" NUMBER(15,2), 
    "FINAL_PAYMENT_DATE" DATE, 
    "PAID_BY_CAPITAL" NUMBER(1,0) DEFAULT 0 NOT NULL ENABLE, 
    "USERNAME" VARCHAR2(25 CHAR), 
    "INDIV_STUDENTID" NUMBER(10,0), 
    "CLIENT" VARCHAR2(70 CHAR), 
     CONSTRAINT "PK_FC_STUDENTRECORDS" PRIMARY KEY ("RECORDID")
  USING INDEX PCTFREE 10 INITRANS 2 MAXTRANS 255 COMPUTE STATISTICS 
  STORAGE(INITIAL 720896 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
  PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT)
  TABLESPACE "USERS"  ENABLE, 
     CONSTRAINT "UK_BOOKNO" UNIQUE ("BOOKNO")
  USING INDEX PCTFREE 10 INITRANS 2 MAXTRANS 255 COMPUTE STATISTICS 
  STORAGE(INITIAL 917504 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
  PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT)
  TABLESPACE "USERS"  ENABLE, 
     CONSTRAINT "UK_FC_STYDENTRECORDS" UNIQUE ("PERSONID", "PLANID")
  USING INDEX PCTFREE 10 INITRANS 2 MAXTRANS 255 COMPUTE STATISTICS 
  STORAGE(INITIAL 2097152 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
  PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT)
  TABLESPACE "USERS"  ENABLE, 
     CONSTRAINT "FK_FCDSTUDRECS_RBCOMMONSTFORMS" FOREIGN KEY ("STUDYFORMID")
      REFERENCES "COPYREAL"."RB_COMMONSTUDYFORMS" ("FORMID") ENABLE, 
     CONSTRAINT "FK_STUDENTRECORDS_DEPARTMENTS" FOREIGN KEY ("BRANCHID")
      REFERENCES "COPYREAL"."RB_DEPARTMENTS" ("CODE") ENABLE, 
     CONSTRAINT "FK_PS_PERSONS" FOREIGN KEY ("PERSONID")
      REFERENCES "COPYREAL"."PS_PERSONS" ("PERSONID") ENABLE, 
     CONSTRAINT "FK_FC_STUDENTREC_PL_EDUCPLANS" FOREIGN KEY ("PLANID")
      REFERENCES "COPYREAL"."PL_EDUCPLANS" ("PLANID") ENABLE, 
     CONSTRAINT "FK_FS_STUDREC_FC_PRIVEL" FOREIGN KEY ("DOCPRIVTYPE")
      REFERENCES "COPYREAL"."FC_PRIVILEGETYPES" ("PRIVILEGEID") ENABLE, 
     CONSTRAINT "FK_FC_STUDENTRECORDS_AD_USERS" FOREIGN KEY ("USERNAME")
      REFERENCES "COPYREAL"."AD_USERS" ("USERNAME") ENABLE, 
     CONSTRAINT "FK_FCSTUDRECS_ENSTATEMENTS" FOREIGN KEY ("STATEMENTID")
      REFERENCES "COPYREAL"."EN_STATEMENTS" ("STATEMENTID") ENABLE, 
     CONSTRAINT "FK_FC_STUDREC_IP_STUDENTS" FOREIGN KEY ("INDIV_STUDENTID")
      REFERENCES "COPYREAL"."IP_STUDENTS" ("STUDENTID") ENABLE
   ) PCTFREE 10 PCTUSED 40 INITRANS 1 MAXTRANS 255 NOCOMPRESS LOGGING
  STORAGE(INITIAL 3145728 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
  PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT)
  TABLESPACE "USERS" ;



  CREATE INDEX "COPYREAL"."FC_STUDENTRECORDS_FIO_IDX" ON "COPYREAL"."FC_STUDENTRECORDS" ("COPYREAL"."PS_FIO"("PERSONID",1)) 
  PCTFREE 10 INITRANS 2 MAXTRANS 167 COMPUTE STATISTICS 
  STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
  PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT)
  TABLESPACE "USERS" ;


  CREATE INDEX "COPYREAL"."FC_STUDENTRECORDS_FIO_IDX2" ON "COPYREAL"."FC_STUDENTRECORDS" ("COPYREAL"."PS_FIO"("PERSONID",0)) 
  PCTFREE 10 INITRANS 2 MAXTRANS 167 COMPUTE STATISTICS 
  STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
  PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT)
  TABLESPACE "USERS" ;


  CREATE INDEX "COPYREAL"."IX_FC_STUDENTRECORDS" ON "COPYREAL"."FC_STUDENTRECORDS" ("PERSONID") 
  PCTFREE 10 INITRANS 2 MAXTRANS 255 COMPUTE STATISTICS 
  STORAGE(INITIAL 2097152 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
  PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT)
  TABLESPACE "USERS" ;


  CREATE INDEX "COPYREAL"."IX_FK_FCDSTUDRECS_RBCOMMONSTFO" ON "COPYREAL"."FC_STUDENTRECORDS" ("STUDYFORMID") 
  PCTFREE 10 INITRANS 2 MAXTRANS 255 COMPUTE STATISTICS 
  STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
  PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT)
  TABLESPACE "USERS" ;


  CREATE INDEX "COPYREAL"."IX_FK_FCSTUDRECS_ENSTATEMENTS" ON "COPYREAL"."FC_STUDENTRECORDS" ("STATEMENTID") 
  PCTFREE 10 INITRANS 2 MAXTRANS 255 COMPUTE STATISTICS 
  STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
  PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT)
  TABLESPACE "USERS" ;


  CREATE INDEX "COPYREAL"."IX_FK_FC_STUDENTREC_PL_EDUCPLA" ON "COPYREAL"."FC_STUDENTRECORDS" ("PLANID") 
  PCTFREE 10 INITRANS 2 MAXTRANS 255 COMPUTE STATISTICS 
  STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
  PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT)
  TABLESPACE "USERS" ;


  CREATE INDEX "COPYREAL"."IX_FK_FS_STUDREC_FC_PRIVEL" ON "COPYREAL"."FC_STUDENTRECORDS" ("DOCPRIVTYPE") 
  PCTFREE 10 INITRANS 2 MAXTRANS 255 COMPUTE STATISTICS 
  STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
  PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT)
  TABLESPACE "USERS" ;


  CREATE INDEX "COPYREAL"."IX_FK_STUDENTRECORDS_DEPARTMEN" ON "COPYREAL"."FC_STUDENTRECORDS" ("BRANCHID") 
  PCTFREE 10 INITRANS 2 MAXTRANS 255 COMPUTE STATISTICS 
  STORAGE(INITIAL 65536 NEXT 1048576 MINEXTENTS 1 MAXEXTENTS 2147483645
  PCTINCREASE 0 FREELISTS 1 FREELIST GROUPS 1 BUFFER_POOL DEFAULT)
  TABLESPACE "USERS" ;

这是 sql developer 的自动跟踪输出

buffer is not pinned count  207278
bytes received via SQL*Net from client  880
bytes sent via SQL*Net to client    20975
calls to get snapshot scn: kcmgss   207199
consistent gets 674851
consistent gets - examination   310692
consistent gets from cache  674851
CPU used by this session    890
CPU used when call started  898
cursor authentications  4
DB time 914
enqueue releases    5
enqueue requests    5
execute count   103599
index fetch by key  103590
no work - consistent read gets  260481
opened cursors cumulative   7
OS Block input operations   872
OS Involuntary context switches 62
OS Page faults  2
OS Page reclaims    1714
OS System time used 159
OS User time used   739
OS Voluntary context switches   39
parse count (hard)  5
parse count (total) 7
parse time cpu  1
parse time elapsed  1
recursive calls 103634
recursive cpu usage 687
rows fetched via callback   103512
session logical reads   674851
session pga memory  -327680
shared hash latch upgrades - no wait    78
sorts (memory)  3
sorts (rows)    53897
sql area evicted    1
SQL*Net roundtrips to/from client   9
table fetch by rowid    103600
table fetch continued row   78
table scan blocks gotten    260393
table scan rows gotten  1948362
table scans (short tables)  51797
user calls  11
workarea executions - optimal   7

我错过了什么吗?

【问题讨论】:

【参考方案1】:

从 SQL 查询内部调用 PL/SQL 函数可能会非常昂贵,因为它会导致每行从 SQL 切换到 PL/SQL。

ad_users.person_id 似乎可以为空?如果u.personidsr.personidNULL,我会排除对ps_fio 的调用,具体取决于这些空值的频率。

接下来,我将从函数ps_fio 中删除gender_int 的计算,因为它似乎没有被使用。

接下来,我将尝试将函数调用替换为表 persons 的连接,并直接在查询中计算全名。丑陋,但可能更快。

最后,您对trim(familyname) 等的调用表明,表 ps_persons 的名称列中有尾随和/或前导空格。速度上的好处是微乎其微的,但可能这种清理可以在表中完成一次,这样就不需要调用修剪函数了吗?

【讨论】:

【参考方案2】:

您在两个FULL TABLE SCANS 执行计划上使用HASH JOIN,这不是分页的好选择

为什么?较小的表必须在哈希表中进行完全扫描和转换,并且必须先探测第二个表的第一个块,然后才能显示第一页。所以你有一个在第一页之前的时间惩罚。没有人这么有耐心。

你应该有一个快速显示第一页并惩罚更高页面的执行计划。

你使用的典型

索引来访问驱动表(避免排序) 内表的嵌套循环连接 对内表的索引访问

这里是一个例子(注意我使用提示来强制执行所需的访问)

select * 
  from ( select 
  a.*, ROWNUM rnum 
      from ( select /*+ ordered use_nl(u sr) index(u AD_Users_idx) */ u.username,u.userfullname,u.usercomment,u.isInner,u.isTeacher,u.isEmployer,u.deleted, 
--  
                case when u.isInner=1 then '' end Inn,  
                case when u.isStudent=1 then '' end Stud,  
                case when u.isTeacher=1 then '' end Teach from AD_Users u  
                left join fc_studentrecords sr on sr.recordid=u.studentid 
    order by u.username) a 
      where ROWNUM <= 120) 
where rnum  > 60; 

请确保username 列已编入索引并且NOT NULL可以(索引访问所必需)。 列 recordid 也必须被索引。

预期的执行计划如下:

---------------------------------------------------------------------------------------------------------
| Id  | Operation                       | Name                  | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT                |                       |   100K|    16M|  2022K  (1)| 06:44:26 |
|*  1 |  VIEW                           |                       |   100K|    16M|  2022K  (1)| 06:44:26 |
|*  2 |   COUNT STOPKEY                 |                       |       |       |            |          |
|   3 |    VIEW                         |                       |  1000K|   150M|  2022K  (1)| 06:44:26 |
|   4 |     NESTED LOOPS OUTER          |                       |  1000K|   182M|  2022K  (1)| 06:44:26 |
|   5 |      TABLE ACCESS BY INDEX ROWID| AD_USERS              |  1000K|   169M| 19871   (1)| 00:03:59 |
|   6 |       INDEX FULL SCAN           | AD_USERS_IDX          |  1000K|       |   272   (1)| 00:00:04 |
|*  7 |      INDEX RANGE SCAN           | FC_STUDENTRECORDS_IDX |     1 |    13 |     2   (0)| 00:00:01 |
---------------------------------------------------------------------------------------------------------

此计划立即返回第一页,更高的页面需要更多时间,因为您必须遍历索引。所以我的测试设置中的第 10.000 页需要几秒钟。但是,如果您有足够的耐心来分页,这将没有问题;)

请注意,我消除了函数调用 - 在最外面的查询中首先调用它们是一个明智的主意,即仅针对选定的行 - 不适用于跳过的行

还要注意,在 12g 中您可以使用 Oracle supported pagination。

【讨论】:

以上是关于使用用户定义的函数和左加入 oracle 10g 的分页速度很慢的主要内容,如果未能解决你的问题,请参考以下文章

Oracle 函数并发

在 Oracle 10g 上创建聚合函数返回无用的错误

oracle 10g中如何缓存函数的查询结果?

在 Oracle 10g 中将参数解析为来自 SP 的视图

在 Oracle 10g 上使用“With”SQL 导致错误

Oracle 10G中导入dmp文件