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).

select count(1)
from employees_tmp e1
where not exists (select  1
                  from employees@db11g e2
                  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:

SQL> select * from table(dbms_xplan.display_cursor('70cmy0u5kpjhn',0,'all'));

PLAN_TABLE_OUTPUT
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID	70cmy0u5kpjhn, child number 0
-------------------------------------
select count(1) from employees_tmp e1 where not exists (select	1
	    from employees@db11g e2		      where
e2.employee_id=e1.employee_id)

Plan hash value: 3650570951

-----------------------------------------------------------------------------------------------------
| Id  | Operation	    | Name	    | Rows  | Bytes | Cost (%CPU)| Time     | Inst   |IN-OUT|
-----------------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT    |		    |	    |	    |	  2 (100)|	    |	     |	    |
|   1 |  SORT AGGREGATE     |		    |	  1 |	 13 |		 |	    |	     |	    |
|*  2 |   FILTER	    |		    |	    |	    |		 |	    |	     |	    |
|   3 |    TABLE ACCESS FULL| EMPLOYEES_TMP |	  1 |	 13 |	  2   (0)| 00:00:01 |	     |	    |
|   4 |    REMOTE	    | EMPLOYEES     |	  1 |	 13 |	  0   (0)|	    |  DB11G | R->S |
-----------------------------------------------------------------------------------------------------

Query Block Name / Object Alias (identified by operation id):
-------------------------------------------------------------

   1 - SEL$1
   3 - SEL$1 / E1@SEL$1
   4 - SEL$2 / E2@SEL$2

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - filter( IS NULL)

Column Projection Information (identified by operation id):
-----------------------------------------------------------

   1 - (#keys=0) COUNT(*)[22]
   3 - "E1"."EMPLOYEE_ID"[NUMBER,22]
   4 - "E2"."EMPLOYEE_ID"[NUMBER,22]

Remote SQL Information (identified by operation id):
----------------------------------------------------

   4 - SELECT "EMPLOYEE_ID" FROM "EMPLOYEES" "A1" WHERE "EMPLOYEE_ID"=:1 (accessing 'DB11G' )


Note
-----
   - dynamic statistics used: dynamic sampling (level=2)


47 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:

[root@rico ~]# ls -al /proc/12925/fd | grep socket
lrwx------. 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
[root@rico ~]# strace -p 12925 -o /tmp/hr_trace
Process 12925 attached
^CProcess 12925 detached
[root@rico ~]# grep -c "read(8," /tmp/hr_trace
0
[root@rico ~]# vim /tmp/hr_trace
  • 100 rows in EMPLOYEES_TMP – 1108 rows in EMPLOYEES@DB11g
[root@rico ~]# strace -p 12925 -o /tmp/hr_trace
Process 12925 attached
^CProcess 12925 detached
[root@rico ~]# grep -c "read(8," /tmp/hr_trace
106
[root@rico ~]# grep -c "write(8," /tmp/hr_trace
106
[root@rico ~]#
  • 201 rows in EMPLOYEES_TMP – 1108 rows in EMPLOYEES@DB11g
[root@rico ~]# strace -p 12925 -o /tmp/hr_trace
Process 12925 attached
^CProcess 12925 detached
[root@rico ~]# strace -p 12925 -o /tmp/hr_trace
Process 12925 attached
^CProcess 12925 detached
[root@rico ~]# grep -c "read(8," /tmp/hr_trace
308
  • 500 rows in EMPLOYEES_TMP – 1108 rows in EMPLOYEES@DB11g
[root@rico ~]# strace -p 12925 -o /tmp/hr_trace
Process 12925 attached
^CProcess 12925 detached
[root@rico ~]# grep -c "read(8," /tmp/hr_trace
764
[root@rico ~]# grep -c "write(8," /tmp/hr_trace
764
  • 14621 rows in EMPLOYEES_TMP – 1108 rows in EMPLOYEES@DB11g
[root@rico ~]# strace -p 12925 -o /tmp/hr_trace
Process 12925 attached
^CProcess 12925 detached
[root@rico ~]# grep -c "read(8," /tmp/hr_trace
15419
[root@rico ~]# grep -c "write(8," /tmp/hr_trace
15419

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:

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

For this plan, strace shows:

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

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

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

Explain plan:

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

In the STRACE we can see huge improvement!

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

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

PL/SQL procedure successfully completed.

Now we can run our SQL even in changed form 🙂

SQL> ;
  1  select COUNT(1)
  2  from employees_tmp e1
  3* where not exists (select 1 from employees@db11g e2 where e2.employee_id=e1.employee_id)
SQL> set autot trace
SQL> /


Execution Plan
----------------------------------------------------------
Plan hash value: 2645133600

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

Predicate Information (identified by operation id):
---------------------------------------------------

   2 - access("E2"."EMPLOYEE_ID"="E1"."EMPLOYEE_ID")

Remote SQL Information (identified by operation id):
----------------------------------------------------

   3 - SELECT /*+ USE_HASH ("E2") */ "EMPLOYEE_ID" FROM "EMPLOYEES" "E2" (accessing 'DB11G' )


Note
-----
   - dynamic statistics used: dynamic sampling (level=2)
   - 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