standingzzz's Oracle memo

OracleSQLチューニングの豊富な経歴を生かして、主に11gと12cのオプティマイザ動作の違いなどについて、実用的な検証結果をメモしています。

Oracle12c SQL*Loader(12.2.0.1)を動かしてみる

概要

こちらでインストールしたSQL*Loaderを動かしてみます。

Oracle Database 12c Enterprise Edition Release 12.2.0.1.0 - 64bit Production
CentOS Linux release 7.3.1611 (Core) 

検証

テスト用テーブル作成
ロード用CSVデータ
[standingzzz@apl work]$ wc -l KEN_ALL_ROME_u.CSV
124117 KEN_ALL_ROME_u.CSV
[standingzzz@apl work]$


0060050,北海道,札幌市 手稲区,星置南,HOKKAIDO,SAPPORO SHI TEINE KU,HOSHIOKIMINAMI
0060811,北海道,札幌市 手稲区,前田 一条,HOKKAIDO,SAPPORO SHI TEINE KU,MAEDA 1-JO
0060812,北海道,札幌市 手稲区,前田 二条,HOKKAIDO,SAPPORO SHI TEINE KU,MAEDA 2-JO
0060813,北海道,札幌市 手稲区,前田 三条,HOKKAIDO,SAPPORO SHI TEINE KU,MAEDA 3-JO
コントロールファイル
options ( errors=-1, rows=10000 )
load data
append
into table tab_KEN_ALL_ROME
fields terminated by ','
trailing nullcols
(   
    postal_cd     char(7)
  , pref_jp       char(256)
  , city_jp       char(256)
  , town_jp       char(256)
  , pref_en       char(256)
  , city_en       char(256)
  , town_en       char(256)
)
ロード
$ sqlldr test01@PDB1 control=KEN_ALL_ROME.ctl data=KEN_ALL_ROME_u.CSV log=KEN_ALL_ROME.log bad=KEN_ALL_ROME.bad readsize=100000000 bindsize=100000000
Password:

SQL*Loader: Release 12.2.0.1.0 - Production on Wed Oct 11 19:32:54 2017

Copyright (c) 1982, 2017, Oracle and/or its affiliates.  All rights reserved.

LRM-00104: '-1' is not a legal integer for 'errors'
SQL*Loader-114: Error in OPTIONS statement
[standingzzz@apl work]$

errors=-1のところで、integerじゃ無いからエラーだと言われました。
確かにマニュアル見るとこのような記述があります。12cから変わったのですね。

エラーを許容しない場合は、ERRORS=0を設定します。
エラーを無制限に許容する場合は、非常に大きい値を指定します。
再ロード errors=2147483648

型はintergerなので、integerのmax+1をあえて設定してみます。

$ sqlldr test01@PDB1 control=KEN_ALL_ROME.ctl data=KEN_ALL_ROME_u.CSV log=KEN_ALL_ROME.log bad=KEN_ALL_ROME.bad readsize=100000000 bindsize=100000000
Password:

SQL*Loader: Release 12.2.0.1.0 - Production on Thu Oct 12 20:40:38 2017

Copyright (c) 1982, 2017, Oracle and/or its affiliates.  All rights reserved.

SQL*Loader-350: Syntax error at line 1.
Expecting non-negative integer, found "-2147483648".
options ( errors=2147483648, rows=10000 )
                  ^
[standingzzz@apl work]$

エラーになりました。

再ロード errors=2147483647

それではintegerのmaxにしてみます。

$ sqlldr test01@PDB1 control=KEN_ALL_ROME.ctl data=KEN_ALL_ROME_u.CSV log=KEN_ALL_ROME.log bad=KEN_ALL_ROME.bad readsize=100000000 bindsize=100000000
Password:

SQL*Loader: Release 12.2.0.1.0 - Production on Thu Oct 12 20:42:06 2017

Copyright (c) 1982, 2017, Oracle and/or its affiliates.  All rights reserved.

