Oracle Performance and Engineering

Random notes on database features, code, practice, optimization and techniques.

Wednesday, April 19, 2006

 

Deleting Duplicate Records

I was recently asked to review some "possible interview questions". One of it was -

Suppose you have a table that looks like:

Name Null? Type
------------------------------- -------- ----
OWNER NOT NULL VARCHAR2(30)
TABLE_NAME NOT NULL VARCHAR2(30)
SEQUENCE_NAME NOT NULL VARCHAR2(30)
DERIVED_COLUMN_NAME NOT NULL VARCHAR2(30)

Because of a logic bug, the table has been filled with numerous duplicate rows (for example, there might be 120 distinct values of the four columns, but the table has 1200 rows).


Write an anonymous PL/SQL block that would remove all the duplicate rows.

ANSWER:


declare
cursor ccc is
select owner, table_name, sequence_name, derived_column_name,
count(*) cnt
from iview
group by owner, table_name, sequence_name, derived_column_name;


begin
for c in ccc loop
delete from trm_tab_sequences
where table_name = c.table_name
and sequence_name = c.sequence_name
and derived_column_name = c.derived_column_name
and rownum < c.cnt;
end loop;
end;
/


Now, I do not really agree with the answer. In fact, I do not agree with the question too.

No one should write a "PL/SQL block" to do things like this. This should ALWAYS be done in SQL.

There are at least two quick ways to delete the duplicate records -

Method ONE -
--------------


DELETE FROM iview t1
WHERE ROWID <> ( SELECT MAX(ROWID)
FROM iview b
WHERE b.owner = t1.owner
AND b.table_name = t1.table_name
AND b.sequence_name = t1.sequence_name
AND b.derived_column_name = t1.derived_column_name);



Method TWO -
--------------


DELETE FROM iview WHERE ROWID IN
(SELECT rid
FROM
(SELECT ROWID rid,
row_number() over (PARTITION BY owner, table_name, sequence_name, derived_column_name ORDER BY ROWID) rn
FROM iview )
WHERE rn <> 1 );



The second solution uses "analytic function" and is more elegant. However, based on some tests - depending on what columns are duplicated and are indexed - the first solution could indeed be faster in some cases.


Thursday, April 13, 2006

 

Query Tuned for complicated OR clause

Investigating a certain Statspack today I noticed that 58% of the DB CPU time is spent in a SQL as follows -


CPU Elapsd
Buffer Gets Executions Gets per Exec %Total Time (s) Time (s)
--------------- ------------ -------------- ------ -------- ---------

478,456,910 3,804 125,777.3 58.5 2334.82 2302.48





SELECT *
FROM knta_exceptions ex
WHERE (source_id IN (
SELECT kp2.project_id
FROM kdrv_projects kp2, kdrv_projects kp1
WHERE kp1.project_id = :p_source_id
AND kp2.project_path_id_list LIKE kp1.project_path_id_list || '%'
AND kp2.project_type_code = 'TASK')
OR
(source_id IN (
SELECT kp2.project_id
FROM kdrv_projects kp2, kdrv_projects kp1
WHERE kp1.project_id = :p_source_id
AND kp2.project_path_id_list LIKE kp1.project_path_id_list || '%'
AND kp2.project_type_code = 'PROJECT')
AND source_entity_id = 59))
AND source_entity_id IN (59, 6);



This SQL was doing 125,777 Logical I/O (buffer gets) per execution.

Explain plan in 9i looks like -



