oracle dblinks – performance considerations


13.01.2016
by Kamil Stawiarski

Let’s imagine the following situation – you have a table called EMPLOYEES_TMP which is a global temporary table with a variable number of rows – totally nondeterministic. And there is a query which runs very often to compare primary key values between this table and a remote one (which is basically the same but not temporary).

1select count(1)
2from employees_tmp e1
3where not exists (select  1
4                  from employees@db11g e2
5                  where e2.employee_id=e1.employee_id);

And the query sometimes run very fast and other times – very very slow. I had a similar situation last week with one of my customers.

Well, actually the case is quite simple here – let’s see the execution plan:

1SQL> select * from table(dbms_xplan.display_cursor('70cmy0u5kpjhn',0,'all'));
2 
3PLAN_TABLE_OUTPUT
4----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
5SQL_ID  70cmy0u5kpjhn, child number 0
6-------------------------------------
7select count(1) from employees_tmp e1 where not exists (select  1
8        from employees@db11g e2           where
9e2.employee_id=e1.employee_id)
10 
11Plan hash value: 3650570951
12 
13-----------------------------------------------------------------------------------------------------
14| Id  | Operation       | Name      | Rows  | Bytes | Cost (%CPU)| Time     | Inst   |IN-OUT|
15-----------------------------------------------------------------------------------------------------
16|   0 | SELECT STATEMENT    |           |       |       |     2 (100)|      |        |      |
17|   1 |  SORT AGGREGATE     |           |     1 |    13 |        |      |        |      |
18|*  2 |   FILTER        |           |       |       |        |      |        |      |
19|   3 |    TABLE ACCESS FULL| EMPLOYEES_TMP |     1 |    13 |     2   (0)| 00:00:01 |        |      |
20|   4 |    REMOTE       | EMPLOYEES     |     1 |    13 |     0   (0)|      |  DB11G | R->S |
21-----------------------------------------------------------------------------------------------------
22 
23Query Block Name / Object Alias (identified by operation id):
24-------------------------------------------------------------
25 
26   1 - SEL$1
27   3 - SEL$1 / E1@SEL$1
28   4 - SEL$2 / E2@SEL$2
29 
30Predicate Information (identified by operation id):
31---------------------------------------------------
32 
33   2 - filter( IS NULL)
34 
35Column Projection Information (identified by operation id):
36-----------------------------------------------------------
37 
38   1 - (#keys=0) COUNT(*)[22]
39   3 - "E1"."EMPLOYEE_ID"[NUMBER,22]
40   4 - "E2"."EMPLOYEE_ID"[NUMBER,22]
41 
42Remote SQL Information (identified by operation id):
43----------------------------------------------------
44 
45   4 - SELECT "EMPLOYEE_ID" FROM "EMPLOYEES" "A1" WHERE "EMPLOYEE_ID"=:1 (accessing 'DB11G' )
46 
47 
48Note
49-----
50   - dynamic statistics used: dynamic sampling (level=2)
51 
52 
5347 rows selected.

OK so we can see that for each row in the local temporary table (EMPLOYEES_TMP – E1@SEL$1) the subquery will be executed (SEL$2).

We can actually check the number of socket reads and writes by this process server, while executing this query for different number of rows in the temporary table.

The process has opened a socket for dblink communication:

1[root@rico ~]# ls -al /proc/12925/fd | grep socket
2lrwx------. 1 oracle oinstall 64 01-13 20:06 8 -> socket:[41123]

File descriptor for the socket is 8 – we will use it with strace.

  • 0 rows in EMPLOYEES_TMP – 1108 rows in EMPLOYEES@DB11g
1[root@rico ~]# strace -p 12925 -o /tmp/hr_trace
2Process 12925 attached
3^CProcess 12925 detached
4[root@rico ~]# grep -c "read(8," /tmp/hr_trace
50
6[root@rico ~]# vim /tmp/hr_trace
  • 100 rows in EMPLOYEES_TMP – 1108 rows in EMPLOYEES@DB11g
1[root@rico ~]# strace -p 12925 -o /tmp/hr_trace
2Process 12925 attached
3^CProcess 12925 detached
4[root@rico ~]# grep -c "read(8," /tmp/hr_trace
5106
6[root@rico ~]# grep -c "write(8," /tmp/hr_trace
7106
8[root@rico ~]#
  • 201 rows in EMPLOYEES_TMP – 1108 rows in EMPLOYEES@DB11g
1[root@rico ~]# strace -p 12925 -o /tmp/hr_trace
2Process 12925 attached
3^CProcess 12925 detached
4[root@rico ~]# strace -p 12925 -o /tmp/hr_trace
5Process 12925 attached
6^CProcess 12925 detached
7[root@rico ~]# grep -c "read(8," /tmp/hr_trace
8308
  • 500 rows in EMPLOYEES_TMP – 1108 rows in EMPLOYEES@DB11g
1[root@rico ~]# strace -p 12925 -o /tmp/hr_trace
2Process 12925 attached
3^CProcess 12925 detached
4[root@rico ~]# grep -c "read(8," /tmp/hr_trace
5764
6[root@rico ~]# grep -c "write(8," /tmp/hr_trace
7764
  • 14621 rows in EMPLOYEES_TMP – 1108 rows in EMPLOYEES@DB11g
1[root@rico ~]# strace -p 12925 -o /tmp/hr_trace
2Process 12925 attached
3^CProcess 12925 detached
4[root@rico ~]# grep -c "read(8," /tmp/hr_trace
515419
6[root@rico ~]# grep -c "write(8," /tmp/hr_trace
715419

So as you can see – it doesn’t scale good. But this should be obvious just by looking on the explain plan – strace here is just for fun 😉

How can we change the query? First of all we have to be sure, that there will be no nested loops in the explain plan because that changes nothing.

SQL Plan with nested loops:

1-----------------------------------------------------------------------------------------------------
2| Id  | Operation       | Name      | Rows  | Bytes | Cost (%CPU)| Time     | Inst   |IN-OUT|
3-----------------------------------------------------------------------------------------------------
4|   0 | SELECT STATEMENT    |           |       |       |    52 (100)|      |        |      |
5|   1 |  SORT AGGREGATE     |           |     1 |    26 |        |      |        |      |
6|   2 |   NESTED LOOPS ANTI |           | 16127 |   409K|    52  (25)| 00:00:01 |        |      |
7|   3 |    TABLE ACCESS FULL| EMPLOYEES_TMP | 16127 |   204K|    39   (0)| 00:00:01 |        |      |
8|   4 |    REMOTE       | EMPLOYEES     |     1 |    13 |     0   (0)|      |  DB11G | R->S |
9-----------------------------------------------------------------------------------------------------

For this plan, strace shows:

1[root@rico ~]# strace -p 12925 -o /tmp/hr_trace
2Process 12925 attached
3^CProcess 12925 detached
4[root@rico ~]# grep -c "read(8," /tmp/hr_trace
516137

So its even worse! Let’s try to force unnesting but with hash join – the query would look like this:

1select /*+ qb_name(local) UNNEST(@remote) USE_HASH(e1@local e2@remote) */ count(1)
2from employees_tmp e1
3where not exists (select /*+ qb_name(remote) */  1
4                  from employees@db11g e2
5                  where e2.employee_id=e1.employee_id);

Explain plan:

1-------------------------------------------------------------------------------------------------------
2| Id  | Operation         | Name          | Rows  | Bytes | Cost (%CPU)| Time     | Inst   |IN-OUT|
3-------------------------------------------------------------------------------------------------------
4|   0 | SELECT STATEMENT      |           |       |       |    40 (100)|          |        |      |
5|   1 |  SORT AGGREGATE       |           |     1 |    26 |        |          |        |      |
6|*  2 |   HASH JOIN RIGHT ANTI|           | 16127 |   409K|    40   (0)| 00:00:01 |        |      |
7|   3 |    REMOTE         | EMPLOYEES     |   107 |  1391 |     1   (0)| 00:00:01 |  DB11G | R->S |
8|   4 |    TABLE ACCESS FULL  | EMPLOYEES_TMP | 16127 |   204K|    39   (0)| 00:00:01 |        |      |
9-------------------------------------------------------------------------------------------------------

In the STRACE we can see huge improvement!

1[root@rico ~]# strace -p 12925 -o /tmp/hr_trace
2Process 12925 attached
3^CProcess 12925 detached
4[root@rico ~]# grep -c "read(8," /tmp/hr_trace
55
6[root@rico ~]# grep -c "write(8," /tmp/hr_trace
74

Reducing the number of SQL*Net socket reads and writes was the solution. But what to do if we can’t change the SQL text? There are a few methods – for example SQLPatch:

1  1  declare
2  2    v_sql_text clob;
3  3  begin
4  4    select sql_fulltext into v_sql_text
5  5    from v$sql
6  6    where sql_id='70cmy0u5kpjhn'
7  7    and child_number=0;
8  8    dbms_sqldiag_internal.I_CREATE_PATCH(SQL_TEXT=>v_sql_text,
9  9                        HINT_TEXT=>'UNNEST(@SEL$2) USE_HASH(e1@SEL$1 e2@SEL$2)',
10 10                        NAME=>'KSTAWIARSKI_UNNEST_HASH');
11 11* end;
12SQL> /
13 
14PL/SQL procedure successfully completed.

Now we can run our SQL even in changed form 🙂

1SQL> ;
2  1  select COUNT(1)
3  2  from employees_tmp e1
4  3* where not exists (select 1 from employees@db11g e2 where e2.employee_id=e1.employee_id)
5SQL> set autot trace
6SQL> /
7 
8 
9Execution Plan
10----------------------------------------------------------
11Plan hash value: 2645133600
12 
13-------------------------------------------------------------------------------------------------------
14| Id  | Operation         | Name          | Rows  | Bytes | Cost (%CPU)| Time     | Inst   |IN-OUT|
15-------------------------------------------------------------------------------------------------------
16|   0 | SELECT STATEMENT      |           |     1 |    26 |    40   (0)| 00:00:01 |        |      |
17|   1 |  SORT AGGREGATE       |           |     1 |    26 |        |          |        |      |
18|*  2 |   HASH JOIN RIGHT ANTI|           | 16127 |   409K|    40   (0)| 00:00:01 |        |      |
19|   3 |    REMOTE         | EMPLOYEES     |   107 |  1391 |     1   (0)| 00:00:01 |  DB11G | R->S |
20|   4 |    TABLE ACCESS FULL  | EMPLOYEES_TMP | 16127 |   204K|    39   (0)| 00:00:01 |        |      |
21-------------------------------------------------------------------------------------------------------
22 
23Predicate Information (identified by operation id):
24---------------------------------------------------
25 
26   2 - access("E2"."EMPLOYEE_ID"="E1"."EMPLOYEE_ID")
27 
28Remote SQL Information (identified by operation id):
29----------------------------------------------------
30 
31   3 - SELECT /*+ USE_HASH ("E2") */ "EMPLOYEE_ID" FROM "EMPLOYEES" "E2" (accessing 'DB11G' )
32 
33 
34Note
35-----
36   - dynamic statistics used: dynamic sampling (level=2)
37   - SQL patch "KSTAWIARSKI_UNNEST_HASH" used for this statement

Contact us

Database Whisperers sp. z o. o. sp. k.
al. Jerozolimskie 200, 3rd floor, room 342
02-486 Warszawa
NIP: 5272744987
REGON:362524978
+48 508 943 051
+48 661 966 009
info@ora-600.pl

Newsletter Sign up to be updated