Oracle 19c慢查询性能优化求助:执行计划相关疑问
- 一条SELECT查询耗时1515秒,需基于提供的执行计划优化性能
- 已在USAGETYPE表的
event_type列创建索引,但执行计划仍显示TABLE FULL SCAN,需明确原因 - 执行计划中的
BUFFER SORT是什么?是否需要避免?
注:使用Oracle 19c版本
SELECT boi.documentid, boi.device_id , bie.end_t , bie.vfqbalance_LOCATION , bie.vfqbalance_CALLED_TO , bie.vfqbalance_DURATION , CEIL(bie.vfqbalance_DURATION / 60.0), bie.vfqbalance_LOC_AREA_CODE , NVL(bie.gsminfoNUMBER, 0) , NVL(bie.evttotal_amount, 0) , bid.vfBucketId, bie.vfqbalance_AMOUNT , 0, bie.vfqbalance_CALLING_NUMBER, bie.crbtinfo_CHANNEL , bie.crbtinfo_PRODUCT_NAME , usageTypeId, usageName, usageType, usageCategory, CASE ut.impact_category WHEN u'VAS CONTENT' THEN COALESCE(bie.crbtinfo_CHANNEL, u' ') || COALESCE(u' ' || bie.crbtinfo_PRODUCT_NAME, u' ') ELSE usageDetail END as usageDetail, usageDst, bie.vfqbalance_RECORD_TYPE, 100,100, bie.vfqbalance_TYPE , bie.vfqbalance_OPTIONS , boi.TYPE_STR , bie.vfqbalance_purchase_start_t , bie.vfqbalance_resource_name , boi.AAC_ACCESS,0,0,0 FROM (SELECT * FROM invoice_items WHERE ( ITEMTYPE = 'AR_ITEMS' OR ITEMTYPE = 'SUB_ITEMS')) boi JOIN invoice_items_events bie ON boi.item_obj_3 = bie.item_obj_3 AND boi.ITEMTYPE = bie.ITEMTYPE AND boi.DOCUMENTID = bie.documentid JOIN USAGETYPE ut ON bie.event_obj_2 = ut.event_obj and ( bie.telcoinfo_USAGE_CLASS = ut.USAGE_CLASS OR bie.telcoinfo_USAGE_CLASS=u'Non-roaming' or ut.usage_class IS NULL ) AND ( bie.vfqbalance_RECORD_TYPE = ut.record_type or ut.record_type IS NULL ) AND ( ut.event_type IS NULL ) AND ( bie.impact_category = ut.impact_category or ut.impact_category IS NULL ) AND ( bie.vfqbalance_CALLED_TO = ut.called_to or ut.called_to IS NULL OR (ut.called_to = u'!' And bie.vfqbalance_CALLED_TO NOT IN ( SELECT DISTINCT u.called_to FROM USAGETYPE u WHERE u.called_to IS NOT NULL AND u.called_to != u'!') ) ) LEFT JOIN VFQBALANCEBUCKET bid on bid.vfBucketId = bie.vfqbalance_VFQ_BUCKET_ID Where bie.vfqbalance_RECORD_TYPE is not null or ut.usageType in (u'vas',u'video') AND CASE WHEN ut.usageType = u'voice' AND bie.vfqbalance_DURATION = u'0' AND bie.evttotal_amount = 0 THEN 0 ELSE 1 END = 1;
Plan hash value: 194770266 ----------------------------------------------------------------------------------------------------------------------- | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | ----------------------------------------------------------------------------------------------------------------------- | 0 | SELECT STATEMENT | | 1 | 5809 | 5 (0)| 00:00:01 | |* 1 | FILTER | | | | | | | 2 | NESTED LOOPS OUTER | | 1 | 5809 | 5 (0)| 00:00:01 | | 3 | NESTED LOOPS | | 1 | 5787 | 5 (0)| 00:00:01 | | 4 | MERGE JOIN CARTESIAN | | 1 | 2068 | 5 (0)| 00:00:01 | | 5 | INLIST ITERATOR | | | | | | | 6 | TABLE ACCESS BY INDEX ROWID BATCHED| INVOICE_ITEMS | 1 | 1313 | 0 (0)| 00:00:01 | |* 7 | INDEX RANGE SCAN | INVOICE_ITEMS_ITEMTYPE | 1 | | 0 (0)| 00:00:01 | | 8 | BUFFER SORT | | 390 | 287K| 5 (0)| 00:00:01 | |* 9 | TABLE ACCESS FULL | USAGETYPE | 390 | 287K| 5 (0)| 00:00:01 | |* 10 | TABLE ACCESS BY INDEX ROWID BATCHED | INVOICE_ITEMS_EVENTS | 1 | 3719 | 0 (0)| 00:00:01 | |* 11 | INDEX RANGE SCAN | INVOICE_ITEMS_EVENTS_DOCID | 1 | | 0 (0)| 00:00:01 | |* 12 | INDEX RANGE SCAN | VFQBALANCEBUCKET | 1 | 22 | 0 (0)| 00:00:01 | |* 13 | INDEX FAST FULL SCAN | USAGETYPE_CALLED_TO | 342 | 6156 | 2 (0)| 00:00:01 | ----------------------------------------------------------------------------------------------------------------------- Predicate Information (identified by operation id): --------------------------------------------------- 1 - filter("UT"."CALLED_TO" IS NULL OR "BIE"."VFQBALANCE_CALLED_TO"="UT"."CALLED_TO" OR "UT"."CALLED_TO"=U'!' AND NOT EXISTS (SELECT 0 FROM "USAGETYPE" "U" WHERE "U"."CALLED_TO" IS NOT NULL AND "U"."CALLED_TO"<>'!' AND LNNVL("U"."CALLED_TO"<>:B1))) 7 - access("ITEMTYPE"='AR_ITEMS' OR "ITEMTYPE"='SUB_ITEMS') 9 - filter("UT"."EVENT_TYPE" IS NULL) 10 - filter("BIE"."EVENT_OBJ_2"="UT"."EVENT_OBJ" AND "INVOICE_ITEMS"."ITEM_OBJ_3"="BIE"."ITEM_OBJ_3" AND "INVOICE_ITEMS"."ITEMTYPE"="BIE"."ITEMTYPE" AND ("BIE"."VFQBALANCE_RECORD_TYPE"="UT"."RECORD_TYPE" OR "UT"."RECORD_TYPE" IS NULL) AND ("BIE"."IMPACT_CATEGORY"="UT"."IMPACT_CATEGORY" OR "UT"."IMPACT_CATEGORY" IS NULL) AND ("BIE"."VFQBALANCE_RECORD_TYPE" IS NOT NULL OR ("UT"."USAGETYPE"=U'vas' OR "UT"."USAGETYPE"=U'video') AND CASE WHEN ("UT"."USAGETYPE"=U'voice' AND "BIE"."VFQBALANCE_DURATION"=U'0' AND "BIE"."EVTTOTAL_AMOUNT"=0) THEN 0 ELSE 1 END =1) AND ("BIE"."ITEMTYPE"='AR_ITEMS' OR "BIE"."ITEMTYPE"='SUB_ITEMS') AND ("BIE"."TELCOINFO_USAGE_CLASS"="UT"."USAGE_CLASS" OR "BIE"."TELCOINFO_USAGE_CLASS"=U'Non-roaming' OR "UT"."USAGE_CLASS" IS NULL)) 11 - access("INVOICE_ITEMS"."DOCUMENTID"="BIE"."DOCUMENTID") 12 - access("BID"."VFBUCKETID"(+)="BIE"."VFQBALANCE_VFQ_BUCKET_ID") 13 - filter("U"."CALLED_TO" IS NOT NULL AND "U"."CALLED_TO"<>'!' AND LNNVL("U"."CALLED_TO"<>:B1)) Note ----- - dynamic statistics used: dynamic sampling (level=2)
一、USAGETYPE表全表扫描的原因
从执行计划的Predicate Information(ID9)可以看到,当前查询对USAGETYPE的过滤条件是"UT"."EVENT_TYPE" IS NULL,即只取event_type为NULL的行。
Oracle普通B树索引默认不会存储NULL值,因此你创建的event_type列索引无法用来定位这些NULL行,Oracle只能通过全表扫描来筛选符合条件的数据。
二、BUFFER SORT的含义与是否需要避免
含义
BUFFER SORT是Oracle将查询结果加载到内存并完成排序的操作,这里它出现在MERGE JOIN CARTESIAN之前,作用是把USAGETYPE全表扫描的结果排序后,为后续的笛卡尔积合并做准备。
是否需要避免
当前场景下不需要:USAGETYPE表只有390行,数据量极小,BUFFER SORT的内存开销可以忽略,Oracle选择这种执行方式的成本很低。只有当大表出现不必要的BUFFER SORT时(比如意外触发笛卡尔积),才需要优化调整。
三、整体性能优化建议
针对USAGETYPE的NULL查询优化
创建函数索引来覆盖event_type IS NULL的过滤场景:CREATE INDEX idx_usagetype_event_null ON USAGETYPE (CASE WHEN event_type IS NULL THEN 1 END);简化复杂JOIN逻辑
针对ut.called_to = u'!'时的NOT IN子查询,可以提前预处理数据:- 用物化视图缓存USAGETYPE中符合条件的
called_to列表,减少查询时的实时计算 - 确认
USAGETYPE_CALLED_TO索引是否包含called_to非NULL且不等于u'!'的过滤条件,确保子查询效率
- 用物化视图缓存USAGETYPE中符合条件的
调整表连接顺序
当前执行计划先关联invoice_items和USAGETYPE做笛卡尔积,再关联invoice_items_events,可通过提示强制调整连接顺序,减少中间结果集大小:SELECT /*+ LEADING(boi bie) */ -- 原查询内容更新统计信息
执行计划显示使用了动态采样,说明表的统计信息可能过期,导致CBO行数估算不准确(当前估算行数为1,实际可能远大于此),更新统计信息:EXEC DBMS_STATS.GATHER_TABLE_STATS(OWNNAME => '你的用户名', TABNAME => 'INVOICE_ITEMS', CASCADE => TRUE); EXEC DBMS_STATS.GATHER_TABLE_STATS(OWNNAME => '你的用户名', TABNAME => 'INVOICE_ITEMS_EVENTS', CASCADE => TRUE); EXEC DBMS_STATS.GATHER_TABLE_STATS(OWNNAME => '你的用户名', TABNAME => 'USAGETYPE', CASCADE => TRUE);简化WHERE子句的CASE表达式
将原CASE表达式改写为更直观的逻辑,帮助CBO更好地优化:WHERE bie.vfqbalance_RECORD_TYPE is not null or ut.usageType in (u'vas',u'video') AND NOT (ut.usageType = u'voice' AND bie.vfqbalance_DURATION = u'0' AND bie.evttotal_amount = 0)
内容的提问来源于stack exchange,提问作者to-find