-----------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost |
-----------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 4049 | 478K| 168 |
| 1 | FILTER | | | | |
| 2 | TABLE ACCESS FULL | KNTA_EXCEPTIONS | 4049 | 478K| 168 |
| 3 | NESTED LOOPS | | 1 | 78 | 3 |
| 4 | TABLE ACCESS BY INDEX ROWID| KDRV_PROJECTS | 1 | 36 | 2 |
| 5 | INDEX UNIQUE SCAN | KDRV_PROJECTS_U1 | 1 | | 1 |
| 6 | TABLE ACCESS BY INDEX ROWID| KDRV_PROJECTS | 1 | 42 | 1 |
| 7 | INDEX UNIQUE SCAN | KDRV_PROJECTS_U1 | 1 | | |
| 8 | NESTED LOOPS | | 1 | 78 | 3 |
| 9 | TABLE ACCESS BY INDEX ROWID| KDRV_PROJECTS | 1 | 36 | 2 |
| 10 | INDEX UNIQUE SCAN | KDRV_PROJECTS_U1 | 1 | | 1 |
| 11 | TABLE ACCESS BY INDEX ROWID| KDRV_PROJECTS | 1 | 42 | 1 |
| 12 | INDEX UNIQUE SCAN | KDRV_PROJECTS_U1 | 1 | | |
-----------------------------------------------------------------------------------



Notice, how we are scanning KDRV_PROJECTS once for each OR-ed parts, eventually doing a full-table scan of KNTA_EXCEPTIONS (a table with more than 480K records).

Taking a close look at the SQL statement, you can see the TWO "OR" parts are exactly the same, except (a) the earlier block searches for PROJECT_TYPE_CODE ='TASK'; and (b) the later block searches for PROJECT_TYPE_CODE = 'PROJECT' *and* SOURCE_ENTITY_ID (a column in outer table KNTA_EXCEPTIONS) = 59.

This SQL can be written in a much simplified -



SELECT * FROM knta_exceptions ex
WHERE source_id IN ( SELECT kp2.project_id
FROM kdrv_projects kp2,
kdrv_projects kp1
WHERE kp1.project_id =:p_source_id
AND kp2.project_path_id_list LIKE kp1.project_path_id_list||'%'
AND (kp2.project_type_code = 'TASK' OR (kp2.project_type_code = 'PROJECT' AND source_entity_id = 59)))
-- just one OR condition that takes care of both.
AND source_entity_id IN (59, 6)



For which the explain plan looks like -



-----------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost |
-----------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1794 | 348K| 639 |
| 1 | HASH JOIN | | 1794 | 348K| 639 |
| 2 | NESTED LOOPS | | 14112 | 1074K| 385 |
| 3 | TABLE ACCESS BY INDEX ROWID| KDRV_PROJECTS | 1 | 36 | 2 |
| 4 | INDEX UNIQUE SCAN | KDRV_PROJECTS_U1 | 1 | | 1 |
| 5 | TABLE ACCESS BY INDEX ROWID| KDRV_PROJECTS | 14112 | 578K| 383 |
| 6 | INDEX RANGE SCAN | KDRV_PROJECTS_N2 | 2540 | | 16 |
| 7 | TABLE ACCESS FULL | KNTA_EXCEPTIONS | 61507 | 7267K| 168 |
-----------------------------------------------------------------------------------



Notice the change in explain plan. Now we are doing a hash join between the KDRV_PROJECTS(s) and KNTA_EXCEPTIONS. KNTA_EXCEPTIONS being the "probe" table. This plan was obtained in 9i. The consistent gets is down to 1800!!

I tried the same SQL in 10g (10.1.0.3 with CPU COSTING off ) and got even better statistics (16 Logical I/O for same data) and a different plan as shown below -



-------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost |
-------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 9953 | 1846K| 146 |
| 1 | TABLE ACCESS BY INDEX ROWID | KNTA_EXCEPTIONS | 1 | 112 | 1 |
| 2 | NESTED LOOPS | | 9953 | 1846K| 146 |
| 3 | NESTED LOOPS | | 14112 | 1074K| 5 |
| 4 | TABLE ACCESS BY INDEX ROWID| KDRV_PROJECTS | 1 | 36 | 1 |
| 5 | INDEX UNIQUE SCAN | KDRV_PROJECTS_U1 | 1 | | 1 |
| 6 | TABLE ACCESS BY INDEX ROWID| KDRV_PROJECTS | 14112 | 578K| 4 |
| 7 | INDEX RANGE SCAN | KDRV_PROJECTS_N2 | 2540 | | 0 |
| 8 | INLIST ITERATOR | | | | |
| 9 | INDEX RANGE SCAN | KNTA_EXCEPTIONS_N1 | 2 | | 0 |
-------------------------------------------------------------------------------------