Path used:      Conventional
Commit point reached - logical record count 10000
Commit point reached - logical record count 20000
Commit point reached - logical record count 30000
Commit point reached - logical record count 40000
Commit point reached - logical record count 50000
Commit point reached - logical record count 60000
Commit point reached - logical record count 70000
Commit point reached - logical record count 80000
Commit point reached - logical record count 90000
Commit point reached - logical record count 100000
Commit point reached - logical record count 110000
Commit point reached - logical record count 120000
Commit point reached - logical record count 124117

Table TAB_KEN_ALL_ROME:
  124117 Rows successfully loaded.

Check the log file:
  KEN_ALL_ROME.log
for more information about the load.
[standingzzz@apl work]$

正常に動きました。まあ、0で良いのでしょうけど。

Oracle12c INDEXがUNUSABLEになるとEXTENT,SEGMENTは削除される

概要

ALTER TABLEなどでINDEXがUNUSABLEとなった場合に、EXTENT、SEGMENTは削除されるという事象の検証(というかメモ)です。 OracleMasterの教科書に載っているような話らしいですね。資格は持っていないので知らないです。

Oracle Database 12c Enterprise Edition Release 12.2.0.1.0 - 64bit Production
CentOS Linux release 7.3.1611 (Core) 

検証

テーブル、インデックスの用意

検証用のテーブルとインデックス、データを用意します。

事前確認

まずは、テーブル、インデックスを作成しただけの状態で、関連ディクショナリを確認します。

ALTER INDEX xxx UNUSABLE

次に、インデックスをUNUSABLEに変更してみます。

SQL> alter index IDX1_TAB_UNUSABLE unusable;

Index altered.

Elapsed: 00:00:00.06
SQL>

すると、user_extents,user_segmentsからは削除されました。

ALTER INDEX xxx REBUILD

UNUSABLEとなった、インデックスをREBUILDします。

SQL> alter index IDX1_TAB_UNUSABLE rebuild;

Index altered.

Elapsed: 00:00:00.08
SQL>

extent,segmentも復活しています。

ALTER TABLE xxx MOVE

テーブル圧縮を実行してみます。

SQL> alter table TAB_UNUSABLE move compress for oltp;

Table altered.

Elapsed: 00:00:00.09
SQL>

テーブルをMOVEすると、インデックスはUNUSABLEになるので、当然と言えば当然なのですが、
やはりextent,segmentからは削除されました。

Centos7にOracle12cのSQL*Loader(12.2.0.1)をインストール

概要

Oracle12cR2のSQL*Loaderをインストールして接続するまでのメモです。

手順

rpmのダウンロード

直リンクだといつしか変わっていたりするので、下記ページで
oracle client download Linux x86-64」を検索して、
http://www.oracle.com/technetwork/topics/index.html

こちらをダウンロード。

oracle-instantclient12.2-tools-12.2.0.1.0-1.x86_64.rpm
インストール
rpm -ivh oracle-instantclient12.2-tools-12.2.0.1.0-1.x86_64.rpm

ORACLE_HOMEのbinにsqlldrが配置されました。

[standingzzz@apl ~]$ which sqlldr 
/usr/lib/oracle/12.2/client64/bin/sqlldr
[standingzzz@apl ~]$ 
[standingzzz@apl ~]$ echo $ORACLE_HOME
/usr/lib/oracle/12.2/client64

とりあえず接続の確認だけしてみます。

[standingzzz@apl ~]$ sqlldr test01@PDB1
Password:

control = 
SQL*Loader-287: No control file name specified.
[standingzzz@apl ~]$ 

python3.6でOracle12c接続の確認(cx_Oracle)

概要

python3.6でOracle12cへ接続してSQLを実行する際のメモです。

Oracle Database 12c Enterprise Edition Release 12.2.0.1.0 - 64bit Production
CentOS Linux release 7.3.1611 (Core) 
Python 3.6.2

確認

接続情報
DBserver:oracle12c.test
port:1521
service_name:orclpdb.test
user:test01
password:xxxx
pythonコード

ora_connect.py

import cx_Oracle
import sys
import os
import configparser


def connect_Oracle( server, port, user, pswd, service ):

    try:

        conn = cx_Oracle.connect( user, pswd, server + ':' + port + '/' + service )
        cur = conn.cursor()

        return ( cur )

    except ( cx_Oracle.DatabaseError ) as ex:
        print ( sys.exc_info()[1] )
        raise ex


