I had a very strange performance related problem with MERGE in Oracle 10. In a few words, I have a stored procedure that calculates and stores user rank based on her activity in the system and contains just one MERGE statement:
MERGE INTO user_ranks target USING
([complex query that returns rank_id and user_id])src ON
(src.user_id = target.user_id)
WHEN MATCHED THEN UPDATE SET target.rank_id = src.rank_id
WHEN NOT MATCHED THEN INSERT (target.user_id, target.rank_id)
VALUES (src.user_id, src.rank_id);
// user_ranks table structure:
CREATE TABLE user_ranks (user_id INT NOT NULL
PRIMARY KEY USING INDEX (CREATE UNIQUE INDEX UQ_uid_uranks ON user_ranks(user_id)),
rank_id INT NOT NULL,
CONSTRAINT FK_uid_uranks FOREIGN KEY (user_id) REFERENCES users(id),
CONSTRAINT FK_rid_uranks FOREIGN KEY(rank_id) REFERENCES ranks(id));
// no index on rank_id - intentionally, ranks table is a lookup with
// a very few records and no delete/update allowed
The subquery which is used as a source for MERGE returns at most 1 record (user_id is passed as parameter to the procedure). It’s quite expensive but execution time is acceptable (1-1.2 sec). The problem is that MERGE execution time hikes to more than 40 seconds, and I have no clue why. I tried using LEADING hint with no success. But if I split the statement into 2 parts, first one – run SELECT subquery and store result(rank_id) into variable and then merge – MERGE ... USING (SELECT user_id, rank_id FROM DUAL)src ... everything works just fine. From what I read, there are known issues with Oracle’s MERGE, but they are mostly related to triggers (no triggers in my case). It also says that MERGE works slower than combination of INSERT and UPDATE, but I believe the “normal” difference is around 5-10%, not 30 times…
I’m trying to understand what I did wrong… Thanks for your suggestions.
Update
Execution plan is quite long to post it here, in short : subquery cost by itself is 12737, with merge – 76305. Stats output for merge :
> Statistics
108 recursive calls
4 db block gets
45630447 consistent gets
24905 physical reads
0 redo size
620 bytes sent via SQL*Net to client
1183 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
2 sorts (memory)
0 sorts (disk)
subquery alone :
> Statistics
8 recursive calls
0 db block gets
34 consistent gets
0 physical reads
0 redo size
558 bytes sent via SQL*Net to client
234 bytes received via SQL*Net from client
1 SQL*Net roundtrips to/from client
1 sorts (memory)
0 sorts (disk)
If you’ve set up SQL*Plus autotrace it would be a matter of seconds to see which part of the actual execution plan has caused the most physical and logical I/O and used the most memory.
Note that the information you get using this method is much more precise than a simple
explain plan.