Notice, the difference in the plan. On 10g, a "nested loop" is chosen ahead of the "hash join". It also uses the index KNTA_EXCEPTIONS_N1 (on SOURCE_ID, SOURCE_ENTITY_ID in KNTA_EXCEPTIONS) for "inlist iterations".

So, assuming the customer changes this code, the number of buffer_gets per execution will come down from 175K to 1.8K -- possibly reducing the CPU_Time by at least 90%-95%. The total CPU Overhead in this 9i DB was 58%. After pushing the change, the CPU Overhead for this SQL should should come to around 5% or less (extrapolating the response time and LIO). Nice, ain't it?

Tuesday, April 11, 2006

 

A query tuned today - bad positioning of IN-list

Nothing new - walked in to work this morning, got apprised of some "P1" customer performance issues. Thankfully, we now have a good metric collection in place. Whenever we hear performance issues we collect the following -

The first customer issue -

Fortunately, this customer had already pinned on one SQL that was a performance disaster. They'd uploaded the email trail discussing about the SQL and I used the email to get an idea about the SQL. After pruning the SELECTed fields, the query looked like -


SELECT
task_id, last_update_date,
local_currency_id
FROM
kdrv_my_tasks_v v
WHERE
state_id IN (9, 10, 11, 12)
AND parent_state_id IN (4, 5, 15, 16)
AND master_project_id = '30062'
AND TRUNC (scheduled_start_date) <= (CURRENT_DATE + 1 + 14)
AND 75 < (SELECT SUM (p.percent_complete)
FROM kdrv_projects p
WHERE p.project_id IN
(SELECT pr.predecessor_project_id
FROM kdrv_predecessors pr
WHERE pr.project_id = v.task_id))
AND assignment_user_id = '49745'
ORDER BY scheduled_start_date ASC, task_name;

The statistics and 'explain plan' from the TKPROF-ed trace file looked like -

call count cpu elapsed disk query current rows
------- ------ -------- ---------- ---------- ---------- ---------- ----------
Parse 2 0.07 0.09 4 54 0 0
Execute 2 0.00 0.00 0 0 0 0
Fetch 2 147.20 164.09 141445 57966443 0 50
------- ------ -------- ---------- ---------- ---------- ---------- ----------
total 6 147.28 164.18 141449 57966497 0 50