def exec_Oracle_SQL( cur, sql ):

    try:

        cur.execute( sql )
        rows = cur.fetchall()

        return ( rows )

    except ( cx_Oracle.DatabaseError ) as ex:
        print ( sys.exc_info()[1] )
        raise ex


if __name__ == '__main__':
    cur = connect_Oracle( 'oracle12c.test', '1521', 'test01', 'xxxx', 'orclpdb.test' )
    rows = exec_Oracle_SQL( cur, 'select username, user_id from user_users' )
    for row in rows:
        print ( row[0] + ', ', end = "" )
        print ( row[1] )
実行結果
[standingzzz@apl common]$ python ora_connect.py 
TEST01, 108
[standingzzz@apl common]$ 

CentOS7にOracle12cのInstantClient(12.2.0.1)をインストール

概要

OracleClientをインストールして、SQLPLUSで接続するまでのメモです。

手順

rpmのダウンロード

直リンクだといつしか変わっていたりするので、下記ページで
oracle client download Linux x86-64」を検索して、
http://www.oracle.com/technetwork/topics/index.html

こちらをダウンロード。

oracle-instantclient12.2-basic-12.2.0.1.0-1.x86_64.rpm
oracle-instantclient12.2-sqlplus-12.2.0.1.0-1.x86_64.rpm
oracle-instantclient12.2-devel-12.2.0.1.0-1.x86_64.rpm
インストール
rpm -ivh oracle-instantclient12.2-basic-12.2.0.1.0-1.x86_64.rpm
rpm -ivh oracle-instantclient12.2-sqlplus-12.2.0.1.0-1.x86_64.rpm
rpm -ivh oracle-instantclient12.2-devel-12.2.0.1.0-1.x86_64.rpm
シンボリックリンクの作成

実行ファイルが「sqlplus64」になっているので、このままでも動くのですが、
「sqlplus」で実行したいのでシンボリックリンクを下記のように作成。

ln -s /usr/bin/sqlplus64 /usr/bin/sqlplus
環境変数の設定

sqlplusを動かすには、下記だけあれば十分。

export TNS_ADMIN=/usr/lib/oracle/12.2/client64/network/admin
export NLS_LANG=American_Japan.JA16EUCTILDE
export LD_LIBRARY_PATH=/usr/lib/oracle/12.2/client64/lib:$LD_LIBRARY_PATH
接続確認
[standingzzz@apl ~]$ sqlplus /nolog

SQL*Plus: Release 12.2.0.1.0 Production on Fri Sep 15 16:15:14 2017

Copyright (c) 1982, 2016, Oracle.  All rights reserved.

SQL> conn test01@PDB1
Enter password: 
Connected.
SQL> 

Oracle12c OPTIMIZER_FEATURESによるNOT EXISTS句の実行計画の違いを確認する その2

概要

OPTIMIZER_FEATURESによるNOT EXISTS句の実行計画の違いを確認します。

その1ではSELECT句をcount(*)のみにしたことで、
12.2.0.1のELIMINATE_JOINにより、tab_bのJOINが省略される実行計画となりましたので、
tab_bのカラムをSELECT句に入れたSQLで確認してみます。

検証のベースとなるSQL

select max(a.cdate)
     , max(b.cdate)
  from tab_a a
  left join tab_b b
    on b.id = a.id
   and b.cdate > sysdate - 1/24
 where not exists ( select 1
                      from tab_b b2
                     where b2.id = a.id
                       and mod( b2.id2, 10 ) = 1
                       and b2.status is null
                  )
Oracle Database 12c Enterprise Edition Release 12.2.0.1.0 - 64bit Production
CentOS Linux release 7.3.1611 (Core) 

検証

実行計画の確認
12.2.0.1
NAME                                 TYPE         VALUE
------------------------------------ ------------ ------------------------------
optimizer_features_enable            string       12.2.0.1
MAX(A.CDATE)        MAX(B.CDATE)
------------------- -------------------
2017/09/08 15:02:54

1 row selected.

Elapsed: 00:00:04.85

Execution Plan
----------------------------------------------------------
Plan hash value: 2392446093

