I'm trying to create a view which will be queried against with a simple WHERE clause. I have the following tables (simplified) :
CREATE TABLE table_1 (
"ID" VARCHAR2(38 BYTE),
"USER" VARCHAR2(255 BYTE),
"FIELD_TYPE_1" VARCHAR2(255 BYTE),
"FIELD_TYPE_2" VARCHAR2(255 BYTE)
);
CREATE TABLE table_2 (
"FK_ID" VARCHAR2(38 BYTE),
"TYPE" NUMBER
);
table_1 have 12M rows and table_2 have 25M rows.
My current view is defined as :
CREATE OR REPLACE VIEW vw_t1 AS
SELECT
CASE
WHEN t2.TYPE = 1 THEN t1.field_type_1
ELSE t1.field_type_2
END FIELD_TYPE,
t1.USER,
COUNT(*)
FROM table_1 t1 JOIN table_2 t2 ON t1.ID = t2.FK_ID
GROUP BY t1.USER,
CASE WHEN t2.TYPE = 1 THEN t1.field_type_1 else t1.field_type_2 end;
and queries against the view perform poorly taking 10s to return:
SELECT * FROM vw_t1 WHERE field_type LIKE 'some text'
-------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 6867K| 491M| | 394K (1)| 00:00:16 |
| 1 | HASH GROUP BY | | 6867K| 491M| 553M| 394K (1)| 00:00:16 |
|* 2 | FILTER | | | | | | |
|* 3 | HASH JOIN RIGHT OUTER | | 6867K| 491M| 57M| 282K (1)| 00:00:12 |
| 4 | TABLE ACCESS STORAGE FULL| table_1 | 2420K| 30M| | 6891 (1)| 00:00:01 |
|* 5 | TABLE ACCESS STORAGE FULL| table_2 | 6835K| 404M| | 248K (1)| 00:00:10 |
-------------------------------------------------------------------------------------------------------
I have thought of returning the fields 'field_type_1', 'field_type_2', and 'type', and have the users do the CASE themselves, but the queries won't return the same results due to the GROUP BY.
You should test your view's explain plan with changed context. For me it is not possible to test it (with just a few rows) but here are 3 different options and explain plans for the view and sql selecting from the view.
No indexes on any table
Primary key on TABLE_1
Primary key on TABLE_1 and NonUnique Index on TABLE_2
-- V i e w ------------------------------------
SELECT
CASE WHEN t2.TYPE = 1 THEN t1.field_type_1
ELSE t1.field_type_2
END "FIELD_TYPE",
--
t1.USER_ID,
--
COUNT(*) "CNT"
FROM table_1 t1 JOIN table_2 t2 ON t1.ID = t2.FK_ID
GROUP BY t1.USER_ID, CASE WHEN t2.TYPE = 1 THEN t1.field_type_1 else t1.field_type_2 END;
1. ******************************************************************************
Table_1 - none
Table_2 - none
View
----------------------------------------------------------
| Id | Operation | Name | Rows | Time |
----------------------------------------------------------
| 0 | SELECT STATEMENT | | 8 | 00:00:01 |
| 1 | HASH GROUP BY | | 8 | 00:00:01 |
|* 2 | HASH JOIN | | 8 | 00:00:01 |
| 3 | TABLE ACCESS FULL| TABLE_1 | 3 | 00:00:01 |
| 4 | TABLE ACCESS FULL| TABLE_2 | 8 | 00:00:01 |
----------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("T1"."ID"="T2"."FK_ID")
=== Query ==================================================================================
SELECT * FROM a__a WHERE field_type LIKE('%FLD%');
----------------------------------------------------------
| Id | Operation | Name | Rows | Time |
----------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 00:00:01 |
| 1 | HASH GROUP BY | | 1 | 00:00:01 |
|* 2 | HASH JOIN | | 1 | 00:00:01 |
| 3 | TABLE ACCESS FULL| TABLE_1 | 3 | 00:00:01 |
| 4 | TABLE ACCESS FULL| TABLE_2 | 8 | 00:00:01 |
----------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("T1"."ID"="T2"."FK_ID")
filter(CASE "T2"."TYPE" WHEN 1 THEN "T1"."FIELD_TYPE_1" ELSE
"T1"."FIELD_TYPE_2" END LIKE '%FLD%')
...
2. ******************************************************************************
Table_1 - PK (ID)
Table_2 - None
View
| Id | Operation | Name | Rows | Time |
|-----|-------------------------------|------------|-------|----------|
| 0 | SELECT STATEMENT | | 8 | 00:00:01 |
| 1 | HASH GROUP BY | | 8 | 00:00:01 |
| 2 | MERGE JOIN | | 8 | 00:00:01 |
| 3 | TABLE ACCESS BY INDEX ROWID| TABLE_1 | 3 | 00:00:01 |
| 4 | INDEX FULL SCAN | TABLE_1_PK | 3 | 00:00:01 |
|* 5 | SORT JOIN | | 8 | 00:00:01 |
| 6 | TABLE ACCESS FULL | TABLE_2 | 8 | 00:00:01 |
-----------------------------------------------------------------------
=== Query ==================================================================================
SELECT * FROM a__a WHERE field_type LIKE('%FLD%');
-----------------------------------------------------------------------
| Id | Operation | Name | Rows | Time |
-----------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 00:00:01 |
| 1 | HASH GROUP BY | | 1 | 00:00:01 |
| 2 | MERGE JOIN | | 1 | 00:00:01 |
| 3 | TABLE ACCESS BY INDEX ROWID| TABLE_1 | 3 | 00:00:01 |
| 4 | INDEX FULL SCAN | TABLE_1_PK | 3 | 00:00:01 |
|* 5 | FILTER | | | |
|* 6 | SORT JOIN | | 8 | 00:00:01 |
| 7 | TABLE ACCESS FULL | TABLE_2 | 8 | 00:00:01 |
-----------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
5 - filter(CASE "T2"."TYPE" WHEN 1 THEN "T1"."FIELD_TYPE_1" ELSE
"T1"."FIELD_TYPE_2" END LIKE '%FLD%')
6 - access("T1"."ID"="T2"."FK_ID")
filter("T1"."ID"="T2"."FK_ID")
...
3. ******************************************************************************
Table_1 - PK (ID)
Table_2 - INDEX NonUnique (FK_ID, TYPE)
VIEW
-------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Time |
-------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 8 | 00:00:01 |
| 1 | HASH GROUP BY | | 8 | 00:00:01 |
| 2 | MERGE JOIN | | 8 | 00:00:01 |
| 3 | TABLE ACCESS BY INDEX ROWID| TABLE_1 | 3 | 00:00:01 |
| 4 | INDEX FULL SCAN | TABLE_1_PK | 3 | 00:00:01 |
|* 5 | SORT JOIN | | 8 | 00:00:01 |
| 6 | INDEX FULL SCAN | TABLE_2_INDEX_FK_ID_TYPE | 8 | 00:00:01 |
-------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
5 - access("T1"."ID"="T2"."FK_ID")
filter("T1"."ID"="T2"."FK_ID")
=== Query ==================================================================================
SELECT * FROM a__a WHERE field_type LIKE('%FLDt%');
-------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Time |
-------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 00:00:01 |
| 1 | HASH GROUP BY | | 1 | 00:00:01 |
| 2 | MERGE JOIN | | 1 | 00:00:01 |
| 3 | TABLE ACCESS BY INDEX ROWID| TABLE_1 | 3 | 00:00:01 |
| 4 | INDEX FULL SCAN | TABLE_1_PK | 3 | 00:00:01 |
|* 5 | FILTER | | | |
|* 6 | SORT JOIN | | 8 | 00:00:01 |
| 7 | INDEX FULL SCAN | TABLE_2_INDEX_FK_ID_TYPE | 8 | 00:00:01 |
-------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
5 - filter(CASE "T2"."TYPE" WHEN 1 THEN "T1"."FIELD_TYPE_1" ELSE
"T1"."FIELD_TYPE_2" END LIKE '%FLD%')
6 - access("T1"."ID"="T2"."FK_ID")
filter("T1"."ID"="T2"."FK_ID")
Related
I have an Oracle 19c database table with "resources" that are organized hierarchically like a nested folder tree. The table contains around 2.5 million rows and the tree is up to 10 levels deep.
create table RESOURCES (
ID_ NUMBER(10) not null constraint PK_RESOURCES primary key,
FOLDERID_ NUMBER(10) constraint FK_PARENTFOLDER references RESOURCES
);
create index FOLDERIDINDEX on RESOURCES (FOLDERID_);
I'm using SQL recursive common table expressions (aka recursive subquery factoring) to find all descendants of some given resources.
In general, this works quite nicely, but if I try to get descendants of multiple folders with one query, some queries don't perform at all using Oracle. I'd like to understand why this is the case, and if there's some easy way to speed things up (query hint?, bugfix?, ...?)
For example, this statement does not return within 60 minutes(!):
WITH cte1 (id_) AS (SELECT id_ FROM Resources where id_ = 11
UNION ALL
SELECT r.id_ FROM Resources r, cte1 c WHERE r.folderId_ = c.id_),
cte2 (id_) AS (SELECT id_ FROM Resources where id_ = 808965
UNION ALL
SELECT r.id_ FROM Resources r, cte2 c WHERE r.folderId_ = c.id_)
SELECT count(*)
FROM Resources r
WHERE (r.folderId_ IN (SELECT * FROM cte1) OR r.folderId_ IN (SELECT * FROM cte2));
If I replace the two sub-selects in the last line with a UNION, it just takes a few seconds:
WITH cte1 (id_) AS (SELECT id_ FROM Resources where id_ = 11
UNION ALL
SELECT r.id_ FROM Resources r, cte1 c WHERE r.folderId_ = c.id_),
cte2 (id_) AS (SELECT id_ FROM Resources where id_ = 808965
UNION ALL
SELECT r.id_ FROM Resources r, cte2 c WHERE r.folderId_ = c.id_)
SELECT count(*)
FROM Resources r
WHERE (r.folderId_ IN (SELECT * FROM cte1 UNION SELECT * FROM cte2));
While that could already be the solution, it's a bit hard to change in my project, because SQL is auto-generated by code from application queries, and at that point not so easy to change. Also, application queries could be much more complex and such a replacement might not even be possible. These are just simplified examples. Maybe there's some other way to speed things up?
Interestingly, the slow query works without any performance problems on other databases like MySQL 8, PostgreSQL 13, SQL Server 2016 (with small syntax changes for the databases). It's just Oracle which seems to have a problem here.
This is the query plan for the first query, i.e. the slow one:
------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 5 | 54G (1)|591:38:12 |
| 1 | SORT AGGREGATE | | 1 | 5 | | |
|* 2 | FILTER | | | | | |
| 3 | TABLE ACCESS FULL | RESOURCES | 2410K| 11M| 9389 (1)| 00:00:01 |
|* 4 | VIEW | | 239K| 3046K| 23128 (1)| 00:00:01 |
| 5 | UNION ALL (RECURSIVE WITH) BREADTH FIRST| | | | | |
|* 6 | INDEX UNIQUE SCAN | PK_RESOURCES | 1 | 6 | 2 (0)| 00:00:01 |
|* 7 | HASH JOIN | | 239K| 5623K| 23126 (1)| 00:00:01 |
| 8 | RECURSIVE WITH PUMP | | | | | |
| 9 | BUFFER SORT (REUSE) | | | | | |
| 10 | TABLE ACCESS FULL | RESOURCES | 2410K| 25M| 9386 (1)| 00:00:01 |
|* 11 | VIEW | | 239K| 3046K| 23128 (1)| 00:00:01 |
| 12 | UNION ALL (RECURSIVE WITH) BREADTH FIRST| | | | | |
|* 13 | INDEX UNIQUE SCAN | PK_RESOURCES | 1 | 6 | 2 (0)| 00:00:01 |
|* 14 | HASH JOIN | | 239K| 5623K| 23126 (1)| 00:00:01 |
| 15 | RECURSIVE WITH PUMP | | | | | |
| 16 | BUFFER SORT (REUSE) | | | | | |
| 17 | TABLE ACCESS FULL | RESOURCES | 2410K| 25M| 9386 (1)| 00:00:01 |
------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
" 2 - filter( EXISTS (SELECT 0 FROM ""CTE1"" ""CTE1"" WHERE ""CTE1"".""ID_""=:B1) OR EXISTS (SELECT 0 "
" FROM ""CTE2"" ""CTE2"" WHERE ""CTE2"".""ID_""=:B2))"
" 4 - filter(""CTE1"".""ID_""=:B1)"
" 6 - access(""ID_""=11)"
" 7 - access(""R"".""FOLDERID_""=""C"".""ID_"")"
" 11 - filter(""CTE2"".""ID_""=:B1)"
" 13 - access(""ID_""=808965)"
" 14 - access(""R"".""FOLDERID_""=""C"".""ID_"")"
For comparison, the faster query using a UNION seems to use a better plan:
------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time |
------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 18 | | 55733 (1)| 00:00:03 |
| 1 | SORT AGGREGATE | | 1 | 18 | | | |
|* 2 | HASH JOIN | | 2806K| 48M| 11M| 55733 (1)| 00:00:03 |
| 3 | VIEW | VW_NSO_1 | 479K| 6092K| | 50820 (1)| 00:00:02 |
| 4 | SORT UNIQUE | | 479K| 6092K| 9424K| 50820 (1)| 00:00:02 |
| 5 | UNION-ALL | | | | | | |
| 6 | VIEW | | 239K| 3046K| | 23128 (1)| 00:00:01 |
| 7 | UNION ALL (RECURSIVE WITH) BREADTH FIRST| | | | | | |
|* 8 | INDEX UNIQUE SCAN | PK_RESOURCES | 1 | 6 | | 2 (0)| 00:00:01 |
|* 9 | HASH JOIN | | 239K| 5623K| | 23126 (1)| 00:00:01 |
| 10 | RECURSIVE WITH PUMP | | | | | | |
| 11 | BUFFER SORT (REUSE) | | | | | | |
| 12 | TABLE ACCESS FULL | RESOURCES | 2410K| 25M| | 9386 (1)| 00:00:01 |
| 13 | VIEW | | 239K| 3046K| | 23128 (1)| 00:00:01 |
| 14 | UNION ALL (RECURSIVE WITH) BREADTH FIRST| | | | | | |
|* 15 | INDEX UNIQUE SCAN | PK_RESOURCES | 1 | 6 | | 2 (0)| 00:00:01 |
|* 16 | HASH JOIN | | 239K| 5623K| | 23126 (1)| 00:00:01 |
| 17 | RECURSIVE WITH PUMP | | | | | | |
| 18 | BUFFER SORT (REUSE) | | | | | | |
| 19 | TABLE ACCESS FULL | RESOURCES | 2410K| 25M| | 9386 (1)| 00:00:01 |
| 20 | INDEX FAST FULL SCAN | FOLDERIDINDEX | 2410K| 11M| | 2392 (1)| 00:00:01 |
------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
" 2 - access(""R"".""FOLDERID_""=""ID_"")"
" 8 - access(""ID_""=11)"
" 9 - access(""R"".""FOLDERID_""=""C"".""ID_"")"
" 15 - access(""ID_""=808965)"
" 16 - access(""R"".""FOLDERID_""=""C"".""ID_"")"
I have query something like
WITH
str_table as (
SELECT stringtext, stringnumberid
FROM STRING_TABLE
WHERE LANGID IN (23,62)
),
data as (
select *
from employee emp
left outer join str_table st on emp.nameid = st.stringnumberid
)
select * from data
I know With clause will work in this manner
Step 1 : The SQL Query within the with clause is executed at first step.
Step 2 : The output of the SQL query is stored into temporary relation of with clause.
Step 3 : The Main query is executed with temporary relation produced at the last stage.
Now I want to ask whether the indexes created on the actual STRING_TABLE are going to help in temporary str_table produce by the With clause? I want to ask whether the indexes also have impact on str_table or not?
Oracle will not process CTE one by one. It will analyze the SQL as a whole. Your SQL is most likely the same as following in the eye of Oracle optimizer
select emp.*
from employee emp left outer join STRING_TABLE st
on emp.nameid = st.stringnumberid
where st.LANGID IN (23,62);
Oracle can use index on STRING_TABLE. Whether it will depends on the table statistics. For example, if the table has few rows (say a few hundred), Oracle will likely not use index.
It depends.
First of all, with clause is not a temporary table. As documentation says:
Oracle Database optimizes the query by treating the query name as either an inline view or as a temporary table.
Optimizer decides to materialize with subquery if either you forse it to do so by using /*+materialize*/ hint inside the subquery or you reuse this with subquery more than once.
In the example below Oracle uses with clause as inline view and merges it within the main query:
explain plan for
with a as (
select
s.textid,
s.textvalue,
a.id,
a.other_column
from string_table s
join another_tab a
on s.textid = a.textid
where langid in (1)
)
select *
from big_table b
join a a_name
on b.name_textid = a_name.textid
and b.job_textid = a_name.id
| PLAN_TABLE_OUTPUT |
| :----------------------------------------------------------------------------------- |
| Plan hash value: 1854049435 |
| |
| ------------------------------------------------------------------------------------ |
| | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | |
| ------------------------------------------------------------------------------------ |
| | 0 | SELECT STATEMENT | | 1 | 1147 | 74 (0)| 00:00:01 | |
| |* 1 | HASH JOIN | | 1 | 1147 | 74 (0)| 00:00:01 | |
| | 2 | TABLE ACCESS FULL | ANOTHER_TAB | 39 | 3042 | 3 (0)| 00:00:01 | |
| |* 3 | HASH JOIN | | 31 | 33139 | 71 (0)| 00:00:01 | |
| | 4 | TABLE ACCESS FULL| BIG_TABLE | 19 | 10279 | 3 (0)| 00:00:01 | |
| |* 5 | TABLE ACCESS FULL| STRING_TABLE | 1143 | 589K| 68 (0)| 00:00:01 | |
| ------------------------------------------------------------------------------------ |
But depending on the statistics and hints it may evaluate subquery first and then add it to the main query:
explain plan for
with a as (
select
s.textid,
s.textvalue,
a.id,
a.other_column
from string_table s
join another_tab a
on s.textid = a.textid
where langid in (1)
)
select /*+NO_MERGE(a_name)*/ *
from big_table b
join a a_name
on b.name_textid = a_name.textid
and b.job_textid = a_name.id
| PLAN_TABLE_OUTPUT |
| :------------------------------------------------------------------------------------ |
| Plan hash value: 4105667421 |
| |
| ------------------------------------------------------------------------------------- |
| | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | |
| ------------------------------------------------------------------------------------- |
| | 0 | SELECT STATEMENT | | 101 | 110K| 74 (0)| 00:00:01 | |
| |* 1 | HASH JOIN | | 101 | 110K| 74 (0)| 00:00:01 | |
| | 2 | TABLE ACCESS FULL | BIG_TABLE | 19 | 10279 | 3 (0)| 00:00:01 | |
| | 3 | VIEW | | 64 | 37120 | 71 (0)| 00:00:01 | |
| |* 4 | HASH JOIN | | 64 | 38784 | 71 (0)| 00:00:01 | |
| | 5 | TABLE ACCESS FULL| ANOTHER_TAB | 39 | 3042 | 3 (0)| 00:00:01 | |
| |* 6 | TABLE ACCESS FULL| STRING_TABLE | 1143 | 589K| 68 (0)| 00:00:01 | |
| ------------------------------------------------------------------------------------- |
When you use with subquery twice, optimizer decides to materialize it:
explain plan for
with a as (
select
s.textid,
s.textvalue
from string_table s
where langid in (1)
)
select *
from big_table b
join a a_name
on b.name_textid = a_name.textid
join a a_job
on b.job_textid = a_job.textid
| PLAN_TABLE_OUTPUT |
| :--------------------------------------------------------------------------------------------------------------------- |
| Plan hash value: 1371454296 |
| |
| ---------------------------------------------------------------------------------------------------------------------- |
| | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | |
| ---------------------------------------------------------------------------------------------------------------------- |
| | 0 | SELECT STATEMENT | | 63 | 98973 | 67 (0)| 00:00:01 | |
| | 1 | TEMP TABLE TRANSFORMATION | | | | | | |
| | 2 | LOAD AS SELECT (CURSOR DURATION MEMORY)| SYS_TEMP_0FD9D7224_469C01 | | | | | |
| | 3 | TABLE ACCESS BY INDEX ROWID BATCHED | STRING_TABLE | 999 | 515K| 22 (0)| 00:00:01 | |
| |* 4 | INDEX RANGE SCAN | IX | 999 | | 4 (0)| 00:00:01 | |
| |* 5 | HASH JOIN | | 63 | 98973 | 45 (0)| 00:00:01 | |
| |* 6 | HASH JOIN | | 35 | 36960 | 24 (0)| 00:00:01 | |
| | 7 | TABLE ACCESS FULL | BIG_TABLE | 19 | 10279 | 3 (0)| 00:00:01 | |
| | 8 | VIEW | | 999 | 502K| 21 (0)| 00:00:01 | |
| | 9 | TABLE ACCESS FULL | SYS_TEMP_0FD9D7224_469C01 | 999 | 502K| 21 (0)| 00:00:01 | |
| | 10 | VIEW | | 999 | 502K| 21 (0)| 00:00:01 | |
| | 11 | TABLE ACCESS FULL | SYS_TEMP_0FD9D7224_469C01 | 999 | 502K| 21 (0)| 00:00:01 | |
| ---------------------------------------------------------------------------------------------------------------------- |
So when there are some indexes on tables inside with subquery they may be used in all above cases: before materialization, when subquery is not merged and when subquery is merged and some idexes provide better query plan on merged subquery (even when those indexes are not used when you execute subquery alone).
What about idexes: if they provide high selectivity (i.e. number of rows retrieved by index is small compared to the overall number of rows), then Oracle will consider to use it. Note, that index access has two steps: read index blocks and then read table blocks that contain rowids found by index. If table size is not much bigger than index size, then Oracle may use table scan instead of index scan even for quite selective predicate (because of doubled IO).
In the below example I've used "small" texts (100 chars) and big_table table of 20 rows and this index for text table:
create index ix
on string_table(langid, textid)
Optimizer decides to use index range scan and read only blocks of the first level (first column of the index):
explain plan for
with a as (
select
s.textid,
s.textvalue
from string_table s
where langid in (1)
)
select *
from big_table b
join a a_name
on b.name_textid = a_name.textid
| PLAN_TABLE_OUTPUT |
| :---------------------------------------------------------------------------------------------------- |
| Plan hash value: 1660330381 |
| |
| ----------------------------------------------------------------------------------------------------- |
| | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | |
| ----------------------------------------------------------------------------------------------------- |
| | 0 | SELECT STATEMENT | | 29 | 31001 | 26 (0)| 00:00:01 | |
| |* 1 | HASH JOIN | | 29 | 31001 | 26 (0)| 00:00:01 | |
| | 2 | TABLE ACCESS FULL | BIG_TABLE | 19 | 10279 | 3 (0)| 00:00:01 | |
| | 3 | TABLE ACCESS BY INDEX ROWID BATCHED| STRING_TABLE | 999 | 515K| 23 (0)| 00:00:01 | |
| |* 4 | INDEX RANGE SCAN | IX | 999 | | 4 (0)| 00:00:01 | |
| ----------------------------------------------------------------------------------------------------- |
| |
| Predicate Information (identified by operation id): |
| --------------------------------------------------- |
| |
| 1 - access("B"."NAME_TEXTID"="S"."TEXTID") |
| 4 - access("LANGID"=1) | |
But when we reduce the number of rows in big_table, it uses both the columns for index scan:
delete from big_table
where id > 4
explain plan for
with a as (
select
s.textid,
s.textvalue
from string_table s
where langid in (1)
)
select *
from big_table b
join a a_name
on b.name_textid = a_name.textid
| PLAN_TABLE_OUTPUT |
| :-------------------------------------------------------------------------------------------- |
| Plan hash value: 1766926914 |
| |
| --------------------------------------------------------------------------------------------- |
| | Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | |
| --------------------------------------------------------------------------------------------- |
| | 0 | SELECT STATEMENT | | 6 | 18216 | 11 (0)| 00:00:01 | |
| | 1 | NESTED LOOPS | | 6 | 18216 | 11 (0)| 00:00:01 | |
| | 2 | NESTED LOOPS | | 6 | 18216 | 11 (0)| 00:00:01 | |
| | 3 | TABLE ACCESS FULL | BIG_TABLE | 4 | 4032 | 3 (0)| 00:00:01 | |
| |* 4 | INDEX RANGE SCAN | IX | 1 | | 1 (0)| 00:00:01 | |
| | 5 | TABLE ACCESS BY INDEX ROWID| STRING_TABLE | 2 | 4056 | 2 (0)| 00:00:01 | |
| --------------------------------------------------------------------------------------------- |
| |
| Predicate Information (identified by operation id): |
| --------------------------------------------------- |
| |
| 4 - access("LANGID"=1 AND "B"."NAME_TEXTID"="S"."TEXTID") |
| |
You may check above code snippets in the db<>fiddle.
I can reproduce the following behavior both with Oracle 11 (see SQL Fiddle) and Oracle 12.
CREATE TYPE my_tab IS TABLE OF NUMBER(3);
CREATE TABLE test AS SELECT ROWNUM AS id FROM dual CONNECT BY ROWNUM <= 1000;
CREATE UNIQUE INDEX idx_test ON test( id );
CREATE VIEW my_view AS
SELECT id, COUNT(1) OVER ( PARTITION BY id ) AS cnt
FROM test;
The following case uses the index as expected:
SELECT * FROM my_view
WHERE id IN ( 1, 2 );
---------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 2 | 52 | 2 (0)| 00:00:01 |
| 1 | VIEW | MY_VIEW | 2 | 52 | 2 (0)| 00:00:01 |
| 2 | WINDOW BUFFER | | 2 | 8 | 2 (0)| 00:00:01 |
| 3 | INLIST ITERATOR | | | | | |
|* 4 | INDEX UNIQUE SCAN| IDX_TEST | 2 | 8 | 2 (0)| 00:00:01 |
---------------------------------------------------------------------------------
The following case does not use the index even though the cardinality hint is provided:
SELECT * FROM my_view
WHERE id IN ( SELECT /*+ CARDINALITY( tab 2 ) */ COLUMN_VALUE
FROM TABLE( NEW my_tab( 1, 2 ) ) tab );
--------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 28 | 33 (4)| 00:00:01 |
|* 1 | HASH JOIN RIGHT SEMI | | 1 | 28 | 33 (4)| 00:00:01 |
| 2 | COLLECTION ITERATOR CONSTRUCTOR FETCH| | 2 | 4 | 29 (0)| 00:00:01 |
| 3 | VIEW | MY_VIEW | 1000 | 26000 | 4 (25)| 00:00:01 |
| 4 | WINDOW SORT | | 1000 | 4000 | 4 (25)| 00:00:01 |
| 5 | TABLE ACCESS FULL | TEST | 1000 | 4000 | 3 (0)| 00:00:01 |
--------------------------------------------------------------------------------------------------
Edit:
Using an inline view and a JOIN instead of IN uses a similar plan:
SELECT /*+ CARDINALITY( tab, 2 ) */ *
FROM ( SELECT id, COUNT(1) OVER ( PARTITION BY id ) AS cnt FROM test ) t
JOIN TABLE( NEW my_tab( 1, 2 ) ) tab ON ( tab.COLUMN_VALUE = t.id );
-----------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 2 | 56 | 33 (4)| 00:00:01 |
|* 1 | HASH JOIN | | 2 | 56 | 33 (4)| 00:00:01 |
| 2 | COLLECTION ITERATOR CONSTRUCTOR FETCH| | 2 | 4 | 29 (0)| 00:00:01 |
| 3 | VIEW | | 1000 | 26000 | 4 (25)| 00:00:01 |
| 4 | WINDOW SORT | | 1000 | 4000 | 4 (25)| 00:00:01 |
| 5 | TABLE ACCESS FULL | TEST | 1000 | 4000 | 3 (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------
Replacing the analytic function by a LEFT JOIN with GROUP BY does not help either:
SELECT *
FROM ( SELECT t.id, s.cnt
FROM test t
LEFT JOIN ( SELECT id, COUNT(*) AS cnt
FROM test
GROUP BY id
) s ON ( s.id = t.id )
)
WHERE id IN ( SELECT /*+ CARDINALITY( tab 2 ) */ COLUMN_VALUE
FROM TABLE( NEW my_tab( 1, 2 ) ) tab );
-----------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 2 | 64 | 34 (6)| 00:00:01 |
|* 1 | HASH JOIN OUTER | | 2 | 64 | 34 (6)| 00:00:01 |
| 2 | NESTED LOOPS | | 2 | 12 | 30 (4)| 00:00:01 |
| 3 | SORT UNIQUE | | 2 | 4 | 29 (0)| 00:00:01 |
| 4 | COLLECTION ITERATOR CONSTRUCTOR FETCH| | 2 | 4 | 29 (0)| 00:00:01 |
|* 5 | INDEX UNIQUE SCAN | IDX_TEST | 1 | 4 | 0 (0)| 00:00:01 |
| 6 | VIEW | | 1000 | 26000 | 4 (25)| 00:00:01 |
| 7 | HASH GROUP BY | | 1000 | 4000 | 4 (25)| 00:00:01 |
| 8 | TABLE ACCESS FULL | TEST | 1000 | 4000 | 3 (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------------
Replacing the PL/SQL Collection by a subselect does not seem to help either. The CARDINALITY hint is considered (the plan says 2 rows), but the index is still ignored.
SELECT *
FROM ( SELECT id, cnt FROM my_view )
WHERE id IN ( SELECT /*+ CARDINALITY( tab 2 ) */ id FROM test tab );
---------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 2 | 60 | 4 (25)| 00:00:01 |
| 1 | NESTED LOOPS | | 2 | 60 | 4 (25)| 00:00:01 |
| 2 | VIEW | MY_VIEW | 1000 | 26000 | 4 (25)| 00:00:01 |
| 3 | WINDOW SORT | | 1000 | 4000 | 4 (25)| 00:00:01 |
| 4 | TABLE ACCESS FULL| TEST | 1000 | 4000 | 3 (0)| 00:00:01 |
|* 5 | INDEX UNIQUE SCAN | IDX_TEST | 1 | 4 | 0 (0)| 00:00:01 |
---------------------------------------------------------------------------------
Adding WHERE tab.id <= 2 to the in-list-subquery uses the index, so the optimizer seems to "not take the CARDINALITY hint serious enough" when selecting from a view with analytic functions (or another subselect) and filtering by a list of values.
How can I make these queries use the index as expected?
I think the one problem might be that the optimizer refuses to merge a view (and consider any indexes on the underlying tables) when the outer query block contains PL/SQL functions (e.g. TABLE()).
If you manually expand the view and query the table directly, it can access the index fine:
SELECT id, COUNT(1) OVER ( PARTITION BY id ) AS cnt
FROM test
WHERE id IN ( SELECT COLUMN_VALUE
FROM TABLE( NEW my_tab( 1, 2 ) ) tab )
;
----------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
----------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 6 | 31 (4)| 00:00:01 |
| 1 | WINDOW SORT | | 1 | 6 | 31 (4)| 00:00:01 |
|* 2 | HASH JOIN SEMI | | 1 | 6 | 30 (0)| 00:00:01 |
| 3 | INDEX FULL SCAN | IDX_TEST | 1000 | 4000 | 1 (0)| 00:00:01 |
| 4 | COLLECTION ITERATOR CONSTRUCTOR FETCH| | 8168 | 16336 | 29 (0)| 00:00:01 |
----------------------------------------------------------------------------------------------------
I'm not sure if there's a way to override this behavior, or if it's a limitation in the optimizer. I tried moving the TABLE function to a CTE, but that doesn't seem to help.
Oracle query.
The following query takes some time to execute:
SELECT GCCOM_ACCOUNT_CONTRACT.ID_ACCOUNT_CONTRACT
FROM GCCOM_ACCOUNT_CONTRACT, GCCOM_ACCOUNT_GROUP
WHERE
GCCOM_ACCOUNT_CONTRACT.ID_ACCOUNT_GROUP = GCCOM_ACCOUNT_GROUP.ID_ACCOUNT_GROUP (+) AND
EXISTS (SELECT 1 FROM GCCOM_CONTRACTED_SERVICE
WHERE ID_ACCOUNT_CONTRACT = GCCOM_ACCOUNT_CONTRACT.ID_ACCOUNT_CONTRACT AND
STATUS = 'ESTSC00002' AND
DROP_DATE IS NULL ) AND
EXISTS (SELECT 1 FROM GCCOM_SEND_SERVICE
WHERE (ID_ACCOUNT_CONTRACT = GCCOM_ACCOUNT_CONTRACT.ID_ACCOUNT_CONTRACT OR
ID_ACCOUNT_GROUP = GCCOM_ACCOUNT_GROUP.ID_ACCOUNT_GROUP)
) AND
(( GCCOM_ACCOUNT_CONTRACT.ACCOUNT_CODE between 200000001 AND 900468243))
ORDER BY
GCCOM_ACCOUNT_CONTRACT.ID_COMPANY,
GCCOM_ACCOUNT_GROUP.ID_ACCOUNT_GROUP,
GCCOM_ACCOUNT_CONTRACT.ACCOUNT_CODE
The explain plan shows as follows:
PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------------------------------------------------
Plan hash value: 391653930
------------------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time | Pstart| Pstop |
------------------------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 45 | | 570K (1)| 01:54:06 | | |
| 1 | SORT ORDER BY | | 1 | 45 | 12M| 570K (1)| 01:54:06 | | |
|* 2 | FILTER | | | | | | | | |
| 3 | NESTED LOOPS OUTER | | 255K| 10M| | 17381 (1)| 00:03:29 | | |
|* 4 | HASH JOIN RIGHT SEMI | | 255K| 9979K| 8648K| 17380 (1)| 00:03:29 | | |
| 5 | PARTITION HASH ALL | | 260K| 5592K| | 7175 (1)| 00:01:27 | 1 | 16 |
|* 6 | TABLE ACCESS FULL | GCCOM_CONTRACTED_SERVICE | 260K| 5592K| | 7175 (1)| 00:01:27 | 1 | 16 |
|* 7 | TABLE ACCESS FULL | GCCOM_ACCOUNT_CONTRACT | 810K| 13M| | 8627 (1)| 00:01:44 | | |
|* 8 | INDEX UNIQUE SCAN | PK_GCCOM_ACCOUNT_GROUP | 1 | 5 | | 1 (0)| 00:00:01 | | |
| 9 | CONCATENATION | | | | | | | | |
| 10 | TABLE ACCESS BY GLOBAL INDEX ROWID| GCCOM_SEND_SERVICE | 1 | 7 | | 1 (0)| 00:00:01 | ROWID | ROWID |
|* 11 | INDEX RANGE SCAN | IDX_GCCOMSENDSERVICE_27 | 1 | | | 1 (0)| 00:00:01 | | |
|* 12 | TABLE ACCESS BY GLOBAL INDEX ROWID| GCCOM_SEND_SERVICE | 2 | 14 | | 1 (0)| 00:00:01 | ROWID | ROWID |
|* 13 | INDEX RANGE SCAN | IDX_GCCOMSENDSERVICE_04 | 1 | | | 1 (0)| 00:00:01 | | |
------------------------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------------------------------------------------
---------------------------------------------------
2 - filter( EXISTS (SELECT 0 FROM "GCCOM_SEND_SERVICE" "GCCOM_SEND_SERVICE"<not feasible>)
4 - access("ID_ACCOUNT_CONTRACT"="GCCOM_ACCOUNT_CONTRACT"."ID_ACCOUNT_CONTRACT")
6 - filter("DROP_DATE" IS NULL AND "STATUS"='ESTSC00002')
7 - filter("GCCOM_ACCOUNT_CONTRACT"."ACCOUNT_CODE">=200000001 AND "GCCOM_ACCOUNT_CONTRACT"."ACCOUNT_CODE"<=900468243)
8 - access("GCCOM_ACCOUNT_CONTRACT"."ID_ACCOUNT_GROUP"="GCCOM_ACCOUNT_GROUP"."ID_ACCOUNT_GROUP"(+))
11 - access("ID_ACCOUNT_CONTRACT"=:B1)
12 - filter(LNNVL("ID_ACCOUNT_CONTRACT"=:B1))
13 - access("ID_ACCOUNT_GROUP"=:B1)
32 filas seleccionadas.
The average cardinality of table looks as follows:
select count(*) from GCCOM_ACCOUNT_CONTRACT >> rows: 810412
select avg(distinct ID_ACCOUNT_GROUP) from GCCOM_ACCOUNT_CONTRACT >> cardinality: 87173
Highly indexed.
Tried many things, but useless.
Any idea ?
I have one query which completes when selecting all columns (using select * from), but it doesn't complete when selecting one column name. I have created necessary indexes. here is my query
SELECT q2.ssn vn_ssn
--when * here instead of column name then the query completes
FROM table_0 q2
LEFT OUTER JOIN
(SELECT ial_t.pin,
ial_t.serial_number,
ial_t.surname,
ial_t.name,
ial_t.patronymic,
ial_t.prev_surname
FROM
(SELECT pin,
MAX(serial_number) m_serial_number
FROM table_1
GROUP BY pin
) ial_m
INNER JOIN table_1 ial_t
ON ial_t.serial_number = ial_m.m_serial_number
) ial ON q2.pincode = ial.pin
LEFT OUTER JOIN table_2 v_q2
ON V_Q2.VN_TPN = Q2.TPN
WHERE v_q2.vn_tpn IS NULL;
** EDIT: **
1. Plan (Select * from table_name)
Plan hash value: 2508092269
---------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time |
---------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 438K| 248M| | 341K (1)| 01:08:13 |
|* 1 | HASH JOIN OUTER | | 438K| 248M| 193M| 341K (1)| 01:08:13 |
|* 2 | HASH JOIN RIGHT OUTER| | 438K| 188M| 54M| 19424 (1)| 00:03:54 |
| 3 | TABLE ACCESS FULL | VN_Q2 | 439K| 49M| | 1673 (2)| 00:00:21 |
| 4 | TABLE ACCESS FULL | Q2 | 438K| 139M| | 7889 (1)| 00:01:35 |
| 5 | VIEW | | 6751K| 914M| | 262K (1)| 00:52:34 |
|* 6 | HASH JOIN | | 6751K| 386M| 122M| 262K (1)| 00:52:34 |
| 7 | VIEW | | 6742K| 45M| | 134K (1)| 00:26:55 |
| 8 | HASH GROUP BY | | 6742K| 109M| 458M| 134K (1)| 00:26:55 |
| 9 | TABLE ACCESS FULL| IAMAS_ALL_LAST_2 | 10M| 167M| | 90003 (1)| 00:18:01 |
| 10 | TABLE ACCESS FULL | IAMAS_ALL_LAST_2 | 10M| 521M| | 90270 (1)| 00:18:04 |
---------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
1 - access("Q2"."PINCODE"="IAL"."PIN"(+))
2 - access("V_Q2"."VN_TPN"(+)="Q2"."TPN")
6 - access("IAL_T"."SERIAL_NUMBER"="IAL_M"."M_SERIAL_NUMBER")
2. Plan (Select column_name from table_name)
Plan hash value: 1784658367
------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time |
------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 55 | | 144K (1)| 00:28:52 |
| 1 | NESTED LOOPS OUTER | | 1 | 55 | | 144K (1)| 00:28:52 |
|* 2 | HASH JOIN RIGHT ANTI | | 1 | 51 | 9880K| 9735 (1)| 00:01:57 |
| 3 | INDEX FAST FULL SCAN | VN_Q2_TPN_IDX | 439K| 4722K| | 301 (2)| 00:00:04 |
| 4 | TABLE ACCESS FULL | Q2 | 438K| 16M| | 7867 (1)| 00:01:35 |
| 5 | VIEW PUSHED PREDICATE | | 1 | 4 | | 134K (1)| 00:26:55 |
|* 6 | HASH JOIN | | 1 | 32 | | 134K (1)| 00:26:55 |
| 7 | TABLE ACCESS BY INDEX ROWID| IAMAS_ALL_LAST_2 | 2 | 50 | | 5 (0)| 00:00:01 |
|* 8 | INDEX RANGE SCAN | IAMAS_ALL_LAST_2_INDEX2 | 2 | | | 3 (0)| 00:00:01 |
| 9 | VIEW | | 6742K| 45M| | 134K (1)| 00:26:55 |
| 10 | SORT GROUP BY | | 6742K| 109M| 458M| 134K (1)| 00:26:55 |
| 11 | TABLE ACCESS FULL | IAMAS_ALL_LAST_2 | 10M| 167M| | 90003 (1)| 00:18:01 |
------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("V_Q2"."VN_TPN"="Q2"."TPN")
6 - access("IAL_T"."SERIAL_NUMBER"="IAL_M"."M_SERIAL_NUMBER")
8 - access("IAL_T"."PIN"="Q2"."PINCODE")
You can replace the first outer join with an OLAP function:
FROM table_0 q2
LEFT OUTER JOIN
(
SELECT *
FROM
(
SELECT ial_t.pin,
ial_t.serial_number,
ial_t.surname,
ial_t.name,
ial_t.patronymic,
ial_t.prev_surname,
ROW_NUMBER() OVER (PARTITION BY pin ORDER BY serial_number DESC) AS rn
FROM table_1
) ial_m
WHERE rn = 1
) ial ON q2.pincode = ial.pin
And if you don't need to access any rows of this table in your select list you can simply remove this join, it will not change the number of rows returned as it's an outer join.