Rows Row Source Operation
------- ---------------------------------------------------
25 SORT ORDER BY
160 COUNT STOPKEY
160 NESTED LOOPS OUTER
160 NESTED LOOPS
160 NESTED LOOPS OUTER
160 NESTED LOOPS
285 NESTED LOOPS
1 NESTED LOOPS
1 NESTED LOOPS
1 NESTED LOOPS
1 NESTED LOOPS
1 NESTED LOOPS
1 NESTED LOOPS
1 NESTED LOOPS
1 NESTED LOOPS
1 NESTED LOOPS
1 NESTED LOOPS
1 NESTED LOOPS
1 NESTED LOOPS
1 NESTED LOOPS
1 TABLE ACCESS BY INDEX ROWID KDRV_PROJECT_SETUPS
1 INDEX UNIQUE SCAN KDRV_PROJECT_SETUPS_U2
1 TABLE ACCESS BY INDEX ROWID KDRV_PROJECTS
1 INDEX UNIQUE SCAN KDRV_PROJECTS_U1
1 TABLE ACCESS BY INDEX ROWID KNTA_REGIONS
1 INDEX UNIQUE SCAN KNTA_REGIONS_U1
1 TABLE ACCESS BY INDEX ROWID KDRV_PROJECT_FIELD_SETUPS
1 INDEX UNIQUE SCAN KDRV_PROJECT_FIELD_SETUPS_U2
1 TABLE ACCESS BY INDEX ROWID KDRV_PROJECT_FIELD_SETUPS
1 INDEX UNIQUE SCAN KDRV_PROJECT_FIELD_SETUPS_U2
1 TABLE ACCESS BY INDEX ROWID KDRV_PROJECT_FIELD_SETUPS
1 INDEX UNIQUE SCAN KDRV_PROJECT_FIELD_SETUPS_U2
1 TABLE ACCESS BY INDEX ROWID KDRV_PROJECT_FIELD_SETUPS
1 INDEX UNIQUE SCAN KDRV_PROJECT_FIELD_SETUPS_U2
1 TABLE ACCESS BY INDEX ROWID KDRV_PROJECT_FIELD_SETUPS
1 INDEX UNIQUE SCAN KDRV_PROJECT_FIELD_SETUPS_U2
1 TABLE ACCESS BY INDEX ROWID KDRV_PROJECT_FIELD_SETUPS
1 INDEX UNIQUE SCAN KDRV_PROJECT_FIELD_SETUPS_U2
1 TABLE ACCESS BY INDEX ROWID KDRV_PROJECT_FIELD_SETUPS
1 INDEX UNIQUE SCAN KDRV_PROJECT_FIELD_SETUPS_U2
1 TABLE ACCESS BY INDEX ROWID KDRV_PROJECT_FIELD_SETUPS
1 INDEX UNIQUE SCAN KDRV_PROJECT_FIELD_SETUPS_U2
1 TABLE ACCESS BY INDEX ROWID KDRV_PROJECT_FIELD_SETUPS
1 INDEX UNIQUE SCAN KDRV_PROJECT_FIELD_SETUPS_U2
1 TABLE ACCESS BY INDEX ROWID KDRV_PROJECT_FIELD_SETUPS
1 INDEX UNIQUE SCAN KDRV_PROJECT_FIELD_SETUPS_U2
1 TABLE ACCESS BY INDEX ROWID KDRV_PROJECT_FIELD_SETUPS
1 INDEX UNIQUE SCAN KDRV_PROJECT_FIELD_SETUPS_U2
285 TABLE ACCESS BY INDEX ROWID KDRV_ASSIGNMENTS
285 INDEX RANGE SCAN KDRV_ASSIGNMENTS_N2
160 TABLE ACCESS BY INDEX ROWID KDRV_PROJECTS
278 INDEX UNIQUE SCAN KDRV_PROJECTS_U1
285 SORT AGGREGATE
7 FILTER
14185875 TABLE ACCESS FULL KDRV_PROJECTS
7 INDEX UNIQUE SCAN KDRV_PREDECESSORS_U2
0 TABLE ACCESS BY INDEX ROWID KCRT_STATUSES
0 INDEX UNIQUE SCAN KCRT_STATUSES_U1
160 TABLE ACCESS BY INDEX ROWID KDRV_PROJECTS
160 INDEX UNIQUE SCAN KDRV_PROJECTS_U1
0 TABLE ACCESS BY INDEX ROWID KCST_CURRENCY_LINES
0 INDEX UNIQUE SCAN KCST_CURRENCY_LINES_U1

So, what do we learn from the query and the plan?

  1. kdrv_my_tasks_v is a view that has the table KDRV_PROJECTS in it's WHERE clause
  2. The blue highlighted part in the query maps with the blue highlighted part in the access plan - and is almost fully responsible for the CPU time (147 secs to full-scan 14 million rows as a result full-scan of kdrv_projects -- sounds logical).
  3. The blue highlighted part in the query has an unnecessary IN-list in the correlated sub-query. This could simply have been an equi-join.

i.e., we could rewrite -

(SELECT SUM (p.percent_complete)
FROM kdrv_projects p
WHERE p.project_id IN
(SELECT pr.predecessor_project_id
FROM kdrv_predecessors pr
WHERE pr.project_id = v.task_id))

to -