----------------------------------------------------------------------------------------
| Id  | Operation              | Name  | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
----------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT       |       |     1 |    73 |       |  4799   (2)| 00:00:01 |
|   1 |  SORT AGGREGATE        |       |     1 |    73 |       |            |          |
|*  2 |   HASH JOIN RIGHT OUTER|       |  1301K|    90M|       |  4799   (2)| 00:00:01 |
|*  3 |    TABLE ACCESS FULL   | TAB_B |    36 |   792 |       |   908   (3)| 00:00:01 |
|*  4 |    HASH JOIN RIGHT ANTI|       |  1301K|    63M|  4192K|  3887   (1)| 00:00:01 |
|*  5 |     TABLE ACCESS FULL  | TAB_B |   104K|  2964K|       |   895   (1)| 00:00:01 |
|   6 |     TABLE ACCESS FULL  | TAB_A |  1301K|    27M|       |   689   (2)| 00:00:01 |
----------------------------------------------------------------------------------------

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

   2 - access("B"."ID"(+)="A"."ID")
   3 - filter("B"."CDATE"(+)>SYSDATE@!-.0416666666666666666666666666666666666667
              )
   4 - access("B2"."ID"="A"."ID")
   5 - filter("B2"."STATUS" IS NULL AND MOD("B2"."ID2",10)=1)

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

Statistics
----------------------------------------------------------
         11  recursive calls
          0  db block gets
       8971  consistent gets
          0  physical reads
          0  redo size
        430  bytes sent via SQL*Net to client
        500  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
          1  rows processed

当然ELIMINATE_JOINは出現せず、ごく当たり前の実行計画となりました。
ですが、全てのテーブルをFULLSCANしてHASHJOINするという、
ちょっと乱暴な実行計画になっているので、ヒント句でNestedLoopにします。

  1  select /*+ use_nl(a b) */
  2         max(a.cdate)
  3       , max(b.cdate)
  4    from tab_a a
  5    left join tab_b b
  6      on b.id = a.id
  7     and b.cdate > sysdate - 1/24
  8   where not exists ( select /*+ use_nl(b2) */
  9                             1
 10                        from tab_b b2
 11                       where b2.id = a.id
 12                         and mod( b2.id2, 10 ) = 1
 13                         and b2.status is null
 14*                   )
 15  /

MAX(A.CDATE)        MAX(B.CDATE)
------------------- -------------------
2017/09/08 15:02:54

1 row selected.

Elapsed: 00:00:06.12

Execution Plan
----------------------------------------------------------
Plan hash value: 2565967754

---------------------------------------------------------------------------------------------
| Id  | Operation                      | Name       | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT               |            |     1 |    73 |  3654K  (1)| 00:02:23 |
|   1 |  SORT AGGREGATE                |            |     1 |    73 |            |          |
|   2 |   NESTED LOOPS ANTI            |            |  1301K|    90M|  3654K  (1)| 00:02:23 |
|   3 |    NESTED LOOPS OUTER          |            |  1301K|    54M|  2351K  (1)| 00:01:32 |
|   4 |     TABLE ACCESS FULL          | TAB_A      |  1301K|    27M|   689   (2)| 00:00:01 |
|*  5 |     TABLE ACCESS BY INDEX ROWID| TAB_B      |     1 |    22 |     2   (0)| 00:00:01 |
|*  6 |      INDEX UNIQUE SCAN         | IDX1_TAB_B |     1 |       |     1   (0)| 00:00:01 |
|*  7 |    TABLE ACCESS BY INDEX ROWID | TAB_B      |     1 |    29 |     1   (0)| 00:00:01 |
|*  8 |     INDEX UNIQUE SCAN          | IDX1_TAB_B |     1 |       |     1   (0)| 00:00:01 |
---------------------------------------------------------------------------------------------

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

   5 - filter("B"."CDATE"(+)>SYSDATE@!-.0416666666666666666666666666666666666667)
   6 - access("B"."ID"(+)="A"."ID")
   7 - filter("B2"."STATUS" IS NULL AND MOD("B2"."ID2",10)=1)
   8 - access("B2"."ID"="A"."ID")

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