(SELECT SUM (p.percent_complete)
FROM kdrv_projects p,
kdrv_predecessors pr
WHERE p.project_id = pr.predecessor_project_id
AND pr.project_id = v.task_id)

Then the query becomes -

SELECT task_id,
last_update_date,
local_currency_id
FROM kdrv_my_tasks_v v
WHERE state_id IN (9, 10, 11, 12) AND
parent_state_id IN (4, 5, 15, 16) AND
master_project_id = '30062'AND
TRUNC (scheduled_start_date) <= (CURRENT_DATE + 1 + 14)AND
75 < (SELECT SUM (p.percent_complete)
FROM kdrv_projects p,
kdrv_predecessors pr
WHERE p.project_id = pr.predecessor_project_id
AND pr.project_id = v.task_id) A
ND
assignment_user_id = '49745'
ORDER BY scheduled_start_date ASC, task_name;

The new Logical IO falls down to just about 1500 (from 57 million) and time taken falls to about 08 secs! There's no more a "full table scan" on kdrv_projects table. On a finishing note, how many times have we seen this type of "redundant" IN-lists with a single table replacing an equi-join?


Friday, April 07, 2006

 

Undocumented Parameter "_optimizer_cost_based_transformation" in 10g

As mentioned in this Oracle optimizer white paper - Oracle can 'transform a query' (rewrite it before execution to give same logical results and better performance) in two ways -
(a) heuristic SQL transformation - Oracle follows a set of rules to do so.
(b) cost-based SQL transformation - Oracle costs the query and data structures to decide whether it will transform or not.

Before 10g (the paper was written in 9i times) - the following transformations were part of the heuristic ones -

For things like 'subquery flattening' (i.e., replacing the 'FILTER' operation in EXISTS with a 'SEMI JOIN'; or 'unnest' it) -- following a simple set of rules can just not be adequate enough for arriving at the best possible plan. Thus, Oracle 10g has started 'costing' the 'complex view merging'; predicate push and subquery flattening (or, unnesting). This is turned on by the undocumented parameter "_OPTIMIZER_COST_BASED_TRANSFORMATION" (value = linear, by default).

I have noticed after upgrading from 9i to 10g, the biggest impacts for query performance were -

(1) subquery unnesting (mostly, unnesting EXISTS to HASH JOIN SEMI), and
(2)right outer hash join (smaller table in an outer join *is* used as the hash table, unlike what was happening in pre-10g versions).

In some cases (till 10.1.0.4) we would have to hint the sub-query with NO_UNNEST to avoid the hash join semi.

Of course, I did not want to turn the parameter off systemwide, because - as usual - some other queries were benefitted from costed unnesting. I am yet to do sufficient analysis but all the queries that performed much worse after costed unnesting had combination of complex views in the sub-query.

I also discovered a unique bug related to view merging that definitely is reproducible across all 9i versions, but may be suppressed on 10g because of costed query transformation.

DROP TABLE testmerge;

CREATE TABLE testmerge (a NUMBER, b NUMBER);

INSERT INTO testmerge VALUES (2,3);

SELECT a,b FROM testmerge GROUP BY b;

ora-979 not a group by expression

SELECT COUNT(*) FROM
(SELECT a, b FROM testmerge
GROUP BY b);

COUNT(*)
----------
1

The onlyways to get rid of this problem (ora-979 is ommited if it is raised in inline view) is to use the hint NO_MERGE in 9i, and depending on 10g version and testmerge table columns - setting the '_OPTIMIZER_COST_BASED_TRANSFORMATION' to off on 10g.

Queries that correctly return the ORA-979 -

SELECT COUNT(*) FROM
(SELECT /*+ no_merge */ a, b FROM testmerge
GROUP BY b);

SELECT COUNT(*) FROM
(SELECT a, b FROM testmerge WHERE ROWNUM > 0
GROUP BY b);


In the first case, I used NO_MERGE hint in the inline view so it does not get merged, and in second query I added a ROWNUM > 0" trick so Oracle does not merge the view (very useful technique when you do not want your view to be merged, rather evaluated before the rest of the query).




Monday, April 03, 2006

 

HASH JOIN RIGHT OUTER on Oracle 10g and SWAP_JOIN_INPUTS

In standard Oracle Hash joins the smaller table is 'hashed' in memory (PGA) - if overflows then to TEMP segments - then the larger table(probe table) is read and probed against the hash partitions.

However, in hash outer joins the implementation was not so perfect as above. Till Oracle 10g that is.

Tests performed on Oracle 10.1.0.4.

So, a query like -

SELECT a.ename,b.dname
FROM scott.emp a, scott.dept b
WHERE a.deptno=b.deptno(+);

Shows an explain plan like -

Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=7 Card=14 Bytes=308)
1 0 HASH JOIN (OUTER) (Cost=7 Card=14 Bytes=308)
2 1 TABLE ACCESS (FULL) OF 'EMP' (TABLE) (Cost=3 Card=14 Bytes=126)
3 1 TABLE ACCESS (FULL) OF 'DEPT' (TABLE) (Cost=3 Card=4 Bytes=52)

But -

SELECT /*+ SWAP_JOIN_INPUTs(b) */ a.ename,b.dname
FROM scott.emp a, scott.dept b
WHERE a.deptno=b.deptno(+);

Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=ALL_ROWS (Cost=7 Card=14 Bytes=308)
1 0 HASH JOIN (RIGHT OUTER) (Cost=7 Card=14 Bytes=308)
2 1 TABLE ACCESS (FULL) OF 'DEPT' (TABLE) (Cost=3 Card=4 Bytes=52)
3 1 TABLE ACCESS (FULL) OF 'EMP' (TABLE) (Cost=3 Card=14 Bytes=126)

Notice the change in the order in which tables are joined.

As Jonathan Lewis shows in this article -

"Now in 10g oracle can switch the driving table for outer joins and semi joins based on the sizes of the row source. The hash table and probe table was fixed in outer join and semi joins till 9i. Now in 10g the hash table and probe table an be switched".

I used little known "swap_join_inputs" hint to change the hash table from EMP to DEPT. Typically, in the production applications I've seen Oracle picking the smaller tables as the hash table (correctly!). For very few cases where Oracle cannot the above hint can possibly used for the smaller table to become the hash table (as it would have happened for an equi join).

I could not, obviously, produce the HASH JOIN RIGHT OUTER with Oracle 9i.

However, with Oracle 9i this hint works to change the order of the table for an equi hash join.

Tests performed with Oracle 9.2.0.6 -

SELECT /*+ use_hash(a) */ a.ename,b.dname
FROM scott.emp a, scott.dept b
WHERE a.deptno=b.deptno;

Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=5 Card=409 Bytes=17178)
1 0 HASH JOIN (Cost=5 Card=409 Bytes=17178)
2 1 TABLE ACCESS (FULL) OF 'EMP' (Cost=2 Card=409 Bytes=8180)
3 1 TABLE ACCESS (FULL) OF 'DEPT' (Cost=2 Card=409 Bytes=8998)

Consistent Gets - 15

SELECT /*+ use_hash(a) SWAP_JOIN_INPUTs(b) */ a.ename,b.dname
FROM scott.emp a, scott.dept b
WHERE a.deptno=b.deptno;

Execution Plan
----------------------------------------------------------
0 SELECT STATEMENT Optimizer=CHOOSE (Cost=5 Card=409 Bytes=17178)
1 0 HASH JOIN (Cost=5 Card=409 Bytes=17178)
2 1 TABLE ACCESS (FULL) OF 'DEPT' (Cost=2 Card=409 Bytes=8998)
3 1 TABLE ACCESS (FULL) OF 'EMP' (Cost=2 Card=409 Bytes=8180)

Consistent Gets - 17!

Archives

January 2005   February 2005   March 2005   April 2005   May 2005   March 2006   April 2006   August 2006   September 2006   October 2006   January 2007   May 2008  

This page is powered by Blogger. Isn't yours?