Statistics
----------------------------------------------------------
         11  recursive calls
          0  db block gets
     117577  consistent gets
       2091  physical reads
          0  redo size
        430  bytes sent via SQL*Net to client
        500  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
          1  rows processed

普通にどちらのtab_bもヒント句が効いて、NestedLoopになりました。
まあこれが効かないようでは困ります。

次は、特に理由はありませんが、ヒント句で結合順がNOT EXISTS句が
先にすることが出来るか確認してみます。

  1  select /*+ use_nl(a b) leading(a b2 b) */
  2         max(a.cdate)
  3       , max(b.cdate)
  4    from tab_a a
  5    left join tab_b b
  6      on b.id = a.id
  7     and b.cdate > sysdate - 1/24
  8   where not exists ( select /*+ use_nl(b2) */
  9                             1
 10                        from tab_b b2
 11                       where b2.id = a.id
 12                         and mod( b2.id2, 10 ) = 1
 13                         and b2.status is null
 14*                   )
 15  /

MAX(A.CDATE)        MAX(B.CDATE)
------------------- -------------------
2017/09/08 15:02:54

1 row selected.

Elapsed: 00:00:01.29

Execution Plan
----------------------------------------------------------
Plan hash value: 2649977225

---------------------------------------------------------------------------------------------
| Id  | Operation                      | Name       | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT               |            |     1 |    73 |  4703K  (1)| 00:03:04 |
|   1 |  SORT AGGREGATE                |            |     1 |    73 |            |          |
|   2 |   NESTED LOOPS OUTER           |            |  1301K|    90M|  4703K  (1)| 00:03:04 |
|   3 |    NESTED LOOPS ANTI           |            |  1301K|    63M|  2351K  (1)| 00:01:32 |
|   4 |     TABLE ACCESS FULL          | TAB_A      |  1301K|    27M|   689   (2)| 00:00:01 |
|*  5 |     TABLE ACCESS BY INDEX ROWID| TAB_B      |     1 |    29 |     2   (0)| 00:00:01 |
|*  6 |      INDEX UNIQUE SCAN         | IDX1_TAB_B |     1 |       |     1   (0)| 00:00:01 |
|*  7 |    TABLE ACCESS BY INDEX ROWID | TAB_B      |     1 |    22 |     2   (0)| 00:00:01 |
|*  8 |     INDEX UNIQUE SCAN          | IDX1_TAB_B |     1 |       |     1   (0)| 00:00:01 |
---------------------------------------------------------------------------------------------

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

   5 - filter("B2"."STATUS" IS NULL AND MOD("B2"."ID2",10)=1)
   6 - access("B2"."ID"="A"."ID")
   7 - filter("B"."CDATE"(+)>SYSDATE@!-.0416666666666666666666666666666666666667)
   8 - access("B"."ID"(+)="A"."ID")

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


Statistics
----------------------------------------------------------
        208  recursive calls
          6  db block gets
     116259  consistent gets
          0  physical reads
          0  redo size
        430  bytes sent via SQL*Net to client
        500  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
         15  sorts (memory)
          0  sorts (disk)
          1  rows processed

Predicate Informationを見ると、LEADINGヒント句通りに結合しているのがわかります。

次は、同じSQLで11.2.0.4の実行計画を確認してみます。

alter session set optimizer_features_enable = '11.2.0.4';

NAME                                 TYPE         VALUE
------------------------------------ ------------ ------------------------------
optimizer_features_enable            string       11.2.0.4
  1  select /*+ use_nl(a b) leading(a b2 b) */
  2         max(a.cdate)
  3       , max(b.cdate)
  4    from tab_a a
  5    left join tab_b b
  6      on b.id = a.id
  7     and b.cdate > sysdate - 1/24
  8   where not exists ( select /*+ use_nl(b2) */
  9                             1
 10                        from tab_b b2
 11                       where b2.id = a.id
 12                         and mod( b2.id2, 10 ) = 1
 13                         and b2.status is null
 14*                   )
 15  /

MAX(A.CDATE)        MAX(B.CDATE)
------------------- -------------------
2017/09/08 15:02:54

1 row selected.

Elapsed: 00:00:01.28

Execution Plan
----------------------------------------------------------
Plan hash value: 2649977225

---------------------------------------------------------------------------------------------
| Id  | Operation                      | Name       | Rows  | Bytes | Cost (%CPU)| Time     |
---------------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT               |            |     1 |    73 |  4703K  (1)| 00:03:04 |
|   1 |  SORT AGGREGATE                |            |     1 |    73 |            |          |
|   2 |   NESTED LOOPS OUTER           |            |  1301K|    90M|  4703K  (1)| 00:03:04 |
|   3 |    NESTED LOOPS ANTI           |            |  1301K|    63M|  2351K  (1)| 00:01:32 |
|   4 |     TABLE ACCESS FULL          | TAB_A      |  1301K|    27M|   689   (2)| 00:00:01 |
|*  5 |     TABLE ACCESS BY INDEX ROWID| TAB_B      |     1 |    29 |     2   (0)| 00:00:01 |
|*  6 |      INDEX UNIQUE SCAN         | IDX1_TAB_B |     1 |       |     1   (0)| 00:00:01 |
|*  7 |    TABLE ACCESS BY INDEX ROWID | TAB_B      |     1 |    22 |     2   (0)| 00:00:01 |
|*  8 |     INDEX UNIQUE SCAN          | IDX1_TAB_B |     1 |       |     1   (0)| 00:00:01 |
---------------------------------------------------------------------------------------------

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

   5 - filter("B2"."STATUS" IS NULL AND MOD("B2"."ID2",10)=1)
   6 - access("B2"."ID"="A"."ID")
   7 - filter("B"."CDATE"(+)>SYSDATE@!-.0416666666666666666666666666666666666667)
   8 - access("B"."ID"(+)="A"."ID")

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


Statistics
----------------------------------------------------------
         21  recursive calls
          4  db block gets
     116063  consistent gets
          0  physical reads
          0  redo size
        430  bytes sent via SQL*Net to client
        500  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
          1  rows processed

12.2.0.1と変わらずでした。何かちょっとつまらない確認になりましたが、
変わらないということを確認することも重要ですね。きっと。

Oracle12c OPTIMIZER_FEATURESによるNOT EXISTS句の実行計画の違いを確認する その1

概要

OPTIMIZER_FEATURESによるNOT EXISTS句の実行計画の違いを確認します。

検証のベースとなるSQL

select count(*)
  from tab_a a
  left join tab_b b
    on b.id = a.id
   and b.cdate > sysdate - 1/24
 where not exists ( select 1
                      from tab_b b2
                     where b2.id = a.id
                       and mod( b2.id2, 10 ) = 1
                       and b2.status is null
                  )
Oracle Database 12c Enterprise Edition Release 12.2.0.1.0 - 64bit Production
CentOS Linux release 7.3.1611 (Core) 

検証

拡張実施前の準備色々
実行計画の確認
12.2.0.1
NAME                                 TYPE         VALUE
------------------------------------ ------------ ------------------------------
optimizer_features_enable            string       12.2.0.1
  COUNT(*)
----------
    900000

1 row selected.

Elapsed: 00:00:00.32

Execution Plan
----------------------------------------------------------
Plan hash value: 3207213458

---------------------------------------------------------------------------------------
| Id  | Operation             | Name  | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
---------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT      |       |     1 |    42 |       |  3332   (1)| 00:00:01 |
|   1 |  SORT AGGREGATE       |       |     1 |    42 |       |            |          |
|*  2 |   HASH JOIN RIGHT ANTI|       |  1301K|    52M|  4192K|  3332   (1)| 00:00:01 |
|*  3 |    TABLE ACCESS FULL  | TAB_B |   104K|  2964K|       |   895   (1)| 00:00:01 |
|   4 |    TABLE ACCESS FULL  | TAB_A |  1301K|    16M|       |   688   (1)| 00:00:01 |
---------------------------------------------------------------------------------------

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

   2 - access("B2"."ID"="A"."ID")
   3 - filter("B2"."STATUS" IS NULL AND MOD("B2"."ID2",10)=1)

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


Statistics
----------------------------------------------------------
         48  recursive calls
         17  db block gets
       5966  consistent gets
          0  physical reads
       3288  redo size
        358  bytes sent via SQL*Net to client
        500  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          5  sorts (memory)
          0  sorts (disk)
          1  rows processed

left joinしているtab_bが実行計画上にいません。どのように処理しているのでしょうか。
続いてoptimizer_features_enableを変更して、リリースバージョンによる違いを確認していきます。

11.2.0.4
alter session set optimizer_features_enable = '11.2.0.4';

NAME                                 TYPE         VALUE
------------------------------------ ------------ ------------------------------
optimizer_features_enable            string       11.2.0.4
  COUNT(*)
----------
    900000

1 row selected.

Elapsed: 00:00:01.21

Execution Plan
----------------------------------------------------------
Plan hash value: 4056045473

---------------------------------------------------------------------------------------
| Id  | Operation             | Name  | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
---------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT      |       |     1 |    64 |       |  8831   (1)| 00:00:01 |
|   1 |  SORT AGGREGATE       |       |     1 |    64 |       |            |          |
|*  2 |   HASH JOIN RIGHT ANTI|       |  1301K|    79M|  4192K|  8831   (1)| 00:00:01 |
|*  3 |    TABLE ACCESS FULL  | TAB_B |   104K|  2964K|       |   895   (1)| 00:00:01 |
|*  4 |    HASH JOIN OUTER    |       |  1301K|    43M|    31M|  4834   (1)| 00:00:01 |
|   5 |     TABLE ACCESS FULL | TAB_A |  1301K|    16M|       |   688   (1)| 00:00:01 |
|*  6 |     TABLE ACCESS FULL | TAB_B |  1049K|    22M|       |   908   (3)| 00:00:01 |
---------------------------------------------------------------------------------------

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

   2 - access("B2"."ID"="A"."ID")
   3 - filter("B2"."STATUS" IS NULL AND MOD("B2"."ID2",10)=1)
   4 - access("B"."ID"(+)="A"."ID")
   6 - filter("B"."CDATE"(+)>SYSDATE@!-.041666666666666666666666666666666666666
              7)

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


Statistics
----------------------------------------------------------
         32  recursive calls
         10  db block gets
       9069  consistent gets
          0  physical reads
       1036  redo size
        358  bytes sent via SQL*Net to client
        500  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
          0  sorts (memory)
          0  sorts (disk)
          1  rows processed

11.2.0.4では、まずtab_aとtab_bをOUTER JOINしてから、もう1つのtab_bにANTI結合しています。
うーん、正しいです。非常に正しい。安心しました(?)
もう1つ、12.1.0.2でも確認してみます。

12.1.0.2
alter session set optimizer_features_enable = '12.1.0.2';

NAME                                 TYPE         VALUE
------------------------------------ ------------ ------------------------------
optimizer_features_enable            string       12.1.0.2
  COUNT(*)
----------
    900000

1 row selected.

Elapsed: 00:00:00.84

Execution Plan
----------------------------------------------------------
Plan hash value: 4056045473

---------------------------------------------------------------------------------------
| Id  | Operation             | Name  | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
---------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT      |       |     1 |    64 |       |  8831   (1)| 00:00:01 |
|   1 |  SORT AGGREGATE       |       |     1 |    64 |       |            |          |
|*  2 |   HASH JOIN RIGHT ANTI|       |  1301K|    79M|  4192K|  8831   (1)| 00:00:01 |
|*  3 |    TABLE ACCESS FULL  | TAB_B |   104K|  2964K|       |   895   (1)| 00:00:01 |
|*  4 |    HASH JOIN OUTER    |       |  1301K|    43M|    31M|  4834   (1)| 00:00:01 |
|   5 |     TABLE ACCESS FULL | TAB_A |  1301K|    16M|       |   688   (1)| 00:00:01 |
|*  6 |     TABLE ACCESS FULL | TAB_B |  1049K|    22M|       |   908   (3)| 00:00:01 |
---------------------------------------------------------------------------------------

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

   2 - access("B2"."ID"="A"."ID")
   3 - filter("B2"."STATUS" IS NULL AND MOD("B2"."ID2",10)=1)
   4 - access("B"."ID"(+)="A"."ID")
   6 - filter("B"."CDATE"(+)>SYSDATE@!-.041666666666666666666666666666666666666
              7)

Note
-----
   - dynamic statistics used: dynamic sampling (level=2)
   - this is an adaptive plan


Statistics
----------------------------------------------------------
        433  recursive calls
          8  db block gets
       9697  consistent gets
        527  physical reads
          0  redo size
        358  bytes sent via SQL*Net to client
        500  bytes received via SQL*Net from client
          2  SQL*Net roundtrips to/from client
         16  sorts (memory)
          0  sorts (disk)
          1  rows processed

これは11.2.0.4と同じです。
もう少しだけ、12.2.0.1でどのように2つのtab_bを処理しているのか確認します。

12.2.0.1 advanced
select * from table(dbms_xplan.display(null, null, 'advanced'));
Plan hash value: 3207213458

---------------------------------------------------------------------------------------
| Id  | Operation             | Name  | Rows  | Bytes |TempSpc| Cost (%CPU)| Time     |
---------------------------------------------------------------------------------------
|   0 | SELECT STATEMENT      |       |     1 |    42 |       |  3332   (1)| 00:00:01 |
|   1 |  SORT AGGREGATE       |       |     1 |    42 |       |            |          |
|*  2 |   HASH JOIN RIGHT ANTI|       |  1301K|    52M|  4192K|  3332   (1)| 00:00:01 |
|*  3 |    TABLE ACCESS FULL  | TAB_B |   104K|  2964K|       |   895   (1)| 00:00:01 |
|   4 |    TABLE ACCESS FULL  | TAB_A |  1301K|    16M|       |   688   (1)| 00:00:01 |
---------------------------------------------------------------------------------------

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

   1 - SEL$085E11BF
   3 - SEL$085E11BF / B2@SEL$3
   4 - SEL$085E11BF / A@SEL$1

Outline Data
-------------

  /*+
      BEGIN_OUTLINE_DATA
      SWAP_JOIN_INPUTS(@"SEL$085E11BF" "B2"@"SEL$3")
      USE_HASH(@"SEL$085E11BF" "B2"@"SEL$3")
      LEADING(@"SEL$085E11BF" "A"@"SEL$1" "B2"@"SEL$3")
      FULL(@"SEL$085E11BF" "B2"@"SEL$3")
      FULL(@"SEL$085E11BF" "A"@"SEL$1")
      OUTLINE(@"SEL$1")
      OUTLINE(@"SEL$2")
      ANSI_REARCH(@"SEL$1")
      OUTLINE(@"SEL$8812AA4E")
      ANSI_REARCH(@"SEL$2")
      OUTLINE(@"SEL$948754D7")
      MERGE(@"SEL$8812AA4E" >"SEL$948754D7")
      OUTLINE(@"SEL$2BFA4EE4")
      OUTLINE(@"SEL$3")
      ELIMINATE_JOIN(@"SEL$2BFA4EE4" "B"@"SEL$1")
      OUTLINE(@"SEL$56824FA9")
      UNNEST(@"SEL$3")
      OUTLINE_LEAF(@"SEL$085E11BF")
      ALL_ROWS
      DB_VERSION('12.2.0.1')
      OPTIMIZER_FEATURES_ENABLE('12.2.0.1')
      IGNORE_OPTIM_EMBEDDED_HINTS
      END_OUTLINE_DATA
  */

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

   2 - access("B2"."ID"="A"."ID")
   3 - filter("B2"."STATUS" IS NULL AND MOD("B2"."ID2",10)=1)

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

   1 - (#keys=0) COUNT(*)[22]
   2 - (#keys=1; rowset=1019)
   3 - (rowset=256) "B2"."ID"[NUMBER,22]
   4 - (rowset=1019) "A"."ID"[NUMBER,22]

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

left joinしているtab_bが出てくるのは、下記のELIMINATE_JOINだけです。

ELIMINATE_JOIN(@"SEL$2BFA4EE4" "B"@"SEL$1")

マニュアルにも記載がありませんので推測になりますが、ELIMINATEは省略する、除くの意味で、
tab_aとtab_bはそれぞれTABLE FULL SCANして結合していますから、
もう1回同じテーブルを結合し直さなくても計算出来ますよ、ということなのでしょう。
何だか賢いですね。その2に続きます。