2024年5月29日 (水)

帰ってきた! 標準はあるにはあるが癖の多いSQL #10、BOOLEAN型にも癖が出る(後編)の おまけ - SQL*PlusのautotraceでSQL Analysis Reportが出力される! (23ai〜)

ちょっと意地悪してみました。

 

前回のエントリで、以下のようにindex only scanにできるBOOLEAN型の単列索引を作成したのを覚えていますか?

 

SCOTT@freepdb1> create index ix_bool_example2 on example2(b1);

Index created.

SCOTT@freepdb1> set autot trace exp stat
SCOTT@freepdb1> select count(1) from example2 where b1;

Execution Plan
----------------------------------------------------------
Plan hash value: 1159143718

--------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 1 | 1 (0)| 00:00:01 |
| 1 | SORT AGGREGATE | | 1 | 1 | | |
|* 2 | INDEX RANGE SCAN| IX_BOOL_EXAMPLE2 | 16 | 16 | 1 (0)| 00:00:01 |
--------------------------------------------------------------------------------------

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

2 - access("B1"=TRUE)

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

 

この状態で、述語のBOOLEAN列(索引列)に暗黙型変換が発生する意地悪をしてみました。
BOOLEAN列を数値として計算した後に、falseと比較しているので、TO_NUMBER() の後に、TO_BOOLEAN()されています。当然、索引は使えなくなるので index only scan から table access fullに変わっています! (狙い通りですね。こんなことしないと思いますけどもw

 

で、みなさん、SQL*Plusのautoraceに見慣れない情報が出力されているのに気づきませんか!?

 

そう、Oracle Database 23ai free developerでは、SQL*PlusのautotraceでSQL Analysis reportが表示され、index range scan できるよう、述語の書き換えをご検討くさい! とレコメンドされています!(赤字部分)

 

このメッセージカスタマイズできたりしたらw 「チッ、少しは考えたSQL書けよな〜。」と上から目線のメッセージに変えてみたいw(無理でしょうけどもw) 

ということで、おまけでした!

 

SCOTT@freepdb1> select b1 from example2 where (b1 - 1) = false;

18 rows selected.

Execution Plan
----------------------------------------------------------
Plan hash value: 1894430233

------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 1 | 3 (0)| 00:00:01 |
|* 1 | TABLE ACCESS FULL| EXAMPLE2 | 1 | 1 | 3 (0)| 00:00:01 |
------------------------------------------------------------------------------

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

1 - filter(TO_BOOLEAN(TO_NUMBER("B1")-1)=FALSE)

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

1 - SEL$1 / "EXAMPLE2"@"SEL$1"
- The following columns have predicates which preclude their
use as keys in index range scan. Consider rewriting the
predicates.
"B1"

Statistics
----------------------------------------------------------
9 recursive calls
6 db block gets
11 consistent gets
0 physical reads
1012 redo size
914 bytes sent via SQL*Net to client
133 bytes received via SQL*Net from client
3 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
18 rows processed

 

この例だと的確なレコメンドしてるんだけど、もっとムズイやつだとどうなるんだろうね。誰か本番で使って結果公開して欲しい :)

 

Enjoy SQL!

 

 



関連エントリー
標準はあるにはあるが癖の多いSQL 全部俺 #1 Pagination
標準はあるにはあるが癖の多いSQL 全部俺 #2 関数名は同じでも引数が逆の罠!
標準はあるにはあるが癖の多いSQL 全部俺 #3 データ型確認したい時あるんです
標準はあるにはあるが癖の多いSQL 全部俺 #4 リテラル値での除算の内部精度も違うのよ!
標準はあるにはあるが癖の多いSQL 全部俺 #5 和暦変換機能ある方が少数派
標準はあるにはあるが癖の多いSQL 全部俺 #6 時間厳守!
標準はあるにはあるが癖の多いSQL 全部俺 #7 期間リテラル!
標準はあるにはあるが癖の多いSQL 全部俺 #8 翌月末日って何日?
標準はあるにはあるが癖の多いSQL 全部俺 #9 部分文字列の扱いでも癖が出る><
標準はあるにはあるが癖の多いSQL 全部俺 #10 文字列連結の罠(有名なやつ)
標準はあるにはあるが癖の多いSQL 全部俺 #11 デュエル、じゃなくて、デュアル
標準はあるにはあるが癖の多いSQL 全部俺 #12 文字[列]探すにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #13 あると便利ですが意外となかったり
標準はあるにはあるが癖の多いSQL 全部俺 #14 連番の集合を返すにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #15 SQL command line client
標準はあるにはあるが癖の多いSQL 全部俺 #16 SQLのレントゲンを撮る方法
標準はあるにはあるが癖の多いSQL 全部俺 #17 その空白は許されないのか?
標準はあるにはあるが癖の多いSQL 全部俺 #18 (+)の外部結合は方言
標準はあるにはあるが癖の多いSQL 全部俺 #19 帰ってきた、部分文字列の扱いでも癖w
標準はあるにはあるが癖の多いSQL 全部俺 #20 結果セットを単一列に連結するにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #21 演算結果にも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #22 集合演算にも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #23 複数行INSERTにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #24 乱数作るにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #25 SQL de Fractalsにも癖がある:)
標準はあるにはあるが癖の多いSQL 全部俺 おまけ SQL de 湯婆婆やるにも癖がでるw
帰ってきた! 標準はあるにはあるが癖の多いSQL #1 SQL de ROT13 やるにも癖が出るw
帰ってきた! 標準はあるにはあるが癖の多いSQL #2 Actual Plan取得中のキャンセルでも癖が出る
帰ってきた! 標準はあるにはあるが癖の多いSQL #3 オプティマイザの結合順評価テーブル数上限にも癖が出る
帰ってきた! 標準はあるにはあるが癖の多いSQL #4 Optimizer Traceの取得でも癖がでる
帰ってきた! 標準はあるにはあるが癖の多いSQL #5 - Optimizer Hint でも癖が多い
帰ってきた! 標準はあるにはあるが癖の多いSQL #6 - Hash Joinの結合ツリーにも癖がでる
帰ってきた! 標準はあるにはあるが癖の多いSQL #7 - Hash Joinの実行計画にも癖がでる
帰ってきた! 標準はあるにはあるが癖の多いSQL #8 - Hash Joinさせるにも癖が出る
帰ってきた! 標準はあるにはあるが癖の多いSQL #9、BOOLEAN型にも癖が出る
帰ってきた! 標準はあるにはあるが癖の多いSQL #10、BOOLEAN型にも癖が出る(後編)

 

 

| | | コメント (0)

2024年3月25日 (月)

Oracleのマニュアルで Bushy Join Tree と説明されてる図、Zigzag Join Treeだよね?、いわゆる、Bushy Join Tree は無いのか? いいえ、あります!

久々の技術ネタの投稿です。
昨年末のエントリーに軽くスルーしていた面白いネタが隠れていたのですが、気付いた方はどれぐいたでしょうか? 多分、ぼぼ居ないだろうとは思いますがw

ということで、本日のお題は、軽くスルーしていた面白いネタとして、

Oracleでも、一般的に Bushy Join って言われているJoin Treeを生成することもできるのだ!

。。。というお話をしたいと思います。

 

Oracleのマニュアル( SQL Tuning Guide / Join - Oracle Database 23c )では、一応、Bushy Join Tree という記述をされているのですが、実際には、Zigzag Join Tree なんですよね。結合ツリーの図も実際の実行計画も。

一方、PostgreSQLでは、見間違えようながない Bushy Join Tree が生成されているのがわかります。
帰ってきた! 標準はあるにはあるが癖の多いSQL #6 - Hash Joinの結合ツリーにも癖がでる

Oracleさんのマニュアルのリンク貼っても、リンク切れしちゃうので、いずれリンク切れしちゃう想定でマニュアルの図を手書きでw (といっても、 Pagesで書いたのですが)を貼っておきますね。
なお、本エントリーの後半に参考になりそうなリンクを貼っておきました。(Oracleさんのマニュアルリンクよりはリンク切れし難いと信じてw)

 


20231208195525
20231208195537

 

 

Oracleでは Zigzag も Bushy Join Tree のように扱われてるかのうように見えちゃいますが、実は、Bushy Join Tree に分類される結合ツリーは別に存在していたりします。
最近のOracleだとあまり目にする機会は無いように思いますが。。。特に、11g以降は見た記憶はないです。。。初期のOracleだと比較的目にしていたような気もしますが。(思い出せない! それぐらい昔ではないかと。。。)

では、さっそく、冒頭で紹介した帰ってきた! 標準はあるにはあるが癖の多いSQL #6 - Hash Joinの結合ツリーにも癖がでるの該当部分を再掲して確認してみましょう!

 

再掲

Oracle Database


SCOTT@orclpdb1> @ora_sql_hj.sql
1 SELECT
2 /*+
3 MONITOR
4 USE_HASH(t1 t2 t3 t4)
5 */
6 t1.id
7 , t1.t1_c1
8 , t2.s_id
9 , t2.t2_c1
10 , t3.b_id
11 , t3.t3_c1
12 , t4.a_id
13 , t4.c_id
14 , t4.t4_c1
15 FROM
16 t1
17 INNER JOIN t2
18 ON
19 t1.id = t2.id
20 INNER JOIN t3
21 ON
22 t1.id = t3.id
23 INNER JOIN t4
24 ON
25* t1.id = t4.id

...略...

SQL Plan Monitoring Details (Plan Hash Value=122725940)
===================================================================================================================================================
| Id | Operation | Name | Rows | Cost | Time | Start | Execs | Rows | Read | Read | Mem | Activity | Activity Detail |
| | | | (Estim) | | Active(s) | Active | | (Actual) | Reqs | Bytes | (Max) | (%) | (# samples) |
===================================================================================================================================================
| 0 | SELECT STATEMENT | | | | 1 | +0 | 1 | 25000 | | | . | | |
| 1 | HASH JOIN | | 25000 | 12 | 1 | +0 | 1 | 25000 | | | 1MB | | |
| 2 | TABLE ACCESS FULL | T4 | 500 | 3 | 1 | +0 | 1 | 500 | 2 | 49152 | . | | |
| 3 | HASH JOIN | | 500 | 9 | 1 | +0 | 1 | 500 | | | 1MB | | |
| 4 | HASH JOIN | | 50 | 6 | 1 | +0 | 1 | 50 | | | 1MB | | |
| 5 | TABLE ACCESS FULL | T1 | 10 | 3 | 1 | +0 | 1 | 10 | 2 | 49152 | . | | |
| 6 | TABLE ACCESS FULL | T2 | 50 | 3 | 1 | +0 | 1 | 50 | 2 | 49152 | . | | |
| 7 | TABLE ACCESS FULL | T3 | 100 | 3 | 1 | +0 | 1 | 100 | 2 | 49152 | . | | |
===================================================================================================================================================

 


20231208195525

 

ご覧のとおり、T1からT3までの結合はLeft Deep Join で、T4をRight Deep Joinで結合している、Zigzag Join Treeになっています(Oracleのマニュアルだと、Bushy Join Treeと記載されている結合ツリー)

 

では、PostgreSQLの Bushy Join Tree を再確認してみましょう。

PostgreSQL


perftestdb=> \! cat pg_sql_hj.sql
explain (analyze)
SELECT
/*+
HashJoin(t1 t2 t3 t4)
*/
t1.id
, t1.t1_c1
, t2.s_id
, t2.t2_c1
, t3.b_id
, t3.t3_c1
, t4.a_id
, t4.c_id
, t4.t4_c1
FROM
t1
INNER JOIN t2
ON
t1.id = t2.id
INNER JOIN t3
ON
t1.id = t3.id
INNER JOIN t4
ON
t1.id = t4.id
;
perftestdb=> \i pg_sql_hj.sql
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------
Id=1 Hash Join (cost=17.48..309.84 rows=25000 width=36) (actual time=3.405..10.466 rows=25000 loops=1)
Hash Cond: (t1.id = t2.id)
Id=2 -> Hash Join (cost=1.23..11.09 rows=500 width=24) (actual time=1.696..1.989 rows=500 loops=1)
Hash Cond: (t4.id = t1.id)
Id=3 -> Seq Scan on t4 (cost=0.00..8.00 rows=500 width=16) (actual time=0.907..0.993 rows=500 loops=1)
Id=4 -> Hash (cost=1.10..1.10 rows=10 width=8) (actual time=0.721..0.722 rows=10 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 9kB
Id=5 -> Seq Scan on t1 (cost=0.00..1.10 rows=10 width=8) (actual time=0.705..0.707 rows=10 loops=1)
Id=6 -> Hash (cost=10.00..10.00 rows=500 width=24) (actual time=1.687..1.687 rows=500 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 36kB
Id=7 -> Hash Join (cost=2.12..10.00 rows=500 width=24) (actual time=1.400..1.554 rows=500 loops=1)
Hash Cond: (t3.id = t2.id))
Id=8 -> Seq Scan on t3 (cost=0.00..2.00 rows=100 width=12) (actual time=0.701..0.712 rows=100 loops=1)
Id=9 -> Hash (cost=1.50..1.50 rows=50 width=12) (actual time=0.679..0.680 rows=50 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 11kB
Id=10 -> Seq Scan on t2 (cost=0.00..1.50 rows=50 width=12) (actual time=0.648..0.656 rows=50 loops=1)

 


20231208195537

 

上記はどちらも、HASH JOINをヒントで強制しているだけですが、それでも、オプティマイザの癖というか特徴は現れています。
ZigzagとBushyの違いと一口に言っちゃうと、簡単過ぎますが。

 

以下、SQL文の結合条件部分を抜粋してみました。赤字部分を注意深く見てください。


FROM
t1
INNER JOIN t2
ON
t1.id = t2.id
INNER JOIN t3
ON
t1.id = t3.id
INNER JOIN t4
ON
t1.id = t4.id

 

PostgreSQLのActual Planに見やすいよう、Idを振ってみました。
Id=7でINNER JOINしていますが、何が気づきませんか? 特に、PostgreSQLの Bushy Join Tree で起きている変化に。。。(前述したSQLの赤字部分に注目

私が実行したSQL文の結合条件と異なっている部分があります! (内部でオプティマイザというかプランナが最適化のために書き換えた部分です) 
結合条件を書き換え t1とt3ではなく、t2とt3を結合し、Bushy Join Tree で Hash Join されていますよね!!!!!(同意なのが自明なので書き換えているわけです。 Bushy Join Tree にするために)

一方、OracleもPostgreSQLとおなじタイプの Bushy Join Tree になることもあるのですが、このような書き換えは起こらなかったはず。。。

念の為、Oracleの結合条件が変化していないことをこの時点で確認しておきましょう。
PostgreSQL同様に、赤字部分に注目してください。SQLに記述されている結合条件のまま。


  1  EXPLAIN PLAN FOR
2 SELECT
3 /*+
4 MONITOR
5 USE_HASH(t1 t2 t3 t4)
6 */
7 t1.id
8 , t1.t1_c1
9 , t2.s_id
10 , t2.t2_c1
11 , t3.b_id
12 , t3.t3_c1
13 , t4.a_id
14 , t4.c_id
15 , t4.t4_c1
16 FROM
17 t1
18 INNER JOIN t2
19 ON
20 t1.id = t2.id
21 INNER JOIN t3
22 ON
23 t1.id = t3.id
24 INNER JOIN t4
25 ON
26* t1.id = t4.id

解析されました。

経過: 00:00:00.02

PLAN_TABLE_OUTPUT
-----------------------------------------------------------------------------
Plan hash value: 122725940

-----------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 25000 | 878K| 12 (0)| 00:00:01 |
|* 1 | HASH JOIN | | 25000 | 878K| 12 (0)| 00:00:01 |
| 2 | TABLE ACCESS FULL | T4 | 500 | 6000 | 3 (0)| 00:00:01 |
|* 3 | HASH JOIN | | 500 | 12000 | 9 (0)| 00:00:01 |
|* 4 | HASH JOIN | | 50 | 750 | 6 (0)| 00:00:01 |
| 5 | TABLE ACCESS FULL| T1 | 10 | 60 | 3 (0)| 00:00:01 |
| 6 | TABLE ACCESS FULL| T2 | 50 | 450 | 3 (0)| 00:00:01 |
| 7 | TABLE ACCESS FULL | T3 | 100 | 900 | 3 (0)| 00:00:01 |
-----------------------------------------------------------------------------

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

1 - access("T1"."ID"="T4"."ID")
3 - access("T1"."ID"="T3"."ID")
4 - access("T1"."ID"="T2"."ID")

では、Oracleには、PostgreSQLで見られる Bushy Join Tree は存在しないのか? 

冒頭でも書きましたが、存在します!

BUSHY_JOINヒントまでありますw

 

ほぼ、お目にかかることは無くなった気がしますが。。。最近は。。

PostgreSQLとおなじ実行計画になるように、ゴニョゴニョしてみましたw (何が起こるでしょう。。。w)

なお、LEADINGヒントで、オプティマイザが内部的に生成する Bushy Join 向けのインラインビュー( VW_BUSHY_0C91E486 )を指定していますが、このインラインビュー名は事前に確認することはできないので、一度、インラインビューを生成させインラインビュー名を確認した後に指定しています。
(内部的に生成されるインラインビュー名称なので、手順としてはそれしかありません!)

BUSHY_JOINヒント、なんとなく、pg_hint_plan で使うような構文に似てますよねw Oracleで ZigZag Join Tree (Oracleのマニュアルだと Zigzag な Tree だけど、Bushy Join となっているので注意)になっている状態を BUSHY_JOINヒントで あえて、Bushy Join に書き換えることは無いと思いますが、使い方を理解していると、何かの役になる、、、かも。(なるとは言ってないw)


SCOTT@orclpdb1> @ora_sql_hj_bushy_join
1 SELECT
2 /*+
3 MONITOR
4 LEADING(VW_BUSHY_0C91E486)
5 USE_HASH(t2 t3)
6 USE_HASH(t1 t4)
7 BUSHY_JOIN((t2 t3) (t1 t4))
8 */
9 t1.id
10 , t1.t1_c1
11 , t2.s_id
12 , t2.t2_c1
13 , t3.b_id
14 , t3.t3_c1
15 , t4.a_id
16 , t4.c_id
17 , t4.t4_c1
18 FROM
19 t1
20 INNER JOIN t2
21 ON
22 t1.id = t2.id
23 INNER JOIN t3
24 ON
25 t1.id = t3.id
26 INNER JOIN t4
27 ON
28* t1.id = t4.id

DBMS_SQLTUNE.REPORT_SQL_MONITOR(SQL_ID=>'',TYPE=>'TEXT')
-------------------------------------------------------------------------------------
SQL Monitoring Report

...略...

SQL Plan Monitoring Details (Plan Hash Value=3887185)
===================================================================================================================================================
| Id | Operation | Name | Rows | Cost | Time | Start | Execs | Rows | Mem | Activity | Activity Detail |
| | | | (Estim) | | Active(s) | Active | | (Actual) | (Max) | (%) | (# samples) |
===================================================================================================================================================
| 0 | SELECT STATEMENT | | | | 3 | +0 | 1 | 25000 | . | | |
| 1 | HASH JOIN | | 250K | 80 | 3 | +0 | 1 | 25000 | 2MB | | |
| 2 | VIEW | VW_BUSHY_0C91E486 | 5000 | 72 | 1 | +0 | 1 | 5000 | . | | |
| 3 | MERGE JOIN CARTESIAN | | 5000 | 72 | 1 | +0 | 1 | 5000 | . | | |
| 4 | TABLE ACCESS FULL | T2 | 50 | 3 | 1 | +0 | 1 | 50 | . | | |
| 5 | BUFFER SORT | | 100 | 69 | 1 | +0 | 50 | 5000 | 6144 | | |
| 6 | TABLE ACCESS FULL | T3 | 100 | 1 | 1 | +0 | 1 | 100 | . | | |
| 7 | VIEW | VW_BUSHY_CF941F82 | 500 | 6 | 3 | +0 | 1 | 500 | . | | |
| 8 | HASH JOIN | | 500 | 6 | 3 | +0 | 1 | 500 | 1MB | | |
| 9 | TABLE ACCESS FULL | T1 | 10 | 3 | 1 | +0 | 1 | 10 | . | | |
| 10 | TABLE ACCESS FULL | T4 | 500 | 3 | 3 | +0 | 1 | 500 | . | | |
===================================================================================================================================================

 

PostgreSQLのように結合条件を書き換えてくれるまでは行わないようですね。Oracleのオプティマイザでは。
MERGE JOIN CARTESIAN となって、直積が発生してしまいました。PostgreSQLのように結合条件を書き換えていない結果として、結合条件の無い結合の強制となってしまった結果、直積が発生したわけです。

であれば、オリジナルの結合条件を書き換えて、それっぽくなるようにしてみましょう!
PostgreSQLのプランナのように結合条件を書き換えて再実行!!。

おお!
思い通りの実行計画になりました!

 

このような箇所でもオプティマイザの特徴は現れるので注意したいですね。Oracleでこの形にしたくなることは無いとは思いますけども。

 

ちなみに、Oracleのオプティマイザが内部的に、インラインビューを作る場合、どうやらネーミングルールがあるようで(マニュアルには記載されていないですけどもw)
VW_BUSHY_xxxxxxx となっている場合は、Bushy Join を行うためのインラインビュー名であるということは知られています
(知らない方もおおいかもしれませんが。Internal Views / Oracle Scratchpad by Jonathan Lewisあたりでそこそこまとめられていますが、Bushy Joinのためのはリストされてないですが、知らなくても困らないとは思います。OracleのBusy Join自体が現状はかなりレアな存在なので)


  1  SELECT
2 /*+
3 MONITOR
4 LEADING(VW_BUSHY_0C91E486)
5 USE_HASH(t2 t3)
6 USE_HASH(t1 t4)
7 BUSHY_JOIN((t2 t3) (t1 t4))
8 */
9 t1.id
10 , t1.t1_c1
11 , t2.s_id
12 , t2.t2_c1
13 , t3.b_id
14 , t3.t3_c1
15 , t4.a_id
16 , t4.c_id
17 , t4.t4_c1
18 FROM
19 t1
20 INNER JOIN t2
21 ON
22 t1.id = t2.id
23 INNER JOIN t3
24 ON
25 t2.id = t3.id
26 INNER JOIN t4
27 ON
28* t1.id = t4.id

DBMS_SQLTUNE.REPORT_SQL_MONITOR(SQL_ID=>'',TYPE=>'TEXT')
----------------------------------------------------------------------------------------------------
SQL Monitoring Report

SQL Text

...略...


SQL Plan Monitoring Details (Plan Hash Value=400336061)
=================================================================================================================================================
| Id | Operation | Name | Rows | Cost | Time | Start | Execs | Rows | Mem | Activity | Activity Detail |
| | | | (Estim) | | Active(s) | Active | | (Actual) | (Max) | (%) | (# samples) |
=================================================================================================================================================
| 0 | SELECT STATEMENT | | | | 3 | +0 | 1 | 25000 | . | | |
| 1 | HASH JOIN | | 25000 | 12 | 3 | +0 | 1 | 25000 | 1MB | | |
| 2 | VIEW | VW_BUSHY_0C91E486 | 500 | 6 | 1 | +0 | 1 | 500 | . | | |
| 3 | HASH JOIN | | 500 | 6 | 1 | +0 | 1 | 500 | 1MB | | |
| 4 | TABLE ACCESS FULL | T2 | 50 | 3 | 1 | +0 | 1 | 50 | . | | |
| 5 | TABLE ACCESS FULL | T3 | 100 | 3 | 1 | +0 | 1 | 100 | . | | |
| 6 | VIEW | VW_BUSHY_CF941F82 | 500 | 6 | 3 | +0 | 1 | 500 | . | | |
| 7 | HASH JOIN | | 500 | 6 | 3 | +0 | 1 | 500 | 1MB | | |
| 8 | TABLE ACCESS FULL | T1 | 10 | 3 | 1 | +0 | 1 | 10 | . | | |
| 9 | TABLE ACCESS FULL | T4 | 500 | 3 | 3 | +0 | 1 | 500 | . | | |
=================================================================================================================================================

 

Oracleでも、一般的に言われている Bushy Join Tree へ持っていくことはできるよ。あえて使わないけど、というかこれに持っていきたい時って想像できないわけですけどもね。Zigzag Join Treeで対応しちゃうだろうから。。。

ということで、Oracleでかなり久々に見た(無理やりですけどもw)、Bushy Join Tree もあるんだよ! の巻。完。。

 

今回使ったスクリプト

Oracle(PostgreSQLと同じ実行計画になるよう結合条件を書き換え,内部生成されるインラインビュー名称を確認した上で、LEADINGで結合順を聖書したもの)


SCOTT@orclpdb1> ! cat ora_sql_hj_bushy_join.sql

SELECT
/*+
MONITOR
LEADING(VW_BUSHY_0C91E486)
USE_HASH(t2 t3)
USE_HASH(t1 t4)
BUSHY_JOIN((t2 t3) (t1 t4))
*/
t1.id
, t1.t1_c1
, t2.s_id
, t2.t2_c1
, t3.b_id
, t3.t3_c1
, t4.a_id
, t4.c_id
, t4.t4_c1
FROM
t1
INNER JOIN t2
ON
t1.id = t2.id
INNER JOIN t3
ON
t2.id = t3.id
INNER JOIN t4
ON
t1.id = t4.id
.
l
set tab off
set termout off
/
set termout on

@show_realtime_sql

 

 

PostgreSQL


perftestdb=> \! cat pg_sql_hj.sql
explain (analyze)
SELECT
/*+
HashJoin(t1 t2 t3 t4)
*/
t1.id
, t1.t1_c1
, t2.s_id
, t2.t2_c1
, t3.b_id
, t3.t3_c1
, t4.a_id
, t4.c_id
, t4.t4_c1
FROM
t1
INNER JOIN t2
ON
t1.id = t2.id
INNER JOIN t3
ON
t1.id = t3.id
INNER JOIN t4
ON
t1.id = t4.id
;

 

なお、テーブル、索引、登録したデータなどは、過去のエントリーを参照ください。
帰ってきた! 標準はあるにはあるが癖の多いSQL #6 - Hash Joinの結合ツリーにも癖がでる

 


参考)
202311232038251

 

Trees

https://resources.mpi-inf.mpg.de/departments/d5/teaching/ss09/queryoptimization/lecture4.pdf

 

Classification of Join Ordering Problems

https://resources.mpi-inf.mpg.de/departments/d5/teaching/ss09/queryoptimization/lecture5.pdf

 

Specialized Course "Query Optimization"

https://resources.mpi-inf.mpg.de/departments/d5/teaching/ss09/queryoptimization/

 

 

では、また、面白そうなネタを見つけたら書こうと思います。:)

 

| | | コメント (0)

2024年2月21日 (水)

Top N Queryの弱点と対策

久々にSQLチューニングネタです。しかも、真面目な感じのタイトルw

11年前に、rownum使って満足しちゃってると.....おまけのおまけ FETCH FIRST N ROWS ONLY編 という、rownumや、fetch first N rows only でTOP N rows を取得する際に、忘れがちな問題として、TOP N rowsの行数未満(空振りを含む)の場合、全行読み込んでしまうので、絞り込みしにくいような検索で、rownumやfetch first N rowsで全表走査を回避したつもりになっていると、痛い目にあうよ!

というネタを書いていました。

この大切な癖、忘れちゃってませんか? 大変なことになりますよ。。ということで、再びこのネタを書くことにしました。

Oracle Database 21cを使っていますが11年も昔から変わったところはないので、塩漬けしている古〜いオラクルでも楽しめる内容にしてありますw

 

まず、今日の準備から

SCOTT@orclpdb1> @fullscanfulness

表が削除されました。

経過: 00:00:01.31
1 create table fullscanfulness
2 (
3 id number not null
4 ,dummy_text varchar2(4000)
5 ,hoge_flg number(1) not null
6 ,constraint pk_fullscanfulness primary key(id)
7* )

表が作成されました。

経過: 00:00:00.29
1 begin
2 for i in 1..100000 loop
3 insert into fullscanfulness values(i,lpad('*',3500,'*'),0);
4 if mod(i,100)=0 then commit; end if;
5 end loop;
6* end;

PL/SQLプロシージャが正常に完了しました。

経過: 00:00:22.45
1* insert into fullscanfulness values(100001,lpad('*',3500,'*'),1)

1行が作成されました。

経過: 00:00:00.01

コミットが完了しました。

経過: 00:00:00.01

PL/SQLプロシージャが正常に完了しました。

経過: 00:00:03.53
1* select count(1) from fullscanfulness

COUNT(1)
----------
100001

経過: 00:00:00.01

SEGMENT_NAME BLOCKS
------------------------------ ----------
FULLSCANFULNESS 51200
PK_FULLSCANFULNESS 256

経過: 00:00:00.19

 

と、クセのありそうな、なんとなく、見る機会の多そうなフラグ列のある表に主キーだけがある状態です。
なお、最後のデータ id=100001だけフラグ列が 1 になっています。最後の行なので、物理的にも最後尾にしてあります(意図的に)

 

フラグ=1になっている行が表どの位置にあるかイメージ図で書くと以下のような感じ

20240221-200436

 

表セグメントのブロック数は上記のとおりですが、まずは、実際にtable full scan させてみましょう physical reads/ consistent gets からもわかるように綺麗にw 読み込まれてます!

SCOTT@orclpdb1> set autot trace exp stat
SCOTT@orclpdb1> @fullscanfulness2
1 SELECT
2 id
3 ,substr(dummy_text,1,10) as dummy
4 FROM
5* fullscanfulness

100001行が選択されました。

経過: 00:00:03.34

実行計画
----------------------------------------------------------
Plan hash value: 1963277787

-------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 100K| 334M| 13807 (1)| 00:00:01 |
| 1 | TABLE ACCESS FULL| FULLSCANFULNESS | 100K| 334M| 13807 (1)| 00:00:01 |
-------------------------------------------------------------------------------------


統計
----------------------------------------------------------
137 recursive calls
0 db block gets
53724 consistent gets
50133 physical reads
0 redo size
2322822 bytes sent via SQL*Net to client
73599 bytes received via SQL*Net from client
6668 SQL*Net roundtrips to/from client
27 sorts (memory)
0 sorts (disk)
100001 rows processed

 

では、早速、本題ですw

rownumでも良いのですが、fetch first N rows onlyで WHERE句なし、つまり、全表走査上等な状態ですが、10行取得だけ。つまり、table full scanではありますが、10行取得したところまでで終了させます。
WINDOW NOSORT STOPKEYというオペレーションが増加しています。rownumだと、STOPKEYだけですよね。ここがrownumとは違うところですが、動作としては行数をカウントして、制限値に達したところで走査終了とするためのオペレーションが追加されています。
これで、table full scanであっても、全データブロックを読み取ることはないですよね。みなさんご存知の通りです。 consistent gets = 8 なので、 8 ブロックしか読みこんでません。

ここまではいいですよね。

  1  SELECT
2 id
3 ,substr(dummy_text,1,10) as dummy
4 FROM
5 fullscanfulness
6* FETCH FIRST 10 ROWS ONLY

10行が選択されました。

経過: 00:00:00.00

実行計画
----------------------------------------------------------
Plan hash value: 1910161288

------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10 | 480 | 3 (0)| 00:00:01 |
|* 1 | VIEW | | 10 | 480 | 3 (0)| 00:00:01 |
|* 2 | WINDOW NOSORT STOPKEY| | 10 | 35060 | 3 (0)| 00:00:01 |
| 3 | TABLE ACCESS FULL | FULLSCANFULNESS | 10 | 35060 | 3 (0)| 00:00:01 |
------------------------------------------------------------------------------------------

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

1 - filter("from$_subquery$_002"."rowlimit_$$_rownumber"<=10)
2 - filter(ROW_NUMBER() OVER ( ORDER BY NULL )<=10)


統計
----------------------------------------------------------
1 recursive calls
0 db block gets
8 consistent gets
0 physical reads
0 redo size
800 bytes sent via SQL*Net to client
52 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
10 rows processed

 

では、次の例

WHERE句で条件を指定しておきます。なかなか渋い検索条件ですよね。一般的に索引は作成しにくいですw。 
TOP N クエリーなので一つ前の例のように table full scanは回避できるは。。。。ですよね。

想定通り、必要な行数取得後に、table full scanは止まって必要最小限の表データブロックだけアクセスしています。 いいじゃないですか。。これで。

  1  SELECT
2 id
3 ,substr(dummy_text,1,10) as dummy
4 FROM
5 fullscanfulness
6 WHERE
7 hoge_flg = 0
8* FETCH FIRST 10 ROWS ONLY

10行が選択されました。

経過: 00:00:00.01

実行計画
----------------------------------------------------------
Plan hash value: 1910161288

------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10 | 480 | 5 (0)| 00:00:01 |
|* 1 | VIEW | | 10 | 480 | 5 (0)| 00:00:01 |
|* 2 | WINDOW NOSORT STOPKEY| | 10 | 35080 | 5 (0)| 00:00:01 |
|* 3 | TABLE ACCESS FULL | FULLSCANFULNESS | 10 | 35080 | 5 (0)| 00:00:01 |
------------------------------------------------------------------------------------------

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

1 - filter("from$_subquery$_002"."rowlimit_$$_rownumber"<=10)
2 - filter(ROW_NUMBER() OVER ( ORDER BY NULL )<=10)
3 - filter("HOGE_FLG"=0)


統計
----------------------------------------------------------
1 recursive calls
0 db block gets
8 consistent gets
0 physical reads
0 redo size
800 bytes sent via SQL*Net to client
52 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
10 rows processed

 

 

では、Top N クエリーをやめて、WHERE条件だけ指定します。 hoge_flg = 1 である、表データブロックの最後のブロック格納されている行を1行取得してみましょう。

hoge_flg列には索引はないので、table full scanしかできません。当然、全データブロックを読み込み(物理読み込み)して1行取得しています。これも想定通りです。Top N クエリーで制限していないので最後まで読み込んでしまいますよね。

  1  SELECT
2 id
3 ,substr(dummy_text,1,10) as dummy
4 FROM
5 fullscanfulness
6 WHERE
7* hoge_flg = 1

経過: 00:00:00.19

実行計画
----------------------------------------------------------
Plan hash value: 1963277787

-------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 50001 | 167M| 13807 (1)| 00:00:01 |
|* 1 | TABLE ACCESS FULL| FULLSCANFULNESS | 50001 | 167M| 13807 (1)| 00:00:01 |
-------------------------------------------------------------------------------------

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

1 - filter("HOGE_FLG"=1)


統計
----------------------------------------------------------
1 recursive calls
0 db block gets
50144 consistent gets
50133 physical reads
0 redo size
673 bytes sent via SQL*Net to client
52 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed

 

さて、次が問題です。

一つ前の例と同じWHERE条件で、FETCH FIRST 10 ROWS ONLYでTop N クエリーしてみましょう。どうなったかわかりますか?

実行計画上は、これまでと同じく、 table full scanで、STOPKEYによる途中停止オペレーションも含まれていますが、、、、全データブロックを読み込んでしまいました。

 

なぜでしょう?

 

理由は、Top N クエリーの条件に指定された行数に満たない行数しか存在しなかったから、ですね!。 
これが rownum / fetch first N rows onlyの弱点なのです。条件に満たないことが確定するのは、table full scanで全データブロックを読み込み終わるまで確定しません。
最後まで読み切ってしまうんです。途中で止まらないのです。。。

大問題ですね!これw(ワロてますが)

僕たちの Top N クエリー、ダメじゃん。みたいな。(そんなことはないですが、考慮が漏れているだけなのでw)

  1  SELECT
2 id
3 ,substr(dummy_text,1,10) as dummy
4 FROM
5 fullscanfulness
6 WHERE
7 hoge_flg = 1
8* FETCH FIRST 10 ROWS ONLY

経過: 00:00:00.34

実行計画
----------------------------------------------------------
Plan hash value: 1910161288

------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10 | 480 | 5 (0)| 00:00:01 |
|* 1 | VIEW | | 10 | 480 | 5 (0)| 00:00:01 |
|* 2 | WINDOW NOSORT STOPKEY| | 10 | 35080 | 5 (0)| 00:00:01 |
|* 3 | TABLE ACCESS FULL | FULLSCANFULNESS | 10 | 35080 | 5 (0)| 00:00:01 |
------------------------------------------------------------------------------------------

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

1 - filter("from$_subquery$_002"."rowlimit_$$_rownumber"<=10)
2 - filter(ROW_NUMBER() OVER ( ORDER BY NULL )<=10)
3 - filter("HOGE_FLG"=1)


統計
----------------------------------------------------------
1 recursive calls
0 db block gets
50153 consistent gets
50082 physical reads
0 redo size
673 bytes sent via SQL*Net to client
52 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed

 

でも、もう一つ、残念なお知らせ。というか例を。

 

FETCH FIRST N ROWSの条件に満たなかったから、全データブロックを読んでしまったわけだから、条件を満たせば、絶対、俺たちの Top N クエリーは、table full scanの途中で止まってくれるはずだ!

そんなことはないですねw

以下の例では、FETCH FISRT N ROWSの条件を満たせてはいますが、ヒットした1行のデータは、運の悪いことに、全表データブロック中、最も最後のブロックに存在しています。
したがって、索引のないこの表では、最後のデータブロックを読み込むまで、FETCH FIRST N ROWSの条件は満たせないため、全データブロックを読み込むしかなくなっています。残念!
(対象行を最後のデータブロックになるような小細工をしていたのはこれを見せたかったわけです)

  1  SELECT
2 id
3 ,substr(dummy_text,1,10) as dummy
4 FROM
5 fullscanfulness
6 WHERE
7 hoge_flg = 1
8* FETCH FIRST 1 ROWS ONLY

経過: 00:00:00.33

実行計画
----------------------------------------------------------
Plan hash value: 1910161288

------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 48 | 2 (0)| 00:00:01 |
|* 1 | VIEW | | 1 | 48 | 2 (0)| 00:00:01 |
|* 2 | WINDOW NOSORT STOPKEY| | 1 | 3508 | 2 (0)| 00:00:01 |
|* 3 | TABLE ACCESS FULL | FULLSCANFULNESS | 1 | 3508 | 2 (0)| 00:00:01 |
------------------------------------------------------------------------------------------

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

1 - filter("from$_subquery$_002"."rowlimit_$$_rownumber"<=1)
2 - filter(ROW_NUMBER() OVER ( ORDER BY NULL )<=1)
3 - filter("HOGE_FLG"=1)


統計
----------------------------------------------------------
1 recursive calls
0 db block gets
50124 consistent gets
50053 physical reads
0 redo size
673 bytes sent via SQL*Net to client
52 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed

 

さらに、Top N クエリーの弱点を再確認してみましょう。

 

今度は、空振りする検索条件です。対象データは0件です。そうでう。ここまでついて来れた皆さんなら、もうお気づきだと思いますが。空振りする検索条件だと、0件であることが確定するのはどういう状態になった時でしょうか?

そうです。 変なマウントおじさんが登場してきた時(違w

ではなくて、全データブロックを読み終えた時ですね。 table full scan は Top N クエリーだけでは止まれないケースが存在するんですよ。みなさん!

  1  SELECT
2 id
3 ,substr(dummy_text,1,10) as dummy
4 FROM
5 fullscanfulness
6 WHERE
7 hoge_flg = 9
8* FETCH FIRST 1 ROWS ONLY

レコードが選択されませんでした。

経過: 00:00:00.20

実行計画
----------------------------------------------------------
Plan hash value: 1910161288

------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 48 | 13807 (1)| 00:00:01 |
|* 1 | VIEW | | 1 | 48 | 13807 (1)| 00:00:01 |
|* 2 | WINDOW NOSORT STOPKEY| | 1 | 3508 | 13807 (1)| 00:00:01 |
|* 3 | TABLE ACCESS FULL | FULLSCANFULNESS | 1 | 3508 | 13807 (1)| 00:00:01 |
------------------------------------------------------------------------------------------

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

1 - filter("from$_subquery$_002"."rowlimit_$$_rownumber"<=1)
2 - filter(ROW_NUMBER() OVER ( ORDER BY NULL )<=1)
3 - filter("HOGE_FLG"=9)


統計
----------------------------------------------------------
1 recursive calls
0 db block gets
50145 consistent gets
50133 physical reads
0 redo size
453 bytes sent via SQL*Net to client
41 bytes received via SQL*Net from client
1 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
0 rows processed

 

では、追加の確認、Top N クエリーの条件が早期に満たされれば、table full scanは回避できますよね。という念の為の確認です。
以下では、表の先頭ブロックに Top N クエリーの条件を満足させるためのデータを用意しました。


20240221-200459

 

この状態であれば、table full scanは止められるはずです。(常にこんな状態になることは稀なわけですけども)

結果は見ての通り、table full scanは途中で止まりました。当然と言えば当然ですが。

  1* update fullscanfulness set hoge_flg = 1 where id between 1 and 9

9行が更新されました。

経過: 00:00:00.02

コミットが完了しました。

経過: 00:00:00.00
1* select id from fullscanfulness where hoge_flg=1

ID
----------
3
4
5
6
7
8
9
1
2
100001

10行が選択されました。

経過: 00:00:00.21
1 SELECT
2 id
3 ,substr(dummy_text,1,10) as dummy
4 FROM
5 fullscanfulness
6 WHERE
7 hoge_flg = 1
8* FETCH FIRST 1 ROWS ONLY

経過: 00:00:00.01

実行計画
----------------------------------------------------------
Plan hash value: 1910161288

------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 48 | 2 (0)| 00:00:01 |
|* 1 | VIEW | | 1 | 48 | 2 (0)| 00:00:01 |
|* 2 | WINDOW NOSORT STOPKEY| | 1 | 3508 | 2 (0)| 00:00:01 |
|* 3 | TABLE ACCESS FULL | FULLSCANFULNESS | 1 | 3508 | 2 (0)| 00:00:01 |
------------------------------------------------------------------------------------------

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

1 - filter("from$_subquery$_002"."rowlimit_$$_rownumber"<=1)
2 - filter(ROW_NUMBER() OVER ( ORDER BY NULL )<=1)
3 - filter("HOGE_FLG"=1)


統計
----------------------------------------------------------
1 recursive calls
0 db block gets
3 consistent gets
0 physical reads
0 redo size
671 bytes sent via SQL*Net to client
52 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed

 

では、事前準備で、表の先頭ブロックには9行だけ WHERE条件に該当するデータを置きました。ただし、 Top N クエリーの条件は満たせません。
Top N クエリーの条件を満たすための最後のピースは、表データの最後尾のブロックに置いてあります。

20240221-200422

 

結果は、またまた、 Top N クエリーの効果で、table full scanが途中で止まることはできず、全データブロックを読み込んでしまいました。辛いですね(この状況)

  1  SELECT
2 id
3 ,substr(dummy_text,1,10) as dummy
4 FROM
5 fullscanfulness
6 WHERE
7 hoge_flg = 1
8* FETCH FIRST 10 ROWS ONLY

10行が選択されました。

経過: 00:00:00.34

実行計画
----------------------------------------------------------
Plan hash value: 1910161288

------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10 | 480 | 5 (0)| 00:00:01 |
|* 1 | VIEW | | 10 | 480 | 5 (0)| 00:00:01 |
|* 2 | WINDOW NOSORT STOPKEY| | 10 | 35080 | 5 (0)| 00:00:01 |
|* 3 | TABLE ACCESS FULL | FULLSCANFULNESS | 10 | 35080 | 5 (0)| 00:00:01 |
------------------------------------------------------------------------------------------

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

1 - filter("from$_subquery$_002"."rowlimit_$$_rownumber"<=10)
2 - filter(ROW_NUMBER() OVER ( ORDER BY NULL )<=10)
3 - filter("HOGE_FLG"=1)


統計
----------------------------------------------------------
1 recursive calls
0 db block gets
50125 consistent gets
50053 physical reads
0 redo size
802 bytes sent via SQL*Net to client
52 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
10 rows processed

 

次の例では、表データの最後尾のデータブロックに置いたデータのフラグを0にして、WHERE条件を満たす行を 9行にしました。
これだと、表データの先頭にある数ブロックだけでは、 Top N クエリーの条件を満たせません。

 

結果は、見るまでもなく(見てますがw)、表の全データブロックを読み込むまで終了できません。

  1* update fullscanfulness set hoge_flg = 0 where id = 100001

1行が更新されました。

経過: 00:00:00.00

コミットが完了しました。

経過: 00:00:00.01
1 SELECT
2 id
3 ,substr(dummy_text,1,10) as dummy
4 FROM
5 fullscanfulness
6 WHERE
7 hoge_flg = 1
8* FETCH FIRST 10 ROWS ONLY

9行が選択されました。

経過: 00:00:00.34

実行計画
----------------------------------------------------------
Plan hash value: 1910161288

------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10 | 480 | 5 (0)| 00:00:01 |
|* 1 | VIEW | | 10 | 480 | 5 (0)| 00:00:01 |
|* 2 | WINDOW NOSORT STOPKEY| | 10 | 35080 | 5 (0)| 00:00:01 |
|* 3 | TABLE ACCESS FULL | FULLSCANFULNESS | 10 | 35080 | 5 (0)| 00:00:01 |
------------------------------------------------------------------------------------------

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

1 - filter("from$_subquery$_002"."rowlimit_$$_rownumber"<=10)
2 - filter(ROW_NUMBER() OVER ( ORDER BY NULL )<=10)
3 - filter("HOGE_FLG"=1)


統計
----------------------------------------------------------
0 recursive calls
0 db block gets
50154 consistent gets
50082 physical reads
0 redo size
792 bytes sent via SQL*Net to client
52 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
9 rows processed

 

では、いよいよ。Top N クエリーの弱点に対する対策には何が効くのかみていきましょう。あれしかないですけどもw

 

一般的な状況だと、ほぼ、作成したくない索引を作ってみます。この状況では仕方ないですw(意図的ですがw)

  1* create index ix_fullscanfulness on fullscanfulness(hoge_flg)

索引が作成されました。

経過: 00:00:00.44

 

念の為、索引を強制するヒントを追加しています。

 

条件は一つ前の例と同じです。違いはフラグ列に作成した単一列索引だけです。

 

結果は、ご覧の通り。 狙い通り、 Top N クエリーの条件は満たせていませんが、WHERE条件に一致する行だけを索引経由で取得することで、 table full scanを完全に回避しています。 これが Top N クエリーの弱点回避への最後の希望です :)
状況次第ですが、かなり、何この索引と突っ込まれる系の索引を作成しなければいけなくなることも多いです。ですが、これしかないのです。

  1  SELECT
2 /*+
3 INDEX(fullscanfulness ix_fullscanfulness)
4 */
5 id
6 ,substr(dummy_text,1,10) as dummy
7 FROM
8 fullscanfulness
9 WHERE
10 hoge_flg = 1
11* FETCH FIRST 10 ROWS ONLY

9行が選択されました。

経過: 00:00:00.01

実行計画
----------------------------------------------------------
Plan hash value: 3248696394

----------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
----------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10 | 480 | 7 (0)| 00:00:01 |
|* 1 | VIEW | | 10 | 480 | 7 (0)| 00:00:01 |
|* 2 | WINDOW NOSORT STOPKEY | | 10 | 35080 | 7 (0)| 00:00:01 |
| 3 | TABLE ACCESS BY INDEX ROWID| FULLSCANFULNESS | 10 | 35080 | 7 (0)| 00:00:01 |
|* 4 | INDEX RANGE SCAN | IX_FULLSCANFULNESS | 50001 | | 1 (0)| 00:00:01 |
----------------------------------------------------------------------------------------------------

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

1 - filter("from$_subquery$_002"."rowlimit_$$_rownumber"<=10)
2 - filter(ROW_NUMBER() OVER ( ORDER BY NULL )<=10)
4 - access("HOGE_FLG"=1)


統計
----------------------------------------------------------
1 recursive calls
0 db block gets
9 consistent gets
1 physical reads
0 redo size
792 bytes sent via SQL*Net to client
52 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
9 rows processed

 

さらに、 Top N クエリーの弱点回避の効果を確認してみましょう。

次は、Top N クエリーの条件は満たせていますが、WHERE条件を満たせる行が、表データブロックの先頭と最後尾に存在しているケースです。

表の先頭ブロックに9行の該当データがあるので、最後尾のデータブロックにある行を更新して置きます。索引が存在しない状態では、table full scanを途中で止めることはできませんでしたが、どうなりましか?

 

止まっていますよね!

  1* update fullscanfulness set hoge_flg = 1 where id = 100001

1行が更新されました。

経過: 00:00:00.00

コミットが完了しました。

経過: 00:00:00.01
1 SELECT
2 /*+
3 INDEX(fullscanfulness ix_fullscanfulness)
4 */
5 id
6 ,substr(dummy_text,1,10) as dummy
7 FROM
8 fullscanfulness
9 WHERE
10 hoge_flg = 1
11* FETCH FIRST 10 ROWS ONLY

10行が選択されました。

経過: 00:00:00.00

実行計画
----------------------------------------------------------
Plan hash value: 3248696394

----------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
----------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10 | 480 | 7 (0)| 00:00:01 |
|* 1 | VIEW | | 10 | 480 | 7 (0)| 00:00:01 |
|* 2 | WINDOW NOSORT STOPKEY | | 10 | 35080 | 7 (0)| 00:00:01 |
| 3 | TABLE ACCESS BY INDEX ROWID| FULLSCANFULNESS | 10 | 35080 | 7 (0)| 00:00:01 |
|* 4 | INDEX RANGE SCAN | IX_FULLSCANFULNESS | 50001 | | 1 (0)| 00:00:01 |
----------------------------------------------------------------------------------------------------

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

1 - filter("from$_subquery$_002"."rowlimit_$$_rownumber"<=10)
2 - filter(ROW_NUMBER() OVER ( ORDER BY NULL )<=10)
4 - access("HOGE_FLG"=1)


統計
----------------------------------------------------------
0 recursive calls
0 db block gets
10 consistent gets
0 physical reads
0 redo size
802 bytes sent via SQL*Net to client
52 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
10 rows processed

 

WHERE条件を変更しました。表データブロックの先頭ブロックに該当行全てが含まれるようにしてみました。もともと、table full scanが途中で止まるケースですが、索引を利用させても遜色ない結果を得られます。唯一の違いは索引の存在ですね。DMLなどには性能的にマイナスの影響はありますが索引の数次第のところもあるので、どちらが大切かをよく検討、検証して決める必要はあります

   1* update fullscanfulness set hoge_flg = 0 where id between 1 and 9

9行が更新されました。

経過: 00:00:00.00

コミットが完了しました。

経過: 00:00:00.00
1 SELECT
2 /*+
3 INDEX(fullscanfulness ix_fullscanfulness)
4 */
5 id
6 ,substr(dummy_text,1,10) as dummy
7 FROM
8 fullscanfulness
9 WHERE
10 hoge_flg = 0
11* FETCH FIRST 10 ROWS ONLY

10行が選択されました。

経過: 00:00:00.00

実行計画
----------------------------------------------------------
Plan hash value: 3248696394

----------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
----------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10 | 480 | 7 (0)| 00:00:01 |
|* 1 | VIEW | | 10 | 480 | 7 (0)| 00:00:01 |
|* 2 | WINDOW NOSORT STOPKEY | | 10 | 35080 | 7 (0)| 00:00:01 |
| 3 | TABLE ACCESS BY INDEX ROWID| FULLSCANFULNESS | 10 | 35080 | 7 (0)| 00:00:01 |
|* 4 | INDEX RANGE SCAN | IX_FULLSCANFULNESS | 50001 | | 1 (0)| 00:00:01 |
----------------------------------------------------------------------------------------------------

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

1 - filter("from$_subquery$_002"."rowlimit_$$_rownumber"<=10)
2 - filter(ROW_NUMBER() OVER ( ORDER BY NULL )<=10)
4 - access("HOGE_FLG"=0)


統計
----------------------------------------------------------
1 recursive calls
0 db block gets
9 consistent gets
0 physical reads
0 redo size
800 bytes sent via SQL*Net to client
52 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
10 rows processed

 

もう一つ、意地悪な確認をしてみましょう。

表データの中間付近に、WHERE条件を満たす行を配置して、索引を利用できないよう NO_INDEXヒントに変更します。


20240221-200451

 

結果は、想定通り、 Top N クエリーは、表データの1/2ほどまで table full scanを行って停止することがわかります。

  1* update fullscanfulness set hoge_flg = 0 where id between 1 and 9 or id = 100001

10行が更新されました。

経過: 00:00:00.00
1* update fullscanfulness set hoge_flg = 1 where id between 50000 and 50010

11行が更新されました。

経過: 00:00:00.01

コミットが完了しました。

経過: 00:00:00.00
1 SELECT
2 /*+
3 NO_INDEX(fullscanfulness ix_fullscanfulness)
4 */
5 id
6 ,substr(dummy_text,1,10) as dummy
7 FROM
8 fullscanfulness
9 WHERE
10 hoge_flg = 1
11* FETCH FIRST 10 ROWS ONLY

10行が選択されました。

経過: 00:00:00.24

実行計画
----------------------------------------------------------
Plan hash value: 1910161288

------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10 | 480 | 5 (0)| 00:00:01 |
|* 1 | VIEW | | 10 | 480 | 5 (0)| 00:00:01 |
|* 2 | WINDOW NOSORT STOPKEY| | 10 | 35080 | 5 (0)| 00:00:01 |
|* 3 | TABLE ACCESS FULL | FULLSCANFULNESS | 10 | 35080 | 5 (0)| 00:00:01 |
------------------------------------------------------------------------------------------

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

1 - filter("from$_subquery$_002"."rowlimit_$$_rownumber"<=10)
2 - filter(ROW_NUMBER() OVER ( ORDER BY NULL )<=10)
3 - filter("HOGE_FLG"=1)


統計
----------------------------------------------------------
1 recursive calls
0 db block gets
24739 consistent gets
24672 physical reads
0 redo size
818 bytes sent via SQL*Net to client
52 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
10 rows processed

 

同じ状態で、ヒントを元に戻し、索引を利用できるようにします。

当然ですが、必要最小限のデータブロックを索引使って取得するようになり、 table full scan は回避できます。

  1  SELECT
2 /*+
3 INDEX(fullscanfulness ix_fullscanfulness)
4 */
5 id
6 ,substr(dummy_text,1,10) as dummy
7 FROM
8 fullscanfulness
9 WHERE
10 hoge_flg = 1
11* FETCH FIRST 10 ROWS ONLY

10行が選択されました。

経過: 00:00:00.01

実行計画
----------------------------------------------------------
Plan hash value: 3248696394

----------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
----------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10 | 480 | 7 (0)| 00:00:01 |
|* 1 | VIEW | | 10 | 480 | 7 (0)| 00:00:01 |
|* 2 | WINDOW NOSORT STOPKEY | | 10 | 35080 | 7 (0)| 00:00:01 |
| 3 | TABLE ACCESS BY INDEX ROWID| FULLSCANFULNESS | 10 | 35080 | 7 (0)| 00:00:01 |
|* 4 | INDEX RANGE SCAN | IX_FULLSCANFULNESS | 50001 | | 1 (0)| 00:00:01 |
----------------------------------------------------------------------------------------------------

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

1 - filter("from$_subquery$_002"."rowlimit_$$_rownumber"<=10)
2 - filter(ROW_NUMBER() OVER ( ORDER BY NULL )<=10)
4 - access("HOGE_FLG"=1)


統計
----------------------------------------------------------
0 recursive calls
0 db block gets
9 consistent gets
0 physical reads
0 redo size
818 bytes sent via SQL*Net to client
52 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
10 rows processed

 

次の例は、 空振りのケースでも索引アクセスなら瞬時に空振りが判定できますよね!! という確認です。

WHERE条件に対応した索引を用意するだけで、索引を見たたけで対象データが存在しないことが判定できます。空振りして、table full scanして、全データブロックを総ナメなんて無駄ですよね。

  1  SELECT
2 /*+
3 INDEX(fullscanfulness ix_fullscanfulness)
4 */
5 id
6 ,substr(dummy_text,1,10) as dummy
7 FROM
8 fullscanfulness
9 WHERE
10 hoge_flg = 9
11* FETCH FIRST 10 ROWS ONLY

レコードが選択されませんでした。

経過: 00:00:00.01

実行計画
----------------------------------------------------------
Plan hash value: 3248696394

----------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
----------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10 | 480 | 2 (0)| 00:00:01 |
|* 1 | VIEW | | 10 | 480 | 2 (0)| 00:00:01 |
|* 2 | WINDOW NOSORT STOPKEY | | 1 | 3508 | 2 (0)| 00:00:01 |
| 3 | TABLE ACCESS BY INDEX ROWID| FULLSCANFULNESS | 1 | 3508 | 2 (0)| 00:00:01 |
|* 4 | INDEX RANGE SCAN | IX_FULLSCANFULNESS | 1 | | 1 (0)| 00:00:01 |
----------------------------------------------------------------------------------------------------

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

1 - filter("from$_subquery$_002"."rowlimit_$$_rownumber"<=10)
2 - filter(ROW_NUMBER() OVER ( ORDER BY NULL )<=10)
4 - access("HOGE_FLG"=9)


統計
----------------------------------------------------------
1 recursive calls
0 db block gets
2 consistent gets
0 physical reads
0 redo size
453 bytes sent via SQL*Net to client
41 bytes received via SQL*Net from client
1 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
0 rows processed

 

そんな索引でも索引に含まれていない列でソートされたりすると困ることもあります。という例です。

次の例は、索引でtable full scanを回避できていたTop N クエリーが仕様変更され、索引に含まれないid列のソートが追加されてしまいました。
その結果。。。索引エントリーが読み込まれ、かつ、ソートまでされる結果となってしまいました。ありゃありゃw

  1  SELECT
2 /*+
3 INDEX(fullscanfulness ix_fullscanfulness)
4 */
5 id
6 ,substr(dummy_text,1,10) as dummy
7 FROM
8 fullscanfulness
9 WHERE
10 hoge_flg = 0
11 ORDER BY
12 id DESC
13* FETCH FIRST 10 ROWS ONLY

10行が選択されました。

経過: 00:00:00.56

実行計画
----------------------------------------------------------
Plan hash value: 3127027845

--------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time |
--------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10 | 610 | | 61653 (1)| 00:00:03 |
|* 1 | VIEW | | 10 | 610 | | 61653 (1)| 00:00:03 |
|* 2 | WINDOW SORT PUSHED RANK | | 50001 | 167M| 195M| 61653 (1)| 00:00:03 |
| 3 | TABLE ACCESS BY INDEX ROWID BATCHED| FULLSCANFULNESS | 50001 | 167M| | 25103 (1)| 00:00:01 |
|* 4 | INDEX RANGE SCAN | IX_FULLSCANFULNESS | 50001 | | | 92 (2)| 00:00:01 |
--------------------------------------------------------------------------------------------------------------------

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

1 - filter("from$_subquery$_002"."rowlimit_$$_rownumber"<=10)
2 - filter(ROW_NUMBER() OVER ( ORDER BY INTERNAL_FUNCTION("ID") DESC )<=10)
4 - access("HOGE_FLG"=0)


統計
----------------------------------------------------------
49 recursive calls
0 db block gets
50651 consistent gets
50123 physical reads
0 redo size
818 bytes sent via SQL*Net to client
52 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
9 sorts (memory)
0 sorts (disk)
10 rows processed

 

前述の例は、索引の効果がなくなってしまったことが原因なので、仕様変更に合わせ、索引も変更しなければなりません!

仕様変更に対応した索引を作成します。新規作成していますが、作り直しでも良いですね。列が増加しただけで他に影響がない索引であれば。

  1* create index ix2_fullscanfulness on fullscanfulness(hoge_flg, id desc)

索引が作成されました。

経過: 00:00:00.51
1 SELECT
2 /*+
3 INDEX(fullscanfulness ix2_fullscanfulness)
4 */
5 id
6 ,substr(dummy_text,1,10) as dummy
7 FROM
8 fullscanfulness
9 WHERE
10 hoge_flg = 0
11 ORDER BY
12 id DESC
13* FETCH FIRST 10 ROWS ONLY

10行が選択されました。

経過: 00:00:00.01

実行計画
----------------------------------------------------------
Plan hash value: 3270412619

------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10 | 610 | 9 (12)| 00:00:01 |
| 1 | SORT ORDER BY | | 10 | 610 | 9 (12)| 00:00:01 |
|* 2 | VIEW | | 10 | 610 | 8 (0)| 00:00:01 |
|* 3 | WINDOW NOSORT STOPKEY | | 10 | 35080 | 8 (0)| 00:00:01 |
| 4 | TABLE ACCESS BY INDEX ROWID| FULLSCANFULNESS | 50001 | 167M| 8 (0)| 00:00:01 |
|* 5 | INDEX RANGE SCAN | IX2_FULLSCANFULNESS | 10 | | 2 (0)| 00:00:01 |
------------------------------------------------------------------------------------------------------

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

2 - filter("from$_subquery$_002"."rowlimit_$$_rownumber"<=10)
3 - filter(ROW_NUMBER() OVER ( ORDER BY SYS_OP_DESCEND("ID"))<=10)
5 - access("HOGE_FLG"=0)


統計
----------------------------------------------------------
1 recursive calls
0 db block gets
8 consistent gets
1 physical reads
0 redo size
818 bytes sent via SQL*Net to client
52 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
1 sorts (memory)
0 sorts (disk)
10 rows processed

 

フルスキャンそんなにしたいのでしょうか? 無駄なエネルギーを消費させて。。謎は深まる、 

 

フルスキャンフルネスな感じなのだろうか。。

 

では、また。

 


rownum使って満足しちゃってると.....おまけ
rownum使って満足しちゃってると.....おまけのおまけ FETCH FIRST N ROWS ONLY編

 

 

| | | コメント (0)

2023年12月22日 (金)

PostgreSQLのexplain analyze中にCTRL+Cでキャンセルすると、途中まで実行されているActual Planの結果は出力されないわけだが、その挙動を、ほんの少し追ってみた件


本エントリーは、PostgreSQL Advent Calendar 2023 シリーズ2の Day 22向けエントリーです


 

今回は、PostgreSQLオンリーで、投げっぱなしだったやつをちょっと掘り下げた時のメモ的なエントリーです。。

 

以前、

MySQL 8.0.32 / explain analyze 実行途中でキャンセルできるみたいだけど、キャンセルしたら、Actual Plan、途中まで出るの?
帰ってきた! 標準はあるにはあるが癖の多いSQL #2 Actual Plan取得中のキャンセルでも癖が出る

というエントリーを書いたのですが、

explain (analyze) 実行中、CTRL+C した時、どのようにハンドングしてるんだろう?
Hookで拾って、extensionで、explain (analyze) でも途中までは結果を書き出せるようにできちゃったりするのだろうか? と。 

気になりすぎてしまったのでw、該当ソースコードを追い、象が生息する森の奥地に入ってみることにした。。。。

https://github.com/postgres/postgres

 

ただ、あてもないのに森林の奥地入り込むのは、迷子になる危険を感じたのでw、ちょっとだけ、生態を調べてみたw

すると、幸運なことに、先人が足を踏み入れた痕跡と思われるメモ見つけることができた。

PostgreSQLのシグナル周り
https://qiita.com/KazuyaTomita/items/6feb708d73190032dfed

 

ふむふむ、なんとなくわかった気持ちになる。。(理解したとは言ってない)

 

PostgreSQLのActual Plan取得のログ。
見ての通り、PostgreSQLは、explain (analyze)実行中にCTRL+Cでキャンセルすると、シンプルなキャンセルメッセージを返すだけ。
(MySQL/Oracleでは、キャンセルされるまでのActual Planを返してくれる。冒頭のリンク参照)

 

この表示されているメッセージは手がかりになりそうだなぁ。。。。


perftestdb=> explain (analyze, buffers, verbose) 
WITH RECURSIVE gen_nums(v)
AS
(
SELECT 1
UNION ALL
SELECT v + 1
FROM
gen_nums
WHERE v + 1 <= 100000000
)
SELECT v from gen_nums;
^Cキャンセル要求を送信しました
ERROR: canceling statement due to user request
perftestdb=>

 

 

シグナル周りのハンドリング部分のコードを追って、このメッセージを出力しているところを特定すると良さげな気がしますよね。

ただ、explain を実行している箇所とは違いそうではある。。。ざっくり目だが。

postgres.cのPostgresMain()に以下の文がありますな〜

 

postgres.c

pqsignal(SIGINT, StatementCancelHandler);   /* cancel current query */

 

そして、至る所で、 CHECK_FOR_INTERRUPTS ()マクロが呼ばれてます〜〜。

CHECK_FOR_INTERRUPTS()マクロで、呼ばれているのが、 ProcessInterrupts() ですね。

 

miscadmin.h

 /* Service interrupt, if one is pending and it's safe to service it now */
#define CHECK_FOR_INTERRUPTS() \
do { \
if (INTERRUPTS_PENDING_CONDITION()) \
ProcessInterrupts(); \
} while(0)


ProcessInterrupts()をみると、以下に見覚えのあるエラーメッセージ "canceling statement due to user request"を出力している箇所を発見。

 

postgres.c

/*
* If we are reading a command from the client, just ignore the cancel
* request --- sending an extra error message won't accomplish
* anything. Otherwise, go ahead and throw the error.
*/
if (!DoingCommandRead)
{
LockErrorCleanup();
ereport(ERROR,
(errcode(ERRCODE_QUERY_CANCELED),
errmsg("canceling statement due to user request")));
}

 

explain (analyze)に関わりそうな以下のコードでは、CHECK_FOR_INTERRUPTSはないのだけど、executer配下のコードには沢山ある。。。

explain.c

execMain.c

 

... extensionでなんとか、、というのは雰囲気的に難しそうな。。。

 

explain (actual plan取得含む)実行中に CTRL+C キャンセルすると、

postgres.c の PostgresMain() -> CHECK_FOR_INTERRUPTS() -> ProcessInterrupts() -> ereport() -> errmsg("canceling statement due to user request") を出力して実行キャンセル
と言う流れっぽい。斜め読みした限りでは。

 

ぱっと見だが、

 

errmsg("canceling statement due to user request")の前に、

1. commandが実行されているか
2. 1.であれば、commandは、explain かつ analyzeか?
3. 2.であれば、explain analyzeのキャンセルされる前に取得されたActual Planの結果を出力

みたいなことを、ProcessInterrupts()内でやらないと難しそうだよなぁ〜と。extensionでやれそうな感じにも見えないし。。。
一旦、そっとGitHubを開いていたブラウザのタブを閉じたww

 

と言うことで、冬の夜長に、PostgreSQLのコードをちょっと追ってみた話はここまで。

 

PostgreSQL Advent Calendar 2023 ですがw 恒例?の、Oracle Pipelined Table Function で Christmas ASCII Art(クロスポストに続き2回目)

 



関連エントリー
MySQL 8.0.32 / explain analyze 実行途中でキャンセルできるみたいだけど、キャンセルしたら、Actual Plan、途中まで出るの?
帰ってきた! 標準はあるにはあるが癖の多いSQL #2 Actual Plan取得中のキャンセルでも癖が出る

| | | コメント (0)

2023年12月17日 (日)

帰ってきた! 標準はあるにはあるが癖の多いSQL #8、Hash Joinさせるにも癖が出る



本エントリーは、
MySQL Advent Calendar 2023 シリーズ1
PostgreSQL Advent Calendar 2023 シリーズ1
JPOUG Advent Calendar 2023

の Day 17 向けエントリーです。

また、本エントリー向け予習エントリーを投稿していますので、一読していただくと私が何と戦っていたのかw 理解しやすいのではないかと思います

予習 その1は、以下。
帰ってきた! 標準はあるにはあるが癖の多いSQL #5 - Optimizer Hint でも癖が多い

予習 その2は、以下。
帰ってきた! 標準はあるにはあるが癖の多いSQL #6 - Hash Joinの結合ツリーにも癖がでる

予習 その3は、以下。
帰ってきた! 標準はあるにはあるが癖の多いSQL #7 - Hash Joinの実行計画の見せ方にも癖がでる


ということで、これまでの予習で学んだことを利用して、もしも、こんなデータ(予習 その2 を参照のこと)で、以下のようなSQLがあった場合、Hash Joinの特性上いい感じの実行計画にさせるにはどうHintingするのが良いのか考えてみたいと思います。
(答えが見えている方は多いとは思いますけどもw)

以下のSQL文を使い、後述のような実行計画に制御することを目指してみます。

SELECT
t1.id
, t1.t1_c1
, t2.s_id
, t2.t2_c1
, t3.b_id
, t3.t3_c1
, t4.a_id
, t4.c_id
, t4.t4_c1
, t0.m_id
, t0.t0_c1
FROM
t1
INNER JOIN t2
ON
t1.id = t2.id
INNER JOIN t3
ON
t1.id = t3.id
INNER JOIN t4
ON
t1.id = t4.id
INNER JOIN t0
ON
t1.id = t0.id
;


いざというときに慌てずに、どうするかを判断、チューニングできるようになっておくと良いですよねー。
そう言う、いざ、という場面には付き合わされない人も、雑学的になんとなーーーく理解しているだけでも良いのではないかと思います。


Oracleの実行計画風ですが、こんな実行計画にオプティマイザーヒントだけで制御してみたいなぁ。というのがお題。右側にコメントしているBuild/Probeの関係を覚えておいてください。この形にするのが目的です。
(予習に目を通した方は、あ”〜っ。と何かに気づいちゃったと思いますけどもw)

ポイントになるのは、t0の結合です。 t1,t2,t3,t4と結合した結果が、t0の行数より多くなります。(そうなるようにデータを用意したので)Hash Joinを想定しているので、最後にt0を結合する場合には、t0をBuildにしたいですよね。。。
(Hash Joinの結合順で理想なのは、常にBuildの方が小さくなる結合順なので、以下の形が理想的だとのがわかりやすいようにJoin cardinarityも記載してあります)

HJ (join card = 2,500,000)
-> t0 (rows = 1,000) -- Build
-> HJ (join card = 250,000) -- Probe
-> HJ (join card = 500) -- Build
-> HJ (join card = 50) -- Build
-> t1 (rows = 10) -- Build
-> t2 (rows = 50) -- Probe
-> t3 (rows = 100) -- Probe
-> t4 (rows = 500) -- Probe

なお、これまでの予習で、MySQLは、Hash Join はできるけど、Build/Probeの制御ができない。 Left Deep Join Treeにしかならないのもどうしようもない。 
オプティマイザヒントでは無理だろうと想像はできるわけですがw 折角なので、できるところまで試して、挙動をみておきましょう:)

再掲
テーブル定義とデータ量は以下、予習 その2 参照のこと。
帰ってきた! 標準はあるにはあるが癖の多いSQL #6 - Hash Joinの結合ツリーにも癖がでる




まず、Oracleからですかねぇ。やはり。豊富なオプティマイザヒントと最適化が行えることが強みの一つだと思います。

Oracle Database 21c

ちょっと暴れてたのでw NO_SWAP_JOIN_INPUTSを使ってますが、実行計画としては狙ったところに持っていけますよね。Oraclerの方なら、ふむふむ。というところだと思います。
t1, t2, t3, t4, t0の順で結合しますが、最後の、t0の結合時は、Buildを t0 にしてね! というHinging。実践でも結構使う場面はありますよね。
(実行計画の右側に確認しやすくするため、どちらが (Build) / (Probe) なのかをコメントしてあります)

SCOTT@ORCLCDB> select banner_full from v$version;

BANNER_FULL
-----------------------------------------------------------------------
Oracle Database 21c Enterprise Edition Release 21.0.0.0.0 - Production
Version 21.3.0.0.0

SCOTT@orclpdb1> @advent_sql_ora
1 SELECT
2 /*+
3 MONITOR
4 LEADING(t1 t2 t3 t4 t0)
5 USE_HASH(t1 t2 t3 t4 t0)
6 NO_SWAP_JOIN_INPUTS(t4)
7 SWAP_JOIN_INPUTS(t0)
8 */
9 t1.id
10 , t1.t1_c1
11 , t2.s_id
12 , t2.t2_c1
13 , t3.b_id
14 , t3.t3_c1
15 , t4.a_id
16 , t4.c_id
17 , t4.t4_c1
18 , t0.m_id
19 , t0.t0_c1
20 FROM
21 t1
22 INNER JOIN t2
23 ON
24 t1.id = t2.id
25 INNER JOIN t3
26 ON
27 t1.id = t3.id
28 INNER JOIN t4
29 ON
30 t1.id = t4.id
31 INNER JOIN t0
32 ON
33* t1.id = t0.id

...略...

SQL Plan Monitoring Details (Plan Hash Value=3728371915)
====================================================================================================================================================
| Id | Operation | Name | Rows | Cost | Time | Start | Execs | Rows | Read | Read | Mem | Activity | Activity Detail |
| | | | (Estim) | | Active(s) | Active | | (Actual) | Reqs | Bytes | (Max) | (%) | (# samples) |
====================================================================================================================================================
| 0 | SELECT STATEMENT | | | | 106 | +0 | 1 | 3M | | | . | | |
| 1 | HASH JOIN | | 3M | 29 | 106 | +0 | 1 | 3M | | | 1MB | | |
| 2 | TABLE ACCESS FULL | T0 | 1000 | 3 | 1 | +0 | 1 | 1000 | 2 | 49152 | . | | | (Build)
| 3 | HASH JOIN | | 25000 | 12 | 106 | +0 | 1 | 25000 | | | 1MB | | | (Probe)
| 4 | HASH JOIN | | 500 | 9 | 1 | +0 | 1 | 500 | | | 1MB | | | (Build)
| 5 | HASH JOIN | | 50 | 6 | 1 | +0 | 1 | 50 | | | 1MB | | | (Build)
| 6 | TABLE ACCESS FULL | T1 | 10 | 3 | 1 | +0 | 1 | 10 | 2 | 49152 | . | | | (Build)
| 7 | TABLE ACCESS FULL | T2 | 50 | 3 | 1 | +0 | 1 | 50 | 2 | 49152 | . | | | (Probe)
| 8 | TABLE ACCESS FULL | T3 | 100 | 3 | 1 | +0 | 1 | 100 | 2 | 49152 | . | | | (Probe)
| 9 | TABLE ACCESS FULL | T4 | 500 | 3 | 106 | +0 | 1 | 500 | 3 | 81920 | . | | | (Probe)
====================================================================================================================================================



簡単に解説すると、t1からt4まではLeft Deep Join Tree、t0は、術式反転w で Right Deep Join Treeになるように、SWAP_JOIN_INPUTSしてます。

Id=5で、Id=6のt1(Build)と、Id=7のt2(Probe)をHash Join
Id=4で、Id=5の結果(Build)と、Id=8のt3(Probe)をHash Join
Id=3で、Id=4の結果(Build)と、Id=9のt4(Probe)をHash Join
Id=2で、SWAP_JOIN_INPUTSヒントで入れ替えた、Id=2のt0(Build)と、Id=3の結果(Probe)をHash Join



狙った実行計画に制御できました。ニッコリ。

参考)使ったSQLスクリプトは以下の通り。
SCOTT@ORCLCDB> !cat show_realtime_sqlplan.sql
set linesize 1000
set long 1000000
set longchunksize 1000000
select dbms_sqltune.report_sql_monitor(sql_id=>'', type=>'text') from dual;

SCOTT@ORCLCDB> !cat advent_sql_ora.sql
SELECT
/*+
MONITOR
LEADING(t1 t2 t3 t4 t0)
USE_HASH(t1 t2 t3 t4 t0)
NO_SWAP_JOIN_INPUTS(t4)
SWAP_JOIN_INPUTS(t0)
*/
t1.id
, t1.t1_c1
, t2.s_id
, t2.t2_c1
, t3.b_id
, t3.t3_c1
, t4.a_id
, t4.c_id
, t4.t4_c1
, t0.m_id
, t0.t0_c1
FROM
t1
INNER JOIN t2
ON
t1.id = t2.id
INNER JOIN t3
ON
t1.id = t3.id
INNER JOIN t4
ON
t1.id = t4.id
INNER JOIN t0
ON
t1.id = t0.id
.
l
set tab off
set termout off
/
set termout on

@show_realtime_sqlplan


PostgreSQL 13.4 with pg_hint_plan 1.3.9
なんで、最新のPostgreSQLじゃないのか? 単にアップデートサボってるだなので、気にししないでw

perftestdb=# select version();
version
--------------------------------------------------------------------------------------------------------
PostgreSQL 13.4 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 8.4.1 20200928 (Red Hat 8.4.1-1), 64-bit

perftestdb=# select * from pg_extension where extname='pg_hint_plan';
oid | extname | extowner | extnamespace | extrelocatable | extversion | extconfig | extcondition
--------+--------------+----------+--------------+----------------+------------+-----------------+--------------
131364 | pg_hint_plan | 10 | 131363 | f | 1.3.9 | {131367,131365} | {"",""}


perftestdb=> show shared_preload_libraries;
shared_preload_libraries
-----------------------------------------------
pg_hint_plan


pg_hint_planのLeadingヒントの使い方慣れるまで結構迷子になってましたw。空気感でなんとなーく使い方を理解できたかなぁ。という感じ。
pg_hint_planのLeading(pair)の書き方と、順序の指定順って、Oraclerの感覚的には逆なんですよね。Hash Joinの時だけは。。。というところ気づきました? みなさん。(主に、Oracle方面の)

perftestdb=> \! cat advent_sql_pg.sql
explain (analyze)
SELECT
/*+
Leading(((t4 (t3 (t2 t1))) t0))
HashJoin(t0 t1 t2 t3 t4)
SeqScan(t0)
SeqScan(t1)
SeqScan(t2)
SeqScan(t3)
SeqScan(t4)
*/
t1.id
, t1.t1_c1
, t2.s_id
, t2.t2_c1
, t3.b_id
, t3.t3_c1
, t4.a_id
, t4.c_id
, t4.t4_c1
, t0.m_id
, t0.t0_c1
FROM
t1
INNER JOIN t2
ON
t1.id = t2.id
INNER JOIN t3
ON
t1.id = t3.id
INNER JOIN t4
ON
t1.id = t4.id
INNER JOIN t0
ON
t1.id = t0.id
;

perftestdb=> \i advent_sql_pg.sql
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------
Id=1 Hash Join (cost=46.66..28524.66 rows=2500000 width=44) (actual time=0.875..574.957 rows=2500000 loops=1)
Hash Cond: (t1.id = t0.id)
Id=2 (Probe) -> Hash Join (cost=18.16..308.66 rows=25000 width=48) (actual time=0.429..11.123 rows=25000 loops=1)
Hash Cond: (t4.id = t1.id)
Id=3 (Probe) -> Seq Scan on t4 (cost=0.00..8.00 rows=500 width=16) (actual time=0.006..0.776 rows=500 loops=1)
Id=4 (Build) -> Hash (cost=11.91..11.91 rows=500 width=32) (actual time=0.418..0.423 rows=500 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 40kB
Id=5 -> Hash Join (cost=4.04..11.91 rows=500 width=32) (actual time=0.072..0.270 rows=500 loops=1)
Hash Cond: (t3.id = t1.id)
Id=6 (Probe) -> Seq Scan on t3 (cost=0.00..2.00 rows=100 width=12) (actual time=0.004..0.016 rows=100 loops=1)
Id=7 (Build) -> Hash (cost=3.41..3.41 rows=50 width=20) (actual time=0.064..0.067 rows=50 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 11kB
Id=8 -> Hash Join (cost=1.23..3.41 rows=50 width=20) (actual time=0.018..0.053 rows=50 loops=1)
Hash Cond: (t2.id = t1.id)
Id=9 (Probe) -> Seq Scan on t2 (cost=0.00..1.50 rows=50 width=12) (actual time=0.004..0.011 rows=50 loops=1)
Id=10 (Build) -> Hash (cost=1.10..1.10 rows=10 width=8) (actual time=0.008..0.009 rows=10 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 9kB
Id=11 -> Seq Scan on t1 (cost=0.00..1.10 rows=10 width=8) (actual time=0.003..0.005 rows=10 loops=1)
Id=12 (Build) -> Hash (cost=16.00..16.00 rows=1000 width=12) (actual time=0.437..0.437 rows=1000 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 51kB
Id=13 -> Seq Scan on t0 (cost=0.00..16.00 rows=1000 width=12) (actual time=0.022..0.219 rows=1000 loops=1)

解説のために、Id=noと、Build/Probeを見やすくするためにコメント (Build) / (Probe) を追加しています

Oracleと同じように制御できているか、確認しましょう。

Id=8で、Id=11のt1をSeq ScanしてId=10でハッシュ表作成(Build)と、Id=9のt2をSec Scan(Probe)をHash Join
Id=5で、Id=8の結果を元に、Id=7でハッシュ表作成(Build)と、Id=6のt3 Seq Scan(Probe)をHash Join
Id=2で、Id=5の結果を元に、Id=4でハッシュ表作成(Build)と、Id=3のt4 Seq Scan(Probe)をHash Join
Id=1で、Id=13のt0をSeq ScanしてId=12でハッシュ表作成(Build)と、Id=2の結果(Probe)をHash Join

狙い通りの実行計画になっています!、Yahooooo!!

今一度、Oracleの実行計画と比較してみると、Hash JoinのBuild/Probeの表現が逆になっていることがよくわかりますよね。(前回の予習の通りです)

               =======================================================================================================================
| Id | Operation | Name | Rows | Cost | Time | Start | Execs | Rows | Read | Read | Mem |
| | | | (Estim) | | Active(s) | Active | | (Actual) | Reqs | Bytes | (Max) |
=======================================================================================================================
| 0 | SELECT STATEMENT | | | | 106 | +0 | 1 | 3M | | | . |
| 1 | HASH JOIN | | 3M | 29 | 106 | +0 | 1 | 3M | | | 1MB |
(Build) | 2 | TABLE ACCESS FULL | T0 | 1000 | 3 | 1 | +0 | 1 | 1000 | 2 | 49152 | . |
(Probe) | 3 | HASH JOIN | | 25000 | 12 | 106 | +0 | 1 | 25000 | | | 1MB |
(Build) | 4 | HASH JOIN | | 500 | 9 | 1 | +0 | 1 | 500 | | | 1MB |
(Build) | 5 | HASH JOIN | | 50 | 6 | 1 | +0 | 1 | 50 | | | 1MB |
(Build) | 6 | TABLE ACCESS FULL | T1 | 10 | 3 | 1 | +0 | 1 | 10 | 2 | 49152 | . |
(Probe) | 7 | TABLE ACCESS FULL | T2 | 50 | 3 | 1 | +0 | 1 | 50 | 2 | 49152 | . |
(Probe) | 8 | TABLE ACCESS FULL | T3 | 100 | 3 | 1 | +0 | 1 | 100 | 2 | 49152 | . |
(Probe) | 9 | TABLE ACCESS FULL | T4 | 500 | 3 | 106 | +0 | 1 | 500 | 3 | 81920 | . |
=======================================================================================================================


最後に、
MySQL 8.0.32
これも現時点のリリースよりちょっと前のですが、こちらの都合なので、気にしないでくださいw

mysql> select version();
+-----------+
| version() |
+-----------+
| 8.0.32 |
+-----------+
1 row in set (0.01 sec)

mysql>


無理やり、Hash Joinにはしていますが、PostgreSQLのpg_hint_planのように結合優先順位をペアで制御する方法も、Oracleのように、SWAP_JOIN_INPUTSヒントを使うこともできません。Left Deep Join Treeにしかならないということなので、それが理由ではありますが。とりあえず、こんな感じにしかなりません。JOIN_ORDERヒントの結合順は、OracleのLEADINGヒントに近い考え方で支えているのがわかります。要するに、Buhy Join Treeが可能であれば、これは解決できるわけですが、それは無理なので、t0をBuildにすることができず、これが限界といことになりますよね。(ちょっと思いついたことがあるので、このあと、もうひとあがきしてみますがw)

あと、ヒントで面白いのは、主キー索引を利用させないためのヒント。primaryって指定が必要なんので覚えておくと便利ですよね。
マニュアルにも記載されていますが、インデックスヒントが非推奨になる前に、オプティマイザヒントに慣れておいたほうがよさそうですし。

MySQL 8.0 / 8.9.4 インデックスヒント

”注記
MySQL 8.0.20 の時点で、サーバーは、インデックスオプティマイザヒント JOIN_INDEX, GROUP_INDEX, ORDER_INDEX、および INDEX(NO_JOIN_INDEX, NO_GROUP_INDEX, NO_ORDER_INDEX および FORCE INDEX オプティマイザヒントに相当およびを置き替える)、および NO_INDEX オプティマイザヒント (IGNORE INDEX インデックスヒントに相当し、それらを置き換える) をサポートします。 したがって、USE INDEX、FORCE INDEX および IGNORE INDEX は、MySQL の将来のリリースで非推奨になり、後で完全に削除される予定です。 詳細は、インデックスレベルのオプティマイザヒントを参照してください。”

mysql> \! cat advent_sql_my.sql
explain analyze
SELECT
/*+
JOIN_ORDER(t1,t2,t3,t4,t0)
NO_JOIN_INDEX(t1 primary)
NO_JOIN_INDEX(t2 primary)
NO_JOIN_INDEX(t3 primary)
NO_JOIN_INDEX(t4 primary)
NO_JOIN_INDEX(t0 primary)
*/
t1.id
, t1.t1_c1
, t2.s_id
, t2.t2_c1
, t3.b_id
, t3.t3_c1
, t4.a_id
, t4.c_id
, t4.t4_c1
, t0.m_id
, t0.t0_c1
FROM
t1
INNER JOIN t2
ON
t1.id = t2.id
INNER JOIN t3
ON
t1.id = t3.id
INNER JOIN t4
ON
t1.id = t4.id
INNER JOIN t0
ON
t1.id = t0.id
;

mysql> \. advent_sql_my.sql
+---------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+---------------------------------------------------------------------------------------------------------------------------+
Id=1 | -> Inner hash join (t0.id = t1.id) (cost=2525558.24 rows=2500000) (actual time=23.139..356.660 rows=2500000 loops=1)
Id=2 (Probe) -> Table scan on t0 (cost=0.00 rows=1000) (actual time=0.083..0.512 rows=1000 loops=1)
Id=3 (Build) -> Hash
Id=4 -> Inner hash join (t4.id = t1.id) (cost=25552.55 rows=25000) (actual time=0.679..4.613 rows=25000 loops=1)
Id=5 (Probe) -> Table scan on t4 (cost=0.01 rows=500) (actual time=0.057..0.330 rows=500 loops=1)
Id=6 (Build) -> Hash
Id=7 -> Inner hash join (t3.id = t1.id) (cost=551.75 rows=500) (actual time=0.223..0.412 rows=500 loops=1)
Id=8 (Probe) -> Table scan on t3 (cost=0.03 rows=100) (actual time=0.041..0.069 rows=100 loops=1)
Id=9 (Build) -> Hash
Id=10 -> Inner hash join (t2.id = t1.id) (cost=51.50 rows=50) (actual time=0.106..0.146 rows=50 loops=1)
Id=11 (Probe) -> Table scan on t2 (cost=0.08 rows=50) (actual time=0.025..0.040 rows=50 loops=1)
Id=12 (Build) -> Hash
Id=13 -> Table scan on t1 (cost=1.25 rows=10) (actual time=0.040..0.048 rows=10 loops=1)
|
+---------------------------------------------------------------------------------------------------------------------------+
(確認し訳するため、Id=nと(Build) / (Probe)を示すコメントを追記してあります)

Id=10で、Id=13のt1をTable Scanてから、Id=12でハッシュ表作成(Build)と、Id=11で、t2をTable Scan(Probe)をHash Join
Id=7で、Id=10の結果を元に、Id=9でハッシュ表作成(Build)と、Id=8のt3 Table Scan(Probe)をHash Join
Id=4で、Id=7の結果を元に、Id=6でハッシュ表作成(Build)と、Id=5のt4 Table Scan(Probe)をHash Join
Id=1で、Id=4の結果を元に、Id=3でハッシュ表作成(Build)と、Id=2のt0 Table Scan(Prove)をHash Hoin

最後のt0をBuildにしたいわけですけども、Build表制御することができないので。こんな感じですね。

と、一つだけ、思いついたので、SQL魔改造でなんとかならんか、試してみます。
(ヒントだけでなんとかするという趣旨からはズレるのですけどもw)

ダメ元で試してみますw

おおおおお、一瞬、できた! か?  と思いましたが、やはり無理です。CTEでサブクエリーとして先に一時表としてマテリアライズしたら行くかなぁ。
と思いましたが、MySQLは、一時表でも、自動的に内部的な主キーが作成されるという、... clustered indexなのか。。。一時表も。という気づき。
この方法では、CTEを使っていますが、インラインビューにして、NO_MERGEヒントにしても同様に、一時表としてマテリアライズされるようで、同様の結果でした。。。。

記載はしていませんが、 マテリアライズされた一時表の  という内部的な主キーですが、現状、 NO_JOIN_INDEXヒントで止められないので、t0の結合はNLJにしかできませんでした。。。

現時点のMySQLのオプティマイザの気持ちが、少しだけ、理解できるようになった気がします :)

mysql> \! cat advent_sql2_my.sql
explain analyze
WITH
t1234 AS
(
SELECT
/*+
QB_NAME(ilv1)
JOIN_ORDER(t1,t2,t3,t4)
NO_JOIN_INDEX(t1 primary)
NO_JOIN_INDEX(t2 primary)
NO_JOIN_INDEX(t3 primary)
NO_JOIN_INDEX(t4 primary)
*/
t1.id
, t1.t1_c1
, t2.s_id
, t2.t2_c1
, t3.b_id
, t3.t3_c1
, t4.a_id
, t4.c_id
, t4.t4_c1
FROM
t1
INNER JOIN t2
ON
t1.id = t2.id
INNER JOIN t3
ON
t1.id = t3.id
INNER JOIN t4
ON
t1.id = t4.id
),
t00 AS
(
SELECT
/*+
QB_NAME(ilv2)
NO_INDEX(t0 primary)
*/
t0.id
, t0.m_id
, t0.t0_c1
FROM
t0
)
SELECT
/*+
JOIN_ORDER(@ilv2)
NO_MERGE(t00)
*/
t1234.id
, t1234.t1_c1
, t1234.s_id
, t1234.t2_c1
, t1234.b_id
, t1234.t3_c1
, t1234.a_id
, t1234.c_id
, t1234.t4_c1
, t00.m_id
, t00.t0_c1
FROM
t1234
INNER JOIN t00
ON
t1234.id = t00.id
;

mysql> \. advent_sql2_my.sql
+---------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+---------------------------------------------------------------------------------------------------------------------------+
| -> Nested loop inner join (cost=2588052.66 rows=25000001) (actual time=2.449..1233.983 rows=2500000 loops=1)
-> Inner hash join (t4.id = t1.id) (cost=25552.55 rows=25000) (actual time=0.626..11.339 rows=25000 loops=1)
-> Table scan on t4 (cost=0.01 rows=500) (actual time=0.031..2.330 rows=500 loops=1)
-> Hash
-> Inner hash join (t3.id = t1.id) (cost=551.75 rows=500) (actual time=0.403..0.483 rows=500 loops=1)
-> Table scan on t3 (cost=0.03 rows=100) (actual time=0.024..0.037 rows=100 loops=1)
-> Hash
-> Inner hash join (t2.id = t1.id) (cost=51.50 rows=50) (actual time=0.329..0.350 rows=50 loops=1)
-> Table scan on t2 (cost=0.08 rows=50) (actual time=0.021..0.028 rows=50 loops=1)
-> Hash
-> Table scan on t1 (cost=1.25 rows=10) (actual time=0.190..0.199 rows=10 loops=1)
-> Index lookup on t00 using (id=t1.id) (cost=201.25..203.50 rows=10) (actual time=0.002..0.036 rows=100 loops=25000)
-> Materialize CTE t00 (cost=201.00..201.00 rows=1000) (actual time=1.803..1.803 rows=1000 loops=1)
-> Table scan on t0 (cost=101.00 rows=1000) (actual time=0.024..0.514 rows=1000 loops=1)
|
+---------------------------------------------------------------------------------------------------------------------------+
1 row in set (2.77 sec)

(Warningも出てないので、オプティマイザヒントの指定方法としては良さげだが、狙い通りにはなりませんw)




では、最後に、このポストへ至るまでの予習ポストも含め、リンクを再掲しておきます

予習 その1

帰ってきた! 標準はあるにはあるが癖の多いSQL #5 - Optimizer Hint でも癖が多い

予習 その2

帰ってきた! 標準はあるにはあるが癖の多いSQL #6 - Hash Joinの結合ツリーにも癖がでる

予習 その3

帰ってきた! 標準はあるにはあるが癖の多いSQL #7 - Hash Joinの実行計画にも癖がでる

皆様、メリークリスマス & 良いお年をお迎えください。 :) / Oracle database 23c with pipe line function / SQL de Christmas Tree 、4K対応で作り直しましたw
恒例?の、Oracle Pipelined Table Function で Christmas ASCII Art








関連エントリー

標準はあるにはあるが癖の多いSQL 全部俺 #1 Pagination
標準はあるにはあるが癖の多いSQL 全部俺 #2 関数名は同じでも引数が逆の罠!
標準はあるにはあるが癖の多いSQL 全部俺 #3 データ型確認したい時あるんです
標準はあるにはあるが癖の多いSQL 全部俺 #4 リテラル値での除算の内部精度も違うのよ!
標準はあるにはあるが癖の多いSQL 全部俺 #5 和暦変換機能ある方が少数派
標準はあるにはあるが癖の多いSQL 全部俺 #6 時間厳守!
標準はあるにはあるが癖の多いSQL 全部俺 #7 期間リテラル!
標準はあるにはあるが癖の多いSQL 全部俺 #8 翌月末日って何日?
標準はあるにはあるが癖の多いSQL 全部俺 #9 部分文字列の扱いでも癖が出る><
標準はあるにはあるが癖の多いSQL 全部俺 #10 文字列連結の罠(有名なやつ)
標準はあるにはあるが癖の多いSQL 全部俺 #11 デュエル、じゃなくて、デュアル
標準はあるにはあるが癖の多いSQL 全部俺 #12 文字[列]探すにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #13 あると便利ですが意外となかったり
標準はあるにはあるが癖の多いSQL 全部俺 #14 連番の集合を返すにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #15 SQL command line client
標準はあるにはあるが癖の多いSQL 全部俺 #16 SQLのレントゲンを撮る方法
標準はあるにはあるが癖の多いSQL 全部俺 #17 その空白は許されないのか?
標準はあるにはあるが癖の多いSQL 全部俺 #18 (+)の外部結合は方言
標準はあるにはあるが癖の多いSQL 全部俺 #19 帰ってきた、部分文字列の扱いでも癖w
標準はあるにはあるが癖の多いSQL 全部俺 #20 結果セットを単一列に連結するにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #21 演算結果にも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #22 集合演算にも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #23 複数行INSERTにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #24 乱数作るにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #25 SQL de Fractalsにも癖がある:)
標準はあるにはあるが癖の多いSQL 全部俺 おまけ SQL de 湯婆婆やるにも癖がでるw
帰ってきた! 標準はあるにはあるが癖の多いSQL #1 SQL de ROT13 やるにも癖が出るw
帰ってきた! 標準はあるにはあるが癖の多いSQL #2 Actual Plan取得中のキャンセルでも癖が出る
帰ってきた! 標準はあるにはあるが癖の多いSQL #3 オプティマイザの結合順評価テーブル数上限にも癖が出る
帰ってきた! 標準はあるにはあるが癖の多いSQL #4 Optimizer Traceの取得でも癖がでる
帰ってきた! 標準はあるにはあるが癖の多いSQL #5 - Optimizer Hint でも癖が多い
帰ってきた! 標準はあるにはあるが癖の多いSQL #6 - Hash Joinの結合ツリーにも癖がでる
帰ってきた! 標準はあるにはあるが癖の多いSQL #7 - Hash Joinの実行計画にも癖がでる

| | | コメント (0)

2023年12月10日 (日)

帰ってきた! 標準はあるにはあるが癖の多いSQL #7 - Hash Joinの実行計画の見せ方にも癖がでる



本エントリーは、MySQL Advent Calendar 2023 シリーズ2 / Day 10PostgreSQL Advent Calendar 2023 シリーズ2 / Day 10へのクロスポスト、および、JPOUG Advent Calendar 2023 / Day 10 の裏番組 (ADVENTARはシリーズ増やせないので) エントリーで、12/17日向けの 予習 その3 という位置付けのエントリーです。

予習 その1は、以下。
帰ってきた! 標準はあるにはあるが癖の多いSQL #5 - Optimizer Hint でも癖が多い

予習 その2は、以下。
帰ってきた! 標準はあるにはあるが癖の多いSQL #6 - Hash Joinの結合ツリーにも癖がでる


MySQLは、Left Deep Join Treeにしかならない、というところまで理解できたのが前回までの予習でした。

今日は、もう少し、理解を深めておこうと言うことで、MySQLでは無理だけど、 PostgreSQL と Oracle で結合ツリーの種別(Oracleのマニュアルに記載されている分類の範囲)で、Right Deep Join、Left Deep Join、Bushy Joinでは、どのような実行計画として見えるのかを確認しておきましょう。


まず初めに、MySQLはできない Right Deep Join Tree で Hash Join させると、どのような実行計画に見えるか? の比較から。

Oracle Database 21c
Oraclerなら見慣れたSWAP_JOIN_INPUTSヒント利用の典型的な例です。Id=6でt1(build)がtable full scanされ、ID=7のt2(Probe)と結合されています。t3, t4はそれぞれ、Buildとなっていることが読み取れます。

Oracle Database 21c / Right Deep Join Tree / Hash Join
(実行計画の右端に (Build) なのか、 (Probe) を確認しやすいようにコメントを付加しています)

SCOTT@orclpdb1> @ora_sql_rightdj.sql
1 SELECT
2 /*+
3 MONITOR
4 LEADING(t1 t2 t3 t4)
5 USE_HASH(t1 t2 t3 t4)
6 SWAP_JOIN_INPUTS(t3)
7 SWAP_JOIN_INPUTS(t4)
8 */
9 t1.id
10 , t1.t1_c1
11 , t2.s_id
12 , t2.t2_c1
13 , t3.b_id
14 , t3.t3_c1
15 , t4.a_id
16 , t4.c_id
17 , t4.t4_c1
18 FROM
19 t1
20 INNER JOIN t2
21 ON
22 t1.id = t2.id
23 INNER JOIN t3
24 ON
25 t1.id = t3.id
26 INNER JOIN t4
27 ON
28* t1.id = t4.id

...略...

SQL Plan Monitoring Details (Plan Hash Value=3835853103)
===================================================================================================================================================
| Id | Operation | Name | Rows | Cost | Time | Start | Execs | Rows | Read | Read | Mem | Activity | Activity Detail |
| | | | (Estim) | | Active(s) | Active | | (Actual) | Reqs | Bytes | (Max) | (%) | (# samples) |
===================================================================================================================================================
| 0 | SELECT STATEMENT | | | | 2 | +0 | 1 | 25000 | | | . | | |
| 1 | HASH JOIN | | 25000 | 12 | 2 | +0 | 1 | 25000 | | | 1MB | | |
| 2 | TABLE ACCESS FULL | T4 | 500 | 3 | 1 | +0 | 1 | 500 | 2 | 49152 | . | | | (Build)
| 3 | HASH JOIN | | 500 | 9 | 2 | +0 | 1 | 500 | | | 1MB | | | (Probe)
| 4 | TABLE ACCESS FULL | T3 | 100 | 3 | 1 | +0 | 1 | 100 | 2 | 49152 | . | | | (Build)
| 5 | HASH JOIN | | 50 | 6 | 2 | +0 | 1 | 50 | | | 1MB | | | (Probe)
| 6 | TABLE ACCESS FULL | T1 | 10 | 3 | 1 | +0 | 1 | 10 | 2 | 49152 | . | | | (Build)
| 7 | TABLE ACCESS FULL | T2 | 50 | 3 | 2 | +0 | 1 | 50 | 2 | 49152 | . | | | (Probe)
===================================================================================================================================================


PostgreSQL 13.4 / pg_hint_plan 1.3.9 / Right Deep Join Tree / Hash Join

OracleとはBuild表の位置が逆なので、OracleのLeft deep join treeの実行計画に読み間違えそうですがw PostgreSQLのRight Deep Join Treeだとこのような実行計画として表示されます。
Oracleでは、Id=6でT1がBuild表になり、Id=7のt2(Probe)と結合するように表現されますが、
PostgreSQLでは、 ★ で示した行で、t1が、Seq ScansされてHash表が作成されている。つまり、Build表になっており、t2(Probe)と結合されています。Oracleの感覚で読んでしまうと、あれ? となるところだと思います。
t3, t4から、それぞれ、Hash表が作成されているので、Buildになっていることがわかります:) 
同じ実行計画になるようにしてみましたが、実行計画の見せ方では、Oracle/PostgreSQLでは差異がありますよね。ただ、MySQLのTree表示もPostgreSQLに類似しているので、PostgreSQLのに慣れてると、MySQLのTree formatの実行計画は読みやすいかもしれません)

Orableのように、SWAP_JOIN_INPUTSヒントでBuild/Probeを制御するヒントが存在しないので、代わりに、LeadingヒントのLeading()構文を利用して制御しています。OracleのLEADING + SWAP_JOIN_JOINPSと比べると可読性は劣化する(個人の感想です)の使い方が難しいく感じますね。慣れなのかなぁ。これ。

(実行計画の左端に (Build) なのか、 (Probe) を確認しやすいようにコメントを付加しています)

perftestdb=> \! cat pg_sql_rightdj.sql
explain (analyze)
SELECT
/*+
Leading((((t2 t1) t3) t4))
HashJoin(t1 t2 t3 t4)
*/
t1.id
, t1.t1_c1
, t2.s_id
, t2.t2_c1
, t3.b_id
, t3.t3_c1
, t4.a_id
, t4.c_id
, t4.t4_c1
FROM
t1
INNER JOIN t2
ON
t1.id = t2.id
INNER JOIN t3
ON
t1.id = t3.id
INNER JOIN t4
ON
t1.id = t4.id
;

perftestdb=> \i pg_sql_rightdj.sql
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------
Hash Join (cost=18.73..308.66 rows=25000 width=36) (actual time=3.352..8.740 rows=25000 loops=1)
Hash Cond: (t1.id = t4.id)
(Probe) -> Hash Join (cost=4.47..11.91 rows=500 width=32) (actual time=2.316..2.460 rows=500 loops=1)
Hash Cond: (t1.id = t3.id)
(Probe) -> Hash Join (cost=1.23..2.91 rows=50 width=20) (actual time=1.534..1.562 rows=50 loops=1)
Hash Cond: (t2.id = t1.id)
(Probe) -> Seq Scan on t2 (cost=0.00..1.50 rows=50 width=12) (actual time=0.744..0.750 rows=50 loops=1)
(Build) -> Hash (cost=1.10..1.10 rows=10 width=8) (actual time=0.723..0.724 rows=10 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 9kB
★ -> Seq Scan on t1 (cost=0.00..1.10 rows=10 width=8) (actual time=0.705..0.707 rows=10 loops=1)
(Build) -> Hash (cost=2.00..2.00 rows=100 width=12) (actual time=0.760..0.761 rows=100 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 13kB
-> Seq Scan on t3 (cost=0.00..2.00 rows=100 width=12) (actual time=0.715..0.732 rows=100 loops=1)
(Build) -> Hash (cost=8.00..8.00 rows=500 width=16) (actual time=1.012..1.013 rows=500 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 32kB
-> Seq Scan on t4 (cost=0.00..8.00 rows=500 width=16) (actual time=0.792..0.905 rows=500 loops=1)


MySQL 8.0.32 / Right Deep Join Tree / Hash Join
できないので、なし。


次、みんさんが、見慣れている(?)、Left Deep Join Tree にすると Hash Join の実行計画は、それぞれどのように見えるのでしょうか? 比較してみましょう。

Oraclerには、わかりやすいですよね(慣れの問題かもしれませんけどもw) 。Id=4で、t1がBuild表となってt2(Probe)と結合。。。以下、t3, t4がそれぞれ、ProbeとしてHash Joinされています。
Oracle 21 / Left Deep Join Tree / Hash Join

Id=4でt1がtable full scan(Build)で、ID=5 のt2 (Probe) と結合されています。t3, t4は それぞれのProbeとなっていることが読み取れます。;)

(実行計画の右端に (Build) なのか、 (Probe) を確認しやすいようにコメントを付加しています)

SCOTT@orclpdb1> @ora_sql_leftdj.sql
1 SELECT
2 /*+
3 MONITOR
4 LEADING(t1 t2 t3 t4)
5 USE_HASH(t1 t2 t3 t4)
6 NO_SWAP_JOIN_INPUTS(t4)
7 */
8 t1.id
9 , t1.t1_c1
10 , t2.s_id
11 , t2.t2_c1
12 , t3.b_id
13 , t3.t3_c1
14 , t4.a_id
15 , t4.c_id
16 , t4.t4_c1
17 FROM
18 t1
19 INNER JOIN t2
20 ON
21 t1.id = t2.idv 22 INNER JOIN t3
23 ON
24 t1.id = t3.id
25 INNER JOIN t4
26 ON
27* t1.id = t4.id

...略...

SQL Plan Monitoring Details (Plan Hash Value=894925296)
====================================================================================================================================
| Id | Operation | Name | Rows | Cost | Time | Start | Execs | Rows | Mem | Activity | Activity Detail |
| | | | (Estim) | | Active(s) | Active | | (Actual) | (Max) | (%) | (# samples) |
====================================================================================================================================
| 0 | SELECT STATEMENT | | | | 2 | +0 | 1 | 25000 | . | | |
| 1 | HASH JOIN | | 25000 | 12 | 2 | +0 | 1 | 25000 | 1MB | | |
| 2 | HASH JOIN | | 500 | 9 | 1 | +0 | 1 | 500 | 1MB | | | (Build)
| 3 | HASH JOIN | | 50 | 6 | 1 | +0 | 1 | 50 | 1MB | | | (Build)
| 4 | TABLE ACCESS FULL | T1 | 10 | 3 | 1 | +0 | 1 | 10 | . | | | (Build)
| 5 | TABLE ACCESS FULL | T2 | 50 | 3 | 1 | +0 | 1 | 50 | . | | | (Probe)
| 6 | TABLE ACCESS FULL | T3 | 100 | 3 | 1 | +0 | 1 | 100 | . | | | (Probe)
| 7 | TABLE ACCESS FULL | T4 | 500 | 3 | 2 | +0 | 1 | 500 | . | | | (Probe)
====================================================================================================================================


PostgreSQL 13.4 / pg_hint_plan 1.3.9 / Left Deep Join Tree / Hash Join
★ のある行で、t1がSeq Scanされ、その上位の行で、Hashが作成されているので、Buildとなっていることがわかります。次に、同一階層で t2がSeq ScanされHash Joinされています。
t3, t4はそれぞれProbeになっていることが読み取れます。ぱっと見は、Oracleの ight Deep Join Tree で見られる実行計画の形に似てますが。。。(^^;;;;

(実行計画の左端に (Build) なのか、 (Probe) を確認しやすいようにコメントを付加しています)

perftestdb=> \! cat pg_sql_leftdj.sql
explain (analyze)
SELECT
/*+
Leading((t4 (t3 (t2 t1))))
HashJoin(t2 t1 t3 t4)
*/
t1.id
, t1.t1_c1
, t2.s_id
, t2.t2_c1
, t3.b_id
, t3.t3_c1
, t4.a_id
, t4.c_id
, t4.t4_c1
FROM
t1
INNER JOIN t2
ON
t1.id = t2.id
INNER JOIN t3
ON
t1.id = t3.id
INNER JOIN t4
ON
t1.id = t4.id
;
perftestdb=>
perftestdb=> \i pg_sql_leftdj.sql
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------
Hash Join (cost=17.66..308.16 rows=25000 width=36) (actual time=0.522..8.362 rows=25000 loops=1)
Hash Cond: (t4.id = t1.id)
(Probe) -> Seq Scan on t4 (cost=0.00..8.00 rows=500 width=16) (actual time=0.012..0.101 rows=500 loops=1)
(Build) -> Hash (cost=11.41..11.41 rows=500 width=32) (actual time=0.462..0.467 rows=500 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 40kB
-> Hash Join (cost=3.54..11.41 rows=500 width=32) (actual time=0.122..0.328 rows=500 loops=1)
Hash Cond: (t3.id = t1.id)
(Probe) -> Seq Scan on t3 (cost=0.00..2.00 rows=100 width=12) (actual time=0.004..0.017 rows=100 loops=1)
(Build) -> Hash (cost=2.91..2.91 rows=50 width=20) (actual time=0.098..0.102 rows=50 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 11kB
-> Hash Join (cost=1.23..2.91 rows=50 width=20) (actual time=0.039..0.068 rows=50 loops=1)
Hash Cond: (t2.id = t1.id)
(Probe) -> Seq Scan on t2 (cost=0.00..1.50 rows=50 width=12) (actual time=0.005..0.011 rows=50 loops=1)
(Build) -> Hash (cost=1.10..1.10 rows=10 width=8) (actual time=0.016..0.018 rows=10 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 9kB
★ -> Seq Scan on t1 (cost=0.00..1.10 rows=10 width=8) (actual time=0.003..0.004 rows=10 loops=1)


MySQL 8.0.32 / Left Deep Join Tree / Hash Join
これは、前回のエントリーでも紹介した無理やり Hash Join にした例なので、特に解説はしませんが、PostgreSQLに類似した実行計画になっていますよね。

現状、MySQLは、HASH JOIN を強制するヒント、Build/Probeを制御するヒント、PostgreSQLのように結合順の優先順位を細かく指定するヒントもないため、Nested Loop Join にならないようヒントで制御するしか Hash Join を強制する方法はなさそうです。
Left Deep Join Tree にしかならないのでこともあり、Hash Join で、 Bushy Join Treeや、Right Deep Join Treeのようなチューニングはできないですね。

実際、関わった案件で、Hash Join が選択されたのは良いと思うけど、Build/Probeを入れ替えられたら、もっとサクッと終わるはずなのに、と言う状況になって初めて、この事実を知りました。(なので、このエントリー書いているのですけどもw)
できないのかよ。。とw

以下実行計画の ★ を付与した行で、t1がTable Scanされ、その上位の行で、Hashが作成されているので、Buildとなっていることがわかります。次に、同一階層で t2がTable Scan後に、Hash Joinされています。
t3, t4は、Probeになっていることも読み取れます。PostgreSQLの実行計画の表示方法に類似してますよね。

(実行計画の左端に (Build) なのか、 (Probe) を確認しやすいようにコメントを付加しています)

mysql> \! cat my_sql_leftdj.sql
explain analyze
SELECT
/*+
JOIN_ORDER(t1,t2,t3,t4)
NO_JOIN_INDEX(t2 primary)
NO_JOIN_INDEX(t3 primary)
NO_JOIN_INDEX(t4 primary)
*/
t1.id
, t1.t1_c1
, t2.s_id
, t2.t2_c1
, t3.b_id
, t3.t3_c1
, t4.a_id
, t4.c_id
, t4.t4_c1
FROM
t1
INNER JOIN t2
ON
t1.id = t2.id
INNER JOIN t3
ON
t1.id = t3.id
INNER JOIN t4
ON
t1.id = t4.id
;

mysql> \. my_sql_leftdj.sql
+-----------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+-----------------------------------------------------------------------------------------------------------------------+
| -> Inner hash join (t4.id = t1.id) (cost=25552.55 rows=25000) (actual time=0.643..5.045 rows=25000 loops=1)
(Probe) -> Table scan on t4 (cost=0.01 rows=500) (actual time=0.052..0.346 rows=500 loops=1)
(Build) -> Hash
-> Inner hash join (t3.id = t1.id) (cost=551.75 rows=500) (actual time=0.216..0.380 rows=500 loops=1)
(Probe) -> Table scan on t3 (cost=0.03 rows=100) (actual time=0.041..0.069 rows=100 loops=1)
(Build) -> Hash
-> Inner hash join (t2.id = t1.id) (cost=51.50 rows=50) (actual time=0.100..0.141 rows=50 loops=1)
(Probe) -> Table scan on t2 (cost=0.08 rows=50) (actual time=0.026..0.041 rows=50 loops=1)
(Build) -> Hash
★ -> Table scan on t1 (cost=1.25 rows=10) (actual time=0.039..0.047 rows=10 loops=1)
|
+-----------------------------------------------------------------------------------------------------------------------+


Bushy Joinのも書こうと思ってましたが、前回ので十分そうなので、省略。(Left/Right Join Treeのミックスですし)MySQLは、Right Deep Join Treeにはならないので、結果なしです。


と言うことで、
Advent Calendar 2023 / Day 17向けの準備運動的な予習(その3)はここまで。

では、また。






関連エントリー

標準はあるにはあるが癖の多いSQL 全部俺 #1 Pagination
標準はあるにはあるが癖の多いSQL 全部俺 #2 関数名は同じでも引数が逆の罠!
標準はあるにはあるが癖の多いSQL 全部俺 #3 データ型確認したい時あるんです
標準はあるにはあるが癖の多いSQL 全部俺 #4 リテラル値での除算の内部精度も違うのよ!
標準はあるにはあるが癖の多いSQL 全部俺 #5 和暦変換機能ある方が少数派
標準はあるにはあるが癖の多いSQL 全部俺 #6 時間厳守!
標準はあるにはあるが癖の多いSQL 全部俺 #7 期間リテラル!
標準はあるにはあるが癖の多いSQL 全部俺 #8 翌月末日って何日?
標準はあるにはあるが癖の多いSQL 全部俺 #9 部分文字列の扱いでも癖が出る><
標準はあるにはあるが癖の多いSQL 全部俺 #10 文字列連結の罠(有名なやつ)
標準はあるにはあるが癖の多いSQL 全部俺 #11 デュエル、じゃなくて、デュアル
標準はあるにはあるが癖の多いSQL 全部俺 #12 文字[列]探すにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #13 あると便利ですが意外となかったり
標準はあるにはあるが癖の多いSQL 全部俺 #14 連番の集合を返すにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #15 SQL command line client
標準はあるにはあるが癖の多いSQL 全部俺 #16 SQLのレントゲンを撮る方法
標準はあるにはあるが癖の多いSQL 全部俺 #17 その空白は許されないのか?
標準はあるにはあるが癖の多いSQL 全部俺 #18 (+)の外部結合は方言
標準はあるにはあるが癖の多いSQL 全部俺 #19 帰ってきた、部分文字列の扱いでも癖w
標準はあるにはあるが癖の多いSQL 全部俺 #20 結果セットを単一列に連結するにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #21 演算結果にも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #22 集合演算にも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #23 複数行INSERTにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #24 乱数作るにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #25 SQL de Fractalsにも癖がある:)
標準はあるにはあるが癖の多いSQL 全部俺 おまけ SQL de 湯婆婆やるにも癖がでるw
帰ってきた! 標準はあるにはあるが癖の多いSQL #1 SQL de ROT13 やるにも癖が出るw
帰ってきた! 標準はあるにはあるが癖の多いSQL #2 Actual Plan取得中のキャンセルでも癖が出る
帰ってきた! 標準はあるにはあるが癖の多いSQL #3 オプティマイザの結合順評価テーブル数上限にも癖が出る
帰ってきた! 標準はあるにはあるが癖の多いSQL #4 Optimizer Traceの取得でも癖がでる
帰ってきた! 標準はあるにはあるが癖の多いSQL #5 - Optimizer Hint でも癖が多い
帰ってきた! 標準はあるにはあるが癖の多いSQL #6 - Hash Joinの結合ツリーにも癖がでる

| | | コメント (0)

2023年12月 9日 (土)

帰ってきた! 標準はあるにはあるが癖の多いSQL #6 - Hash Joinの結合ツリーにも癖がでる



本エントリーは、MySQL Advent Calendar 2023 シリーズ2 / Day 9PostgreSQL Advent Calendar 2023 シリーズ2 / Day 9へのクロスポスト、および、JPOUG Advent Calendar 2023 / Day 9 の裏番組 (ADVENTARはシリーズ増やせないので) エントリーで、12/17日向けの 予習 その2 という位置付けのエントリーです。

予習 その1は、以下。
帰ってきた! 標準はあるにはあるが癖の多いSQL #5 - Optimizer Hint でも癖が多い



少し前に、MySQLでもHash Joinになるんだね。といいうエントリーを書きました。覚えてますか? 
MySQL 8.0.32では NLJに使えるINDEXが存在していても、Hash Joinをヒントで強制することができる!(オプティマイザが選択することもある!)

その時点では、

MySQLのHash Joinを強制するオプティマイザーヒントも廃止されているようだし、Build/Probeの制御もできないよね? ヒントないし。
とか、
オプティマイザーヒントで強制できないけど、オプティマイザーが勝手に選んじゃうこともあるのか〜、NLJに利用できる索引あっても。。
勝手に選ばれることはあってもヒントではもろもろ制御できない。最初に戻る。

という状況ループ状態でしたw

そこで、閃いたのが、MySQLで、Hash Joinが自動発動する条件。その条件に合う状況にすればいいじゃん。と!
Hash Joinを強制するヒントはなくても、Nested Loop Joinに使ってる主索引(MySQLの場合は主キーはクラスター索引)を、オプティマイザーヒントで無効化してしまえば、勝手に、Hash Joinするよね!? 

という確認ばかりに目が行ってしまって、気づきませんでした!!!!


MySQLのHash Joinの実行計画ツリーを見て、Oracle/PostgreSQLと違う点、何か気づきませんか? 
みなさん! これ試験に出ますよw(嘘
答え合わせは後半でw

それでは、環境づくりから(少々長めですw)
それぞれの DB( Oracle Database 21c / PostgreSQL 13.4 with pg_hint_plan 1.3.9 / MySQL 8.0.32 )で、そのまま実行できるDDLにしてあります。
表と索引作成(各DB共通)、データ登録、そして、統計取得まで行っています。

Oracle/PostgreSQL/MySQL共通DDL

cre_advent_tabs.sql

-- 10 rows
CREATE TABLE t1
(
id INT NOT NULL
, t1_c1 INT NOT NULL
, CONSTRAINT t1_pk PRIMARY KEY (id)
);

-- 50 rows
CREATE TABLE t2
(
id INT NOT NULL
, s_id INT NOT NULL
, t2_c1 INT NOT NULL
, CONSTRAINT t2_pk PRIMARY KEY (id, s_id)
);

-- 100 rows
CREATE TABLE t3
(
id INT NOT NULL
, b_id INT NOT NULL
, t3_c1 INT NOT NULL
, CONSTRAINT t3_pk PRIMARY KEY (id, b_id)
);

-- 500 rows
CREATE TABLE t4
(
id INT NOT NULL
, a_id INT NOT NULL
, c_id INT NOT NULL
, t4_c1 INT NOT NULL
, CONSTRAINT t4_pk PRIMARY KEY (id, a_id, c_id)
);

-- 1000 rows
CREATE TABLE t0
(
id INT NOT NULL
, m_id INT NOT NULL
, t0_c1 INT NOT NULL
, CONSTRAINT t0_pk PRIMARY KEY (id, m_id)
);

Oracle 21c向けデータ登録と統計取得からデータ件数確認までのスクリプトと実行例

make_data4oracle.sql

-- for t1
BEGIN
FOR i IN 1..10 LOOP
INSERT INTO t1 VALUES(i,i);
END LOOP;
COMMIT;
DBMS_STATS.GATHER_TABLE_STATS(ownname=>'SCOTT', tabname=>'T1', cascade=>true, no_invalidate=>false);
END;
/
SELECT COUNT(1) FROM t1;

-- for t2
BEGIN
FOR i IN 1..10 LOOP
FOR j IN 1..5 LOOP
INSERT INTO t2 VALUES(i,j,i+j);
END LOOP;
END LOOP;
COMMIT;v DBMS_STATS.GATHER_TABLE_STATS(ownname=>'SCOTT', tabname=>'T2', cascade=>true, no_invalidate=>false);
END;
/
SELECT COUNT(1) FROM t2;

-- for t3
BEGINv FOR i IN 1..10 LOOP
FOR j IN 1..10 LOOP
INSERT INTO t3 VALUES(i,j,i+j);
END LOOP;
END LOOP;
COMMIT;
DBMS_STATS.GATHER_TABLE_STATS(ownname=>'SCOTT', tabname=>'T3', cascade=>true, no_invalidate=>false);
END;
/
SELECT COUNT(1) FROM t3;

-- for t4
BEGIN
FOR i IN 1..10 LOOP
FOR j IN 1..10 LOOP
FOR k IN 1..5 LOOP
INSERT INTO t4 VALUES(i,j,k,i+j);
END LOOP;
END LOOP;
COMMIT;
END LOOP;
DBMS_STATS.GATHER_TABLE_STATS(ownname=>'SCOTT', tabname=>'T4', cascade=>true, no_invalidate=>false);
END;
/
SELECT COUNT(1) FROM t4;

-- for t0
BEGIN
FOR i IN 1..10 LOOP
FOR j IN 1..100 LOOP
INSERT INTO t0 VALUES(i,j,i+j);
END LOOP;
COMMIT;
END LOOP;
DBMS_STATS.GATHER_TABLE_STATS(ownname=>'SCOTT', tabname=>'T0', cascade=>true, no_invalidate=>false);
END;
/
SELECT COUNT(1) FROM t0;

SCOTT@orclpdb1> @make_data4oracle

PL/SQLプロシージャが正常に完了しました。

COUNT(1)
----------
10

PL/SQLプロシージャが正常に完了しました。

COUNT(1)
----------
50

PL/SQLプロシージャが正常に完了しました。

COUNT(1)
----------
100

PL/SQLプロシージャが正常に完了しました。

COUNT(1)
----------
500

PL/SQLプロシージャが正常に完了しました。

COUNT(1)
----------
1000

PostgreSQL 13.4向けデータ登録と統計取得からデータ件数確認までのスクリプトと実行例

perftestdb=> \i cre_advent_tabs.sql
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
CREATE TABLE
perftestdb=>

PostgreSQL向けデータ登録と統計取得からデータ件数確認までのスクリプトと実行例

make_data4postgresql.sql

-- for t1
DO $$
BEGIN
FOR i IN 1..10 LOOP
INSERT INTO t1 VALUES(i,i);
END LOOP;
COMMIT;
END
$$
;

VACUUM VERBOSE ANALYZE t1;
SELECT COUNT(1) FROM t1;

-- for t2
DO $$
BEGIN
FOR i IN 1..10 LOOP
FOR j IN 1..5 LOOP
INSERT INTO t2 VALUES(i,j,i+1);
END LOOP;
END LOOP;
COMMIT;
END
$$
;

VACUUM VERBOSE ANALYZE t2;
SELECT COUNT(1) FROM t2;

-- for t3
DO $$
BEGIN
FOR i IN 1..10 LOOP
FOR j IN 1..10 LOOP
INSERT INTO t3 VALUES(i,j,i+1);
END LOOP;
END LOOP;
COMMIT;
END
$$
;

VACUUM VERBOSE ANALYZE t3;
SELECT COUNT(1) FROM t3;

-- for t4
DO $$
BEGIN
FOR i IN 1..10 LOOP
FOR j IN 1..10 LOOP
FOR k IN 1..5 LOOP
INSERT INTO t4 VALUES(i,j,k,i+j);
END LOOP;
END LOOP;
COMMIT;
END LOOP;
END
$$
;

VACUUM VERBOSE ANALYZE t4;
SELECT COUNT(1) FROM t4;

-- for t0
DO $$
BEGIN
FOR i IN 1..10 LOOP
FOR j IN 1..100 LOOP
INSERT INTO t0 VALUES(i,j,i+j);
END LOOP;
COMMIT;
END LOOP;
END
$$
;

VACUUM VERBOSE ANALYZE t0;
SELECT COUNT(1) FROM t0;

perftestdb=> \i make_data4postgresql.sql 
DO
psql:make_data4postgresql.sql:12: INFO: vacuuming "public.t1"
psql:make_data4postgresql.sql:12: INFO: "t1": found 0 removable, 10 nonremovable row versions in 1 out of 1 pages
DETAIL: 0 dead row versions cannot be removed yet, oldest xmin: 530026
There were 0 unused item identifiers.
Skipped 0 pages due to buffer pins, 0 frozen pages.
0 pages are entirely empty.
CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s.
psql:make_data4postgresql.sql:12: INFO: analyzing "public.t1"
psql:make_data4postgresql.sql:12: INFO: "t1": scanned 1 of 1 pages, containing 10 live rows and 0 dead rows; 10 rows in sample, 10 estimated total rows
VACUUM
count
-------
10
(1 行)

DO
psql:make_data4postgresql.sql:30: INFO: vacuuming "public.t2"
psql:make_data4postgresql.sql:30: INFO: "t2": found 0 removable, 50 nonremovable row versions in 1 out of 1 pages
DETAIL: 0 dead row versions cannot be removed yet, oldest xmin: 530028
There were 0 unused item identifiers.
Skipped 0 pages due to buffer pins, 0 frozen pages.
0 pages are entirely empty.
CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s.
psql:make_data4postgresql.sql:30: INFO: analyzing "public.t2"
psql:make_data4postgresql.sql:30: INFO: "t2": scanned 1 of 1 pages, containing 50 live rows and 0 dead rows; 50 rows in sample, 50 estimated total rows
VACUUM
count
-------
50
(1 行)

DO
psql:make_data4postgresql.sql:48: INFO: vacuuming "public.t3"
psql:make_data4postgresql.sql:48: INFO: "t3": found 0 removable, 100 nonremovable row versions in 1 out of 1 pages
DETAIL: 0 dead row versions cannot be removed yet, oldest xmin: 530030
There were 0 unused item identifiers.
Skipped 0 pages due to buffer pins, 0 frozen pages.
0 pages are entirely empty.
CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s.
psql:make_data4postgresql.sql:48: INFO: analyzing "public.t3"
psql:make_data4postgresql.sql:48: INFO: "t3": scanned 1 of 1 pages, containing 100 live rows and 0 dead rows; 100 rows in sample, 100 estimated total rows
VACUUM
count
-------
100
(1 行)

DO
psql:make_data4postgresql.sql:67: INFO: vacuuming "public.t4"
psql:make_data4postgresql.sql:67: INFO: "t4": found 0 removable, 500 nonremovable row versions in 3 out of 3 pages
DETAIL: 0 dead row versions cannot be removed yet, oldest xmin: 530041
There were 0 unused item identifiers.
Skipped 0 pages due to buffer pins, 0 frozen pages.
0 pages are entirely empty.
CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s.
psql:make_data4postgresql.sql:67: INFO: analyzing "public.t4"
psql:make_data4postgresql.sql:67: INFO: "t4": scanned 3 of 3 pages, containing 500 live rows and 0 dead rows; 500 rows in sample, 500 estimated total rows
VACUUM
count
-------
500
(1 行)

DO
psql:make_data4postgresql.sql:85: INFO: vacuuming "public.t0"
psql:make_data4postgresql.sql:85: INFO: "t0": found 0 removable, 1000 nonremovable row versions in 6 out of 6 pages
DETAIL: 0 dead row versions cannot be removed yet, oldest xmin: 530052
There were 0 unused item identifiers.
Skipped 0 pages due to buffer pins, 0 frozen pages.
0 pages are entirely empty.
CPU: user: 0.00 s, system: 0.00 s, elapsed: 0.00 s.
psql:make_data4postgresql.sql:85: INFO: analyzing "public.t0"
psql:make_data4postgresql.sql:85: INFO: "t0": scanned 6 of 6 pages, containing 1000 live rows and 0 dead rows; 1000 rows in sample, 1000 estimated total rows
VACUUM
count
-------
1000
(1 行)

MySQL 8.0.32向けデータ登録と統計取得からデータ件数確認までのスクリプトと実行例

mysql> \. cre_advent_tabs.sql
Query OK, 0 rows affected (0.06 sec)

Query OK, 0 rows affected (0.05 sec)

Query OK, 0 rows affected (0.05 sec)

Query OK, 0 rows affected (0.04 sec)

Query OK, 0 rows affected (0.07 sec)

mysql>

MySQL向けデータ登録と統計取得からデータ件数確認までのスクリプトと実行例

make_data4mysql.sql

DELIMITER //
CREATE PROCEDURE make_data4mysql()
BEGIN
DECLARE i, j, k INT;
-- for t1
SET i = 1;
WHILE i <= 10 DO
INSERT INTO t1 VALUES(i,i);
SET i = i + 1;
END WHILE;
COMMIT;

-- for t2
SET i = 1;
WHILE i <= 10 DO
SET j = 1;
WHILE j <= 5 DO
INSERT INTO t2 VALUES(i,j,i+1);
SET j = j + 1;
END WHILE;
SET i = i + 1;
END WHILE;
COMMIT;

-- for t3
SET i = 1;
WHILE i <= 10 DO
SET j = 1;
WHILE j <= 10 DO
INSERT INTO t3 VALUES(i,j,i+1);
SET j = j + 1;
END WHILE;
SET i = i + 1;
END WHILE;
COMMIT;

-- for t4
SET i = 1;
WHILE i <= 10 DO
SET j = 1;
WHILE j <= 10 DO
SET k = 1;
WHILE k <= 5 DO
INSERT INTO t4 VALUES(i,j,k,i+j);
SET k = k + 1;
END WHILE;
SET j = j + 1;
END WHILE;
SET i = i + 1;
COMMIT;
END WHILE;

-- for t0
SET i = 1;
WHILE i <= 10 DO
SET j = 1;
WHILE j <= 100 DO
INSERT INTO t0 VALUES(i,j,i+j);
SET j = j + 1;
END WHILE;
SET i = i + 1;
COMMIT;
END WHILE;
END
//

DELIMITER ;

CALL make_data4mysql();

ANALYZE TABLE t1, t2, t3, t4, t0;

SELECT COUNT(1) FROM t1;
SELECT COUNT(1) FROM t2;
SELECT COUNT(1) FROM t3;
SELECT COUNT(1) FROM t4;
SELECT COUNT(1) FROM t0;

DROP PROCEDURE make_data4mysql;

mysql> \. make_data4mysql.sql
Query OK, 0 rows affected (0.03 sec)

Query OK, 0 rows affected (11.20 sec)

+---------------+---------+----------+----------+
| Table | Op | Msg_type | Msg_text |
+---------------+---------+----------+----------+
| perftestdb.t1 | analyze | status | OK |
| perftestdb.t2 | analyze | status | OK |
| perftestdb.t3 | analyze | status | OK |
| perftestdb.t4 | analyze | status | OK |
| perftestdb.t0 | analyze | status | OK |
+---------------+---------+----------+----------+
5 rows in set (0.07 sec)

+----------+
| COUNT(1) |
+----------+
| 10 |
+----------+
1 row in set (0.01 sec)

+----------+
| COUNT(1) |
+----------+
| 50 |
+----------+
1 row in set (0.00 sec)

+----------+
| COUNT(1) |
+----------+
| 100 |
+----------+
1 row in set (0.02 sec)

+----------+
| COUNT(1) |v+----------+
| 500 |
+----------+
1 row in set (0.01 sec)

+----------+
| COUNT(1) |
+----------+
| 1000 |
+----------+
1 row in set (0.02 sec)

Query OK, 0 rows affected (0.01 sec)

mysql>
準備できた!




実行計画を理解するための参考
(Oracleのマニュアルで解説されているBushy Join Treeは、Zigzag Join Treeと言われている結合ツリーに見えますよね。Oracleは、Bushy/Zigzagは区別してなさそう)

参考)
Hash JoinのBuild/Probeって何? という方は、以下も読んでおくと良いです

Oracle Database Release 23 / SQL Tuning Guide - 9.2.2.2.2 Hash Join: Basic Steps

https://docs.oracle.com/en/database/oracle/oracle-database/23/tgsql/joins.html#GUID-9EE5CD4C-B90C-4E61-83DC-BD585D79635C


結合ツリーついて

Oracle Database Release 23 / SQL Tuning Guide - 9.1.1 Join Trees

https://docs.oracle.com/en/database/oracle/oracle-database/23/tgsql/joins.html#GUID-31B0F249-A5AA-41E9-AE98-A484FC5C487C


20231123-203825




では最初に、Oracle Database 21cから。
それっぽいの形にしたHash Joinの実行計画をお見せします。
これは、Oracleのマニュアルで言うと、Bushy Join Tree です。

SCOTT@orclpdb1> @ora_sql_hj.sql
1 SELECT
2 /*+
3 MONITOR
4 USE_HASH(t1 t2 t3 t4)
5 */
6 t1.id
7 , t1.t1_c1
8 , t2.s_id
9 , t2.t2_c1
10 , t3.b_id
11 , t3.t3_c1
12 , t4.a_id
13 , t4.c_id
14 , t4.t4_c1
15 FROM
16 t1
17 INNER JOIN t2
18 ON
19 t1.id = t2.id
20 INNER JOIN t3
21 ON
22 t1.id = t3.id
23 INNER JOIN t4
24 ON
25* t1.id = t4.id

...略...

SQL Plan Monitoring Details (Plan Hash Value=122725940)
===================================================================================================================================================
| Id | Operation | Name | Rows | Cost | Time | Start | Execs | Rows | Read | Read | Mem | Activity | Activity Detail |
| | | | (Estim) | | Active(s) | Active | | (Actual) | Reqs | Bytes | (Max) | (%) | (# samples) |
===================================================================================================================================================
| 0 | SELECT STATEMENT | | | | 1 | +0 | 1 | 25000 | | | . | | |
| 1 | HASH JOIN | | 25000 | 12 | 1 | +0 | 1 | 25000 | | | 1MB | | |
| 2 | TABLE ACCESS FULL | T4 | 500 | 3 | 1 | +0 | 1 | 500 | 2 | 49152 | . | | |
| 3 | HASH JOIN | | 500 | 9 | 1 | +0 | 1 | 500 | | | 1MB | | |
| 4 | HASH JOIN | | 50 | 6 | 1 | +0 | 1 | 50 | | | 1MB | | |
| 5 | TABLE ACCESS FULL | T1 | 10 | 3 | 1 | +0 | 1 | 10 | 2 | 49152 | . | | |
| 6 | TABLE ACCESS FULL | T2 | 50 | 3 | 1 | +0 | 1 | 50 | 2 | 49152 | . | | |
| 7 | TABLE ACCESS FULL | T3 | 100 | 3 | 1 | +0 | 1 | 100 | 2 | 49152 | . | | |
===================================================================================================================================================

上記の解説

Id=5 T1をTable Full Scan (Build表)
Id=6 T2をTable Full Scan (Probe表).
Id=4 T1とT2をHash Join (on memoryでHash Join完了)

Id=4 Id=5,6のHash Joinの結果(Build表)
Id=7 T3をTable Full Scan (Probe表)
Id=3 Id=4の結合結果とT3をHash Join (on memoryでHash Join完了)

Id=2 T4をTable Full Scan (Build表)
Id=3 Id=4の結合結果とT3の結合結果 (Probe表)
Id=2 T4とId=3の結合結果をHash Join (On memoryでHash Join完了)

という順序なので、
引用したOracleのマニュアルで言う、Bushy Join Tree になっています。
Oracleのマニュアルでは、Bushy Join Treeとなっていますが、他では、Zigzag Join Treeとして分類されていたりするツリーです。。LeftとRight Deep Join TreeがMixされた形ですよね、これ。

これを結合ツリーで書くと、以下のようになります。

20231208-195525


次は、PostgreSQL 13.4 with pg_hint_plan 1.3.9で同じくHash Joinになるようにヒントで強制した実行計画です。

この結合ツリーは、Bushy Join Treeですね。

perftestdb=> \! cat pg_sql_hj.sql
explain (analyze)
SELECT
/*+
HashJoin(t1 t2 t3 t4)
*/
t1.id
, t1.t1_c1
, t2.s_id
, t2.t2_c1
, t3.b_id
, t3.t3_c1
, t4.a_id
, t4.c_id
, t4.t4_c1
FROM
t1
INNER JOIN t2
ON
t1.id = t2.id
INNER JOIN t3
ON
t1.id = t3.id
INNER JOIN t4
ON
t1.id = t4.id
;
perftestdb=> \i pg_sql_hj.sql
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------
Id=1 Hash Join (cost=17.48..309.84 rows=25000 width=36) (actual time=3.405..10.466 rows=25000 loops=1)
Hash Cond: (t1.id = t2.id)
Id=2 -> Hash Join (cost=1.23..11.09 rows=500 width=24) (actual time=1.696..1.989 rows=500 loops=1)
Hash Cond: (t4.id = t1.id)
Id=3 -> Seq Scan on t4 (cost=0.00..8.00 rows=500 width=16) (actual time=0.907..0.993 rows=500 loops=1)
Id=4 -> Hash (cost=1.10..1.10 rows=10 width=8) (actual time=0.721..0.722 rows=10 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 9kB
Id=5 -> Seq Scan on t1 (cost=0.00..1.10 rows=10 width=8) (actual time=0.705..0.707 rows=10 loops=1)
Id=6 -> Hash (cost=10.00..10.00 rows=500 width=24) (actual time=1.687..1.687 rows=500 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 36kB
Id=7 -> Hash Join (cost=2.12..10.00 rows=500 width=24) (actual time=1.400..1.554 rows=500 loops=1)
Hash Cond: (t3.id = t2.id)
Id=8 -> Seq Scan on t3 (cost=0.00..2.00 rows=100 width=12) (actual time=0.701..0.712 rows=100 loops=1)
Id=9 -> Hash (cost=1.50..1.50 rows=50 width=12) (actual time=0.679..0.680 rows=50 loops=1)
Buckets: 1024 Batches: 1 Memory Usage: 11kB
Id=10 -> Seq Scan on t2 (cost=0.00..1.50 rows=50 width=12) (actual time=0.648..0.656 rows=50 loops=1)

解説しやすいように実行計画のノードにIdを振っています:)

Oracleの実行計画と大きく違うのは、Build表とProbe表の順序です。Oracleの場合、同一階層の上位が、Build表で、その下にProbe表がリストされますが、
PostgreSQL(MySQLも同様)では、同一階層上で、Probe表が上位に、その下に Hash(ハッシュ表を作成している操作。Oracleでは実行計画上この操作は現れません)、さらにその下位階層にBuild表のTable Full scanという形で表現されています。
階層の一番深い部分から実行されるので、上記例だと、Id=16のt2(Build表)のSeq Scan (Oracleで言うTable Full Scanが最初に実行され、次に、Id=15のHashが行われていることがわかります。


Id=10 t2 Seq Scan (Oracleで言うTable Full Scan)
Id=9 Id=10の結果を元にHash表作成 (つまり、t2がBuild)
Id=8 t3 Seq Scan (Probe)
Id=7 t2, t3をHash Join

Id=6 Id=7(t2, t3のHash Join)の結果を元にHash表作成(Build)

Id=5 t1 Seq Scan
Id=4 Id=5の結果を元にHash表作成(Build)
Id=3 t4 Seq Scan (Probe)
Id=2 t1, t4をHash Join

Id=1 Id=6の結果(Build), Id=2の結果(Probe)としてHash Join

こうやって1Step毎に追っていくと、OracleとPostgreSQLの実行計画のTree listで、Hash JoinのBuildとProbeの位置が異なっていることに気づきやすいですよね。(ちなみに、MySQLのTree formatも同様です)

これを結合ツリーで書くと、以下のようになります。

20231208-195537


最後に、MySQL 8.0.32です。
Hash Joinを強制する直接的なヒントも、Budling/Probe表を制御するヒントも方法存在しませんが、Hash Joinになるよう結合順を指定した上で、Nested Loop Joinになってしまうのを抑止するために、Nested Loop Joindで利用する主キー索引の利用を抑止しました。
(MySQLの主キーはクラスター索引なので、主キーがNested Loop Joinに利用される場合、索引の利用を抑止するには、NO_JOIN_INDEXヒントで primary を指定する必要があります)

なお、JOIN_ORDERヒントで結合順を指定した理由は、結合順を指定することでNested Loop Joinに利用される主キー(primary)を、NO_JOIN_INDEXヒントで確実抑止したかったためです。

ちなみに、この結合ツリーは、Left Deep Join Treeですね。

mysql> \! cat my_sql_leftdj.sql
explain analyze
SELECT
/*+
JOIN_ORDER(t1,t2,t3,t4)
NO_JOIN_INDEX(t2 primary)
NO_JOIN_INDEX(t3 primary)
NO_JOIN_INDEX(t4 primary)
*/
t1.id
, t1.t1_c1
, t2.s_id
, t2.t2_c1
, t3.b_id
, t3.t3_c1
, t4.a_id
, t4.c_id
, t4.t4_c1
FROM
t1
INNER JOIN t2
ON
t1.id = t2.id
INNER JOIN t3
ON
t1.id = t3.id
INNER JOIN t4
ON
t1.id = t4.id
;

mysql> \. my_sql_leftdj.sql
+-----------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+-----------------------------------------------------------------------------------------------------------------------+
Id=1 | -> Inner hash join (t4.id = t1.id) (cost=25552.55 rows=25000) (actual time=0.643..5.045 rows=25000 loops=1)
Id=2 -> Table scan on t4 (cost=0.01 rows=500) (actual time=0.052..0.346 rows=500 loops=1)
Id=3 -> Hash
Id=4 -> Inner hash join (t3.id = t1.id) (cost=551.75 rows=500) (actual time=0.216..0.380 rows=500 loops=1)
Id=5 -> Table scan on t3 (cost=0.03 rows=100) (actual time=0.041..0.069 rows=100 loops=1)
Id=6 -> Hash
Id=7 -> Inner hash join (t2.id = t1.id) (cost=51.50 rows=50) (actual time=0.100..0.141 rows=50 loops=1)
Id=8 -> Table scan on t2 (cost=0.08 rows=50) (actual time=0.026..0.041 rows=50 loops=1)
Id=9 -> Hash
Id=10 -> Table scan on t1 (cost=1.25 rows=10) (actual time=0.039..0.047 rows=10 loops=1)
|
+-----------------------------------------------------------------------------------------------------------------------+

既にお分かりだと思いますが、答え合わせです。:)

OracleのRight Deep Joinに見慣れている皆さん(私も含むw)が、Right Deep Joinと勘違いしちゃうケースはありますが、Build表とProbe表の現れる位置が、Oracleの実行計画と逆なんですよね。
(何度見ても、OracleでRight Deep Join Treeにした実行計画の形とダブってしまって、慣れないw)
このような形になっているHash Joinの実行計画って、PostgreSQLも含め、Left Deep Join Treeなんですよね。


参考)MySQLが、Left Deep Join Treeにしかならない理由はこれ。
Chapter 4. Query Performance Optimization The execution plan
https://www.oreilly.com/library/view/high-performance-mysql/9780596101718/ch04.html#how_mysql_joins_multiple_table
"MySQL always begins with one table and finds matching rows in the next table. Thus, MySQL’s query execution plans always take the form of a left-deep tree"とあります。

Nested Loop Joinって、Left Deep Join Treeになるから、MySQLだとHash Joinでも、 Left Deep Join Treeなのかなぁ。と。ただ、一時期、クラウド方面にあるMySQL互換で、HASH_JOIN_BUILDINGとかいうヒントが加えられていたこともありました。NewSQL系でも拡張されているケースもあるようです。

前述のMySQLの実行計画を追っかけてみると、、、


Id=10 t1 Table Scan
Id=11 Id=10で読み込んだ結果からHash表を作成(つまり、t1がBuild)
Id=7,8 T2 Table Scan (Probe)してId=11とHash Join
Id=6 Id=7のHash Joinの結果からHash表作成 (Build)
Id=4,5 T3 Table Scan (Probe)してId=6とHash Join
Id=3 Id=4のHash Joinの結果からHash表作成(Build)
Id=1,2 T4 Table Scan (Probe)しId=3とHash Join


これを結合ツリーで書くと、以下のように Left Deep Treeになりまよね。
(表示されているExlplainの実行計画を見ていると、OracleのHash JoinでRight Deep Join Treeに錯覚してしまうのなんとかならんかねぇ。と思いつつ、慣れるしかないな。と言う答えしか出てこなかった。PostgreSQLも同じ表示方法だし。慣れろ→自分w)

20231208-195545




ということで、
MySQLのHash Joinの実行計画ツリーを見て、何か気づきませんか? 
みなさん! これ試験に出ますよw(嘘

の答えは、

MySQLは、Left Deep Join Tree しか、しない。でした!



ざっくりとしたまとめ的なやつ
Oracle/PostgreSQL/MySQLそれぞれ、Hash Joinだけを強制して結合順はオプティマイザー任せにした場合。

Oracle/PostgreSQLは、Bushy Join Tree
データ次第では、Left/Right Deep Join Treeの可能性もあるでしょうけども。今回のケースでは、Bushy Join Tree, 細かめに分類すると、Oracleの結合ツリーは、Zigzagですね。ちなみに、Oracleには、BUSHY_JOINというヒントがあったりします(使ったことないけどw)。

MySQLは、Left Deep Join Tree にしかならんよ。と。

以下の方針が変更されない限り、変わることはなさそう。

再掲

https://www.oreilly.com/library/view/high-performance-mysql/9780596101718/ch04.html#how_mysql_joins_multiple_table
"MySQL always begins with one table and finds matching rows in the next table. Thus, MySQL’s query execution plans always take the form of a left-deep tree"


予習、その3へ続く。

では、また。:)







関連エントリー

標準はあるにはあるが癖の多いSQL 全部俺 #1 Pagination
標準はあるにはあるが癖の多いSQL 全部俺 #2 関数名は同じでも引数が逆の罠!
標準はあるにはあるが癖の多いSQL 全部俺 #3 データ型確認したい時あるんです
標準はあるにはあるが癖の多いSQL 全部俺 #4 リテラル値での除算の内部精度も違うのよ!
標準はあるにはあるが癖の多いSQL 全部俺 #5 和暦変換機能ある方が少数派
標準はあるにはあるが癖の多いSQL 全部俺 #6 時間厳守!
標準はあるにはあるが癖の多いSQL 全部俺 #7 期間リテラル!
標準はあるにはあるが癖の多いSQL 全部俺 #8 翌月末日って何日?
標準はあるにはあるが癖の多いSQL 全部俺 #9 部分文字列の扱いでも癖が出る><
標準はあるにはあるが癖の多いSQL 全部俺 #10 文字列連結の罠(有名なやつ)
標準はあるにはあるが癖の多いSQL 全部俺 #11 デュエル、じゃなくて、デュアル
標準はあるにはあるが癖の多いSQL 全部俺 #12 文字[列]探すにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #13 あると便利ですが意外となかったり
標準はあるにはあるが癖の多いSQL 全部俺 #14 連番の集合を返すにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #15 SQL command line client
標準はあるにはあるが癖の多いSQL 全部俺 #16 SQLのレントゲンを撮る方法
標準はあるにはあるが癖の多いSQL 全部俺 #17 その空白は許されないのか?
標準はあるにはあるが癖の多いSQL 全部俺 #18 (+)の外部結合は方言
標準はあるにはあるが癖の多いSQL 全部俺 #19 帰ってきた、部分文字列の扱いでも癖w
標準はあるにはあるが癖の多いSQL 全部俺 #20 結果セットを単一列に連結するにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #21 演算結果にも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #22 集合演算にも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #23 複数行INSERTにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #24 乱数作るにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #25 SQL de Fractalsにも癖がある:)
標準はあるにはあるが癖の多いSQL 全部俺 おまけ SQL de 湯婆婆やるにも癖がでるw
帰ってきた! 標準はあるにはあるが癖の多いSQL #1 SQL de ROT13 やるにも癖が出るw
帰ってきた! 標準はあるにはあるが癖の多いSQL #2 Actual Plan取得中のキャンセルでも癖が出る
帰ってきた! 標準はあるにはあるが癖の多いSQL #3 オプティマイザの結合順評価テーブル数上限にも癖が出る
帰ってきた! 標準はあるにはあるが癖の多いSQL #4 Optimizer Traceの取得でも癖がでる
帰ってきた! 標準はあるにはあるが癖の多いSQL #5 - Optimizer Hint でも癖が多い


| | | コメント (0)

2023年11月22日 (水)

帰ってきた! 標準はあるにはあるが癖の多いSQL #5 - Optimizer Hint でも癖が多い

癖の多いSQLシリーズネタは沢山あるわけですが、今回は、Oracle/PostgreSQL/MySQLで利用可能なオプティマイザヒントのざっくりしたまとめ。

というか、Advent Calendar向けの準備運動的ななエントリーその1. 意味の同じオプティマイザーヒントを使った何かをネタにしようとしてまーすw(その2は、どうするか検討中w)

各DBの Optimizer Hint (PostgreSQLは、pg_hint_plan拡張機能を利用)の数(特定の最適化の有効化、無効化ヒントはそれぞれ1ヒントとしてカウント。e.g. INDEX, NO_INDEXで2つとカウントする)

 

Oracle Database 21c : 388

(圧倒的に多いヒント。まあ、Oracle Database 8.1ぐらいからありますからねぇw 様々な最適化を制御できるようになっているということでもあるわけですけども。それにしても多いですね)

 

PostgreSQL with pg_hint_plan : 26

(pg_hint_planのドキュメントから拾って数得ました。これぐらいなんですね。)

 

MySQL 8.0.x : 37

(MySQL 8.0の Optimizer Hint には、HASH_JOIN/NO_HASH_JOINのように特定のリリースでしか使えないものなども含まれる)

 

MySQL系のAurora MySQL compatible, TiDBなどは、独自のヒントをいくつか追加しているものもある。e.g. HASH JOINのBUILD/PROBE表を制御するものなど。。ただし、Aurora MySQLの場合は、Vanila MySQLでHASH_JOINヒントが廃止された影響で、HASH_JOIN_BUILDINGなどの関連ヒントが廃止されるなど、各DB毎に多少温度差のある対応があり、Vanila以外のMySQLを利用する場合には利用可能な Optimizer Hint は個別に調査動作確認しておくことをお勧めする。

 

参考 Aurora MySQL hints(独自拡張あり)

https://docs.aws.amazon.com/AmazonRDS/latest/AuroraUserGuide/AuroraMySQL.Reference.Hints.html

 

TiDB(独自拡張あり)

https://docs.pingcap.com/ja/tidb/stable/optimizer-hints

 

YagabyteDB (pg_hint_planを使うようなので、Vanila PostgreSQLと同じ!)

https://docs.yugabyte.com/preview/explore/query-1-performance/pg-hint-plan/

 

 

Oracleのマニュアルでは、全量が解説されているわけではないので、v$sql_hintビューを問い合わせた結果も載せておきます。(ドキュメントのリンク貼っても、OracleさんのドキュメントURLって簡単にリンク切れになるのですが、取り敢えず貼っときますw)
YOracle Database 21c SQL Language Reference / Table 2-23 Hints by Functional Category

https://docs.oracle.com/en/database/oracle/oracle-database/21/sqlrf/Comments.html#GUID-D316D545-89E2-4D54-977F-FC97815CD62E__BABEAFGF

中には、黒魔術すぎた影響でw 指定しても無視(ヒントレポートでは、常に Error 扱い)されるヒントもあります( 参考: https://blogs.oracle.com/otnjp/post/tsushima-hakushi-11 )


(10gぐらいまでのSQLチューニング経験者なら一度ぐらいは聞いたことがあるかもしれねい、BYPASS_UJVC ヒントが有名ですね. ちなみに、このヒント単体で使うと単純に無視されヒントレーポートにも現れません。なお、v$sql_hintには残されていますw)


DISCUS@ORCLCDB> select * from v$sql_hint order by name;

NAME SQL_FEATURE CLASS INVERSE TARGET_LEVEL PROPERTY VERSION VERSION_OU
---------------------------------------- ------------------------------ ------------------------------ ------------------------------ ------------ ---------- ---------- ----------
ADAPTIVE_PLAN QKSFM_ADAPTIVE_PLAN ADAPTIVE_PLAN NO_ADAPTIVE_PLAN 1 16 12.1.0.2 12.1.0.2
ALL_ROWS QKSFM_ALL_ROWS MODE 1 16 8.1.0 10.2.0.1
ANALYTIC_VIEW_SQL QKSFM_COMPILATION ANALYTIC_VIEW_SQL 2 0 20.1.0
AND_EQUAL QKSFM_AND_EQUAL ACCESS 4 304 8.1.0 8.1.7
ANSI_REARCH QKSFM_ANSI_REARCH ANSI_REARCH NO_ANSI_REARCH 2 16 12.1.0.2 12.1.0.2
ANSWER_QUERY_USING_STATS QKSFM_ANSWER_QUERY_USING_STATS ANSWER_QUERY_USING_STATS NO_ANSWER_QUERY_USING_STATS 2 16 18.1.0 18.1.0
ANTIJOIN QKSFM_TRANSFORMATION ANTIJOIN 2 16 9.0.0
APPEND QKSFM_CBO APPEND NOAPPEND 1 0 8.1.0
APPEND_VALUES QKSFM_CBO APPEND_VALUES NOAPPEND 1 0 11.2.0.1
AUTO_REOPTIMIZE QKSFM_AUTO_REOPT AUTO_REOPTIMIZE NO_AUTO_REOPTIMIZE 1 0 12.1.0.1
AV_CACHE QKSFM_EXECUTION AV_CACHE 2 0 18.1.0
BATCH_TABLE_ACCESS_BY_ROWID QKSFM_EXECUTION BATCH_TABLE_ACCESS_BY_ROWID NO_BATCH_TABLE_ACCESS_BY_ROWID 4 272 12.1.0.1 12.1.0.1
BIND_AWARE QKSFM_CURSOR_SHARING BIND_AWARE NO_BIND_AWARE 1 0 11.1.0.7
BITMAP QKSFM_CBO BITMAP 2 256 8.1.0 8.1.5
BITMAP_AND QKSFM_BITMAP_TREE BITMAP_AND 4 48 12.1.0.1 12.1.0.1
BITMAP_TREE QKSFM_BITMAP_TREE ACCESS 4 304 10.2.0.1 10.2.0.1
BUFFER QKSFM_CBO BUFFER NO_BUFFER 2 0 8.1.5
BUSHY_JOIN QKSFM_BUSHY_JOIN BUSHY_JOIN NO_BUSHY_JOIN 2 16 12.2.0.1 12.2.0.1
BYPASS_RECURSIVE_CHECK QKSFM_ALL BYPASS_RECURSIVE_CHECK 2 0 9.0.0
BYPASS_UJVC QKSFM_CBO BYPASS_UJVC 2 0 8.1.5
CACHE QKSFM_EXECUTION CACHE NOCACHE 4 256 8.1.0
CACHE_CB QKSFM_CBO CACHE_CB NOCACHE 4 256 8.1.5
CARDINALITY QKSFM_STATS CARDINALITY 14 272 9.0.0
CHANGE_DUPKEY_ERROR_INDEX QKSFM_DML CHANGE_DUPKEY_ERROR_INDEX 4 288 11.1.0.7
CHECK_ACL_REWRITE QKSFM_CHECK_ACL_REWRITE CHECK_ACL_REWRITE NO_CHECK_ACL_REWRITE 1 0 11.1.0.6
CHOOSE QKSFM_CHOOSE MODE 1 16 8.1.0
CLUSTER QKSFM_CBO ACCESS 4 272 8.0.0 8.1.7
CLUSTERING QKSFM_CLUSTERING CLUSTERING NO_CLUSTERING 1 0 12.1.0.1 12.1.0.1
CLUSTER_BY_ROWID QKSFM_CBO CLUSTER_BY_ROWID NO_CLUSTER_BY_ROWID 4 272 12.1.0.1 12.1.0.1
COALESCE_SQ QKSFM_COALESCE_SQ COALESCE_SQ NO_COALESCE_SQ 2 16 11.2.0.1 11.2.0.1
COLUMN_STATS QKSFM_STATS TABLE_STATS 1 272 10.1.0.3
CONNECT_BY_CB_WHR_ONLY QKSFM_TRANSFORMATION CONNECT_BY_CB_WHR_ONLY NO_CONNECT_BY_CB_WHR_ONLY 2 16 10.2.0.5 10.2.0.5
CONNECT_BY_COMBINE_SW QKSFM_ALL CONNECT_BY_COMBINE_SW NO_CONNECT_BY_COMBINE_SW 2 16 10.2.0.4 10.2.0.4
CONNECT_BY_COST_BASED QKSFM_TRANSFORMATION CONNECT_BY_COST_BASED NO_CONNECT_BY_COST_BASED 2 16 10.2.0.2 10.2.0.2
CONNECT_BY_ELIM_DUPS QKSFM_ALL CONNECT_BY_ELIM_DUPS NO_CONNECT_BY_ELIM_DUPS 2 16 11.2.0.1 11.2.0.1
CONNECT_BY_FILTERING QKSFM_ALL CONNECT_BY_FILTERING NO_CONNECT_BY_FILTERING 2 16 10.2.0.2 10.2.0.2
CONTAINERS QKSFM_ALL CONTAINERS 1 0 12.2.0.1 12.2.0.1
COST_XML_QUERY_REWRITE QKSFM_COST_XML_QUERY_REWRITE COST_XML_QUERY_REWRITE NO_COST_XML_QUERY_REWRITE 1 0 11.1.0.6 11.1.0.6
CPU_COSTING QKSFM_CPU_COSTING CPU_COSTING NO_CPU_COSTING 2 16 9.0.0
CUBE_AJ QKSFM_JOIN_METHOD ANTIJOIN 2 16 12.1.0.1 12.1.0.1
CUBE_GB QKSFM_CBO CUBE_GB 2 0 8.1.5
CUBE_SJ QKSFM_JOIN_METHOD SEMIJOIN 2 16 12.1.0.1 12.1.0.1
CURRENT_INSTANCE QKSFM_ALL CURRENT_INSTANCE 1 0 18.1.0
CURSOR_SHARING_EXACT QKSFM_CBO CURSOR_SHARING_EXACT 2 0 9.0.0
DAGG_OPTIM_GSETS QKSFM_GROUPING_SET_XFORM DAGG_OPTIM_GSETS NO_DAGG_OPTIM_GSETS 2 0 21.1.0 21.1.0
DATA_SECURITY_REWRITE_LIMIT QKSFM_DATA_SECURITY_REWRITE DATA_SECURITY_REWRITE_LIMIT NO_DATA_SECURITY_REWRITE 1 0 12.1.0.1 12.1.0.1
DATA_VALIDATE QKSFM_EXECUTION DATA_VALIDATE 1 0 12.2.0.1
DBMS_STATS QKSFM_DBMS_STATS DBMS_STATS 1 0 10.2.0.1
DB_VERSION QKSFM_ALL DB_VERSION 1 272 11.1.0.6 11.1.0.6
DECORRELATE QKSFM_DECORRELATE DECORRELATE NO_DECORRELATE 2 16 12.1.0.1 12.1.0.1
DENORM_AV QKSFM_COMPILATION DENORM_AV 2 0 20.1.0
DEREF_NO_REWRITE QKSFM_ALL DEREF_NO_REWRITE 1 0 8.1.0
DISABLE_PARALLEL_DML QKSFM_DML ENABLE_PARALLEL_DML ENABLE_PARALLEL_DML 1 0 11.2.0.4
DIST_AGG_PROLLUP_PUSHDOWN QKSFM_PQ DIST_AGG_PROLLUP_PUSHDOWN NO_DIST_AGG_PROLLUP_PUSHDOWN 2 16 12.2.0.1 12.2.0.1
DML_UPDATE QKSFM_CBO DML_UPDATE 1 0 9.0.0
DOMAIN_INDEX_FILTER QKSFM_CBO DOMAIN_INDEX_FILTER NO_DOMAIN_INDEX_FILTER 4 304 11.1.0.6 11.1.0.6
DOMAIN_INDEX_NO_SORT QKSFM_CBO DOMAIN_INDEX_SORT DOMAIN_INDEX_SORT 2 0 8.1.5 10.2.0.1
DOMAIN_INDEX_SORT QKSFM_CBO DOMAIN_INDEX_SORT DOMAIN_INDEX_NO_SORT 2 0 8.1.5 10.2.0.1
DRIVING_SITE QKSFM_ALL DRIVING_SITE 4 256 8.1.0 8.1.7
DST_UPGRADE_INSERT_CONV QKSFM_ALL DST_UPGRADE_INSERT_CONV NO_DST_UPGRADE_INSERT_CONV 1 0 11.2.0.1
DYNAMIC_SAMPLING QKSFM_DYNAMIC_SAMPLING DYNAMIC_SAMPLING 6 272 9.2.0
DYNAMIC_SAMPLING_EST_CDN QKSFM_DYNAMIC_SAMPLING_EST_CDN DYNAMIC_SAMPLING_EST_CDN 4 272 9.2.0
ELIMINATE_JOIN QKSFM_TABLE_ELIM ELIMINATE_JOIN NO_ELIMINATE_JOIN 4 16 10.2.0.1 10.2.0.1
ELIMINATE_OBY QKSFM_OBYE ELIMINATE_OBY NO_ELIMINATE_OBY 2 16 10.2.0.1 10.2.0.1
ELIMINATE_SQ QKSFM_ELIMINATE_SQ ELIMINATE_SQ NO_ELIMINATE_SQ 2 16 12.2.0.1 12.2.0.1
ELIM_GROUPBY QKSFM_TRANSFORMATION ELIM_GROUPBY NO_ELIM_GROUPBY 2 16 12.1.0.2 12.1.0.2
ENABLE_PARALLEL_DML QKSFM_DML ENABLE_PARALLEL_DML DISABLE_PARALLEL_DML 1 0 11.2.0.4
EXPAND_GSET_TO_UNION QKSFM_TRANSFORMATION EXPAND_GSET_TO_UNION NO_EXPAND_GSET_TO_UNION 2 0 9.2.0 10.1.0
EXPAND_TABLE QKSFM_TABLE_EXPANSION EXPAND_TABLE NO_EXPAND_TABLE 4 16 11.2.0.1 11.2.0.1
EXPR_CORR_CHECK QKSFM_CBO EXPR_CORR_CHECK 1 0 8.0.0
FACT QKSFM_STAR_TRANS FACT NO_FACT 4 272 8.1.0 8.1.7
FACTORIZE_JOIN QKSFM_JOINFAC FACTORIZE_JOIN NO_FACTORIZE_JOIN 2 16 11.2.0.1 11.2.0.1
FBTSCAN QKSFM_CBO FBTSCAN 1 0 10.1.0.3
FIRST_ROWS QKSFM_FIRST_ROWS MODE 1 16 8.1.0 10.2.0.1
FORCE_JSON_TABLE_TRANSFORM QKSFM_JSON_REWRITE FORCE_JSON_TABLE_TRANSFORM NO_JSON_TABLE_TRANSFORM 1 0 20.1.0 20.1.0
FORCE_XML_QUERY_REWRITE QKSFM_XML_REWRITE FORCE_XML_QUERY_REWRITE NO_XML_QUERY_REWRITE 1 0 9.2.0 11.1.0.6
FRESH_MV QKSFM_MVIEWS FRESH_MV 1 0 12.2.0.1
FULL QKSFM_FULL ACCESS 4 272 8.1.0 8.1.7
FULL_OUTER_JOIN_TO_OUTER QKSFM_CBO FULL_OUTER_JOIN_TO_OUTER NO_FULL_OUTER_JOIN_TO_OUTER 4 272 11.2.0.3 11.2.0.3
GATHER_OPTIMIZER_STATISTICS QKSFM_DBMS_STATS GATHER_OPTIMIZER_STATISTICS NO_GATHER_OPTIMIZER_STATISTICS 1 0 12.1.0.1
GATHER_PLAN_STATISTICS QKSFM_GATHER_PLAN_STATISTICS GATHER_PLAN_STATISTICS 1 0 10.1.0.3
GBY_CONC_ROLLUP QKSFM_TRANSFORMATION GBY_CONC_ROLLUP 2 0 9.0.0
GBY_PUSHDOWN QKSFM_ALL GBY_PUSHDOWN NO_GBY_PUSHDOWN 2 16 10.2.0.5 10.2.0.5
HASH QKSFM_ALL ACCESS 4 272 8.1.0 8.1.7
HASHSET_BUILD QKSFM_EXECUTION HASHSET_BUILD 2 16 21.1.0 21.1.0
HASH_AJ QKSFM_JOIN_METHOD ANTIJOIN 2 16 8.1.0 8.1.7
HASH_SJ QKSFM_JOIN_METHOD SEMIJOIN 2 16 8.1.0 8.1.7
HWM_BROKERED QKSFM_CBO HWM_BROKERED 2 0 9.0.0
IGNORE_OPTIM_EMBEDDED_HINTS QKSFM_ALL IGNORE_OPTIM_EMBEDDED_HINTS 1 0 10.1.0.3 10.2.0.1
IGNORE_ROW_ON_DUPKEY_INDEX QKSFM_DML IGNORE_ROW_ON_DUPKEY_INDEX 4 288 11.1.0.7
IGNORE_WHERE_CLAUSE QKSFM_ALL IGNORE_WHERE_CLAUSE 1 0 9.2.0
INCLUDE_VERSION QKSFM_ALL INCLUDE_VERSION 1 0 10.1.0.3
INDEX QKSFM_INDEX ACCESS NO_INDEX 4 304 8.0.0 8.1.7
INDEX_ASC QKSFM_INDEX_ASC ACCESS NO_INDEX 4 304 8.1.0
INDEX_COMBINE QKSFM_INDEX_COMBINE ACCESS 4 432 8.1.0 8.1.7
INDEX_DESC QKSFM_INDEX_DESC ACCESS NO_INDEX 4 304 8.1.0 8.1.7
INDEX_FFS QKSFM_INDEX_FFS ACCESS 4 304 8.1.0 8.1.7
INDEX_JOIN QKSFM_INDEX_JOIN ACCESS 4 304 8.1.5 10.1.0.3
INDEX_RRS QKSFM_CBO ACCESS 4 304 9.0.0
INDEX_RS_ASC QKSFM_INDEX_RS_ASC ACCESS 4 304 11.1.0.6 11.1.0.6
INDEX_RS_DESC QKSFM_INDEX_RS_DESC ACCESS 4 304 11.1.0.6 11.1.0.6
INDEX_SS QKSFM_INDEX_SS ACCESS NO_INDEX_SS 4 304 9.0.0 10.2.0.1
INDEX_SS_ASC QKSFM_INDEX_SS_ASC ACCESS NO_INDEX_SS 4 304 9.0.0
INDEX_SS_DESC QKSFM_INDEX_SS_DESC ACCESS NO_INDEX_SS 4 304 9.0.0 10.2.0.1
INDEX_STATS QKSFM_STATS TABLE_STATS 1 272 10.1.0.3
INLINE QKSFM_TRANSFORMATION INLINE MATERIALIZE 2 16 9.0.0 18.1.0
INLINE_XMLTYPE_NT QKSFM_ALL INLINE_XMLTYPE_NT 1 0 10.2.0.1
INMEMORY QKSFM_EXECUTION INMEMORY NO_INMEMORY 6 64 12.1.0.2 12.1.0.2
INMEMORY_PRUNING QKSFM_EXECUTION INMEMORY_PRUNING NO_INMEMORY_PRUNING 6 64 12.1.0.2 12.1.0.2
JSON_LENGTH QKSFM_EXECUTION JSON_LENGTH 1 0 19.1.0
LEADING QKSFM_JOIN_ORDER LEADING 8 272 8.1.6 10.1.0.3
LOCAL_INDEXES QKSFM_CBO LOCAL_INDEXES 2 0 9.0.0
MATERIALIZE QKSFM_TRANSFORMATION INLINE INLINE 2 16 9.0.0 18.1.0
MEMOPTIMIZE_WRITE QKSFM_EXECUTION MEMOPTIMIZE_WRITE 1 0 18.1.0
MERGE QKSFM_CVM MERGE NO_MERGE 6 16 8.1.0 10.1.0
MERGE_AJ QKSFM_JOIN_METHOD ANTIJOIN 2 16 8.1.0 8.1.7
MERGE_CONST_ON QKSFM_CBO MERGE_CONST_ON 1 0 8.0.0
MERGE_SJ QKSFM_JOIN_METHOD SEMIJOIN 2 16 8.1.0 8.1.7
MODEL_COMPILE_SUBQUERY QKSFM_TRANSFORMATION MODEL_COMPILE_SUBQUERY 2 0 10.2.0.1
MODEL_DONTVERIFY_UNIQUENESS QKSFM_TRANSFORMATION MODEL_DONTVERIFY_UNIQUENESS 2 0 10.1.0.3
MODEL_DYNAMIC_SUBQUERY QKSFM_TRANSFORMATION MODEL_DYNAMIC_SUBQUERY 2 0 10.2.0.1
MODEL_MIN_ANALYSIS QKSFM_TRANSFORMATION MODEL_MIN_ANALYSIS 2 0 10.1.0.3
MODEL_NO_ANALYSIS QKSFM_ALL MODEL_MIN_ANALYSIS 2 0 10.1.0.3
MODEL_PUSH_REF QKSFM_TRANSFORMATION MODEL_PUSH_REF NO_MODEL_PUSH_REF 2 0 10.1.0.3
MONITOR QKSFM_ALL MONITOR NO_MONITOR 1 0 11.1.0.6
MV_MERGE QKSFM_TRANSFORMATION MV_MERGE 2 0 9.0.0
NATIVE_FULL_OUTER_JOIN QKSFM_ALL NATIVE_FULL_OUTER_JOIN NO_NATIVE_FULL_OUTER_JOIN 2 16 10.2.0.3 10.2.0.3
NESTED_TABLE_FAST_INSERT QKSFM_ALL NESTED_TABLE_FAST_INSERT 1 0 10.1.0.3
NESTED_TABLE_GET_REFS QKSFM_ALL NESTED_TABLE_GET_REFS 1 0 8.1.0
NESTED_TABLE_SET_SETID QKSFM_ALL NESTED_TABLE_SET_SETID 1 0 8.1.5
NLJ_BATCHING QKSFM_EXECUTION ACCESS NO_NLJ_BATCHING 4 272 11.1.0.6 11.1.0.6
NLJ_PREFETCH QKSFM_EXECUTION NLJ_PREFETCH NO_NLJ_PREFETCH 4 272 11.1.0.6 11.1.0.6
NL_AJ QKSFM_JOIN_METHOD ANTIJOIN 2 16 8.0.0
NL_SJ QKSFM_JOIN_METHOD SEMIJOIN 2 16 8.0.0
NOAPPEND QKSFM_CBO APPEND APPEND 1 0 8.1.0
NOCACHE QKSFM_EXECUTION CACHE CACHE 4 256 8.1.0
NOPARALLEL QKSFM_PARALLEL SHARED SHARED 5 256 8.1.0
NO_ACCESS QKSFM_ALL NO_ACCESS 4 256 8.1.5 8.1.7
NO_ADAPTIVE_PLAN QKSFM_ADAPTIVE_PLAN ADAPTIVE_PLAN ADAPTIVE_PLAN 1 16 12.1.0.2 12.1.0.2
NO_ANSI_REARCH QKSFM_ANSI_REARCH ANSI_REARCH ANSI_REARCH 2 16 12.1.0.2 12.1.0.2
NO_ANSWER_QUERY_USING_STATS QKSFM_ANSWER_QUERY_USING_STATS ANSWER_QUERY_USING_STATS ANSWER_QUERY_USING_STATS 2 16 18.1.0 18.1.0
NO_AUTO_REOPTIMIZE QKSFM_AUTO_REOPT AUTO_REOPTIMIZE AUTO_REOPTIMIZE 1 0 12.1.0.1
NO_BASETABLE_MULTIMV_REWRITE QKSFM_ALL REWRITE REWRITE 2 16 10.1.0.3 10.1.0.3
NO_BATCH_TABLE_ACCESS_BY_ROWID QKSFM_EXECUTION BATCH_TABLE_ACCESS_BY_ROWID BATCH_TABLE_ACCESS_BY_ROWID 4 272 12.1.0.1 12.1.0.1
NO_BIND_AWARE QKSFM_CURSOR_SHARING BIND_AWARE BIND_AWARE 1 0 11.1.0.7
NO_BUFFER QKSFM_CBO BUFFER BUFFER 2 0 8.1.5
NO_BUSHY_JOIN QKSFM_BUSHY_JOIN BUSHY_JOIN BUSHY_JOIN 2 16 12.2.0.1 12.2.0.1
NO_CARTESIAN QKSFM_ALL NO_CARTESIAN 4 336 10.2.0.1
NO_CHECK_ACL_REWRITE QKSFM_CHECK_ACL_REWRITE NO_CHECK_ACL_REWRITE CHECK_ACL_REWRITE 1 0 11.1.0.6
NO_CLUSTERING QKSFM_CLUSTERING CLUSTERING CLUSTERING 1 0 12.1.0.1 12.1.0.1
NO_CLUSTER_BY_ROWID QKSFM_CBO CLUSTER_BY_ROWID CLUSTER_BY_ROWID 4 272 12.1.0.1 12.1.0.1
NO_COALESCE_SQ QKSFM_COALESCE_SQ COALESCE_SQ COALESCE_SQ 2 16 11.2.0.1 11.2.0.1
NO_CONNECT_BY_CB_WHR_ONLY QKSFM_TRANSFORMATION CONNECT_BY_CB_WHR_ONLY CONNECT_BY_CB_WHR_ONLY 2 16 10.2.0.5 10.2.0.5
NO_CONNECT_BY_COMBINE_SW QKSFM_ALL CONNECT_BY_COMBINE_SW CONNECT_BY_COMBINE_SW 2 16 10.2.0.4 10.2.0.4
NO_CONNECT_BY_COST_BASED QKSFM_TRANSFORMATION CONNECT_BY_COST_BASED CONNECT_BY_COST_BASED 2 16 10.2.0.2 10.2.0.2
NO_CONNECT_BY_ELIM_DUPS QKSFM_ALL CONNECT_BY_ELIM_DUPS CONNECT_BY_ELIM_DUPS 2 16 11.2.0.1 11.2.0.1
NO_CONNECT_BY_FILTERING QKSFM_ALL CONNECT_BY_FILTERING CONNECT_BY_FILTERING 2 16 10.2.0.2 10.2.0.2
NO_COST_XML_QUERY_REWRITE QKSFM_COST_XML_QUERY_REWRITE NO_COST_XML_QUERY_REWRITE COST_XML_QUERY_REWRITE 1 0 11.1.0.6 11.1.0.6
NO_CPU_COSTING QKSFM_CPU_COSTING CPU_COSTING CPU_COSTING 2 16 9.0.0
NO_DAGG_OPTIM_GSETS QKSFM_GROUPING_SET_XFORM DAGG_OPTIM_GSETS DAGG_OPTIM_GSETS 2 0 21.1.0 21.1.0
NO_DATA_SECURITY_REWRITE QKSFM_DATA_SECURITY_REWRITE DATA_SECURITY_REWRITE_LIMIT DATA_SECURITY_REWRITE_LIMIT 1 0 12.1.0.1 12.1.0.1
NO_DECORRELATE QKSFM_DECORRELATE DECORRELATE DECORRELATE 2 16 12.1.0.1 12.1.0.1
NO_DIST_AGG_PROLLUP_PUSHDOWN QKSFM_PQ DIST_AGG_PROLLUP_PUSHDOWN DIST_AGG_PROLLUP_PUSHDOWN 2 16 12.2.0.1 12.2.0.1
NO_DOMAIN_INDEX_FILTER QKSFM_CBO NO_DOMAIN_INDEX_FILTER DOMAIN_INDEX_FILTER 4 304 11.1.0.6 11.1.0.6
NO_DST_UPGRADE_INSERT_CONV QKSFM_ALL DST_UPGRADE_INSERT_CONV DST_UPGRADE_INSERT_CONV 1 0 11.2.0.1 21.1.0.1
NO_ELIMINATE_JOIN QKSFM_TABLE_ELIM ELIMINATE_JOIN ELIMINATE_JOIN 4 16 10.2.0.1 10.2.0.1
NO_ELIMINATE_OBY QKSFM_OBYE ELIMINATE_OBY ELIMINATE_OBY 2 16 10.2.0.1 10.2.0.1
NO_ELIMINATE_SQ QKSFM_ELIMINATE_SQ ELIMINATE_SQ ELIMINATE_SQ 2 16 12.2.0.1 12.2.0.1
NO_ELIM_GROUPBY QKSFM_TRANSFORMATION ELIM_GROUPBY ELIM_GROUPBY 2 16 12.1.0.2 12.1.0.2
NO_EXPAND QKSFM_USE_CONCAT OR_EXPAND USE_CONCAT 2 16 8.1.0 8.1.7
NO_EXPAND_GSET_TO_UNION QKSFM_TRANSFORMATION EXPAND_GSET_TO_UNION EXPAND_GSET_TO_UNION 2 0 9.2.0 10.1.0
NO_EXPAND_TABLE QKSFM_TABLE_EXPANSION EXPAND_TABLE EXPAND_TABLE 4 16 11.2.0.1 11.2.0.1
NO_FACT QKSFM_STAR_TRANS FACT FACT 4 272 8.1.0 8.1.7
NO_FACTORIZE_JOIN QKSFM_JOINFAC FACTORIZE_JOIN FACTORIZE_JOIN 2 16 11.2.0.1 11.2.0.1
NO_FULL_OUTER_JOIN_TO_OUTER QKSFM_CBO FULL_OUTER_JOIN_TO_OUTER FULL_OUTER_JOIN_TO_OUTER 4 272 11.2.0.3 11.2.0.3
NO_GATHER_OPTIMIZER_STATISTICS QKSFM_DBMS_STATS GATHER_OPTIMIZER_STATISTICS GATHER_OPTIMIZER_STATISTICS 1 0 12.1.0.1
NO_GBY_PUSHDOWN QKSFM_ALL GBY_PUSHDOWN GBY_PUSHDOWN 2 16 10.2.0.5 10.2.0.5
NO_INDEX QKSFM_INDEX NO_INDEX INDEX 4 304 8.1.5 8.1.7
NO_INDEX_FFS QKSFM_INDEX_FFS NO_INDEX_FFS INDEX_FFS 4 304 10.1.0.3 10.1.0.3
NO_INDEX_SS QKSFM_INDEX_SS NO_INDEX_SS INDEX_SS 4 304 10.1.0.3 10.1.0.3
NO_INMEMORY QKSFM_EXECUTION INMEMORY INMEMORY 6 64 12.1.0.2 12.1.0.2
NO_INMEMORY_PRUNING QKSFM_EXECUTION INMEMORY_PRUNING INMEMORY_PRUNING 6 64 12.1.0.2 12.1.0.2
NO_JSON_TABLE_TRANSFORM QKSFM_JSON_REWRITE FORCE_JSON_TABLE_TRANSFORM FORCE_JSON_TABLE_TRANSFORM 1 0 20.1.0 20.1.0
NO_LOAD QKSFM_EXECUTION NO_LOAD 1 0 11.1.0.6
NO_MERGE QKSFM_CVM MERGE MERGE 6 16 8.0.0 10.1.0
NO_MODEL_PUSH_REF QKSFM_ALL MODEL_PUSH_REF MODEL_PUSH_REF 2 0 10.1.0.3
NO_MONITOR QKSFM_ALL MONITOR MONITOR 1 0 11.1.0.6
NO_MONITORING QKSFM_ALL NO_MONITORING 1 0 8.0.0
NO_MULTIMV_REWRITE QKSFM_ALL REWRITE REWRITE 2 16 10.1.0.3 10.1.0.3
NO_NATIVE_FULL_OUTER_JOIN QKSFM_ALL NATIVE_FULL_OUTER_JOIN NATIVE_FULL_OUTER_JOIN 2 16 10.2.0.3 10.2.0.3
NO_NLJ_BATCHING QKSFM_EXECUTION ACCESS NLJ_BATCHING 4 272 11.1.0.6 11.1.0.6
NO_NLJ_PREFETCH QKSFM_EXECUTION NLJ_PREFETCH NLJ_PREFETCH 4 272 11.1.0.6 11.1.0.6
NO_OBY_GBYPD_SEPARATE QKSFM_PQ OBY_GBYPD_SEPARATE OBY_GBYPD_SEPARATE 2 16 21.1.0 21.1.0
NO_ORDER_ROLLUPS QKSFM_TRANSFORMATION NO_ORDER_ROLLUPS 2 0 8.0.0
NO_OR_EXPAND QKSFM_CBQT_OR_EXPANSION OR_EXPAND OR_EXPAND 2 16 12.2.0.1 12.2.0.1
NO_OUTER_JOIN_TO_ANTI QKSFM_CBO OUTER_JOIN_TO_ANTI OUTER_JOIN_TO_ANTI 4 272 11.2.0.3 11.2.0.3
NO_OUTER_JOIN_TO_INNER QKSFM_OUTER_JOIN_TO_INNER OUTER_JOIN_TO_INNER OUTER_JOIN_TO_INNER 6 16 11.1.0.6 11.1.0.6
NO_PARALLEL QKSFM_CBO SHARED SHARED 5 256 10.1.0.3
NO_PARALLEL_INDEX QKSFM_PQ PARALLEL_INDEX PARALLEL_INDEX 4 288 8.1.0
NO_PARTIAL_COMMIT QKSFM_CBO NO_PARTIAL_COMMIT 1 0 10.1.0.3
NO_PARTIAL_JOIN QKSFM_PARTIAL_JOIN PARTIAL_JOIN PARTIAL_JOIN 4 272 12.1.0.1 12.1.0.1
NO_PARTIAL_ROLLUP_PUSHDOWN QKSFM_PQ PARTIAL_ROLLUP_PUSHDOWN PARTIAL_ROLLUP_PUSHDOWN 2 16 12.1.0.1 12.1.0.1
NO_PLACE_DISTINCT QKSFM_DIST_PLCMT PLACE_DISTINCT PLACE_DISTINCT 2 16 11.2.0.1 11.2.0.1
NO_PLACE_GROUP_BY QKSFM_PLACE_GROUP_BY PLACE_GROUP_BY PLACE_GROUP_BY 2 16 11.1.0.6 11.1.0.6
NO_PQ_CONCURRENT_UNION QKSFM_PQ PQ_CONCURRENT_UNION PQ_CONCURRENT_UNION 3 0 12.1.0.1 12.1.0.1
NO_PQ_EXPAND_TABLE QKSFM_TABLE_EXPANSION PQ_EXPAND_TABLE PQ_EXPAND_TABLE 4 16 19.1.0 19.1.0
NO_PQ_NONLEAF_SKEW QKSFM_PQ PQ_NONLEAF_SKEW PQ_NONLEAF_SKEW 4 272 21.1.0 21.1.0
NO_PQ_REPLICATE QKSFM_PQ_REPLICATE PQ_REPLICATE PQ_REPLICATE 4 272 12.1.0.1 12.1.0.1
NO_PQ_SKEW QKSFM_PQ PQ_SKEW PQ_SKEW 4 272 12.1.0.1 12.1.0.1
NO_PRUNE_GSETS QKSFM_TRANSFORMATION NO_PRUNE_GSETS 2 0 9.0.0
NO_PULL_PRED QKSFM_PULL_PRED PULL_PRED PULL_PRED 4 16 10.2.0.1 10.2.0.1
NO_PUSH_HAVING_TO_GBY QKSFM_EXECUTION PUSH_HAVING_TO_GBY PUSH_HAVING_TO_GBY 2 0 18.1.0 18.1.0
NO_PUSH_PRED QKSFM_FILTER_PUSH_PRED PUSH_PRED PUSH_PRED 6 16 8.1.0 8.1.5
NO_PUSH_SUBQ QKSFM_TRANSFORMATION PUSH_SUBQ PUSH_SUBQ 2 16 9.2.0 10.2.0.5
NO_PX_FAULT_TOLERANCE QKSFM_PQ PX_FAULT_TOLERANCE PX_FAULT_TOLERANCE 1 0 12.1.0.1 12.1.0.1
NO_PX_JOIN_FILTER QKSFM_PX_JOIN_FILTER PX_JOIN_FILTER PX_JOIN_FILTER 4 336 10.2.0.1 11.1.0.6
NO_QKN_BUFF QKSFM_CBO NO_QKN_BUFF 2 0 9.2.0
NO_QUERY_TRANSFORMATION QKSFM_TRANSFORMATION NO_QUERY_TRANSFORMATION 1 16 10.1.0.3
NO_REF_CASCADE QKSFM_CBO REF_CASCADE_CURSOR REF_CASCADE_CURSOR 1 0 9.2.0
NO_REORDER_WIF QKSFM_PARTITION REORDER_WIF REORDER_WIF 2 0 18.1.0 18.1.0
NO_RESULT_CACHE QKSFM_EXECUTION RESULT_CACHE RESULT_CACHE 2 0 11.1.0.6
NO_REWRITE QKSFM_TRANSFORMATION REWRITE REWRITE 2 16 8.1.5 8.1.7
NO_SEMIJOIN QKSFM_TRANSFORMATION SEMIJOIN SEMIJOIN 2 16 9.0.0
NO_SEMI_TO_INNER QKSFM_CBO NO_SEMI_TO_INNER SEMI_TO_INNER 4 272 11.2.0.3 11.2.0.3
NO_SET_GBY_PUSHDOWN QKSFM_ALL SET_GBY_PUSHDOWN SET_GBY_PUSHDOWN 2 16 20.1.0 20.1.0
NO_SET_TO_JOIN QKSFM_SET_TO_JOIN SET_TO_JOIN SET_TO_JOIN 2 16 10.1.0.3 10.1.0.3
NO_SQL_TUNE QKSFM_ALL NO_SQL_TUNE 1 0 10.2.0.1
NO_STAR_TRANSFORMATION QKSFM_STAR_TRANS STAR_TRANSFORMATION STAR_TRANSFORMATION 6 16 10.1.0.3 10.1.0.3
NO_STATEMENT_QUEUING QKSFM_PARALLEL STATEMENT_QUEUING STATEMENT_QUEUING 1 0 11.2.0.1
NO_STATS_GSETS QKSFM_ALL NO_STATS_GSETS 2 0 8.0.0
NO_SUBQUERY_PRUNING QKSFM_CBO SUBQUERY_PRUNING SUBQUERY_PRUNING 4 272 11.1.0.6 11.1.0.6
NO_SUBSTRB_PAD QKSFM_EXECUTION NO_SUBSTRB_PAD 1 0 11.2.0.1
NO_SWAP_JOIN_INPUTS QKSFM_CBO SWAP_JOIN_INPUTS SWAP_JOIN_INPUTS 4 272 10.1.0.3 10.1.0.3
NO_TABLE_LOOKUP_BY_NL QKSFM_TABLE_LOOKUP_BY_NL TABLE_LOOKUP_BY_NL TABLE_LOOKUP_BY_NL 4 16 11.2.0.2 11.2.0.2
NO_TRANSFORM_DISTINCT_AGG QKSFM_TRANSFORMATION TRANSFORM_DISTINCT_AGG TRANSFORM_DISTINCT_AGG 2 0 11.2.0.1 11.2.0.1
NO_UNNEST QKSFM_UNNEST UNNEST UNNEST 2 16 8.1.6 10.1.0
NO_USE_CUBE QKSFM_USE_CUBE JOIN USE_CUBE 4 336 12.1.0.1 12.1.0.1
NO_USE_DAGG_UNION_ALL_GSETS QKSFM_GROUPING_SET_XFORM DAGG_OPTIM_GSETS USE_DAGG_UNION_ALL_GSETS 2 0 12.2.0.1 12.2.0.1
NO_USE_HASH QKSFM_USE_HASH NO_USE_HASH USE_HASH 4 336 10.1.0.3 10.1.0.3
NO_USE_HASH_AGGREGATION QKSFM_ALL USE_HASH_AGGREGATION USE_HASH_AGGREGATION 2 0 10.2.0.1 10.2.0.5
NO_USE_HASH_GBY_FOR_DAGGPSHD QKSFM_ALL USE_HASH_GBY_FOR_DAGGPSHD USE_HASH_GBY_FOR_DAGGPSHD 2 0 12.2.0.1 12.2.0.1
NO_USE_HASH_GBY_FOR_PUSHDOWN QKSFM_ALL USE_HASH_GBY_FOR_PUSHDOWN USE_HASH_GBY_FOR_PUSHDOWN 2 0 11.2.0.2 11.2.0.2
NO_USE_INVISIBLE_INDEXES QKSFM_INDEX USE_INVISIBLE_INDEXES USE_INVISIBLE_INDEXES 1 0 11.1.0.6 11.1.0.6
NO_USE_MERGE QKSFM_USE_MERGE NO_USE_MERGE USE_MERGE 4 336 10.1.0.3 10.1.0.3
NO_USE_NL QKSFM_USE_NL NO_USE_NL USE_NL 4 336 10.1.0.3 10.1.0.3
NO_USE_PARTITION_WISE_DISTINCT QKSFM_PARTITION USE_PARTITION_WISE_DISTINCT USE_PARTITION_WISE_DISTINCT 2 0 12.2.0.1 12.2.0.1
NO_USE_PARTITION_WISE_GBY QKSFM_PARTITION USE_PARTITION_WISE_GBY USE_PARTITION_WISE_GBY 2 0 12.2.0.1 12.2.0.1
NO_USE_PARTITION_WISE_WIF QKSFM_PARTITION USE_PARTITION_WISE_WIF USE_PARTITION_WISE_WIF 2 0 18.1.0 18.1.0
NO_USE_SCALABLE_GBY_INVDIST QKSFM_PQ USE_SCALABLE_GBY_INVDIST USE_SCALABLE_GBY_INVDIST 2 0 19.1.0 19.1.0
NO_USE_VECTOR_AGGREGATION QKSFM_VECTOR_AGG USE_VECTOR_AGGREGATION USE_VECTOR_AGGREGATION 2 16 12.1.0.2 12.1.0.2
NO_VECTOR_TRANSFORM QKSFM_VECTOR_AGG VECTOR_TRANSFORM VECTOR_TRANSFORM 2 16 12.1.0.2 12.1.0.2
NO_VECTOR_TRANSFORM_DIMS QKSFM_VECTOR_AGG VECTOR_TRANSFORM_DIMS VECTOR_TRANSFORM_DIMS 4 80 12.1.0.2 12.1.0.2
NO_VECTOR_TRANSFORM_FACT QKSFM_VECTOR_AGG VECTOR_TRANSFORM_FACT VECTOR_TRANSFORM_FACT 4 80 12.1.0.2 12.1.0.2
NO_XDB_FASTPATH_INSERT QKSFM_ALL XDB_FASTPATH_INSERT XDB_FASTPATH_INSERT 1 0 11.2.0.2
NO_XMLINDEX_REWRITE QKSFM_XMLINDEX_REWRITE XMLINDEX_REWRITE XMLINDEX_REWRITE 1 0 11.1.0.6 11.1.0.6
NO_XMLINDEX_REWRITE_IN_SELECT QKSFM_XMLINDEX_REWRITE XMLINDEX_REWRITE XMLINDEX_REWRITE_IN_SELECT 1 0 11.1.0.6 11.1.0.6
NO_XML_DML_REWRITE QKSFM_XML_REWRITE NO_XML_DML_REWRITE 1 0 10.2.0.1 11.1.0.6
NO_XML_QUERY_REWRITE QKSFM_XML_REWRITE FORCE_XML_QUERY_REWRITE FORCE_XML_QUERY_REWRITE 1 0 9.2.0 11.1.0.6
NO_ZONEMAP QKSFM_ZONEMAP ZONEMAP ZONEMAP 4 256 12.1.0.1 12.1.0.1
NUM_INDEX_KEYS QKSFM_CBO ACCESS 4 304 10.2.0.3 10.2.0.3
OBY_GBYPD_SEPARATE QKSFM_PQ OBY_GBYPD_SEPARATE NO_OBY_GBYPD_SEPARATE 2 16 21.1.0 21.1.0
OLD_PUSH_PRED QKSFM_OLD_PUSH_PRED OLD_PUSH_PRED 6 16 10.2.0.1 10.2.0.1
OPAQUE_TRANSFORM QKSFM_TRANSFORMATION OPAQUE_TRANSFORM 1 0 10.1.0.3
OPAQUE_XCANONICAL QKSFM_TRANSFORMATION OPAQUE_XCANONICAL 1 0 10.1.0.3
OPTIMIZER_FEATURES_ENABLE QKSFM_ALL OPTIMIZER_FEATURES_ENABLE 1 272 10.1.0.3 10.2.0.1
OPT_ESTIMATE QKSFM_OPT_ESTIMATE OPT_ESTIMATE 14 272 10.1.0.3
OPT_PARAM QKSFM_ALL OPT_PARAM 1 272 10.2.0.1 10.2.0.1
ORDERED QKSFM_CBO ORDERED 2 16 8.1.0 8.1.7
ORDERED_PREDICATES QKSFM_CBO ORDERED_PREDICATES 2 16 8.0.0
ORDER_KEY_VECTOR_USE QKSFM_VECTOR_AGG ORDER_KEY_VECTOR_USE 2 272 21.1.0 21.1.0
ORDER_SUBQ QKSFM_TRANSFORMATION ORDER_SUBQ 2 16 12.2.0.1 12.2.0.1
OR_EXPAND QKSFM_CBQT_OR_EXPANSION OR_EXPAND NO_OR_EXPAND 2 16 12.2.0.1 12.2.0.1
OSON_GET_CONTENT QKSFM_JSON OSON_GET_CONTENT 1 0 21.1.0 21.1.0
OUTER_JOIN_TO_ANTI QKSFM_CBO OUTER_JOIN_TO_ANTI NO_OUTER_JOIN_TO_ANTI 4 272 11.2.0.3 11.2.0.3
OUTER_JOIN_TO_INNER QKSFM_OUTER_JOIN_TO_INNER OUTER_JOIN_TO_INNER NO_OUTER_JOIN_TO_INNER 6 16 11.1.0.6 11.1.0.6
OUTLINE QKSFM_ALL OUTLINE 2 0 10.2.0.1 10.2.0.1
OUTLINE_LEAF QKSFM_ALL OUTLINE_LEAF 2 0 10.2.0.1 10.2.0.1
OVERFLOW_NOMOVE QKSFM_CBO OVERFLOW_NOMOVE 2 0 9.0.0
PARALLEL_INDEX QKSFM_PQ PARALLEL_INDEX NO_PARALLEL_INDEX 4 288 8.1.0
PARTIAL_JOIN QKSFM_PARTIAL_JOIN PARTIAL_JOIN NO_PARTIAL_JOIN 4 272 12.1.0.1 12.1.0.1
PARTIAL_ROLLUP_PUSHDOWN QKSFM_PQ PARTIAL_ROLLUP_PUSHDOWN NO_PARTIAL_ROLLUP_PUSHDOWN 2 16 12.1.0.1 12.1.0.1
PDB_LOCAL_ONLY QKSFM_DML PDB_LOCAL_ONLY 1 0 18.1.0
PIV_GB QKSFM_ALL PIV_GB 2 0 8.1.0
PIV_SSF QKSFM_ALL PIV_SSF 2 0 8.1.0
PLACE_DISTINCT QKSFM_DIST_PLCMT PLACE_DISTINCT NO_PLACE_DISTINCT 2 16 11.2.0.1 11.2.0.1
PLACE_GROUP_BY QKSFM_PLACE_GROUP_BY PLACE_GROUP_BY NO_PLACE_GROUP_BY 2 16 11.1.0.6 11.1.0.6
PQ_CONCURRENT_UNION QKSFM_PQ PQ_CONCURRENT_UNION NO_PQ_CONCURRENT_UNION 3 0 12.1.0.1 12.1.0.1
PQ_DISTRIBUTE QKSFM_PQ_DISTRIBUTE PQ_DISTRIBUTE 4 272 8.1.5 8.1.7
PQ_DISTRIBUTE_WINDOW QKSFM_PQ PQ_DISTRIBUTE_WINDOW 2 16 12.1.0.1 12.1.0.1
PQ_EXPAND_TABLE QKSFM_TABLE_EXPANSION PQ_EXPAND_TABLE NO_PQ_EXPAND_TABLE 4 16 19.1.0 19.1.0
PQ_FILTER QKSFM_PQ PQ_FILTER 2 0 12.1.0.1 12.1.0.1
PQ_MAP QKSFM_PQ_MAP PQ_MAP PQ_NOMAP 4 272 9.0.0 10.2.0.1
PQ_NOMAP QKSFM_PQ_MAP PQ_MAP PQ_MAP 4 272 9.0.0 10.2.0.1
PQ_NONLEAF_SKEW QKSFM_PQ PQ_NONLEAF_SKEW NO_PQ_NONLEAF_SKEW 4 272 21.1.0 21.1.0
PQ_REPLICATE QKSFM_PQ_REPLICATE PQ_REPLICATE NO_PQ_REPLICATE 4 272 12.1.0.1 12.1.0.1
PQ_SKEW QKSFM_PQ PQ_SKEW NO_PQ_SKEW 4 272 12.1.0.1 12.1.0.1
PRECOMPUTE_SUBQUERY QKSFM_TRANSFORMATION PRECOMPUTE_SUBQUERY 2 0 10.2.0.1
PRESERVE_OID QKSFM_ALL PRESERVE_OID 1 0 10.2.0.1
PULL_PRED QKSFM_PULL_PRED PULL_PRED NO_PULL_PRED 4 16 10.2.0.1 10.2.0.1
PUSH_HAVING_TO_GBY QKSFM_EXECUTION PUSH_HAVING_TO_GBY NO_PUSH_HAVING_TO_GBY 2 0 18.1.0 18.1.0
PUSH_PRED QKSFM_FILTER_PUSH_PRED PUSH_PRED NO_PUSH_PRED 6 16 8.1.0 8.1.5
PUSH_SUBQ QKSFM_TRANSFORMATION PUSH_SUBQ NO_PUSH_SUBQ 2 16 8.1.0 10.2.0.5
PX_FAULT_TOLERANCE QKSFM_PQ PX_FAULT_TOLERANCE NO_PX_FAULT_TOLERANCE 1 0 12.1.0.1 12.1.0.1
PX_JOIN_FILTER QKSFM_PX_JOIN_FILTER PX_JOIN_FILTER NO_PX_JOIN_FILTER 4 336 10.2.0.1 11.1.0.6
QB_NAME QKSFM_ALL QB_NAME 2 256 10.1.0.3
QUARANTINE QKSFM_EXECUTION QUARANTINE 1 0 19.1.0
QUEUE_CURR QKSFM_CBO ACCESS 4 256 8.0.0
QUEUE_ROWP QKSFM_CBO ACCESS 4 256 8.0.0
RBO_OUTLINE QKSFM_RBO RBO_OUTLINE 1 0 10.2.0.1 10.2.0.1
REF_CASCADE_CURSOR QKSFM_CBO REF_CASCADE_CURSOR NO_REF_CASCADE 1 0 9.2.0
REMOTE_MAPPED QKSFM_ALL REMOTE_MAPPED 2 272 8.1.0
REORDER_WIF QKSFM_PARTITION REORDER_WIF NO_REORDER_WIF 2 0 18.1.0 18.1.0
RESERVOIR_SAMPLING QKSFM_EXECUTION RESERVOIR_SAMPLING 1 0 12.1.0.2
RESTORE_AS_INTERVALS QKSFM_CBO RESTORE_AS_INTERVALS 2 0 8.1.5
RESTRICT_ALL_REF_CONS QKSFM_ALL RESTRICT_ALL_REF_CONS 1 0 10.1.0.3
RESULT_CACHE QKSFM_EXECUTION RESULT_CACHE NO_RESULT_CACHE 2 0 11.1.0.6
RETRY_ON_ROW_CHANGE QKSFM_DML RETRY_ON_ROW_CHANGE 1 0 11.1.0.7
REWRITE QKSFM_TRANSFORMATION REWRITE NO_REWRITE 2 16 8.1.5 8.1.7
REWRITE_OR_ERROR QKSFM_TRANSFORMATION REWRITE 2 0 10.1.0.3
ROWID QKSFM_CBO ACCESS 4 272 8.0.0 8.1.7
RULE QKSFM_RBO MODE 1 16 8.1.0 8.1.5
SAVE_AS_INTERVALS QKSFM_CBO SAVE_AS_INTERVALS 2 0 8.1.5
SCN_ASCENDING QKSFM_ALL SCN_ASCENDING 1 0 8.1.5
SEMIJOIN QKSFM_TRANSFORMATION SEMIJOIN NO_SEMIJOIN 2 16 9.0.0
SEMIJOIN_DRIVER QKSFM_CBO SEMIJOIN_DRIVER 2 16 8.1.0 8.1.7
SEMI_TO_INNER QKSFM_CBO SEMI_TO_INNER NO_SEMI_TO_INNER 4 272 11.2.0.3 11.2.0.3
SET_GBY_PUSHDOWN QKSFM_ALL SET_GBY_PUSHDOWN NO_SET_GBY_PUSHDOWN 2 16 20.1.0 20.1.0
SET_TO_JOIN QKSFM_SET_TO_JOIN SET_TO_JOIN NO_SET_TO_JOIN 2 16 10.1.0.3 10.1.0.3
SHARED QKSFM_PARALLEL SHARED NO_PARALLEL 5 256 8.1.0
SKIP_EXT_OPTIMIZER QKSFM_CBO SKIP_EXT_OPTIMIZER 2 16 9.0.0
SKIP_PROXY QKSFM_ALL SKIP_PROXY 1 0 18.1.0
SKIP_UNQ_UNUSABLE_IDX QKSFM_CBO SKIP_UNQ_UNUSABLE_IDX 1 0 10.1.0.3
SQLLDR QKSFM_CBO SQLLDR 1 0 9.0.0
SQL_SCOPE QKSFM_COMPILATION SQL_SCOPE 1 0 12.2.0.1
STAR QKSFM_STAR_TRANS STAR 2 16 8.1.0
STAR_TRANSFORMATION QKSFM_STAR_TRANS STAR_TRANSFORMATION NO_STAR_TRANSFORMATION 6 16 8.1.0 8.1.7
STATEMENT_QUEUING QKSFM_PARALLEL STATEMENT_QUEUING NO_STATEMENT_QUEUING 1 0 11.2.0.1
STREAMS QKSFM_CBO STREAMS 1 0 10.1.0.3
SUBQUERY_PRUNING QKSFM_CBO SUBQUERY_PRUNING NO_SUBQUERY_PRUNING 4 272 11.1.0.6 11.1.0.6
SUPPRESS_LOAD QKSFM_DDL SUPPRESS_LOAD 1 0 18.1.0
SWAP_JOIN_INPUTS QKSFM_CBO SWAP_JOIN_INPUTS NO_SWAP_JOIN_INPUTS 4 272 8.1.0 8.1.7
SYSTEM_STATS QKSFM_ALL SYSTEM_STATS 1 272 18.1.0
SYS_DL_CURSOR QKSFM_CBO SYS_DL_CURSOR 1 0 9.2.0
SYS_PARALLEL_TXN QKSFM_CBO SYS_PARALLEL_TXN 2 0 8.1.6
SYS_RID_ORDER QKSFM_ALL SYS_RID_ORDER 2 0 9.2.0
TABLE_LOOKUP_BY_NL QKSFM_TABLE_LOOKUP_BY_NL TABLE_LOOKUP_BY_NL NO_TABLE_LOOKUP_BY_NL 4 16 11.2.0.2 11.2.0.2
TABLE_STATS QKSFM_STATS TABLE_STATS 1 272 10.1.0.3
TIV_GB QKSFM_ALL PIV_GB 2 0 8.1.0
TIV_SSF QKSFM_ALL PIV_SSF 2 0 8.1.0
TRACING QKSFM_EXECUTION TRACING 1 0 10.1.0.3
TRANSFORM_DISTINCT_AGG QKSFM_TRANSFORMATION TRANSFORM_DISTINCT_AGG NO_TRANSFORM_DISTINCT_AGG 2 0 11.2.0.1 11.2.0.1
UNNEST QKSFM_UNNEST UNNEST NO_UNNEST 2 16 8.1.6 10.1.0
USE_ANTI QKSFM_CBO USE_ANTI 4 272 8.1.0
USE_CONCAT QKSFM_USE_CONCAT OR_EXPAND NO_EXPAND 2 16 8.1.0 8.1.7
USE_CUBE QKSFM_USE_CUBE JOIN NO_USE_CUBE 4 336 12.1.0.1 12.1.0.1
USE_DAGG_UNION_ALL_GSETS QKSFM_GROUPING_SET_XFORM DAGG_OPTIM_GSETS NO_USE_DAGG_UNION_ALL_GSETS 2 0 12.2.0.1 12.2.0.1
USE_HASH QKSFM_USE_HASH JOIN NO_USE_HASH 4 464 8.1.0 8.1.7
USE_HASH_AGGREGATION QKSFM_ALL USE_HASH_AGGREGATION NO_USE_HASH_AGGREGATION 2 0 10.2.0.1 10.2.0.5
USE_HASH_GBY_FOR_DAGGPSHD QKSFM_ALL USE_HASH_GBY_FOR_DAGGPSHD NO_USE_HASH_GBY_FOR_DAGGPSHD 2 0 12.2.0.1 12.2.0.1
USE_HASH_GBY_FOR_PUSHDOWN QKSFM_ALL USE_HASH_GBY_FOR_PUSHDOWN NO_USE_HASH_GBY_FOR_PUSHDOWN 2 0 11.2.0.2 11.2.0.2
USE_HIDDEN_PARTITIONS QKSFM_PARTITION USE_HIDDEN_PARTITIONS 2 0 12.1.0.1
USE_INVISIBLE_INDEXES QKSFM_INDEX USE_INVISIBLE_INDEXES NO_USE_INVISIBLE_INDEXES 1 0 11.1.0.6 11.1.0.6
USE_MERGE QKSFM_USE_MERGE JOIN NO_USE_MERGE 4 336 8.1.0 8.1.7
USE_MERGE_CARTESIAN QKSFM_USE_MERGE_CARTESIAN JOIN 4 336 11.1.0.6 11.1.0.6
USE_NL QKSFM_USE_NL JOIN NO_USE_NL 4 336 8.1.0 8.1.7
USE_NL_WITH_INDEX QKSFM_USE_NL_WITH_INDEX USE_NL_WITH_INDEX NO_USE_NL 4 304 10.1.0.3
USE_PARTITION_WISE_DISTINCT QKSFM_PARTITION USE_PARTITION_WISE_DISTINCT NO_USE_PARTITION_WISE_DISTINCT 2 0 12.2.0.1 12.2.0.1
USE_PARTITION_WISE_GBY QKSFM_PARTITION USE_PARTITION_WISE_GBY NO_USE_PARTITION_WISE_GBY 2 0 12.2.0.1 12.2.0.1
USE_PARTITION_WISE_WIF QKSFM_PARTITION USE_PARTITION_WISE_WIF NO_USE_PARTITION_WISE_WIF 2 0 18.1.0 18.1.0
USE_SCALABLE_GBY_INVDIST QKSFM_PQ USE_SCALABLE_GBY_INVDIST NO_USE_SCALABLE_GBY_INVDIST 2 0 19.1.0 19.1.0
USE_SEMI QKSFM_CBO USE_SEMI 4 272 8.1.0
USE_TTT_FOR_GSETS QKSFM_TRANSFORMATION USE_TTT_FOR_GSETS 2 0 9.0.0
USE_VECTOR_AGGREGATION QKSFM_VECTOR_AGG USE_VECTOR_AGGREGATION NO_USE_VECTOR_AGGREGATION 2 16 12.1.0.2 12.1.0.2
USE_WEAK_NAME_RESL QKSFM_ALL USE_WEAK_NAME_RESL 1 0 10.1.0.3
VECTOR_READ QKSFM_CBO VECTOR_READ 1 0 10.1.0.3
VECTOR_READ_TRACE QKSFM_CBO VECTOR_READ_TRACE 1 0 10.1.0.3
VECTOR_TRANSFORM QKSFM_VECTOR_AGG VECTOR_TRANSFORM NO_VECTOR_TRANSFORM 2 16 12.1.0.2 12.1.0.2
VECTOR_TRANSFORM_DIMS QKSFM_VECTOR_AGG VECTOR_TRANSFORM_DIMS NO_VECTOR_TRANSFORM_DIMS 4 80 12.1.0.2 12.1.0.2
VECTOR_TRANSFORM_FACT QKSFM_VECTOR_AGG VECTOR_TRANSFORM_FACT NO_VECTOR_TRANSFORM_FACT 4 80 12.1.0.2 12.1.0.2
WITH_PLSQL QKSFM_ALL WITH_PLSQL 1 0 12.1.0.1
XDB_FASTPATH_INSERT QKSFM_ALL XDB_FASTPATH_INSERT NO_XDB_FASTPATH_INSERT 1 0 11.2.0.2
XMLINDEX_REWRITE QKSFM_XMLINDEX_REWRITE XMLINDEX_REWRITE NO_XMLINDEX_REWRITE 1 0 11.1.0.6 11.1.0.6
XMLINDEX_REWRITE_IN_SELECT QKSFM_XMLINDEX_REWRITE XMLINDEX_REWRITE NO_XMLINDEX_REWRITE_IN_SELECT 1 0 11.1.0.6 11.1.0.6
XMLINDEX_SEL_IDX_TBL QKSFM_ALL XMLINDEX_SEL_IDX_TBL 1 0 11.2.0.1
XMLTSET_DML_ENABLE QKSFM_ALL XMLTSET_DML_ENABLE 1 0 12.2.0.1
XML_DML_RWT_STMT QKSFM_XML_REWRITE XML_DML_RWT_STMT 1 0 11.1.0.6 11.1.0.6
X_DYN_PRUNE QKSFM_CBO X_DYN_PRUNE 2 0 10.1.0.3
ZONEMAP QKSFM_ZONEMAP ZONEMAP NO_ZONEMAP 4 256 12.1.0.1 12.1.0.1

388行が選択されました。

 

 

PostgreSQL / pg_hint_plan (22-Nov-2023時点)

pg_hint_planのヒントもOracleの影響を受けているところもあるけども、PostgreSQL独特の言い回しを使う傾向はありますよね。面白いのは、LEADINGヒントで(a b)みたいなペアで優先度を記述する部分。これかなり慣れが必要な気がします。

https://github.com/ossc-db/pg_hint_plan

https://github.com/ossc-db/pg_hint_plan/blob/master/docs/hint_list.md

Hint list

  Format Description
Scan method SeqScan(table) Forces sequential scan on the table.
  TidScan(table) Forces TID scan on the table.
  IndexScan(table[ index...]) Forces index scan on the table. Restricts to specified indexes if any.
  IndexOnlyScan(table[ index...]) Forces index-only scan on the table. Restricts to specified indexes if any. Index scan may be used if index-only scan is not available.
  BitmapScan(table[ index...]) Forces bitmap scan on the table. Restricts to specified indexes if any.
  IndexScanRegexp(table[ POSIX Regexp...])
IndexOnlyScanRegexp(table[ POSIX Regexp...])
BitmapScanRegexp(table[ POSIX Regexp...])
Forces index scan, index-only scan (For PostgreSQL 9.2 and later) or bitmap scan on the table. Restricts to indexes that matches the specified POSIX regular expression pattern.
  NoSeqScan(table) Forces to not do sequential scan on the table.
  NoTidScan(table) Forces to not do TID scan on the table.
  NoIndexScan(table) Forces to not do index scan and index-only scan on the table.
  NoIndexOnlyScan(table) Forces to not do index only scan on the table.
  NoBitmapScan(table) Forces to not do bitmap scan on the table.
Join method NestLoop(table table[ table...]) Forces nested loop for the joins on the tables specified.
  HashJoin(table table[ table...]) Forces hash join for the joins on the tables specified.
  MergeJoin(table table[ table...]) Forces merge join for the joins on the tables specified.
  NoNestLoop(table table[ table...]) Forces to not do nested loop for the joins on the tables specified.
  NoHashJoin(table table[ table...]) Forces to not do hash join for the joins on the tables specified.
  NoMergeJoin(table table[ table...]) Forces to not do merge join for the joins on the tables specified.
Join order Leading(table table[ table...]) Forces join order as specified.
  Leading(<join pair>) Forces join order and directions as specified. A join pair is a pair of tables and/or other join pairs enclosed by parentheses, which can make a nested structure.
Behavior control on Join Memoize(table table[ table...]) Allows the topmost join of a join among the specified tables to Memoize the inner result. Not enforced.
  NoMemoize(table table[ table...]) Inhibits the topmost join of a join among the specified tables from Memoizing the inner result.
Row number correction Rows(table table[ table...] correction) Corrects row number of a result of the joins on the tables specified. The available correction methods are absolute (#), addition (+), subtract (-) and multiplication (*). should be a string that strtod() can understand.
Parallel query configuration Parallel(table <# of workers> [soft|hard]) Enforces or inhibits parallel execution of the specified table. <# of workers> is the desired number of parallel workers, where zero means inhibiting parallel execution. If the third parameter is soft (default), it just changes max_parallel_workers_per_gather and leaves everything else to the planner. Hard enforces the specified number of workers.
GUC Set(GUC-param value) Sets GUC parameter to the value defined while planner is running.

 

MySQL 8.0

https://dev.mysql.com/doc/refman/8.0/en/optimizer-hints.html

Table 8.2 Optimizer Hints Available

MySQLのOptimizer HintsはOracleにもある同一用途のヒントもちらほら。ヒント名も同一で:)
Hint Name Description Applicable Scopes
BKA, NO_BKA Affects Batched Key Access join processing Query block, table
BNL, NO_BNL Prior to MySQL 8.0.20: affects Block Nested-Loop join processing; MySQL 8.0.18 and later: also affects hash join optimization; MySQL 8.0.20 and later: affects hash join optimization only Query block, table
DERIVED_CONDITION_PUSHDOWN, NO_DERIVED_CONDITION_PUSHDOWN Use or ignore the derived condition pushdown optimization for materialized derived tables (Added in MySQL 8.0.22) Query block, table
GROUP_INDEX, NO_GROUP_INDEX Use or ignore the specified index or indexes for index scans in GROUP BY operations (Added in MySQL 8.0.20) Index
HASH_JOIN, NO_HASH_JOIN Affects Hash Join optimization (MySQL 8.0.18 only Query block, table
INDEX, NO_INDEX Acts as the combination of JOIN_INDEX, GROUP_INDEX, and ORDER_INDEX, or as the combination of NO_JOIN_INDEX, NO_GROUP_INDEX, and NO_ORDER_INDEX (Added in MySQL 8.0.20) Index
INDEX_MERGE, NO_INDEX_MERGE Affects Index Merge optimization Table, index
JOIN_FIXED_ORDER Use table order specified in FROM clause for join order Query block
JOIN_INDEX, NO_JOIN_INDEX Use or ignore the specified index or indexes for any access method (Added in MySQL 8.0.20) Index
JOIN_ORDER Use table order specified in hint for join order Query block
JOIN_PREFIX Use table order specified in hint for first tables of join order Query block
JOIN_SUFFIX Use table order specified in hint for last tables of join order Query block
MAX_EXECUTION_TIME Limits statement execution time Global
MERGE, NO_MERGE Affects derived table/view merging into outer query block Table
MRR, NO_MRR Affects Multi-Range Read optimization Table, index
NO_ICP Affects Index Condition Pushdown optimization Table, index
NO_RANGE_OPTIMIZATION Affects range optimization Table, index
ORDER_INDEX, NO_ORDER_INDEX Use or ignore the specified index or indexes for sorting rows (Added in MySQL 8.0.20) Index
QB_NAME Assigns name to query block Query block
RESOURCE_GROUP Set resource group during statement execution Global
SEMIJOIN, NO_SEMIJOIN Affects semijoin strategies; beginning with MySQL 8.0.17, this also applies to antijoins Query block
SKIP_SCAN, NO_SKIP_SCAN Affects Skip Scan optimization Table, index
SET_VAR Set variable during statement execution Global
SUBQUERY Affects materialization, IN-to-EXISTS subquery strategies Query block

 

いよいよ、Advent Calendar 2023の季節が近くなってきました。今年は、全部俺シリーズは、やらずに、Oracle/PostgreSQL/MySQLのカレンダーへクロスポストすることだけは決めてます〜 :)
全部俺では無いけど、いくつかエントリーは書く予定ではいます。このエントリーもAdvent Calendarネタへ繋がるネタなのですけどね。w

では、また。



関連エントリー


Oracle Database 20c 20.1.0以降〜21c 21.1.0で v$sql_hintに追加されたヒント/ FAQ

標準はあるにはあるが癖の多いSQL 全部俺 #1 Pagination
標準はあるにはあるが癖の多いSQL 全部俺 #2 関数名は同じでも引数が逆の罠!
標準はあるにはあるが癖の多いSQL 全部俺 #3 データ型確認したい時あるんです
標準はあるにはあるが癖の多いSQL 全部俺 #4 リテラル値での除算の内部精度も違うのよ!
標準はあるにはあるが癖の多いSQL 全部俺 #5 和暦変換機能ある方が少数派
標準はあるにはあるが癖の多いSQL 全部俺 #6 時間厳守!
標準はあるにはあるが癖の多いSQL 全部俺 #7 期間リテラル!
標準はあるにはあるが癖の多いSQL 全部俺 #8 翌月末日って何日?
標準はあるにはあるが癖の多いSQL 全部俺 #9 部分文字列の扱いでも癖が出る><
標準はあるにはあるが癖の多いSQL 全部俺 #10 文字列連結の罠(有名なやつ)
標準はあるにはあるが癖の多いSQL 全部俺 #11 デュエル、じゃなくて、デュアル
標準はあるにはあるが癖の多いSQL 全部俺 #12 文字[列]探すにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #13 あると便利ですが意外となかったり
標準はあるにはあるが癖の多いSQL 全部俺 #14 連番の集合を返すにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #15 SQL command line client
標準はあるにはあるが癖の多いSQL 全部俺 #16 SQLのレントゲンを撮る方法
標準はあるにはあるが癖の多いSQL 全部俺 #17 その空白は許されないのか?
標準はあるにはあるが癖の多いSQL 全部俺 #18 (+)の外部結合は方言
標準はあるにはあるが癖の多いSQL 全部俺 #19 帰ってきた、部分文字列の扱いでも癖w
標準はあるにはあるが癖の多いSQL 全部俺 #20 結果セットを単一列に連結するにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #21 演算結果にも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #22 集合演算にも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #23 複数行INSERTにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #24 乱数作るにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #25 SQL de Fractalsにも癖がある:)
標準はあるにはあるが癖の多いSQL 全部俺 おまけ SQL de 湯婆婆やるにも癖がでるw
帰ってきた! 標準はあるにはあるが癖の多いSQL #1 SQL de ROT13 やるにも癖が出るw
帰ってきた! 標準はあるにはあるが癖の多いSQL #2 Actual Plan取得中のキャンセルでも癖が出る
帰ってきた! 標準はあるにはあるが癖の多いSQL #3 オプティマイザの結合順評価テーブル数上限にも癖が出る
帰ってきた! 標準はあるにはあるが癖の多いSQL #4 Optimizer Traceの取得でも癖がでる

 

 

 

| | | コメント (0)

2023年10月11日 (水)

Oracle SQL Hinting Tips / #JoelKallmanDay


Joel Kallman Day 2023 : Announcement
の通り、本ポストは、Joel Kallman Day 2023 向けのブログポストでもあります。詳細はTimeのブログ参照のこと。


Previously on Mac De Oracle
前回は、rebuild index後のindexサイズって結局、create indexのexplainで見積もれるんだよね、というお話でした。

今回から数回、OracleのSQL ヒントの使い方テクニックでも紹介して行こうかなと思っています。
まず、最近のヒント周りの劇的な進化というか便利になったこと、最近といっても、数ヶ月とかいう単位ではないのですけどもw

Oracle 19c から hint usage report が表示されるようになりました。

19.3.3.4 ヒント使用状況レポート: 例
https://docs.oracle.com/cd/F19136_01/tgsql/influencing-the-optimizer.html#GUID-1697E7CA-9DD0-4C0D-9BC9-E4E17334C0AA


これ、今までなんでなかったの? という感じはあります。(全くなかったわけではないのですが、よほどのことがなけれそこまでトレースしなかったというのが一番の理由でしょうね。取得および、確認方法が面倒だったのでw)

最近は、ヒントの種類も、使用頻度も多くなってきたことが影響しているのでしょうか。。。ヒント使用状況レポートがほしいという要望が多くなったのでしょうかね? 

ただ、hint usage report で簡単に確認できるようになったのは良いことだと思うのですが、その副作用というような現象に気づくことも、それなりに多くなってきました。。。

その副作用とは、ヒントが、Unusedとなるような記述は避けるべき! のような話です
(風の便り程度で実際そうなっている状況に遭遇はしていないのですが)

それ以来、
ヒントの書き方の標準を決める際に、過度に、strictになって、逆にヒントを使いこなせない状況になったりしていないだろうか?
もしくは、その兆候があるのではないかと心配になることがあります。
昔から使われてきた柔軟なヒント記述方法が、変に制限されたりすることのないようにしてほしいものだとは思います。
(そう意味も込めて、マニュアルにも書いてるからね、この方法というのも点も強調しつつ、簡単な検証方法も合わせて紹介しています)

USE_HASHヒント
https://docs.oracle.com/cd/F19136_01/sqlrf/Comments.html#GUID-FA1147B3-BCAA-41F9-B6A2-8DEDABF1C021

USE_NLヒント
https://docs.oracle.com/cd/F19136_01/sqlrf/Comments.html#GUID-56DAA0EC-54BB-4E9D-9049-BCEA934F7A89


マニュアルにも例が記載されていますが、USE_NLやUSE_HASHヒントには、内部表(HASH結合の場合は、プローブ表)を指定します、単一表指定、複数表指定どちらも可能です。

例えば、A表とB表を2表を結合する例で言えば、
Nested Loop Join(NLJ)の駆動表、Hash Join(HJ)のビルド表が、A表だとして、

/*+ LEADING(A) USE_NL(B) */
/*+ LEADING(A) USE_HASH(B) */

のようにヒントを書くことができます。LEADINGヒントで駆動表(NLJの場合)または、ビルド表(HJの場合)を指定し、内部表または、ビルド表をUSE_NL(NLJの場合)または、USE_HASH(HJの場合)で指定する。

では、マニュアルにも記載されている以下のような記述は、どのような意味なのか、みなさん、お分かりでしょうか?

/*+ USE_NL(A B) */ 同じ意味ですが、 /*+ USE_NL(A) USE_NL(B) */
/*+ USE_HASH(A B) */ 同じ意味ですが、 /*+ USE_HASH(A) USE_HASH(B) */

A表とB表の2表ですから、どちらかが、駆動表(NLJの場合)、または、ビルド表(HJの場合)で、どちらかが、内部表(NLJの場合)、または、プローブ表(HJの場合)となります。
これらのヒントに指定した表は、指定しても無視される表(駆動表または、ビルド表)、つまり、Unused となる表も含めて記載しています。(エラーではないく、使われないだけだからですが)

あえて、そうする意味はなんでしょう。みなさんお分かりでしょうか?

答えは、結合順序はオプティマイザの判断に任せ、結合方法だけを、NLJ や HJ にしたい!!!!!

ということです。


意外にこのような状況は多くあります。
要するに、駆動表や、ビルド表は状況に応じて柔軟に変えてもらって良いが、結合方法だけは、絶対、NLJにしたい。もしくは、HJにしたいというケースですね!
(チューニングで呼ばれて行った先で、業務観点からどの表が駆動表や、ビルド表ですか? と聞いて、即答してくれないと、おじさん困ってしまうんですねw わからんと言われてたら、リスク覚悟で現状のデータ量から決めるか、このように決めないで、オプティマイザに任せる。みなさんもオプティマイザを信じてください! と、言ったもののw、統計情報取得止められてたりすると、まあ、信じられないみたいな状況もあるわけです。はい。wwwww)


他のTech Tipsとして、結合順は状況に応じて人の手で書き換えること(前述したように担当者も結合順を把握していないとか、一旦、固定したけども、やはり間違ってたケースや、経年で傾向が変わったので変更したい etc.)を想定して、ヒント修正によるミスのリスクを最小化するため。(修正範囲をLEADINGヒントだけにして、変更箇所を最小にしたい場合です

駆動表または、ビルド表の変更前
/*+ LEADING(A B) USE_NL(A B) */
/*+ LEADING(A B) USE_HASH(A B) */
/*+ LEADING(A) USE_NL(A B) */
/*+ LEADING(A) USE_HASH(A B) */

駆動表または、ビルド表の変更後
/*+ LEADING(B A) USE_NL(A B) */
/*+ LEADING(B A) USE_HASH(A B) */
/*+ LEADING(B) USE_NL(A B) */
/*+ LEADING(B) USE_HASH(A B) */

これらのヒントは、LEADINGヒントの結合順を変更するだけで、駆動表やビルド表を切り替えることができます。USE_NL、USE_HASHヒントは変更する必要がありません。(ヒント指定時のミスの発生箇所を最小化できるわけです=変更箇所を少なくした)

しかし、以下のように記述していた場合はどうでしょう?
駆動表や、ビルド表を変更する場合、LEADING/USE_NL/USE_HASH全てのヒントを適切に修正する必要があります。

変更前
/*+ LEADING(A) USE_NL(B) */
/*+ LEADING(A) USE_HASH(B) */

変更後
/*+ LEADING(B) USE_NL(A) */
/*+ LEADING(B) USE_HASH(A) */

変更し忘れたことによりヒントが無効となり、遅延に繋がってしまったということは意外に多いです。修正しているのは、人ですからね。間違いはあります。修正する箇所と量を最小化すれば、ミスの発生リスクは減らせます。
もう一つの効果として、可読性が向上する(個人的な感覚かもしれませんが)のではないかと考えています。


ということで長い前置きはこれぐらいにして挙動を確認してみましょう。


結合順はオプティマイザ任せにして固定せず、結合方法だけをHJにした場合、Hint Usage Reportにはどうのようにレポートされるのか、また実行計画は想定通りなのか等を確認してみましょう。


Oracle Database 21cを利用して検証します。ちなみに、この方法は、Oracle 11gの頃から実戦で利用されている方法です。(マニュアルにも例が記載されているので、将来的にも挙動が変わることはないでしょう。影響でかいですからね。挙動が変わるとw)
なお、隠しパラメータのカスタマイズはせず、インストール時のままです。

SCOTT@orclpdb1> select banner from v$version;

BANNER
-----------------------------------------------------------------------
Oracle Database 21c Enterprise Edition Release 21.0.0.0.0 - Production


検証用の表とデータ登録、統計情報取得、データ件数確認
(なお、本検証で利用したスクリプトはこのブログの後半に記載してあります)

SCOTT@orclpdb1> @hinting_tech1

表が削除されました。

経過: 00:00:00.84

表が作成されました。

経過: 00:00:00.19

表が削除されました。

経過: 00:00:00.51

表が作成されました。

経過: 00:00:00.07

PL/SQLプロシージャが正常に完了しました。

経過: 00:00:06.41

PL/SQLプロシージャが正常に完了しました。

経過: 00:00:13.93
1 BEGIN
2 DBMS_STATS.GATHER_TABLE_STATS(ownname=>'SCOTT', tabname=>'TABLE_A', cascade=>true, no_invalidate=>false);
3* END;

PL/SQLプロシージャが正常に完了しました。

経過: 00:00:01.71
1 BEGIN
2 DBMS_STATS.GATHER_TABLE_STATS(ownname=>'SCOTT', tabname=>'TABLE_B', cascade=>true, no_invalidate=>false);
3* END;

PL/SQLプロシージャが正常に完了しました。

経過: 00:00:01.16
1* SELECT COUNT(1) FROM table_a

COUNT(1)
----------
50000

経過: 00:00:00.01
1* SELECT COUNT(1) FROM table_b

COUNT(1)
----------
100000

経過: 00:00:00.01

PL/SQLプロシージャが正常に完了しました。

経過: 00:00:00.03


何もチューニングしていないオリジナルのSQLと実行計画を確認しておきます。少量のデータに絞って、結合(索引あり)しています。
駆動表がTABLE_Aで、NLJしていることが確認できます。

  1  SELECT
2 *
3 FROM
4 table_a a
5 INNER JOIN table_b b
6 ON
7 a.id = b.id
8* AND a.id BETWEEN :s AND :e

経過: 00:00:00.04

実行計画
----------------------------------------------------------
Plan hash value: 1883513077

------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 125 | 489K| 203 (0)| 00:00:01 |
|* 1 | FILTER | | | | | |
| 2 | NESTED LOOPS | | 125 | 489K| 203 (0)| 00:00:01 |
| 3 | NESTED LOOPS | | 125 | 489K| 203 (0)| 00:00:01 |
| 4 | TABLE ACCESS BY INDEX ROWID BATCHED| TABLE_A | 125 | 244K| 78 (0)| 00:00:01 |
|* 5 | INDEX RANGE SCAN | SYS_C009317 | 225 | | 2 (0)| 00:00:01 |
|* 6 | INDEX UNIQUE SCAN | SYS_C009318 | 1 | | 0 (0)| 00:00:01 |
| 7 | TABLE ACCESS BY INDEX ROWID | TABLE_B | 1 | 2006 | 1 (0)| 00:00:01 |
------------------------------------------------------------------------------------------------------

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

1 - filter(TO_NUMBER(:E)>=TO_NUMBER(:S))
5 - access("A"."ID">=TO_NUMBER(:S) AND "A"."ID"<=TO_NUMBER(:E))
6 - access("A"."ID"="B"."ID")
filter("B"."ID">=TO_NUMBER(:S) AND "B"."ID"<=TO_NUMBER(:E))

Note
-----
- this is an adaptive plan


統計
----------------------------------------------------------
73 recursive calls
21 db block gets
143 consistent gets
8 physical reads
4108 redo size
8944 bytes sent via SQL*Net to client
52 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
11 sorts (memory)
0 sorts (disk)
5 rows processed


NLJをヒント使って、HJに強制します。索引アクセスを行わないようにFULLヒントも併用していますが、LEADINGヒントは利用せず、ビルド表の決定はオプティマイザに任せています。
実行結果とヒント使用状況レポートを確認すると、オプティマイザの判断により、TABLE_Aがビルド表となっため、USE_HASH(A)がUnusedとしてレポートされてますが、残るヒントは利用され、NLJからHJへ変更が行われています。
Unusedは最終的に利用されなかったという意味で、ヒント構文場問題があるわけではなくヒントとしては正しいが、最終的に利用されなかった。このケースではビルド表となった表TABLE_Aを指定していた、USE_HASH(A)が利用されなかったということですね。
理由は、USE_HASH/USE_NLに指定する表は、プローブ表または、内部表となっているためで、このケースではオプティマイザが最終的にTABLE_Aをビルド表としたためUSE_HASH(A)が利用されなかったということを意味しています。

  1  SELECT
2 /*+
3 FULL(a)
4 FULL(b)
5 USE_HASH(a)
6 USE_HASH(b)
7 */
8 *
9 FROM
10 table_a a
11 INNER JOIN table_b b
12 ON
13 a.id = b.id
14* AND a.id BETWEEN :s AND :e

経過: 00:00:00.24

実行計画
----------------------------------------------------------
Plan hash value: 3176705182

-------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 125 | 489K| 13783 (1)| 00:00:01 |
|* 1 | FILTER | | | | | |
|* 2 | HASH JOIN | | 125 | 489K| 13783 (1)| 00:00:01 |
|* 3 | TABLE ACCESS FULL| TABLE_A | 125 | 244K| 4678 (1)| 00:00:01 |
|* 4 | TABLE ACCESS FULL| TABLE_B | 250 | 489K| 9105 (1)| 00:00:01 |
-------------------------------------------------------------------------------

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

1 - filter(TO_NUMBER(:E)>=TO_NUMBER(:S))
2 - access("A"."ID"="B"."ID")
3 - filter("A"."ID">=TO_NUMBER(:S) AND "A"."ID"<=TO_NUMBER(:E))
4 - filter("B"."ID">=TO_NUMBER(:S) AND "B"."ID"<=TO_NUMBER(:E))

Hint Report (identified by operation id / Query Block Name / Object Alias):
Total hints for statement: 1 (U - Unused (1))
---------------------------------------------------------------------------

3 - SEL$58A6D7F6 / "A"@"SEL$1"
U - USE_HASH(a)


統計
----------------------------------------------------------
30 recursive calls
0 db block gets
50366 consistent gets
50282 physical reads
0 redo size
8944 bytes sent via SQL*Net to client
52 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
4 sorts (memory)
0 sorts (disk)
5 rows processed


USE_HASH(a b)という記載ですが、前述のヒントと同じ意味です。この例のような指定方法だと、ヒント使用状況レポートの読み方の理解が重要になります。
U - USE_HASH(a b) というレポートがありますが、これだけだと、ヒント全体が利用されなかったのか? と思われるかもしれませんが、
もう一つ上の、3 - SEL$58A6D7F6 / "A"@"SEL$1"に着目する必要があります。 該当ヒントの A が利用されなかったということを意味しています。

  1  SELECT
2 /*+
3 FULL(a)
4 FULL(b)
5 USE_HASH(a b)
6 */
7 *
8 FROM
9 table_a a
10 INNER JOIN table_b b
11 ON
12 a.id = b.id
13* AND a.id BETWEEN :s AND :e

経過: 00:00:00.23

実行計画
----------------------------------------------------------
Plan hash value: 3176705182

-------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 125 | 489K| 13783 (1)| 00:00:01 |
|* 1 | FILTER | | | | | |
|* 2 | HASH JOIN | | 125 | 489K| 13783 (1)| 00:00:01 |
|* 3 | TABLE ACCESS FULL| TABLE_A | 125 | 244K| 4678 (1)| 00:00:01 |
|* 4 | TABLE ACCESS FULL| TABLE_B | 250 | 489K| 9105 (1)| 00:00:01 |
-------------------------------------------------------------------------------

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

1 - filter(TO_NUMBER(:E)>=TO_NUMBER(:S))
2 - access("A"."ID"="B"."ID")
3 - filter("A"."ID">=TO_NUMBER(:S) AND "A"."ID"<=TO_NUMBER(:E))
4 - filter("B"."ID">=TO_NUMBER(:S) AND "B"."ID"<=TO_NUMBER(:E))

Hint Report (identified by operation id / Query Block Name / Object Alias):
Total hints for statement: 1 (U - Unused (1))
---------------------------------------------------------------------------

3 - SEL$58A6D7F6 / "A"@"SEL$1"
U - USE_HASH(a b)


統計
----------------------------------------------------------
24 recursive calls
0 db block gets
50298 consistent gets
50282 physical reads
0 redo size
8944 bytes sent via SQL*Net to client
52 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
5 rows processed


この検証では、LEADINGヒントを利用せず、オプティマイザ任せにしてあります。もう一つの検証として、オプティマイザが駆動表というかビルド表にする表を変えたケースも確認しておきたいですよね! ヒント指定の思惑通りの挙動になるか。。。確認は必要ですよからねw (実案件ではここまで確認はしませんけどもw 本記事の目的がその確認ですのでw)

データをからにして、行数を逆にして、駆動表というかビルド表が変わるように仕掛けておきます。

  1* truncate table table_a

表が切り捨てられました。

経過: 00:00:00.44
1* truncate table table_b

表が切り捨てられました。

経過: 00:00:00.13

PL/SQLプロシージャが正常に完了しました。

経過: 00:00:07.02

PL/SQLプロシージャが正常に完了しました。

経過: 00:00:13.49
1 BEGIN
2 DBMS_STATS.GATHER_TABLE_STATS(ownname=>'SCOTT', tabname=>'TABLE_A', cascade=>true, no_invalidate=>false);
3* END;

PL/SQLプロシージャが正常に完了しました。

経過: 00:00:03.19
1 BEGIN
2 DBMS_STATS.GATHER_TABLE_STATS(ownname=>'SCOTT', tabname=>'TABLE_B', cascade=>true, no_invalidate=>false);
3* END;

PL/SQLプロシージャが正常に完了しました。

経過: 00:00:01.54
1* SELECT COUNT(1) FROM table_a

COUNT(1)
----------
100000

経過: 00:00:00.02
1* SELECT COUNT(1) FROM table_b

COUNT(1)
----------
50000

経過: 00:00:00.01


何もしていないオプティマイザ任せの実行計画は、NLJで、狙い通りにTABLE_Bが駆動表に切り替わっています。(うまく行ってよかったw)

  1  SELECT
2 *
3 FROM
4 table_a a
5 INNER JOIN table_b b
6 ON
7 a.id = b.id
8* AND b.id BETWEEN :s AND :e

経過: 00:00:00.05

実行計画
----------------------------------------------------------
Plan hash value: 3627193911

------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 125 | 489K| 203 (0)| 00:00:01 |
|* 1 | FILTER | | | | | |
| 2 | NESTED LOOPS | | 125 | 489K| 203 (0)| 00:00:01 |
| 3 | NESTED LOOPS | | 125 | 489K| 203 (0)| 00:00:01 |
| 4 | TABLE ACCESS BY INDEX ROWID BATCHED| TABLE_B | 125 | 244K| 78 (0)| 00:00:01 |
|* 5 | INDEX RANGE SCAN | SYS_C009318 | 225 | | 2 (0)| 00:00:01 |
|* 6 | INDEX UNIQUE SCAN | SYS_C009317 | 1 | | 0 (0)| 00:00:01 |
| 7 | TABLE ACCESS BY INDEX ROWID | TABLE_A | 1 | 2006 | 1 (0)| 00:00:01 |
------------------------------------------------------------------------------------------------------

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

1 - filter(TO_NUMBER(:E)>=TO_NUMBER(:S))
5 - access("B"."ID">=TO_NUMBER(:S) AND "B"."ID"<=TO_NUMBER(:E))
6 - access("A"."ID"="B"."ID")
filter("A"."ID">=TO_NUMBER(:S) AND "A"."ID"<=TO_NUMBER(:E))

Note
-----
- this is an adaptive plan


統計
----------------------------------------------------------
86 recursive calls
0 db block gets
113 consistent gets
7 physical reads
0 redo size
8944 bytes sent via SQL*Net to client
52 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
9 sorts (memory)
0 sorts (disk)
5 rows processed


先ほどと同じヒントを指定しました。LEADINGヒントはないので、オプティマイザは、TABLE_Bをビルド表として選択しました。狙い通りですね。
ということは、今度は、USE_HASH(B)がUnusedになるはずです。ビルド表になりましたからね。先ほどは、プローブ表だったわけですが。

ヒント使用状況レポートの詳細にはいかのようにレポートされました。USE_HASH(A B)という表よりは直感的に理解しやすいと思いますが、B Unusedですよということですね。
3 - SEL$58A6D7F6 / "B"@"SEL$1"
U - USE_HASH(b)

  1  SELECT
2 /*+
3 FULL(a)
4 FULL(b)
5 USE_HASH(a)
6 USE_HASH(b)
7 */
8 *
9 FROM
10 table_a a
11 INNER JOIN table_b b
12 ON
13 a.id = b.id
14* AND b.id BETWEEN :s AND :e

経過: 00:00:00.22

実行計画
----------------------------------------------------------
Plan hash value: 3989351480

-------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 125 | 489K| 13783 (1)| 00:00:01 |
|* 1 | FILTER | | | | | |
|* 2 | HASH JOIN | | 125 | 489K| 13783 (1)| 00:00:01 |
|* 3 | TABLE ACCESS FULL| TABLE_B | 125 | 244K| 4678 (1)| 00:00:01 |
|* 4 | TABLE ACCESS FULL| TABLE_A | 250 | 489K| 9105 (1)| 00:00:01 |
-------------------------------------------------------------------------------

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

1 - filter(TO_NUMBER(:E)>=TO_NUMBER(:S))
2 - access("A"."ID"="B"."ID")
3 - filter("B"."ID">=TO_NUMBER(:S) AND "B"."ID"<=TO_NUMBER(:E))
4 - filter("A"."ID">=TO_NUMBER(:S) AND "A"."ID"<=TO_NUMBER(:E))

Hint Report (identified by operation id / Query Block Name / Object Alias):
Total hints for statement: 1 (U - Unused (1))
---------------------------------------------------------------------------

3 - SEL$58A6D7F6 / "B"@"SEL$1"
U - USE_HASH(b)


統計
----------------------------------------------------------
113 recursive calls
0 db block gets
50787 consistent gets
50526 physical reads
0 redo size
8944 bytes sent via SQL*Net to client
52 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
25 sorts (memory)
0 sorts (disk)
5 rows processed


USE_HASH(a b)という表記にした例です意味は同じですが、USE_HASH(a b)全体がUnusedなのかと勘違いしそうですよね。よーく見てみましょう。SEL$58A6D7F6 / "B"@"SEL$1" とあり、 USE_HASH(a b)のうち、B が Unusedであることが確認できます。
3 - SEL$58A6D7F6 / "B"@"SEL$1"
U - USE_HASH(a b)

  1  SELECT
2 /*+
3 FULL(a)
4 FULL(b)
5 USE_HASH(a b)
6 */
7 *
8 FROM
9 table_a a
10 INNER JOIN table_b b
11 ON
12 a.id = b.id
13* AND b.id BETWEEN :s AND :e

経過: 00:00:00.23

実行計画
----------------------------------------------------------
Plan hash value: 3989351480

-------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 125 | 489K| 13783 (1)| 00:00:01 |
|* 1 | FILTER | | | | | |
|* 2 | HASH JOIN | | 125 | 489K| 13783 (1)| 00:00:01 |
|* 3 | TABLE ACCESS FULL| TABLE_B | 125 | 244K| 4678 (1)| 00:00:01 |
|* 4 | TABLE ACCESS FULL| TABLE_A | 250 | 489K| 9105 (1)| 00:00:01 |
-------------------------------------------------------------------------------

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

1 - filter(TO_NUMBER(:E)>=TO_NUMBER(:S))
2 - access("A"."ID"="B"."ID")
3 - filter("B"."ID">=TO_NUMBER(:S) AND "B"."ID"<=TO_NUMBER(:E))
4 - filter("A"."ID">=TO_NUMBER(:S) AND "A"."ID"<=TO_NUMBER(:E))

Hint Report (identified by operation id / Query Block Name / Object Alias):
Total hints for statement: 1 (U - Unused (1))
---------------------------------------------------------------------------

3 - SEL$58A6D7F6 / "B"@"SEL$1"
U - USE_HASH(a b)


統計
----------------------------------------------------------
5 recursive calls
0 db block gets
50541 consistent gets
50526 physical reads
0 redo size
8944 bytes sent via SQL*Net to client
52 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
5 rows processed



本検証で利用したスクリプト( hinting_tech1.sql )

drop table table_a purge
/
create table table_a
(
id number primary key
, dummy_str varchar2(2000)
)
/

drop table table_b purge
/
create table table_b
(
id number primary key
, dummy_str varchar2(2000)
)
/


BEGIN
FOR i IN 1..50000 LOOP
INSERT INTO table_a VALUES(i,LPAD('*',2000,'*'));
IF MOD(i,100) = 0
THEN
COMMIT;
END IF;
END LOOP;
END;
/


BEGIN
FOR i IN 1..100000 LOOP
INSERT INTO table_b VALUES(i,LPAD('*',2000,'*'));
IF MOD(i,100) = 0
THEN
COMMIT;
END IF;
END LOOP;
END;
/

BEGIN
DBMS_STATS.GATHER_TABLE_STATS(ownname=>'SCOTT', tabname=>'TABLE_A', cascade=>true, no_invalidate=>false);
END;
.
l
/

BEGIN
DBMS_STATS.GATHER_TABLE_STATS(ownname=>'SCOTT', tabname=>'TABLE_B', cascade=>true, no_invalidate=>false);
END;
.
l
/


SELECT COUNT(1) FROM table_a
.
l
/
SELECT COUNT(1) FROM table_b
.
l
/

REM **** Original - No Hint - 1-0 ****
VARIABLE s NUMBER
VARIABLE e NUMBER
BEGIN
:s := 1;
:e := 5;
END;
.
/


SELECT
*
FROM
table_a a
INNER JOIN table_b b
ON
a.id = b.id
AND a.id BETWEEN :s AND :e
.
l

set autot trace exp stat
/
set autot off


REM **** pattern 1-1 ****
SELECT
/*+
FULL(a)
FULL(b)
USE_HASH(a)
USE_HASH(b)
*/
*
FROM
table_a a
INNER JOIN table_b b
ON
a.id = b.id
AND a.id BETWEEN :s AND :e
.
l

set autot trace exp stat
/
set autot off


REM **** pattern 1-2 ****
SELECT
/*+
FULL(a)
FULL(b)
USE_HASH(a b)
*/
*
FROM
table_a a
INNER JOIN table_b b
ON
a.id = b.id
AND a.id BETWEEN :s AND :e
.
l

set autot trace exp stat
/
set autot off



truncate table table_a
.
l
/

truncate table table_b
.
l
/


BEGIN
FOR i IN 1..50000 LOOP
INSERT INTO table_b VALUES(i,LPAD('*',2000,'*'));
IF MOD(i,100) = 0
THEN
COMMIT;
END IF;
END LOOP;
END;
/


BEGIN
FOR i IN 1..100000 LOOP
INSERT INTO table_a VALUES(i,LPAD('*',2000,'*'));
IF MOD(i,100) = 0
THEN
COMMIT;
END IF;
END LOOP;
END;
/

BEGIN
DBMS_STATS.GATHER_TABLE_STATS(ownname=>'SCOTT', tabname=>'TABLE_A', cascade=>true, no_invalidate=>false);
END;
.
l
/

BEGIN
DBMS_STATS.GATHER_TABLE_STATS(ownname=>'SCOTT', tabname=>'TABLE_B', cascade=>true, no_invalidate=>false);
END;
.
l
/

SELECT COUNT(1) FROM table_a
.
l
/
SELECT COUNT(1) FROM table_b
.
l
/

REM **** Original - No Hint - 2-0 ****
SELECT
*
FROM
table_a a
INNER JOIN table_b b
ON
a.id = b.id
AND b.id BETWEEN :s AND :e
.
l

set autot trace exp stat
/
set autot off


REM **** pattern 2-1 ****
SELECT
/*+
FULL(a)
FULL(b)
USE_HASH(a)
USE_HASH(b)
*/
*
FROM
table_a a
INNER JOIN table_b b
ON
a.id = b.id
AND b.id BETWEEN :s AND :e
.
l

set autot trace exp stat
/
set autot off


REM **** pattern 2-2 ****
SELECT
/*+
FULL(a)
FULL(b)
USE_HASH(a b)
*/
*
FROM
table_a a
INNER JOIN table_b b
ON
a.id = b.id
AND b.id BETWEEN :s AND :e
.
l

set autot trace exp stat
/
set autot off


やっと、涼しくなってきたけど、涼しくなるのが急すぎて、まじで季節の変化が急激になってきたなぁと。

では、また。

| | | コメント (0)

2023年9月23日 (土)

帰ってきた! 標準はあるにはあるが癖の多いSQL #4 Optimizer Traceの取得でも癖がでる

Previously on Mac De Oracle
前回は、MySQLのHash Join を取り上げました。MySQL 8.0.32では NLJに使えるINDEXが存在していても、Hash Joinをヒントで強制することができる!(オプティマイザが選択することがある!)
(MySQLの生い立ちを考えると、Hash Joinとか言われると、MySQLもHash Joinが必要な時代になったのか〜。と遠くを見る自分がいるw)

ということで、今回は、前々回とりあげた、
悩ませ過ぎは及ばざるがごとし (MySQL 8.0.32編)で思い出したネタで、オプティマイザがパースに悩むというか、考えすぎている姿を、時間ではなく、オプティマイザトレースのサイズから見える化して比較!!! してみたいと思います。


実は、
10億年以上前に、Oracle Databaseで同じことやってましたw

MySQLへ話を戻すと、
MySQLのオプティマイザトレースは、Oracle Databaseのそれとは少々ことなり、パラメータで指定されたメモリー上にJSON形式で記録され、INFORMATION_SCHEMA経由で問い合わせて確認します。
パラメータで指定されたメモリーサイズを超えるトレースは切り捨てられてしまいます。Oracle Databaseのオプティマイザトレースとは異なる注意点ですね。

また、面白い特徴もあります。
オプティマイザトレースの出力サイズを事前に予測することはできないわけですが、メモリー内に記録しきれず切り捨てたバイト数をレポートしてくれます。
その機能を利用し、切り捨てたれたトレースのバイト数とトレースを記録するために指定したメモリーのバイト数の合計からトレースのサイズを確認することはできます! 面白い仕組みを提供してくれてますよね。

余談
Oracle/MySQLは、オプティマイザトレースを明示的に取得できますが、PostgreSQLって、オプティマイザトレースだけを取得するOracleの10053トレースMySQLのoptimizer_trace=onに類似する方法はなかったはず。
(DB側の機能の一部としてはないという認識なので、もし勘違いしていたら、ツッコミ🙇よろしくお願いします)



細けーことはこれぐらいにして、比較してみましょう。

まず、前回もネタにした、パース時間のながーーーーーーい、クエリー。
explainだけで、29秒 です!!  11! の組み合わせをみているわけですから、デフォルト設定のMySQL、ものすごーく考えてますよね。
OracleとかPostgreSQLならとっくに諦めている数です。

参考)
検証で利用しているMySQLの表及び索引定義とSQLスクリプトは以下ブログ参照のこと。
帰ってきた! 標準はあるにはあるが癖の多いSQL #3 オプティマイザの結合順評価テーブル数上限にも癖が出る


mysql> select version();
+-----------+
| version() |
+-----------+
| 8.0.32 |
+-----------+
1 row in set (0.00 sec)


mysql> \. test1.sql
Database changed
+----------------------------------------------------------------------------------------------------------------------+
| PLAIN |
+----------------------------------------------------------------------------------------------------------------------+
| -> Nested loop inner join (cost=7163.00 rows=10240)
-> Nested loop inner join (cost=3579.00 rows=5120)
-> Nested loop inner join (cost=1787.00 rows=2560)
-> Nested loop inner join (cost=891.00 rows=1280)
-> Nested loop inner join (cost=443.00 rows=640)
-> Nested loop inner join (cost=219.00 rows=320)
-> Nested loop inner join (cost=107.00 rows=160)
-> Nested loop inner join (cost=51.00 rows=80)
-> Nested loop inner join (cost=23.00 rows=40)
-> Nested loop inner join (cost=9.00 rows=20)
-> Index scan on master using ix_master (cost=2.00 rows=10)
-> Index lookup on detail using PRIMARY (id=`master`.id) (cost=0.52 rows=2)
-> Index lookup on t2 using PRIMARY (id=`master`.id) (cost=0.51 rows=2)
-> Index lookup on t3 using PRIMARY (id=`master`.id) (cost=0.51 rows=2)
-> Index lookup on t4 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t5 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t6 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t7 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t8 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t9 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t10 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
|
+----------------------------------------------------------------------------------------------------------------------+
1 row in set (29.05 sec)

MySQLでオプティマイザトレースを取得するには、optimizer_trace を on 、optimizer_trace_max_mem_size に オプティマイザトレース記録に必要なメモリーサイズを指定します。
その後、explain文でオプティマイザトレースを取得します。なお、今回は、explainでパース部分だけをトレースして比較します。
https://dev.mysql.com/doc/refman/8.0/en/information-schema-optimizer-trace-table.html


optimizer_trace_max_mem_size を 100 bytesにして、確実に切り捨てが起きるように設定します。

mysql> show variables like 'optimizer_trace';
+-----------------+--------------------------+
| Variable_name | Value |
+-----------------+--------------------------+
| optimizer_trace | enabled=off,one_line=off |
+-----------------+--------------------------+
1 row in set (0.18 sec)

mysql> SET optimizer_trace="enabled=on";
Query OK, 0 rows affected (0.01 sec)

mysql> show variables like 'optimizer_trace';
+-----------------+-------------------------+
| Variable_name | Value |
+-----------------+-------------------------+
| optimizer_trace | enabled=on,one_line=off |
+-----------------+-------------------------+
1 row in set (0.00 sec)

mysql> show variables like 'optimizer_trace_max_mem_size';
+------------------------------+---------+
| Variable_name | Value |
+------------------------------+---------+
| optimizer_trace_max_mem_size | 1048576 |
+------------------------------+---------+
1 row in set (0.00 sec)

mysql> SET optimizer_trace_max_mem_size = 100;
Query OK, 0 rows affected (0.00 sec)

mysql> show variables like 'optimizer_trace_max_mem_size';
+------------------------------+-------+
| Variable_name | Value |
+------------------------------+-------+
| optimizer_trace_max_mem_size | 100 |
+------------------------------+-------+
1 row in set (0.00 sec)

オプティマイザトレースの準備ができたので、explainしてみましょう!

mysql> \. test1.sql
Database changed
+----------------------------------------------------------------------------------------------------------------------+
| PLAIN |
+----------------------------------------------------------------------------------------------------------------------+
| -> Nested loop inner join (cost=7163.00 rows=10240)
-> Nested loop inner join (cost=3579.00 rows=5120)
-> Nested loop inner join (cost=1787.00 rows=2560)
-> Nested loop inner join (cost=891.00 rows=1280)
-> Nested loop inner join (cost=443.00 rows=640)
-> Nested loop inner join (cost=219.00 rows=320)
-> Nested loop inner join (cost=107.00 rows=160)
-> Nested loop inner join (cost=51.00 rows=80)
-> Nested loop inner join (cost=23.00 rows=40)
-> Nested loop inner join (cost=9.00 rows=20)
-> Index scan on master using ix_master (cost=2.00 rows=10)
-> Index lookup on detail using PRIMARY (id=`master`.id) (cost=0.52 rows=2)
-> Index lookup on t2 using PRIMARY (id=`master`.id) (cost=0.51 rows=2)
-> Index lookup on t3 using PRIMARY (id=`master`.id) (cost=0.51 rows=2)
-> Index lookup on t4 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t5 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t6 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t7 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t8 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t9 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t10 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
|
+----------------------------------------------------------------------------------------------------------------------+
1 row in set (7 min 3.67 sec)

次に、INFORMATION_SCHEMA.OPTIMIZER_TRACEからオプティマイザトレースを取得してみます。

2147483647 bytes (切り捨てられたトレースのサイズ)+ 100 bytes (optimizer_trace_max_mem_sizeパラメータに指定したサイズ)= 2,147,483,747 bytes およそ 2GB です

mysql> SELECT * FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE\G
*************************** 1. row ***************************
QUERY: explain format=tree
select
*
from
master inner join detail
on master.id = detail.id
inner join detail t2
on
t2.id = detail.id
inner join detail t3
on
t3.id = t2.id
inner join detail t4
on
t4.id = t3.id
inner join detail t5
on
t5.id = t4.id
inner join detail t6
on
t6.id = t5.id
inner join detail t7
on
t7.id = t6.id
inner join detail t8
on
t8.id = t7.id
inner join detail t9
on
t9.id = t8.id
inner join detail t10
on
t10.id = t9.id
TRACE: {
"steps": [
{
"join_preparation": {
"select#": 1,
"steps": [
{
"expanded_query": "/* select#1 */ select `master`.`id` AS `id`,`master`.`dummya` AS `dummya`,`detail`.`id` AS `id`,`detail`.`subid` AS `subid`,`detail`.`dummya` AS `dummya`,`t2`.`id` AS `id`,
`t2`.`subid` AS `subid`,`t2`.`dummya` AS `dummya`,`t3`.`id` AS `id`,`t3`.`subid` AS `subid`,`t3`.`dummya` AS `dummya`,`t4`.`id` AS `id`,`t4`.`subid` AS `subid`,`t4`.`dummya` AS `dummya`,
`t5`.`id` AS `id`,`t5`.`subid` AS `subid`,`t5`.`dummya` AS `dummya`,`t6`.`id` AS `id`,`t6`.`subid` AS `subid`,`t6`.`dummya` AS `dummya`,`t7`.`id` AS `id`,`t7`.`subid` AS `subid`,
`t7`.`dummya` AS `dummya`,`t8`.`id` AS `id`,`t8`.`subid` AS `subid`,`t8`.`dummya` AS `dummya`,`t9`.`id` AS `id`,`t9`.`subid` AS `subid`,`t9`.`dummya` AS `dummya`,`t10`.`id` AS `id`,
`t10`.`subid` AS `subid`,`t10`.`dummya` AS `dummya` from ((((((((((`master` join `detail` on((`master`.`id` = `detail`.`id`))) join `detail` `t2` on((`t2`.`id` = `detail`.`id`)))
join `detail` `t3` on((`t3`.`id` = `t2`.`id`))) join `detail` `t4` on((`t4`.`id` = `t3`.`id`))) join `detail` `t5` on((`t5`.`id` = `t4`.`id`))) join `detail` `t6` on((`t6`.`id` = `t5`.`id`)))
join `detail` `t7` on((`t7`.`id` = `t6`.`id`))) join `detail` `t8` on((`t8`.`id` = `t7`.`id`))) join `detail` `t9` on((`t9`.`id` = `t8`.`id`))) join `detail` `t10` on((`t10`.`id` = `t9`.`id`)))"
},
{
"transformations_to_nested_joins": {
"transformations": [
"JOIN_condition_to_WHERE",
"parenthesis_removal"
],
"expanded_query": "/* select#1 */ select `master`.`id` AS `id`,`master`.`dummya` AS `dummya`,`detail`.`id` AS `id`,`detail`.`subid` AS `subid`,`detail`.`dummya` AS `dummya`,
`t2`.`id` AS `id`,`t2`.`subid` AS `subid`,`t2`.`dummya` AS `dummya`,`t3`.`id` AS `id`,`t3`.`subid` AS `subid`,`t3`.`dummya` AS `dummya`,`t4`.`id` AS `id`,`t4`.`subid` AS `subid`,
`t4`.`dummya` AS `dummya`,`t5`.`id` AS `id`,`t5`.`subid` AS `subid`,`t5`.`dummya` AS `dummya`,`t6`.`id` AS `id`,`t6`.`subid` AS `subid`,`t6`.`dummya` AS `dummya`,`t7`.`id` AS `id`,
`t7`.`subid` AS `subid`,`t7`.`dummya` AS `dummya`,`t8`.`id` AS `id`,`t8`.`subid` AS `subid`,`t8`.`dummya` AS `dummya`,`t9`.`id` AS `id`,`t9`.`subid` AS `subid`,
`t9`.`dummya` AS `dummya`,`t10`.`id` AS `id`,`t10`.`subid` AS `subid`,`t10`.`dummya` AS `dummya` from `master` join `detail` join `detail` `t2` join `detail` `t3`
join `detail` `t4` join `detail` `t5` join `detail` `t6` join `detail` `t7` join `detail` `t8` join `detail` `t9` join `detail` `t10`
where ((`t10`.`id` = `t9`.`id`) and (`t9`.`id` = `t8`.`id`) and (`t8`.`id` = `t7`.`id`) and (`t7`.`id` = `t6`.`id`) and (`t6`.`id` = `t5`.`id`) and (`t5`.`id` = `t4`.`id`)
and (`t4`.`id` = `t3`.`id`) and (`t3`.`id` = `t2`.`id`) and (`t2`.`id` = `detail`.`id`) and (`master`.`id` = `detail`.`id`))"
}
}
]
}
},


....中略....

},
"condition_filtering_pct": 100,
"rows_for_plan": 10240,
"cost_for_plan": 7163,
"pruned_by_cost": true
}

MISSING_BYTES_BEYOND_MAX_MEM_SIZE: 2147483647
INSUFFICIENT_PRIVILEGES: 0
1 row in set (0.26 sec)

mysql>


次に、考え過ぎているオプティマイザに、
JOIN_ORDERヒントを利用し、結合順を 考えるな! 感じろ! 作戦でオプティマイザに考えさせないように。。。パースも速いし、オプティマイザトレースのJSONのサイズも小さい!

mysql> \. test1.sql
Database changed
+----------------------------------------------------------------------------------------------------------------------+
| PLAIN |
+----------------------------------------------------------------------------------------------------------------------+
| -> Nested loop inner join (cost=7163.00 rows=10240)
-> Nested loop inner join (cost=3579.00 rows=5120)
-> Nested loop inner join (cost=1787.00 rows=2560)
-> Nested loop inner join (cost=891.00 rows=1280)
-> Nested loop inner join (cost=443.00 rows=640)
-> Nested loop inner join (cost=219.00 rows=320)
-> Nested loop inner join (cost=107.00 rows=160)
-> Nested loop inner join (cost=51.00 rows=80)
-> Nested loop inner join (cost=23.00 rows=40)
-> Nested loop inner join (cost=9.00 rows=20)
-> Index scan on master using ix_master (cost=2.00 rows=10)
-> Index lookup on detail using PRIMARY (id=`master`.id) (cost=0.52 rows=2)
-> Index lookup on t2 using PRIMARY (id=`master`.id) (cost=0.51 rows=2)
-> Index lookup on t3 using PRIMARY (id=`master`.id) (cost=0.51 rows=2)
-> Index lookup on t4 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t5 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t6 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t7 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t8 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t9 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t10 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
|
+----------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)


オプティマイザトレース結果のJSONサイズは、

57379 bytes (切り捨てられたトレースのバイト数) + 100 bytes(optimizer_trace_max_mem_sizeパラメータに指定したサイズ) = 57,479 bytes 、約 56 KBとなりました。

2GB vs 56KB

オプティマイザトレースのJSONサイズからオプティマイザの仕事量を覗いてみるのも面白いですよね。(俺だけか喜んでるのw)

mysql> SELECT * FROM INFORMATION_SCHEMA.OPTIMIZER_TRACE\G
*************************** 1. row ***************************
QUERY: explain format=tree
select
/*+
JOIN_ORDER(master,detail,t2,t3,t4,t5,t6,t7,t8,t9,t10)
*/
*
from
master inner join detail
on master.id = detail.id
inner join detail t2
on
t2.id = detail.id
inner join detail t3
on
t3.id = t2.id
inner join detail t4
on
t4.id = t3.id
inner join detail t5
on
t5.id = t4.id
inner join detail t6
on
t6.id = t5.id
inner join detail t7
on
t7.id = t6.id
inner join detail t8
on
t8.id = t7.id
inner join detail t9
on
t9.id = t8.id
inner join detail t10
on
t10.id = t9.id
TRACE:
MISSING_BYTES_BEYOND_MAX_MEM_SIZE: 57379
INSUFFICIENT_PRIVILEGES: 0
1 row in set (0.00 sec)

ということで、 MySQLのオプティマイザトレースはメモリー中に記録され、メモリーに記録できないトレースは切り捨てられる。
また、切り捨てられたサイズ+オプティマイザトレース向けメモリーサイズからトレースを完全に取得するためのメモリーサイズを確認できる。
ただし、必要がメモリーの空き次第ではありそうですね。ということで、Oracle Databaseの 10053トレースとはちょっと違う注意点もあるな。

という知見を得た :)

Oracle Database の 10053トレースでのトレースによる考えている仕事量の見える化同様に、MySQLのオプティマイザトレースでも、オプティマイザが考えすぎていると様子をトレースサイズで見える化してみるという検証はここまで。
MySQLのオプティマイザトレースをなんとなく眺めて、なぜ、その判断をしたのだなどを追うのはまた別の機会に;)


10月も近いのに、muggy な気候の続く、東京より。

ではまた。





関連エントリー
悩ませ過ぎは及ばざるがごとし (MySQL 8.0.32編)
悩ませ過ぎは及ばざるがごとし #7 - おまけ



標準はあるにはあるが癖の多いSQL 全部俺 #1 Pagination
標準はあるにはあるが癖の多いSQL 全部俺 #2 関数名は同じでも引数が逆の罠!
標準はあるにはあるが癖の多いSQL 全部俺 #3 データ型確認したい時あるんです
標準はあるにはあるが癖の多いSQL 全部俺 #4 リテラル値での除算の内部精度も違うのよ!
標準はあるにはあるが癖の多いSQL 全部俺 #5 和暦変換機能ある方が少数派
標準はあるにはあるが癖の多いSQL 全部俺 #6 時間厳守!
標準はあるにはあるが癖の多いSQL 全部俺 #7 期間リテラル!
標準はあるにはあるが癖の多いSQL 全部俺 #8 翌月末日って何日?
標準はあるにはあるが癖の多いSQL 全部俺 #9 部分文字列の扱いでも癖が出る><
標準はあるにはあるが癖の多いSQL 全部俺 #10 文字列連結の罠(有名なやつ)
標準はあるにはあるが癖の多いSQL 全部俺 #11 デュエル、じゃなくて、デュアル
標準はあるにはあるが癖の多いSQL 全部俺 #12 文字[列]探すにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #13 あると便利ですが意外となかったり
標準はあるにはあるが癖の多いSQL 全部俺 #14 連番の集合を返すにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #15 SQL command line client
標準はあるにはあるが癖の多いSQL 全部俺 #16 SQLのレントゲンを撮る方法
標準はあるにはあるが癖の多いSQL 全部俺 #17 その空白は許されないのか?
標準はあるにはあるが癖の多いSQL 全部俺 #18 (+)の外部結合は方言
標準はあるにはあるが癖の多いSQL 全部俺 #19 帰ってきた、部分文字列の扱いでも癖w
標準はあるにはあるが癖の多いSQL 全部俺 #20 結果セットを単一列に連結するにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #21 演算結果にも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #22 集合演算にも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #23 複数行INSERTにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #24 乱数作るにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #25 SQL de Fractalsにも癖がある:)
標準はあるにはあるが癖の多いSQL 全部俺 おまけ SQL de 湯婆婆やるにも癖がでるw
帰ってきた! 標準はあるにはあるが癖の多いSQL #1 SQL de ROT13 やるにも癖が出るw
帰ってきた! 標準はあるにはあるが癖の多いSQL #2 Actual Plan取得中のキャンセルでも癖が出る
帰ってきた! 標準はあるにはあるが癖の多いSQL #3 オプティマイザの結合順評価テーブル数上限にも癖が出る

| | | コメント (0)

2023年9月 7日 (木)

MySQL 8.0.32では NLJに使えるINDEXが存在していても、Hash Joinをヒントで強制することができる!(オプティマイザが選択することがある!)

Previously on Mac De Oracle
前回は悩ませ過ぎは及ばざるがごとし (MySQL 8.0.32編)で、パース時間が長い場合、ヒントを使うことでパース時間を短縮させることが可能なケースもあるというという話でした。Oracleでも同様のケースがあることもお話ししました。が、その中で、MySQL 8.0.32 ではオプティマイザが、NLJで利用するINDEXが存在していても、HJを選択することがあることに気づいた!(どうやら、該当表がバッファープールに乗っているとNLJを選択しやすくなる傾向もありそう)という、予告編までw

ということで、今日は、MySQL 8.0.32 では、NLJに利用するINDEXが存在していても、オプティマイザは、HJを選択することもあるし、ヒントによってHJを矯正できるのか? ということを確認してみました。


これまでの、MySQL 8.0.x台のHash Joinの適用範囲および挙動は以下のブログで取り上げられています。(非常に参考になりました。ありがたいですね。本当に ;)

MySQL 8.0.18のHASH JOINを試した(tom__bo’s Blog)
https://tombo2.hatenablog.com/entry/2019/10/14/212100

MySQL 8.0.20 のハッシュジョイン(Hash Join)を INDEX があるテーブルで試してみる(Qiita @hmatsu47)
https://qiita.com/hmatsu47/items/e473a3e566b910d61f5b

MySQL 8.0.20 でHASH JOINが効くケースが拡大した (mita2 database life)
https://mita2db.hateblo.jp/entry/2020/05/03/174101

と、NLJ可能な索引があるとなぜか、HJにならなかった。ついでに、HASH_JOINというヒントは、8.0.18でしか効果がない!
https://dev.mysql.com/doc/refman/8.0/ja/optimizer-hints.html#optimizer-hints-table-level


MySQL 8.0 Manual
https://dev.mysql.com/doc/refman/8.0/en/hash-joins.html

"8.2.1.4 Hash Join Optimization By default, MySQL (8.0.18 and later) employs hash joins whenever possible. It is possible to control whether hash joins are employed using one of the BNL and NO_BNL optimizer hints, or by setting block_nested_loop=on or block_nested_loop=off as part of the setting for the optimizer_switch server system variable.

Note
MySQL 8.0.18 supported setting a hash_join flag in optimizer_switch, as well as the optimizer hints HASH_JOIN and NO_HASH_JOIN. In MySQL 8.0.19 and later, none of these have any effect any longer.

Beginning with MySQL 8.0.18, MySQL employs a hash join for any query for which each join has an equi-join condition, and in which there are no indexes that can be applied to any join conditions, such as this one:"


ななな、なーーーん、だってーーーーーーっ!。


だが、しかし、8.0.32 になるとマニュアルの記載では特に見かけなかったが、NLJに使える索引がある場合でも等価結合でHash Joinになるではありませんか。みなさん!

まじでーーーというか、実際、そうなるもん!

以下、前回のブログ参照のこと。
悩ませ過ぎは及ばざるがごとし (MySQL 8.0.32編) (Mac De Oracle)
https://discus-hamburg.cocolog-nifty.com/mac_de_oracle/2023/09/post-bee50f.html


ただし、 HASH_JOINヒントは使えないので、どのようにヒントを書けば Hash Joinを強制できるのだろう? 8.0.32編 ということで、試してみた!

結論から言ってしまうと

MySQL 8.0.32 では NLJに使えるINDEXが存在していても、Hash Joinを強制することができる! (いつから変わったかは不明だが、8.0.32では可能!)
MySQL 8.0.32 では NLJに使えるINDEXが存在していても、Hash Joinを強制することができる! (いつから変わったかは不明だが、8.0.32では可能!)


詳細は、以下の検証を。比較的気楽にヒントでHJを強制できるようです(だたしちょっと分かりずらいね。Oracleとかと比べると)

表定義等は前々回のエントリー参照のこと。帰ってきた! 標準はあるにはあるが癖の多いSQL #3 オプティマイザの結合順評価テーブル数上限にも癖が出る

起動直後、バッファキャッシュが空だと(未検証)Hash Joinが選択されるので、まずは、オプティマイザが Hash Join (等価結合でNLJに利用できる索引が存在する状況でも)が選択されている(前回のブログのおさらい)

[master@localhost ~]$ sudo service mysqld restart
Redirecting to /bin/systemctl restart mysqld.service
[master@localhost ~]$ mysql -u scott -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.32 Source distribution

Copyright (c) 2000, 2023, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> select version();
+-----------+
| version() |
+-----------+
| 8.0.32 |
+-----------+
1 row in set (0.00 sec)

mysql>
mysql> \. test1.sql
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
+----------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+----------------------------------------------------------------------------------------------------------------------+
| -> Inner hash join (t10.id = `master`.id) (cost=20489.43 rows=10240)
-> Table scan on t10 (cost=0.00 rows=20)
-> Hash
-> Inner hash join (t9.id = `master`.id) (cost=10238.74 rows=5120)
-> Table scan on t9 (cost=0.00 rows=20)
-> Hash
-> Inner hash join (t8.id = `master`.id) (cost=5113.38 rows=2560)
-> Table scan on t8 (cost=0.00 rows=20)
-> Hash
-> Inner hash join (t7.id = `master`.id) (cost=2550.45 rows=1280)
-> Table scan on t7 (cost=0.00 rows=20)
-> Hash
-> Inner hash join (t6.id = `master`.id) (cost=1268.60 rows=640)
-> Table scan on t6 (cost=0.00 rows=20)
-> Hash
-> Inner hash join (t5.id = `master`.id) (cost=627.24 rows=320)
-> Table scan on t5 (cost=0.01 rows=20)
-> Hash
-> Inner hash join (t4.id = `master`.id) (cost=306.09 rows=160)
-> Table scan on t4 (cost=0.02 rows=20)
-> Hash
-> Inner hash join (t3.id = `master`.id) (cost=145.03 rows=80)
-> Table scan on t3 (cost=0.03 rows=20)
-> Hash
-> Inner hash join (t2.id = `master`.id) (cost=64.01 rows=40)
-> Table scan on t2 (cost=0.06 rows=20)
-> Hash
-> Inner hash join (detail.id = `master`.id) (cost=23.00 rows=20)
-> Table scan on detail (cost=0.12 rows=20)
-> Hash
-> Index scan on master using ix_master (cost=2.00 rows=10)
|
+----------------------------------------------------------------------------------------------------------------------+
1 row in set (0.01 sec)

関連する表をアクセスしてバッファープールに乗った状態にすると。オプティマイザは、INDEXを利用したNLJを選択するようになった。(前回のブログのおさらい)

mysql> 
mysql> select * from master;
+----+--------+
| id | dummya |
+----+--------+
| 1 | 1 |
| 10 | 10 |

....中略....

| 7 | 7 |
| 8 | 8 |
| 9 | 9 |
+----+--------+
10 rows in set (0.01 sec)

mysql> select * from detail;
+----+-------+--------+
| id | subid | dummya |
+----+-------+--------+
| 1 | 1 | 11 |
| 1 | 2 | 12 |
| 2 | 1 | 21 |
| 2 | 2 | 22 |

....中略....

| 9 | 1 | 91 |
| 9 | 2 | 92 |
| 10 | 1 | 101 |
| 10 | 2 | 102 |
+----+-------+--------+
20 rows in set (0.01 sec)

mysql> \. test1.sql
Database changed
+----------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+----------------------------------------------------------------------------------------------------------------------+
| -> Nested loop inner join (cost=7163.00 rows=10240)
-> Nested loop inner join (cost=3579.00 rows=5120)
-> Nested loop inner join (cost=1787.00 rows=2560)
-> Nested loop inner join (cost=891.00 rows=1280)
-> Nested loop inner join (cost=443.00 rows=640)
-> Nested loop inner join (cost=219.00 rows=320)
-> Nested loop inner join (cost=107.00 rows=160)
-> Nested loop inner join (cost=51.00 rows=80)
-> Nested loop inner join (cost=23.00 rows=40)
-> Nested loop inner join (cost=9.00 rows=20)
-> Index scan on master using ix_master (cost=2.00 rows=10)
-> Index lookup on detail using PRIMARY (id=`master`.id) (cost=0.52 rows=2)
-> Index lookup on t2 using PRIMARY (id=`master`.id) (cost=0.51 rows=2)
-> Index lookup on t3 using PRIMARY (id=`master`.id) (cost=0.51 rows=2)
-> Index lookup on t4 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t5 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t6 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t7 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t8 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t9 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t10 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
|
+----------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)


オプティマイザがNLJを選択するようになった状況で、Hash Join となるヒントを追加(ヒント等は後半のスクリプト test1_hj.sql を参照のこと)すると、NLJで利用可能なINDEXが存在していたとしても、HJを強制できる。(8.0.20ごろまでは、HJは選択されなかったようなので、8.0.21から8.0.32の間でHJの適用範囲がさらに拡大されたのでしょうね。想像ですけども)

mysql> 
mysql> \. test1_hj.sql
Database changed
+----------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+----------------------------------------------------------------------------------------------------------------------+
| -> Inner hash join (t10.id = `master`.id) (cost=20468.86 rows=10240)
-> Table scan on t10 (cost=0.00 rows=20)
-> Hash
-> Inner hash join (t9.id = `master`.id) (cost=10226.19 rows=5120)
-> Table scan on t9 (cost=0.00 rows=20)
-> Hash
-> Inner hash join (t8.id = `master`.id) (cost=5104.85 rows=2560)
-> Table scan on t8 (cost=0.00 rows=20)
-> Hash
-> Inner hash join (t7.id = `master`.id) (cost=2544.11 rows=1280)
-> Table scan on t7 (cost=0.00 rows=20)
-> Hash
-> Inner hash join (t6.id = `master`.id) (cost=1263.65 rows=640)
-> Table scan on t6 (cost=0.00 rows=20)
-> Hash
-> Inner hash join (t5.id = `master`.id) (cost=623.31 rows=320)
-> Table scan on t5 (cost=0.00 rows=20)
-> Hash
-> Inner hash join (t4.id = `master`.id) (cost=303.02 rows=160)
-> Table scan on t4 (cost=0.01 rows=20)
-> Hash
-> Inner hash join (t3.id = `master`.id) (cost=142.76 rows=80)
-> Table scan on t3 (cost=0.01 rows=20)
-> Hash
-> Inner hash join (t2.id = `master`.id) (cost=62.50 rows=40)
-> Table scan on t2 (cost=0.02 rows=20)
-> Hash
-> Inner hash join (detail.id = `master`.id) (cost=22.25 rows=20)
-> Table scan on detail (cost=0.05 rows=20)
-> Hash
-> Table scan on master (cost=2.00 rows=10)
|
+----------------------------------------------------------------------------------------------------------------------+
1 row in set (0.03 sec)


ついでなので、NLJになるようにがっつりヒントで固めてみた場合はどうなるか? 想定通り、NLJに倒せますね。(前回のブログのおさらい)

mysql> 
mysql> \. test1_nlj.sql
Database changed
+----------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+----------------------------------------------------------------------------------------------------------------------+
| -> Nested loop inner join (cost=7173.00 rows=10240)
-> Nested loop inner join (cost=3589.00 rows=5120)
-> Nested loop inner join (cost=1797.00 rows=2560)
-> Nested loop inner join (cost=901.00 rows=1280)
-> Nested loop inner join (cost=453.00 rows=640)
-> Nested loop inner join (cost=229.00 rows=320)
-> Nested loop inner join (cost=117.00 rows=160)
-> Nested loop inner join (cost=61.00 rows=80)
-> Nested loop inner join (cost=33.00 rows=40)
-> Nested loop inner join (cost=19.00 rows=20)
-> Index scan on master using ix_master (cost=12.00 rows=10)
-> Index lookup on detail using PRIMARY (id=`master`.id) (cost=0.52 rows=2)
-> Index lookup on t2 using PRIMARY (id=`master`.id) (cost=0.51 rows=2)
-> Index lookup on t3 using PRIMARY (id=`master`.id) (cost=0.51 rows=2)
-> Index lookup on t4 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t5 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t6 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t7 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t8 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t9 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t10 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
|
+----------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)

mysql>


検証で利用した 表と索引定義は前回のブログ参照のこと。

以下、今回検証で利用したスクリプト

オリジナル。結合順だけ JOIN_ORDERで制御。オプティマイザが、NLJ/HJをその時の気分で(嘘w)、その時の状況で判断して、NLJ/HJのいずれかを選択する(ことがわかっている 8.0.32 では)

[master@localhost ~]$ cat test1.sql
use perftestdb;


explain format=tree
select
/*+
JOIN_ORDER(master,detail,t2,t3,t4,t5,t6,t7,t8,t9,t10)
*/
*
from
master inner join detail
on master.id = detail.id
inner join detail t2
on
t2.id = detail.id
inner join detail t3
on
t3.id = t2.id
inner join detail t4
on
t4.id = t3.id
inner join detail t5
on
t5.id = t4.id
inner join detail t6
on
t6.id = t5.id
inner join detail t7
on
t7.id = t6.id
inner join detail t8
on
t8.id = t7.id
inner join detail t9
on
t9.id = t8.id
inner join detail t10
on
t10.id = t9.id
;


JOIN_ORDER/NO_INDEX/NO_JOIN_INDEXヒントを利用して、結合順と索引スキャンの抑止、そして、NLJで利用する索引の利用を抑止することで HJ になるようにしたスクリプト

[master@localhost ~]$ cat test1_hj.sql
use perftestdb;


explain format=tree
select
/*+
JOIN_ORDER(master,detail,t2,t3,t4,t5,t6,t7,t8,t9,t10)
NO_INDEX(master ix_master)
NO_JOIN_INDEX(detail primary)
NO_JOIN_INDEX(t2 primary)
NO_JOIN_INDEX(t3 primary)
NO_JOIN_INDEX(t4 primary)
NO_JOIN_INDEX(t5 primary)
NO_JOIN_INDEX(t6 primary)
NO_JOIN_INDEX(t7 primary)
NO_JOIN_INDEX(t8 primary)
NO_JOIN_INDEX(t9 primary)
NO_JOIN_INDEX(t10 primary)
*/
*
from
master inner join detail
on master.id = detail.id
inner join detail t2
on
t2.id = detail.id
inner join detail t3
on
t3.id = t2.id
inner join detail t4
on
t4.id = t3.id
inner join detail t5
on
t5.id = t4.id
inner join detail t6
on
t6.id = t5.id
inner join detail t7
on
t7.id = t6.id
inner join detail t8
on
t8.id = t7.id
inner join detail t9
on
t9.id = t8.id
inner join detail t10
on
t10.id = t9.id
;


最後に、前回も利用した NLJ に倒すためのガチガチヒントバージョンのスクリプト

[master@localhost ~]$ cat test1_nlj.sql
use perftestdb;


explain format=tree
select
/*+
JOIN_ORDER(master,detail,t2,t3,t4,t5,t6,t7,t8,t9,t10)
INDEX(master ix_master)
JOIN_INDEX(detail primary)
JOIN_INDEX(t2 primary)
JOIN_INDEX(t3 primary)
JOIN_INDEX(t4 primary)
JOIN_INDEX(t5 primary)
JOIN_INDEX(t6 primary)
JOIN_INDEX(t7 primary)
JOIN_INDEX(t8 primary)
JOIN_INDEX(t9 primary)
JOIN_INDEX(t10 primary)
*/
*
from
master inner join detail
on master.id = detail.id
inner join detail t2
on
t2.id = detail.id
inner join detail t3
on
t3.id = t2.id
inner join detail t4
on
t4.id = t3.id
inner join detail t5
on
t5.id = t4.id
inner join detail t6
on
t6.id = t5.id
inner join detail t7
on
t7.id = t6.id
inner join detail t8
on
t8.id = t7.id
inner join detail t9
on
t9.id = t8.id
inner join detail t10
on
t10.id = t9.id
;

ということで、まだまだまだ、蒸し暑いし、台風直撃しそうな、東京からお送りしました。台風に注意しつつ週末を過ごすしかなさそう。

では、また。





悩ませ過ぎは及ばざるがごとし (MySQL 8.0.32編)

| | | コメント (0)

2023年9月 2日 (土)

帰ってきた! 標準はあるにはあるが癖の多いSQL #3 オプティマイザの結合順評価テーブル数上限にも癖が出る

Previously on Mac De Oracle前回の癖はw
帰ってきた! 標準はあるにはあるが癖の多いSQL #2 Actual Plan取得中のキャンセルでも癖が出る
でした。
実は、今日の癖は、その検証時に気づいた、MySQLで結合表数が多いと、パース時間長くなってね? というか長いよね! という点。

オプティマイザは、皆さんの難しい(いや、難しい必要はないですが)SQLを無駄なく、そのSQLに見合った最短の時間で結果を返そうと努力して実行計画を立てています。これを解析フェーズとかパース時間とか、ハードパース時間とかいくつかのバリエーションで表現していたりします。
今回は、中をとってw パース時間としますね。SQL文の実行計画を立てる、オプティマイザがあれやこれや考えて実行計画を立て終えるまでの時間のことです。

先日の検証で、10表結合したのですが、MySQLでは、20秒以上要していました。実行計画でオプティマイザ考えすぎて帰ってこない。SQL文の実行ではなく実行計画を立てるところで時間がかかるケースはいくつか有名なのがありますが。
そうならないように各データベースは閾値を設けています。今回は、結合順評価テーブル数上限 ってやつです。結合する表の順序の組み合わせをどこまで検討するかということなのですが、これが、票数が増えると鰻登りに増加します。
今回のケースだと、1011表なので、10!11!ですね。2表をを繰り返し結合していてもしっかり考えてくれちゃいます。

ただ、何も考えずにオプティマイザに時間を与えていると、パース時間だけでとんでもない時間になったり、性能要件に見合わないといったケースが出てきます。なので、適当なところで手を打って、オプティマイザに考えさせ過ぎないようしています。
その閾値のデフォルト値が、MySQLとOracle/PostgreSQLとでは結構違うみたい。というところが今回の癖!

ちなみに、Oracleの例ですが、結合以外にINリストに大量にリテラルがあり、かつ考慮する必要のある索引も多いケースでは、悩ませ過ぎは及ばざるがごとし #7 - おまけなんてことも起こりますw オプティマイザを悩ませすぎると大変なので、そんな時は、SQLヒントなどを使って、オプティマイザに考えさせないという治療で回避するのが特効薬ですw

余談はこれぐらいにして、本題へ。

 

Note:
(MySQL/PostgreSQL/Oracle 23c freeそれぞれ、同一Virtualbox VMを利用して検証しているため、CPUリソース等の差異はありません)

最初は、ネタの発端になったMySQL 8.0.32から


mysql> select version();
+-----------+
| version() |
+-----------+
| 8.0.32 |
+-----------+
1 row in set (0.00 sec)

mysql> show tables;
+----------------------+
| Tables_in_perftestdb |
+----------------------+
| detail |
| master |
+----------------------+
2 rows in set (0.00 sec)

mysql> desc detail;
+--------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------+-------------+------+-----+---------+-------+
| id | int | NO | PRI | NULL | |
| subid | int | NO | PRI | NULL | |
| dummya | varchar(10) | YES | | NULL | |
+--------+-------------+------+-----+---------+-------+
3 rows in set (0.00 sec)

mysql> show indexes from detail;
+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression |
+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| detail | 0 | PRIMARY | 1 | id | A | 10 | NULL | NULL | | BTREE | | | YES | NULL |
| detail | 0 | PRIMARY | 2 | subid | A | 20 | NULL | NULL | | BTREE | | | YES | NULL |
+--------+------------+----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
2 rows in set (0.02 sec)

mysql> desc master;
+--------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------+-------------+------+-----+---------+-------+
| id | int | NO | PRI | NULL | |
| dummya | varchar(10) | YES | MUL | NULL | |
+--------+-------------+------+-----+---------+-------+
2 rows in set (0.00 sec)

mysql> show indexes from master;
+--------+------------+-----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Collation | Cardinality | Sub_part | Packed | Null | Index_type | Comment | Index_comment | Visible | Expression |
+--------+------------+-----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
| master | 0 | PRIMARY | 1 | id | A | 10 | NULL | NULL | | BTREE | | | YES | NULL |
| master | 1 | ix_master | 1 | dummya | A | 10 | NULL | NULL | YES | BTREE | | | YES | NULL |
+--------+------------+-----------+--------------+-------------+-----------+-------------+----------+--------+------+------------+---------+---------------+---------+------------+
2 rows in set (0.02 sec)

mysql> analyze table master;
+-------------------+---------+----------+----------+
| Table | Op | Msg_type | Msg_text |
+-------------------+---------+----------+----------+
| perftestdb.master | analyze | status | OK |
+-------------------+---------+----------+----------+
1 row in set (0.04 sec)

mysql> analyze table detail;
+-------------------+---------+----------+----------+
| Table | Op | Msg_type | Msg_text |
+-------------------+---------+----------+----------+
| perftestdb.detail | analyze | status | OK |
+-------------------+---------+----------+----------+
1 row in set (0.02 sec)

mysql>

 

 

表定義とデータは以前のエントリーも参考に
MySQL 8.0.32 / explain analyze 実行途中でキャンセルできるみたいだけど、キャンセルしたら、Actual Plan、途中まで出るの?

 

では、

MySQL 8.0.32で、パースに時間を要するケースの再現。optimizer_search_depthパラメータはデフォルトのまま

 

MySQL : optimizer_search_depth optimizer_search_depth


mysql> show variables like 'optimizer_search_depth';
+------------------------+-------+
| Variable_name | Value |
+------------------------+-------+
| optimizer_search_depth | 62 |
+------------------------+-------+
1 row in set (0.00 sec)

mysql> \. test1.sql
Database changed
+----------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+----------------------------------------------------------------------------------------------------------------------+
| -> Nested loop inner join (cost=7162.25 rows=10240)
-> Nested loop inner join (cost=3578.25 rows=5120)
-> Nested loop inner join (cost=1786.25 rows=2560)
-> Nested loop inner join (cost=890.25 rows=1280)
-> Nested loop inner join (cost=442.25 rows=640)
-> Nested loop inner join (cost=218.25 rows=320)
-> Nested loop inner join (cost=106.25 rows=160)
-> Nested loop inner join (cost=50.25 rows=80)
-> Nested loop inner join (cost=22.25 rows=40)
-> Nested loop inner join (cost=8.25 rows=20)
-> Index scan on master using ix_master (cost=1.25 rows=10)
-> Index lookup on detail using PRIMARY (id=`master`.id) (cost=0.52 rows=2)
-> Index lookup on t2 using PRIMARY (id=`master`.id) (cost=0.51 rows=2)
-> Index lookup on t3 using PRIMARY (id=`master`.id) (cost=0.51 rows=2)
-> Index lookup on t4 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t5 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t6 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t7 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t8 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t9 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t10 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
|
+----------------------------------------------------------------------------------------------------------------------+
1 row in set (21.37 sec)

パースに 21.37 sec 要していますね。 62!まで結合順を探査する設定ですから、このケースだと、10!まで行っているということなのでしょうね。

では、Oracleなどで設けられてる閾値と同等の 6 にせってしてみた場合のパース時間はどうでしょうね。このケースで実行計画を間違うことはない(単純なので)ですが、複雑な結合だと、実行計画をミスする率は多くなりそうですけども、Oracleとかだとその程度で使ってますし。。この程度でも良いのかもしれません。時間かかってもいいよというケースも当然あるので、非機能要件次第ではありますけどね。


mysql> set optimizer_search_depth = 6;
Query OK, 0 rows affected (0.00 sec)

mysql> show variables like 'optimizer_search_depth';
+------------------------+-------+
| Variable_name | Value |
+------------------------+-------+
| optimizer_search_depth | 6 |
+------------------------+-------+
1 row in set (0.00 sec)

mysql> \. test1.sql
Database changed
+----------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+----------------------------------------------------------------------------------------------------------------------+
| -> Nested loop inner join (cost=7162.25 rows=10240)
-> Nested loop inner join (cost=3578.25 rows=5120)
-> Nested loop inner join (cost=1786.25 rows=2560)
-> Nested loop inner join (cost=890.25 rows=1280)
-> Nested loop inner join (cost=442.25 rows=640)
-> Nested loop inner join (cost=218.25 rows=320)
-> Nested loop inner join (cost=106.25 rows=160)
-> Nested loop inner join (cost=50.25 rows=80)
-> Nested loop inner join (cost=22.25 rows=40)
-> Nested loop inner join (cost=8.25 rows=20)
-> Index scan on master using ix_master (cost=1.25 rows=10)
-> Index lookup on detail using PRIMARY (id=`master`.id) (cost=0.52 rows=2)
-> Index lookup on t2 using PRIMARY (id=`master`.id) (cost=0.51 rows=2)
-> Index lookup on t3 using PRIMARY (id=`master`.id) (cost=0.51 rows=2)
-> Index lookup on t4 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t5 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t6 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t7 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t8 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t9 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t10 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
|
+----------------------------------------------------------------------------------------------------------------------+
1 row in set (0.27 sec)

はい! パース時間は、一気短縮され、 0.27 sec になりました。Oracleのハードパースでもこれぐらいなこんな程度だとは思います。

 

次に、PostgreSQLで確認してみましょ。 PostgreSQLの場合は、Oracleに近かったはず。表定義とデータ量は同じです。

PostgreSQLでは、結合順序評価上限は以下join_collapse_limitパラメータで制御するようですね。あまり気にしてなかったので脳のしわが一つ増えた! defaultは、8 でOracleより多少、多いというところですね。
join_collapse_limit

 


perftestdb=> select version();
version
--------------------------------------------------------------------------------------------------------
PostgreSQL 13.6 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 8.5.0 20210514 (Red Hat 8.5.0-4), 64-bit
(1 行)

perftestdb=> show join_collapse_limit;
join_collapse_limit
---------------------
8
(1 行)

perftestdb=> \d+ master
テーブル"public.master"
列 | タイプ | 照合順序 | Null 値を許容 | デフォルト | ストレージ | 統計目標 | 説明
--------+-----------------------+----------+---------------+------------+------------+----------+------
id | integer | | not null | | plain | |
dummya | character varying(10) | | | | extended | |
インデックス:
"master_pkey" PRIMARY KEY, btree (id)
アクセスメソッド: heap

perftestdb=> \d+ detail
テーブル"public.detail"
列 | タイプ | 照合順序 | Null 値を許容 | デフォルト | ストレージ | 統計目標 | 説明
--------+-----------------------+----------+---------------+------------+------------+----------+------
id | integer | | not null | | plain | |
subid | integer | | not null | | plain | |
dummya | character varying(10) | | | | extended | |
インデックス:
"pk_detail" PRIMARY KEY, btree (id, subid)
アクセスメソッド: heap

perftestdb=> analyze verbose master;
INFO: analyzing "public.master"
INFO: "master": scanned 1 of 1 pages, containing 10 live rows and 0 dead rows; 10 rows in sample, 10 estimated total rows
ANALYZE
perftestdb=> analyze verbose detail;
INFO: analyzing "public.detail"
INFO: "detail": scanned 1 of 1 pages, containing 20 live rows and 0 dead rows; 20 rows in sample, 20 estimated total rows
ANALYZE
perftestdb=>

 

では試してみましょう。


perftestdb=> \timing on
タイミングは on です。
perftestdb=>
perftestdb=>
perftestdb=> \i ./test1_pg.sql
QUERY PLAN
-----------------------------------------------------------------------------------------------
Hash Join (cost=19.53..156.40 rows=10240 width=116)
Hash Cond: (master.id = t8.id)
-> Hash Join (cost=12.93..31.40 rows=1280 width=83)
Hash Cond: (master.id = detail.id)
-> Hash Join (cost=6.33..10.00 rows=160 width=50)
Hash Cond: (master.id = t4.id)
-> Hash Join (cost=2.67..4.45 rows=40 width=28)
Hash Cond: (master.id = t7.id)
-> Hash Join (cost=1.23..2.50 rows=20 width=17)
Hash Cond: (t6.id = master.id)
-> Seq Scan on detail t6 (cost=0.00..1.20 rows=20 width=11)
-> Hash (cost=1.10..1.10 rows=10 width=6)
-> Seq Scan on master (cost=0.00..1.10 rows=10 width=6)
-> Hash (cost=1.20..1.20 rows=20 width=11)
-> Seq Scan on detail t7 (cost=0.00..1.20 rows=20 width=11)
-> Hash (cost=3.15..3.15 rows=40 width=22)
-> Hash Join (cost=1.45..3.15 rows=40 width=22)
Hash Cond: (t4.id = t5.id)
-> Seq Scan on detail t4 (cost=0.00..1.20 rows=20 width=11)
-> Hash (cost=1.20..1.20 rows=20 width=11)
-> Seq Scan on detail t5 (cost=0.00..1.20 rows=20 width=11)
-> Hash (cost=5.60..5.60 rows=80 width=33)
-> Hash Join (cost=2.90..5.60 rows=80 width=33)
Hash Cond: (detail.id = t3.id)
-> Hash Join (cost=1.45..3.15 rows=40 width=22)
Hash Cond: (detail.id = t2.id)
-> Seq Scan on detail (cost=0.00..1.20 rows=20 width=11)
-> Hash (cost=1.20..1.20 rows=20 width=11)
-> Seq Scan on detail t2 (cost=0.00..1.20 rows=20 width=11)
-> Hash (cost=1.20..1.20 rows=20 width=11)
-> Seq Scan on detail t3 (cost=0.00..1.20 rows=20 width=11)
-> Hash (cost=5.60..5.60 rows=80 width=33)
-> Hash Join (cost=2.90..5.60 rows=80 width=33)
Hash Cond: (t8.id = t10.id)
-> Hash Join (cost=1.45..3.15 rows=40 width=22)
Hash Cond: (t8.id = t9.id)
-> Seq Scan on detail t8 (cost=0.00..1.20 rows=20 width=11)
-> Hash (cost=1.20..1.20 rows=20 width=11)
-> Seq Scan on detail t9 (cost=0.00..1.20 rows=20 width=11)
-> Hash (cost=1.20..1.20 rows=20 width=11)
-> Seq Scan on detail t10 (cost=0.00..1.20 rows=20 width=11)
(41 行)

時間: 29.117 ミリ秒
perftestdb=>

 

MySQLで上限を制限した時間と似たような時間でパースが行われていますね。ただ、HASH JOINが選択されていますがw この辺りも、MySQLの生い立ちと、PostgreSQL、そしてOracleの違いとして現れてきます。MySQL 8のHash Joinって、MySQL 8.0.20 でHASH JOINが効くケースが拡大したという元々、Block Nested Loopの置き換えとしての意味が強かったという生い立ちが影響しているようにも見えます。なので、等価結合のINNER JOIN(今回の例)の場合、MySQL 8.0.32でもNLJが選択されているということのようですね。似てるようで似てないRDBMSの世界。まだまだ、私も学びが必要ですね。ちょっと曲のあるMySQLのHash Joinの発動条件。。いずれPostgreSQLやOracleっぽくHash Joinをカジュアルに使えちゃう日が来るのだろうか。。。

 

最後に真打w Oracle Database

 

今回は、23c Freeで試します。ただし、前回のエントリーにあるように、23c freeでは、"_optimizer_max_permutations" = 300と少なく設定されています。
今回の確認では、正式リリースされれば、通常の 2000 になるだろう。という想定で、 2000 にしてハードパース時間を確認しておきます。


SCOTT@freepdb1> select banner from v$version;

BANNER
--------------------------------------------------------------------------------
Oracle Database 23c Free, Release 23.0.0.0.0 - Developer-Release

 

デフォルトの設定はいかですが、.sqlスクリプト内で、 _optimizer_max_permutations を 2000 に設定して実行します!


parameter name                   parameter value
-------------------------------- ------------------------------
_optimizer_max_permutations 300
_optimizer_search_limit 5


SCOTT@freepdb1> desc master
名前 NULL? 型
----------------------------------------- -------- ----------------------------
ID NOT NULL NUMBER
DUMMYA VARCHAR2(10)

SCOTT@freepdb1> desc detail
名前 NULL? 型
----------------------------------------- -------- ----------------------------
ID NOT NULL NUMBER
SUBID NOT NULL NUMBER
DUMMYA VARCHAR2(10)

SCOTT@freepdb1> select table_name,index_name,column_name from user_ind_columns where table_name in ('MASTER','DETAIL') order by table_name,index_name,column_position;

TABLE_NAME INDEX_NAME COLUMN_NAME
------------------------------ ------------------------------ ------------------------------
DETAIL PK_DETAIL ID
DETAIL PK_DETAIL SUBID
MASTER SYS_C008265 ID

SCOTT@freepdb1> exec dbms_stats.gather_table_stats(ownname=>'SCOTT',tabname=>'MASTER',cascade=>true,no_invalidate=>false);

PL/SQLプロシージャが正常に完了しました。

経過: 00:00:01.08
SCOTT@freepdb1> exec dbms_stats.gather_table_stats(ownname=>'SCOTT',tabname=>'DETAIL',cascade=>true,no_invalidate=>false);

PL/SQLプロシージャが正常に完了しました。

経過: 00:00:00.13

では、試してみましょう!


SCOTT@freepdb1> @test1_ora

セッションが変更されました。

経過: 00:00:00.01

NAME TYPE VALUE
------------------------------------ ----------- ------------------------------
_optimizer_max_permutations integer 2000
1* alter system flush shared_pool

システムが変更されました。

経過: 00:00:00.68

解析されました。

経過: 00:00:00.27

 

一般的なOracleの_optimizer_max_permutationsパラメータの設定で、 0.27 sec となりました. こんなもんでしょうね。- this is an adaptive plan となっているので、ACTUAL PLANを見ないと確定できないケースですが、多分
Hash joinが選ばれていることでしょうw(なので確認まではしませんが)

 

ついでなので、実行計画(見積もり)を見ておきましょう。 hash joinになってますね。一部、SORT MERGE JOINになってますが、


SCOTT@freepdb1> @?/rdbms/admin/utlxpls

PLAN_TABLE_OUTPUT
-----------------------------------------------------------------------------------------------------
Plan hash value: 3620033460

-----------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10240 | 950K| 33 (4)| 00:00:01 |
|* 1 | HASH JOIN | | 10240 | 950K| 33 (4)| 00:00:01 |
| 2 | TABLE ACCESS FULL | DETAIL | 20 | 180 | 3 (0)| 00:00:01 |
|* 3 | HASH JOIN | | 5120 | 430K| 30 (4)| 00:00:01 |
| 4 | TABLE ACCESS FULL | DETAIL | 20 | 180 | 3 (0)| 00:00:01 |
|* 5 | HASH JOIN | | 2560 | 192K| 27 (4)| 00:00:01 |
| 6 | TABLE ACCESS FULL | DETAIL | 20 | 180 | 3 (0)| 00:00:01 |
|* 7 | HASH JOIN | | 1280 | 87040 | 24 (5)| 00:00:01 |
| 8 | TABLE ACCESS FULL | DETAIL | 20 | 180 | 3 (0)| 00:00:01 |
|* 9 | HASH JOIN | | 640 | 37760 | 21 (5)| 00:00:01 |
| 10 | TABLE ACCESS FULL | DETAIL | 20 | 180 | 3 (0)| 00:00:01 |
|* 11 | HASH JOIN | | 320 | 16000 | 18 (6)| 00:00:01 |
| 12 | TABLE ACCESS FULL | DETAIL | 20 | 180 | 3 (0)| 00:00:01 |
|* 13 | HASH JOIN | | 160 | 6560 | 15 (7)| 00:00:01 |
|* 14 | HASH JOIN | | 80 | 2560 | 12 (9)| 00:00:01 |
|* 15 | HASH JOIN | | 40 | 920 | 9 (12)| 00:00:01 |
| 16 | MERGE JOIN | | 20 | 280 | 6 (17)| 00:00:01 |
| 17 | TABLE ACCESS BY INDEX ROWID| MASTER | 10 | 50 | 2 (0)| 00:00:01 |
| 18 | INDEX FULL SCAN | SYS_C008265 | 10 | | 1 (0)| 00:00:01 |
|* 19 | SORT JOIN | | 20 | 180 | 4 (25)| 00:00:01 |
| 20 | TABLE ACCESS FULL | DETAIL | 20 | 180 | 3 (0)| 00:00:01 |
| 21 | TABLE ACCESS FULL | DETAIL | 20 | 180 | 3 (0)| 00:00:01 |
| 22 | TABLE ACCESS FULL | DETAIL | 20 | 180 | 3 (0)| 00:00:01 |
| 23 | TABLE ACCESS FULL | DETAIL | 20 | 180 | 3 (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------------


Note
-----
- this is an adaptive plan

174行が選択されました。

経過: 00:00:00.71

MySQL/PostgreSQL/Oracleそれぞれ、ハードパースの結合順評価テーブル数上限にも癖というか違いがあって、かつ、それが、パース時間の差になってみたり。癖が多いですよね。それぞれの特徴というか。
ついでに、実行計画、MySQLのHash Joinの発動ケース。現状OracleやPostgreSQLのようには使えないので、その辺りは、認識しておいた方が良いですよね。癖として ;)

8月末に東北に居たのですが、例年だと朝晩は涼しくて、過ごしやすいのに、今年は、ダメですね。東京と同じです。農作物への影響が気になりますね。寒暖の差が美味しさに影響するのもありますからね。。。

 

では、また。

 

次回は、OracleのSQLヒントネタでもしようかと思ってます。Oracleのヒントレポートが出力されるようになって、そこ気になる、なぜ。みたいな方も多かったり、それにより無用な議論があったりと聞いているのでw 一応書いておこうかなと。

 



MySQL/PostgreSQL/Oracleで利用したスクリプト(テーブル、索引定義およびデータ取得と統計取得コマンドは本文参照のこと)

MySQL


[master@localhost ~]$ cat test1.sql
use perftestdb;


explain
select *
from
master inner join detail
on master.id = detail.id
inner join detail t2
on
t2.id = detail.id
inner join detail t3
on
t3.id = t2.id
inner join detail t4
on
t4.id = t3.id
inner join detail t5
on
t5.id = t4.id
inner join detail t6
on
t6.id = t5.id
inner join detail t7
on
t7.id = t6.id
inner join detail t8
on
t8.id = t7.id
inner join detail t9
on
t9.id = t8.id
inner join detail t10
on
t10.id = t9.id
;

 

PostgreSQL


[master@localhost ~]$ cat test1_pg.sql
explain
select *
from
master inner join detail
on master.id = detail.id
inner join detail t2
on
t2.id = detail.id
inner join detail t3
on
t3.id = t2.id
inner join detail t4
on
t4.id = t3.id
inner join detail t5
on
t5.id = t4.id
inner join detail t6
on
t6.id = t5.id
inner join detail t7
on
t7.id = t6.id
inner join detail t8
on
t8.id = t7.id
inner join detail t9
on
t9.id = t8.id
inner join detail t10
on
t10.id = t9.id
;

 

Oracle


[master@localhost ~]$ cat test1_ora.sql
alter session set "_optimizer_max_permutations"=2000;
show parameter "_optimizer_max_permutations"

alter system flush shared_pool
.
l
/


explain plan for
select *
from
master inner join detail
on master.id = detail.id
inner join detail t2
on
t2.id = detail.id
inner join detail t3
on
t3.id = t2.id
inner join detail t4
on
t4.id = t3.id
inner join detail t5
on
t5.id = t4.id
inner join detail t6
on
t6.id = t5.id
inner join detail t7
on
t7.id = t6.id
inner join detail t8
on
t8.id = t7.id
inner join detail t9
on
t9.id = t8.id
inner join detail t10
on
t10.id = t9.id
;

 



関連エントリー
Oracle Database 23c Free Developer Releaseの”_optimizer_max_permutations” parameterの設定値について

 


標準はあるにはあるが癖の多いSQL 全部俺 #1 Pagination
標準はあるにはあるが癖の多いSQL 全部俺 #2 関数名は同じでも引数が逆の罠!
標準はあるにはあるが癖の多いSQL 全部俺 #3 データ型確認したい時あるんです
標準はあるにはあるが癖の多いSQL 全部俺 #4 リテラル値での除算の内部精度も違うのよ!
標準はあるにはあるが癖の多いSQL 全部俺 #5 和暦変換機能ある方が少数派
標準はあるにはあるが癖の多いSQL 全部俺 #6 時間厳守!
標準はあるにはあるが癖の多いSQL 全部俺 #7 期間リテラル!
標準はあるにはあるが癖の多いSQL 全部俺 #8 翌月末日って何日?
標準はあるにはあるが癖の多いSQL 全部俺 #9 部分文字列の扱いでも癖が出る><
標準はあるにはあるが癖の多いSQL 全部俺 #10 文字列連結の罠(有名なやつ)
標準はあるにはあるが癖の多いSQL 全部俺 #11 デュエル、じゃなくて、デュアル
標準はあるにはあるが癖の多いSQL 全部俺 #12 文字[列]探すにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #13 あると便利ですが意外となかったり
標準はあるにはあるが癖の多いSQL 全部俺 #14 連番の集合を返すにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #15 SQL command line client
標準はあるにはあるが癖の多いSQL 全部俺 #16 SQLのレントゲンを撮る方法
標準はあるにはあるが癖の多いSQL 全部俺 #17 その空白は許されないのか?
標準はあるにはあるが癖の多いSQL 全部俺 #18 (+)の外部結合は方言
標準はあるにはあるが癖の多いSQL 全部俺 #19 帰ってきた、部分文字列の扱いでも癖w
標準はあるにはあるが癖の多いSQL 全部俺 #20 結果セットを単一列に連結するにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #21 演算結果にも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #22 集合演算にも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #23 複数行INSERTにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #24 乱数作るにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #25 SQL de Fractalsにも癖がある:)
標準はあるにはあるが癖の多いSQL 全部俺 おまけ SQL de 湯婆婆やるにも癖がでるw
帰ってきた! 標準はあるにはあるが癖の多いSQL #1 SQL de ROT13 やるにも癖が出るw帰ってきた! 標準はあるにはあるが癖の多いSQL #2 Actual Plan取得中のキャンセルでも癖が出る

| | | コメント (0)

2023年9月 1日 (金)

Oracle Database 23c Free Developer Releaseの”_optimizer_max_permutations” parameterの設定値について

Previously on Mac De Oracle..
前回のお話は、帰ってきた! 標準はあるにはあるが癖の多いSQL #2 Actual Plan取得中のキャンセルでも癖が出るでした。Oracle/PostgreSQL/MySQLそれぞれ、再帰問合せで試してみましたが、興味深い違いが出ました。思わず、PostgreSQLのソースコード読み始めてしまいましたwwww

ということで、本日は、その流れで気づいてOracle 23c Freeのひみつ!


たまたま気づいたのですけど、 Oracle Database 23c Free Developer Release で、
under scored parameterの "_optimizer_max_permutations" って、 300なんですね。随分少ない設定にされていました。

これ

5! (120) < 6! (720) ということを意味するので、6表以上の結合では、通常のOracleより実行計画をミスりやすいということを意味しています。
(なぜなのでしょう、Free Developer Releaseだからではないか? というコメントももらいましたが、そうなのですかねぇ。でもそうかもしれないw。理由はなぜなのかわからんので何とも言えないですね)
なので、多数の結合を伴うSQLの検証には注意した方が良いですね。もしくは、通常の 2000 ぐらいまであげて試すとかしておいた方が良いかもね。
FreeのDeveloper Releaseだから 300 になっているのか確認できる資料は見つからなかったけど。


念の為、調べてみるとやはり、23c Free Developer Releaseのみ少なく設定されていました。

SYS@free> select banner from v$version;
BANNER
--------------------------------------------------------------------------------
Oracle Database 23c Free, Release 23.0.0.0.0 - Developer-Release

parameter name parameter value
-------------------------------- ------------------------------
_optimizer_max_permutations 300
_optimizer_search_limit 5



BANNER
----------------------------------------------------------------------
Oracle Database 21c Enterprise Edition Release 21.0.0.0.0 - Production

parameter name parameter value
------------------------------ ------------------------------
_optimizer_max_permutations 2000
_optimizer_search_limit 5


BANNER
--------------------------------------------------------------------------------
Oracle Database 19c Enterprise Edition Release 19.0.0.0.0 - Production

parameter name parameter value
------------------------------ ------------------------------
optimizer_max_permutations 2000
_optimizer_search_limit 5


BANNER
--------------------------------------------------------------------------------
Oracle Database 18c Enterprise Edition Release 18.0.0.0.0 - Production

parameter name parameter value
------------------------------ ------------------------------
_optimizer_max_permutations 2000
_optimizer_search_limit 5


BANNER
--------------------------------------------------------------------------------
Oracle Database 12c Enterprise Edition Release 12.2.0.1.0 - 64bit Production
PL/SQL Release 12.2.0.1.0 - Production
CORE 12.2.0.1.0 Production
TNS for Linux: Version 12.2.0.1.0 - Production
NLSRTL Version 12.2.0.1.0 - Production

parameter name parameter value
------------------------------ ------------------------------
_optimizer_max_permutations 2000
_optimizer_search_limit 5


Oracle 23c の正式版がリリースされたら皆さんも、2000になっているか確認しましょうね!(多分、2000になっていると思うけどw)

東京の8月が毎日真夏日だったなんで、話題。来年は毎日、猛暑日じゃなければ良いのですけども。。。
残暑厳しい東京からお送りしました。

では、また。

| | | コメント (0)

2023年8月29日 (火)

帰ってきた! 標準はあるにはあるが癖の多いSQL #2 Actual Plan取得中のキャンセルでも癖が出る

前々回のエントリーで、MySQL 8.0.32のActual Plan取得中にキャンセルしてもキャンセルされるまでのActual Planを取得できることを確認した。

では、OracleやPostgreSQLではどうなのだろう?(Oracleは皆さんご存知だと思いますが、確認の意味も含めて)

Oracle Database 21c と PostgreSQL 13.6 (私の環境の都合上w)で確認しておこうと思います。

Actual Plan欲しいけど、本番環境でしか試せないとか、実行に数時間以上要するような状況と大人の事情で、泣く泣くキャンセルしなければいけない。
でも、途中まででもいいからActual Plan返してくれたら、原因特定できる。。。かもしれないし。。。と思いますよね。

MySQL 8.0.32では、キャンセルしても途中まで取得できました。
(SQLパースフェーズの場合を除く。これはOracleでも、PostgreSQLでも同じでしょう。だって、実行前の解析フェーズでキャンセルされたらActual Planなんて返しようがないですからね)

 

では、Oracle Database 21cから見てみましょう!
(ご存知の方も多いと思いますので最初に答えを言っちゃうと、Oracleの場合、Actual Plan取得でも、SQLモニターの場合でもキャンセルしても途中までの結果を見ることができます!)

 

再帰問合せを利用した連番生成を行う方法で試してみます。


SYS@ORCLCDB> select banner_full from v$version;

BANNER_FULL
------------------------------------------------------------------------------------------
Oracle Database 21c Enterprise Edition Release 21.0.0.0.0 - Production Version 21.3.0.0.0

 

gather_plan_statisticsヒントを利用した、Actual Plan取得(キャンセルせず最後まで実行した例です)


SCOTT@freepdb1> @actual
1 WITH gen_nums(v)
2 AS
3 (
4 SELECT /*+ gather_plan_statistics */ 1
5 FROM
6 dual
7 UNION ALL
8 SELECT v + 1
9 FROM
10 gen_nums
11 WHERE v + 1 <= 10000000
12 )
13* SELECT v from gen_nums

 


SCOTT@freepdb1>  @actual_plan 6n3sc0n82t9dq

PLAN_TABLE_OUTPUT
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID 6n3sc0n82t9dq, child number 0
-------------------------------------
WITH gen_nums(v) AS ( SELECT /*+ gather_plan_statistics */ 1 FROM
dual UNION ALL SELECT v + 1 FROM gen_nums WHERE v + 1 <=
10000000 ) SELECT v from gen_nums

Plan hash value: 1492144221

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows |E-Bytes| Cost (%CPU)| E-Time | A-Rows | A-Time | Buffers | Writes | OMem | 1Mem | Used-Mem |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | | 4 (100)| | 10M|00:07:57.97 | 105M| 28831 | | | |
| 1 | VIEW | | 1 | 2 | 26 | 4 (0)| 00:00:01 | 10M|00:07:57.97 | 105M| 28831 | | | |
| 2 | UNION ALL (RECURSIVE WITH) BREADTH FIRST| | 1 | | | | | 10M|00:07:53.30 | 105M| 28831 | 2048 | 2048 | 97M (0)|
| 3 | FAST DUAL | | 1 | 1 | | 2 (0)| 00:00:01 | 1 |00:00:00.01 | 0 | 0 | | | |
| 4 | RECURSIVE WITH PUMP | | 10M| | | | | 9999K|00:00:16.81 | 0 | 0 | | | |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

 

同SQLを途中でキャンセルした例です。
A-Rows/A-Timeが途中まで実行されたことを示しています。


SCOTT@freepdb1> @actual
1 WITH gen_nums(v)
2 AS
3 (
4 SELECT /*+ gather_plan_statistics */ 1
5 FROM
6 dual
7 UNION ALL
8 SELECT v + 1
9 FROM
10 gen_nums
11 WHERE v + 1 <= 10000000
12 )
13* SELECT v from gen_nums
^CSCOTT@freepdb1>

 


SCOTT@freepdb1> @actual_plan 6n3sc0n82t9dq

PLAN_TABLE_OUTPUT
---------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
SQL_ID 6n3sc0n82t9dq, child number 0
-------------------------------------
WITH gen_nums(v) AS ( SELECT /*+ gather_plan_statistics */ 1 FROM
dual UNION ALL SELECT v + 1 FROM gen_nums WHERE v + 1 <=
10000000 ) SELECT v from gen_nums

Plan hash value: 1492144221

--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows |E-Bytes| Cost (%CPU)| E-Time | A-Rows | A-Time | Buffers | Writes | OMem | 1Mem | Used-Mem |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | | 4 (100)| | 7479K|00:06:29.48 | 76M| 21535 | | | |
| 1 | VIEW | | 1 | 2 | 26 | 4 (0)| 00:00:01 | 7479K|00:06:29.48 | 76M| 21535 | | | |
| 2 | UNION ALL (RECURSIVE WITH) BREADTH FIRST| | 1 | | | | | 7479K|00:06:25.85 | 76M| 21535 | 2048 | 2048 | 2048 (0)|
| 3 | FAST DUAL | | 1 | 1 | | 2 (0)| 00:00:01 | 1 |00:00:00.01 | 0 | 0 | | | |
| 4 | RECURSIVE WITH PUMP | | 7479K| | | | | 7479K|00:00:12.40 | 0 | 0 | | | |
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

 

次に、SQL監視の例を見てみます。まず、キャンセルせず、最後まで実行した場合の例


Global Information
------------------------------
Status : DONE (ALL ROWS)
Instance ID : 1
Session : SCOTT (379:17125)
SQL ID : 8m5nwydj0rk2t

...中略...

Global Stats
============================================================================
| Elapsed | Cpu | IO | Other | Fetch | Buffer | Write | Write |
| Time(s) | Time(s) | Waits(s) | Waits(s) | Calls | Gets | Reqs | Bytes |
============================================================================
| 294 | 195 | 3.56 | 95 | 100K | 105M | 1126 | 225MB |
============================================================================

SQL Plan Monitoring Details (Plan Hash Value=1492144221)
============================================================================================================================================================================================
| Id | Operation | Name | Rows | Cost | Time | Start | Execs | Rows | Write | Write | Mem | Temp | Activity | Activity Detail |
| | | | (Estim) | | Active(s) | Active | | (Actual) | Reqs | Bytes | (Max) | (Max) | (%) | (# samples) |
============================================================================================================================================================================================
| 0 | SELECT STATEMENT | | | | 411 | +0 | 1 | 10M | | | . | . | | |
| 1 | VIEW | | 2 | 4 | 411 | +0 | 1 | 10M | | | . | . | | |
| 2 | UNION ALL (RECURSIVE WITH) BREADTH FIRST | | | | 411 | +0 | 1 | 10M | 1126 | 225MB | 98MB | 226MB | 95.44 | Cpu (248) |
| | | | | | | | | | | | | | | direct path write temp (3) |
| 3 | FAST DUAL | | 1 | 2 | 1 | +0 | 1 | 1 | | | . | . | | |
| 4 | RECURSIVE WITH PUMP | | | | 411 | +0 | 10M | 10M | | | . | . | 1.90 | Cpu (5) |
============================================================================================================================================================================================

 

次に途中でキャンセルした場合の例。 410sec要していたので、10sec後ぐらいにキャンセルしました。どちらの方法でもキャンセルした場合でも途中までのActual Planを取得できました。MySQLは、Actual Plan取得時のキャンセルの挙動をOracle Database側にあわせたのでしょうかね?

 

SQLモニターの場合、Global informationのStatusにも着目してください。全行取得できたのか、途中で止められたのか、エラーで途中終了したのかなどの情報も確認できます。


  1  WITH gen_nums(v)
2 AS
3 (
4 SELECT /*+ MONITOR */ 1
5 FROM
6 dual
7 UNION ALL
8 SELECT v + 1
9 FROM
10 gen_nums
11 WHERE v + 1 <= 10000000
12 )
13* SELECT v from gen_nums
^CSCOTT@orclpdb1>

 


Global Information
------------------------------
Status : DONE (FIRST N ROWS)
Instance ID : 1
Session : SCOTT (379:17125)
SQL ID : 8m5nwydj0rk2t

...中略...

Global Stats
=================================================
| Elapsed | Cpu | Other | Fetch | Buffer |
| Time(s) | Time(s) | Waits(s) | Calls | Gets |
=================================================
| 10 | 5.49 | 4.45 | 7216 | 2M |
=================================================

SQL Plan Monitoring Details (Plan Hash Value=1492144221)
=========================================================================================================================================================
| Id | Operation | Name | Rows | Cost | Time | Start | Execs | Rows | Mem | Activity | Activity Detail |
| | | | (Estim) | | Active(s) | Active | | (Actual) | (Max) | (%) | (# samples) |
=========================================================================================================================================================
| 0 | SELECT STATEMENT | | | | 19 | +0 | 1 | 722K | . | | |
| 1 | VIEW | | 2 | 4 | 19 | +0 | 1 | 722K | . | | |
| 2 | UNION ALL (RECURSIVE WITH) BREADTH FIRST | | | | 19 | +0 | 1 | 722K | 34MB | 83.33 | Cpu (5) |
| 3 | FAST DUAL | | 1 | 2 | 1 | +0 | 1 | 1 | . | | |
| 4 | RECURSIVE WITH PUMP | | | | 19 | +0 | 722K | 722K | . | | |
=========================================================================================================================================================

 

次にPostgreSQL


perftestdb=> select version();
version
--------------------------------------------------------------------------------------------------------
PostgreSQL 13.6 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 8.5.0 20210514 (Red Hat 8.5.0-4), 64-bit
(1 行)

 

キャンセルせず、最後まで実行した例


perftestdb=> explain (analyze, buffers, verbose) 
perftestdb-> WITH RECURSIVE gen_nums(v)
perftestdb-> AS
perftestdb-> (
perftestdb(> SELECT 1
perftestdb(> UNION ALL
perftestdb(> SELECT v + 1
perftestdb(> FROM
perftestdb(> gen_nums
perftestdb(> WHERE v + 1 <= 100000000
perftestdb(> )
perftestdb-> SELECT v from gen_nums;

QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------
CTE Scan on gen_nums (cost=3.21..3.83 rows=31 width=4) (actual time=0.007..98357.448 rows=100000000 loops=1)
Output: gen_nums.v
Buffers: temp written=170898
CTE gen_nums
-> Recursive Union (cost=0.00..3.21 rows=31 width=4) (actual time=0.005..67023.784 rows=100000000 loops=1)
-> Result (cost=0.00..0.01 rows=1 width=4) (actual time=0.002..0.002 rows=1 loops=1)
Output: 1
-> WorkTable Scan on gen_nums gen_nums_1 (cost=0.00..0.26 rows=3 width=4) (actual time=0.000..0.000 rows=1 loops=100000000)
Output: (gen_nums_1.v + 1)
Filter: ((gen_nums_1.v + 1) <= 100000000)
Rows Removed by Filter: 0
Planning Time: 0.090 ms
Execution Time: 103968.625 ms
(13 行)

 

キャンセルした例。
Planning Time: 0.090 ms なので、パースフェーズ中にキャンセルする方が難しいですね。103秒ほど要するので、60秒ぐらいでキャンセルしましたが、PostgreSQLの場合は、Actual Plan取得中のキャンセルでは何も出力されません。
XにPostしたところ、篠田さんから、本体側で対応しないと返せなようだ、とコメントをもらいコードを見始めたり() 機能として、あったら便利かもしれないですね。PostgreSQLでも。キャンセルしたところまでのActual Planを返してくれたら問題解決の糸口になることもあるだろうし。


perftestdb=> explain (analyze, buffers, verbose) 
WITH RECURSIVE gen_nums(v)
AS
(
SELECT 1
UNION ALL
SELECT v + 1
FROM
gen_nums
WHERE v + 1 <= 100000000
)
SELECT v from gen_nums;
^Cキャンセル要求を送信しました
ERROR: canceling statement due to user request
perftestdb=>

 


 

温泉三昧と、いつも満席で諦めてた蕎麦屋のいくつかは食べ歩けた遅いお盆休みも終わり。もう9月が目の前ですね。

 

では、また。

 

 



標準はあるにはあるが癖の多いSQL 全部俺 #1 Pagination
標準はあるにはあるが癖の多いSQL 全部俺 #2 関数名は同じでも引数が逆の罠!
標準はあるにはあるが癖の多いSQL 全部俺 #3 データ型確認したい時あるんです
標準はあるにはあるが癖の多いSQL 全部俺 #4 リテラル値での除算の内部精度も違うのよ!
標準はあるにはあるが癖の多いSQL 全部俺 #5 和暦変換機能ある方が少数派
標準はあるにはあるが癖の多いSQL 全部俺 #6 時間厳守!
標準はあるにはあるが癖の多いSQL 全部俺 #7 期間リテラル!
標準はあるにはあるが癖の多いSQL 全部俺 #8 翌月末日って何日?
標準はあるにはあるが癖の多いSQL 全部俺 #9 部分文字列の扱いでも癖が出る><
標準はあるにはあるが癖の多いSQL 全部俺 #10 文字列連結の罠(有名なやつ)
標準はあるにはあるが癖の多いSQL 全部俺 #11 デュエル、じゃなくて、デュアル
標準はあるにはあるが癖の多いSQL 全部俺 #12 文字[列]探すにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #13 あると便利ですが意外となかったり
標準はあるにはあるが癖の多いSQL 全部俺 #14 連番の集合を返すにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #15 SQL command line client
標準はあるにはあるが癖の多いSQL 全部俺 #16 SQLのレントゲンを撮る方法
標準はあるにはあるが癖の多いSQL 全部俺 #17 その空白は許されないのか?
標準はあるにはあるが癖の多いSQL 全部俺 #18 (+)の外部結合は方言
標準はあるにはあるが癖の多いSQL 全部俺 #19 帰ってきた、部分文字列の扱いでも癖w
標準はあるにはあるが癖の多いSQL 全部俺 #20 結果セットを単一列に連結するにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #21 演算結果にも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #22 集合演算にも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #23 複数行INSERTにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #24 乱数作るにも癖がある
標準はあるにはあるが癖の多いSQL 全部俺 #25 SQL de Fractalsにも癖がある:)
標準はあるにはあるが癖の多いSQL 全部俺 おまけ SQL de 湯婆婆やるにも癖がでるw
帰ってきた! 標準はあるにはあるが癖の多いSQL #1 SQL de ROT13 やるにも癖が出るw

| | | コメント (0)

2023年8月 4日 (金)

MySQL 8.0.32 / explain analyze 実行途中でキャンセルできるみたいだけど、キャンセルしたら、Actual Plan、途中まで出るの?

本年度一発目のw Databaseネタは、なんと、MySQLネタからのスタートですw

MySQL 8.0.18以降、Actualプランが取得できるようになってた。
また、MySQL 8.0.20 以降、KILL QUERY または CTRL-C を使用してこのステートメントを終了できるとある。
ただ、マニュアルに記載されているのはここまでで、キャンセルできることは記載されているが、キャンセルした場合のActual Planはどこまで出力されるのだろう? or 全く出力されない?

OracleのリアルタイムSQL監視だとキャンセルすると不完全ではあるけども、なんとなーく判断できる程度の情報は途中まで取得できるが、
GATHER_PLAN_STATISTICSヒントなどを使ったActualプランは最後まで実行しないとアcつあlはしゅとくできない。どちらのタイプに似ているのでしょうね?

長時間(数日とかw)かかってしまうようになったSQLのActualプラン取得するの意外と難しいケースも場合によってはあったりするわけで。。。そんなとき、Actualみたいけど、キャンセルしたらどうなるのだろうと。。
途中まででも取得できるのか、それとも、Nothingなのか。。。

MySQLのActualプランの取得はどうなんだろう。。。ということで、試してみた。

EXPLAIN ANALYZE による情報の取得
https://dev.mysql.com/doc/refman/8.0/ja/explain.html#explain-analyze





結論から書くと、
MySQL 8.0.32での検証だが、

explain analyzeで取得できるActual Planは、対応しているSQL文の実行中にCTRL-Cでキャンセルしても、その時点までの、Actual Planを表示してくれる!!!!!!

ただし、パース時間が異常に長めのSQL文(結合数めちゃ多いとか)だと、実行以前に、パース時間が長いため、パースフェーズでキャンセルしてしまうの何も表示されない。。と(最初はこちらを引いてしまったので、常に表示されないものかと思い込んでしまった)
他の方法で実行をキャンセルした場合も同じだろう。とは思う。

いや、他にも出力されないケースがあるとか、その検証方法だから出力されているだけどか、MySQLのexplain analyzeのディープなツッコミがありましたら、よろしくお願いします。:)

MySQLの8.0台ってマイナー番号変わっても機能追加されたり変化しているので、一応。 MySQL 8.0.32 上では。ということにしておく。




以下、検証の記録的なもの。

確認に使用した MySQLのバージョン

[master@localhost ~]$ mysql -u scott -p
Enter password:
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 8
Server version: 8.0.32 Source distribution

Copyright (c) 2000, 2023, Oracle and/or its affiliates.

Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.

Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.

mysql> select version();
+-----------+
| version() |
+-----------+
| 8.0.32 |
+-----------+
1 row in set (0.00 sec)

事前に準備したクエリ向け表と索引など

Database changed
mysql> use perftestdb
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -A

Database changed
mysql>
mysql> desc master;
+--------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------+-------------+------+-----+---------+-------+
| id | int | NO | PRI | NULL | |
| dummya | varchar(10) | YES | | NULL | |
+--------+-------------+------+-----+---------+-------+
2 rows in set (0.01 sec)

mysql> desc detail;
+--------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+--------+-------------+------+-----+---------+-------+
| id | int | NO | PRI | NULL | |
| subid | int | NO | PRI | NULL | |
| dummya | varchar(10) | YES | | NULL | |
+--------+-------------+------+-----+---------+-------+
3 rows in set (0.00 sec)

mysql> show indexes from master;
+--------+------------+----------+--------------+-------------+------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Index_type |
+--------+------------+----------+--------------+-------------+------------+
| master | 0 | PRIMARY | 1 | id | BTREE |
+--------+------------+----------+--------------+-------------+------------+
1 row in set (0.02 sec)

mysql> show indexes from detail;
+--------+------------+----------+--------------+-------------+------------+
| Table | Non_unique | Key_name | Seq_in_index | Column_name | Index_type |
+--------+------------+----------+--------------+-------------+------------+
| detail | 0 | PRIMARY | 1 | id | BTREE |
| detail | 0 | PRIMARY | 2 | subid | BTREE |
+--------+------------+----------+--------------+-------------+------------+
2 rows in set (0.02 sec)

mysql> select * from master;
+----+--------+
| id | dummya |
+----+--------+
| 1 | 1 |
| 2 | 2 |
| 3 | 3 |
| 4 | 4 |
| 5 | 5 |
| 6 | 6 |
| 7 | 7 |
| 8 | 8 |
| 9 | 9 |
| 10 | 10 |
+----+--------+
10 rows in set (0.01 sec)

mysql> select * from detail;
+----+-------+--------+
| id | subid | dummya |
+----+-------+--------+
| 1 | 1 | 11 |
| 1 | 2 | 12 |
| 2 | 1 | 21 |
| 2 | 2 | 22 |
| 3 | 1 | 31 |
...略...
| 7 | 1 | 71 |
| 7 | 2 | 72 |
| 8 | 1 | 81 |
| 8 | 2 | 82 |
| 9 | 1 | 91 |
| 9 | 2 | 92 |
| 10 | 1 | 101 |
| 10 | 2 | 102 |
+----+-------+--------+
20 rows in set (0.01 sec)

検証用に作ったSQL。どのぐらいのElapsed Timeかanaluyzeをつけて実行して、キャンセルしやすい程度のElapsed Timeか確認しておきます。

mysql> explain analyze
-> select *
-> from
-> master inner join detail
-> on master.id = detail.id
-> inner join detail t2
-> on
-> t2.id = detail.id
-> inner join detail t3
-> on
-> t3.id = t2.id
-> inner join detail t4
-> on
-> t4.id = t3.id
-> inner join detail t5
-> on
-> t5.id = t4.id
-> inner join detail t6
-> on
-> t6.id = t5.id
-> inner join detail t7
-> on
-> t7.id = t6.id
-> inner join detail t8
-> on
-> t8.id = t7.id
-> inner join detail t9
-> on
-> t9.id = t8.id
-> inner join detail t10
-> on
-> t10.id = t9.id
-> ;
+------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| -> Nested loop inner join (cost=5729.85 rows=8192) (actual time=0.115..37.938 rows=10240 loops=1)
-> Nested loop inner join (cost=2862.65 rows=4096) (actual time=0.108..18.747 rows=5120 loops=1)
-> Nested loop inner join (cost=1429.05 rows=2048) (actual time=0.102..9.267 rows=2560 loops=1)
-> Nested loop inner join (cost=712.25 rows=1024) (actual time=0.097..4.689 rows=1280 loops=1)
-> Nested loop inner join (cost=353.85 rows=512) (actual time=0.091..2.336 rows=640 loops=1)
-> Nested loop inner join (cost=174.65 rows=256) (actual time=0.086..1.203 rows=320 loops=1)
-> Nested loop inner join (cost=85.05 rows=128) (actual time=0.080..0.630 rows=160 loops=1)
-> Nested loop inner join (cost=40.25 rows=64) (actual time=0.075..0.337 rows=80 loops=1)
-> Nested loop inner join (cost=17.85 rows=32) (actual time=0.069..0.189 rows=40 loops=1)
-> Nested loop inner join (cost=6.65 rows=16) (actual time=0.063..0.110 rows=20 loops=1)
-> Table scan on master (cost=1.05 rows=8) (actual time=0.041..0.047 rows=10 loops=1)
-> Index lookup on detail using PRIMARY (id=`master`.id) (cost=0.53 rows=2) (actual time=0.005..0.006 rows=2 loops=10)
-> Index lookup on t2 using PRIMARY (id=`master`.id) (cost=0.51 rows=2) (actual time=0.003..0.004 rows=2 loops=20)
-> Index lookup on t3 using PRIMARY (id=`master`.id) (cost=0.51 rows=2) (actual time=0.002..0.003 rows=2 loops=40)
-> Index lookup on t4 using PRIMARY (id=`master`.id) (cost=0.50 rows=2) (actual time=0.002..0.003 rows=2 loops=80)
-> Index lookup on t5 using PRIMARY (id=`master`.id) (cost=0.50 rows=2) (actual time=0.002..0.003 rows=2 loops=160)
-> Index lookup on t6 using PRIMARY (id=`master`.id) (cost=0.50 rows=2) (actual time=0.002..0.003 rows=2 loops=320)
-> Index lookup on t7 using PRIMARY (id=`master`.id) (cost=0.50 rows=2) (actual time=0.002..0.003 rows=2 loops=640)
-> Index lookup on t8 using PRIMARY (id=`master`.id) (cost=0.50 rows=2) (actual time=0.002..0.003 rows=2 loops=1280)
-> Index lookup on t9 using PRIMARY (id=`master`.id) (cost=0.50 rows=2) (actual time=0.002..0.003 rows=2 loops=2560)
-> Index lookup on t10 using PRIMARY (id=`master`.id) (cost=0.50 rows=2) (actual time=0.002..0.003 rows=2 loops=5120)
|
+------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (21.18 sec)


では、explain analyseを途中でキャンセルしてみます。elapsed timeが21秒ほどなので、20秒後にキャンセルしてみます。

mysql> explain analyze
select *
from
master inner join detail
on master.id = detail.id
inner join detail t2
on t2.id = master.id
inner join detail t3
on t3.id = master.id
inner join detail t4
on t4.id = master.id
inner join detail t5
on t5.id = master.id
inner join detail t6
on t6.id = master.id
inner join detail t7
on t7.id = master.id
inner join detail t8
on t8.id = master.id
inner join detail t9
on t9.id = master.id
inner join detail t10
on t10.id = master.id
;
^C^C -- query aborted
ERROR 1317 (70100): Query execution was interrupted
mysql>


! やはり、実行途中のキャンセルだとActual Planは何も出力されないみたい。。。。

??ん? いや、なんか違うな。これw!

SQL文の実行時間はたいしたことない。人間がこの時間内にピンポイントでキャンセルするには短すぎるよね。
SQL文の実行時間は、約38ミリ秒!!! こっちだw 最終に表示されている21秒には、SQLのパース時間も含まれているはず。。。。見逃した。俺としたことが。。。

| -> Nested loop inner join  (cost=5729.85 rows=8192) (actual time=0.115..37.938 rows=10240 loops=1)

キャンセルしたのは、SQL実行後、おおよそ、20秒後。SQLのActualタイムを見ると38ミリ秒なので、ギリギリ、パース時間中に被ってそう。

1 row in set (21.00 sec)

だとしたら、まず、そこから確認。パースにどの程度要しているか、explainだけにすれば、良さそうだよね。Oracle Databaseでもそうだし。(PostgreSQLはパース時間も表示してくれたりするけども。あれ意外に便利なんだよ)

mysql> explain format=tree    -> select *
-> from
-> master inner join detail
-> on master.id = detail.id
-> inner join detail t2
-> on
-> t2.id = detail.id
-> inner join detail t3
-> on
-> t3.id = t2.id
-> inner join detail t4
-> on
-> t4.id = t3.id
-> inner join detail t5
-> on
-> t5.id = t4.id
-> inner join detail t6
-> on
-> t6.id = t5.id
-> inner join detail t7
-> on
-> t7.id = t6.id
-> inner join detail t8
-> on
-> t8.id = t7.id
-> inner join detail t9
-> on
-> t9.id = t8.id
-> inner join detail t10
-> on
-> t10.id = t9.id
-> ;
+------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| -> Nested loop inner join (cost=5729.85 rows=8192)
-> Nested loop inner join (cost=2862.65 rows=4096)
-> Nested loop inner join (cost=1429.05 rows=2048)
-> Nested loop inner join (cost=712.25 rows=1024)
-> Nested loop inner join (cost=353.85 rows=512)
-> Nested loop inner join (cost=174.65 rows=256)
-> Nested loop inner join (cost=85.05 rows=128)
-> Nested loop inner join (cost=40.25 rows=64)
-> Nested loop inner join (cost=17.85 rows=32)
-> Nested loop inner join (cost=6.65 rows=16)
-> Table scan on master (cost=1.05 rows=8)
-> Index lookup on detail using PRIMARY (id=`master`.id) (cost=0.53 rows=2)
-> Index lookup on t2 using PRIMARY (id=`master`.id) (cost=0.51 rows=2)
-> Index lookup on t3 using PRIMARY (id=`master`.id) (cost=0.51 rows=2)
-> Index lookup on t4 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t5 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t6 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t7 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t8 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t9 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
-> Index lookup on t10 using PRIMARY (id=`master`.id) (cost=0.50 rows=2)
|
+------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (21.84 sec)


お! 予想通り、explainでパースだけさせたら、explain analyze(SQL文の実行を含む)とほぼ同じ 21秒台。想定は正しそう。

だとすると、今回確認したいことを実現するには、..... SQLの実行時間をもっと長くして、人がw キャンセルしやすい程度に実行時間を間延びさせておく必要がある。。
とはいえ、大量のデータを用意する時間もVMのストレージもないと。。。

さて、どうするか。。。。

***** ここで、 一休さん、考え中。。。 木魚の音。ポク、ポク、ポク。。。。w *****


***** ここで、 一休さん、閃いた。。。 鐘の音、チーーーーーん *****
 
sleep()関数を使って、眠らせると時間稼ぎできるのでは?  やってみよう!
おおおおお、いい感じだ。

mysql> explain analyze
-> select *, sleep(0.01)
-> from
-> master inner join detail
-> on master.id = detail.id
-> inner join detail t2
-> on
-> t2.id = detail.id
-> inner join detail t3
-> on
-> t3.id = t2.id
-> inner join detail t4
-> on
-> t4.id = t3.id
-> inner join detail t5
-> on
-> t5.id = t4.id
-> inner join detail t6
-> on
-> t6.id = t5.id
-> inner join detail t7
-> on
-> t7.id = t6.id
-> inner join detail t8
-> on
-> t8.id = t7.id
-> inner join detail t9
-> on
-> t9.id = t8.id
-> inner join detail t10
-> on
-> t10.id = t9.id
-> ;
+------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| -> Nested loop inner join (cost=5729.85 rows=8192) (actual time=0.170..232.399 rows=10240 loops=1)
-> Nested loop inner join (cost=2862.65 rows=4096) (actual time=0.164..75.457 rows=5120 loops=1)
-> Nested loop inner join (cost=1429.05 rows=2048) (actual time=0.159..37.659 rows=2560 loops=1)
-> Nested loop inner join (cost=712.25 rows=1024) (actual time=0.154..17.709 rows=1280 loops=1)
-> Nested loop inner join (cost=353.85 rows=512) (actual time=0.149..9.612 rows=640 loops=1)
-> Nested loop inner join (cost=174.65 rows=256) (actual time=0.143..4.658 rows=320 loops=1)
-> Nested loop inner join (cost=85.05 rows=128) (actual time=0.138..2.285 rows=160 loops=1)
-> Nested loop inner join (cost=40.25 rows=64) (actual time=0.132..1.174 rows=80 loops=1)
-> Nested loop inner join (cost=17.85 rows=32) (actual time=0.126..0.625 rows=40 loops=1)
-> Nested loop inner join (cost=6.65 rows=16) (actual time=0.120..0.347 rows=20 loops=1)
-> Table scan on master (cost=1.05 rows=8) (actual time=0.097..0.122 rows=10 loops=1)
-> Index lookup on detail using PRIMARY (id=`master`.id) (cost=0.53 rows=2) (actual time=0.017..0.021 rows=2 loops=10)
-> Index lookup on t2 using PRIMARY (id=`master`.id) (cost=0.51 rows=2) (actual time=0.010..0.013 rows=2 loops=20)
-> Index lookup on t3 using PRIMARY (id=`master`.id) (cost=0.51 rows=2) (actual time=0.010..0.013 rows=2 loops=40)
-> Index lookup on t4 using PRIMARY (id=`master`.id) (cost=0.50 rows=2) (actual time=0.010..0.013 rows=2 loops=80)
-> Index lookup on t5 using PRIMARY (id=`master`.id) (cost=0.50 rows=2) (actual time=0.010..0.014 rows=2 loops=160)
-> Index lookup on t6 using PRIMARY (id=`master`.id) (cost=0.50 rows=2) (actual time=0.011..0.015 rows=2 loops=320)
-> Index lookup on t7 using PRIMARY (id=`master`.id) (cost=0.50 rows=2) (actual time=0.009..0.012 rows=2 loops=640)
-> Index lookup on t8 using PRIMARY (id=`master`.id) (cost=0.50 rows=2) (actual time=0.011..0.015 rows=2 loops=1280)
-> Index lookup on t9 using PRIMARY (id=`master`.id) (cost=0.50 rows=2) (actual time=0.010..0.014 rows=2 loops=2560)
-> Index lookup on t10 using PRIMARY (id=`master`.id) (cost=0.50 rows=2) (actual time=0.010..0.028 rows=2 loops=5120)
|
+------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (2 min 47.64 sec)


Elapsed Timeがパース時間より長くなったので、できそう。 60秒後にキャンセルすれば余裕はありそう。

1 row in set (2 min 47.64 sec)

explain analyzeを実行して、60秒後にキャンセルした!

mysql> explain analyze
-> select *, sleep(0.01)
-> from
-> master inner join detail
-> on master.id = detail.id
-> inner join detail t2
-> on
-> t2.id = detail.id
-> inner join detail t3
-> on
-> t3.id = t2.id
-> inner join detail t4
-> on
-> t4.id = t3.id
-> inner join detail t5
-> on
-> t5.id = t4.id
-> inner join detail t6
-> on
-> t6.id = t5.id
-> inner join detail t7
-> on
-> t7.id = t6.id
-> inner join detail t8
-> on
-> t8.id = t7.id
-> inner join detail t9
-> on
-> t9.id = t8.id
-> inner join detail t10
-> on
-> t10.id = t9.id
-> ;

^C^C -- query aborted

+------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| -> Nested loop inner join (cost=5729.85 rows=8192) (actual time=0.113..92.382 rows=2639 loops=1)
-> Nested loop inner join (cost=2862.65 rows=4096) (actual time=0.106..29.480 rows=1320 loops=1)
-> Nested loop inner join (cost=1429.05 rows=2048) (actual time=0.101..14.323 rows=660 loops=1)
-> Nested loop inner join (cost=712.25 rows=1024) (actual time=0.096..7.161 rows=330 loops=1)
-> Nested loop inner join (cost=353.85 rows=512) (actual time=0.090..3.507 rows=165 loops=1)
-> Nested loop inner join (cost=174.65 rows=256) (actual time=0.085..1.624 rows=83 loops=1)
-> Nested loop inner join (cost=85.05 rows=128) (actual time=0.079..0.867 rows=42 loops=1)
-> Nested loop inner join (cost=40.25 rows=64) (actual time=0.073..0.352 rows=21 loops=1)
-> Nested loop inner join (cost=17.85 rows=32) (actual time=0.068..0.210 rows=11 loops=1)
-> Nested loop inner join (cost=6.65 rows=16) (actual time=0.062..0.122 rows=6 loops=1)
-> Table scan on master (cost=1.05 rows=8) (actual time=0.040..0.044 rows=3 loops=1)
-> Index lookup on detail using PRIMARY (id=`master`.id) (cost=0.53 rows=2) (actual time=0.021..0.024 rows=2 loops=3)
-> Index lookup on t2 using PRIMARY (id=`master`.id) (cost=0.51 rows=2) (actual time=0.011..0.014 rows=2 loops=6)
-> Index lookup on t3 using PRIMARY (id=`master`.id) (cost=0.51 rows=2) (actual time=0.009..0.012 rows=2 loops=11)
-> Index lookup on t4 using PRIMARY (id=`master`.id) (cost=0.50 rows=2) (actual time=0.020..0.024 rows=2 loops=21)
-> Index lookup on t5 using PRIMARY (id=`master`.id) (cost=0.50 rows=2) (actual time=0.014..0.017 rows=2 loops=42)
-> Index lookup on t6 using PRIMARY (id=`master`.id) (cost=0.50 rows=2) (actual time=0.015..0.022 rows=2 loops=83)
-> Index lookup on t7 using PRIMARY (id=`master`.id) (cost=0.50 rows=2) (actual time=0.014..0.021 rows=2 loops=165)
-> Index lookup on t8 using PRIMARY (id=`master`.id) (cost=0.50 rows=2) (actual time=0.016..0.021 rows=2 loops=330)
-> Index lookup on t9 using PRIMARY (id=`master`.id) (cost=0.50 rows=2) (actual time=0.015..0.022 rows=2 loops=660)
-> Index lookup on t10 using PRIMARY (id=`master`.id) (cost=0.50 rows=2) (actual time=0.015..0.044 rows=2 loops=1320)
|
+------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set, 1 warning (1 min 3.57 sec)


SQLのActualタイムは、92ミリ秒、処理した行数が、2629行。

| -> Nested loop inner join  (cost=5729.85 rows=8192) (actual time=0.113..92.382 rows=2639 loops=1)

以下を見ると、loopsが、1320回となっている。

    -> Index lookup on t10 using PRIMARY (id=`master`.id)  (cost=0.50 rows=2) (actual time=0.015..0.044 rows=2 loops=1320)

やった〜〜〜。 MySQLの explain analyzeは、"SQLの実行途中"(パース時間が長い割に、実行時間が短い場合は、実行中にキャンセルするのが難しいので表示されないケースはあるので注意)でキャンセルしても、途中までのActual Planを返してくれる!


念の為、sleep()関数を使ったトリッキーな再現方法ではなく、実際にSQLの実行中にキャンセルする方法を思いついたので、さらに、試してみる!


お分かりだろうか。 みなさん大好き(?)、 再起問い合わせ再帰問合せでシーケンス番号を生成するクエリーだ。100000000ぐらい生成すれば、いい感じの実行時間になるだろうと、思われるので、まず、このSQLのパース時間だけ確認。


mysql> SET SESSION cte_max_recursion_depth = 100000000;
Query OK, 0 rows affected (0.00 sec)

パース時間はほぼかかってないですね。先ほどの例は、結合する表が多いので、パース時間は長くなる傾向があるので、シンプルなSQLだけど、実行時間は、なげ〜〜〜〜ぞ〜というのにしてみた。

mysql> explain format=tree
-> WITH RECURSIVE gen_nums(v)
-> AS
-> (
-> SELECT 1
-> UNION ALL
-> SELECT v + 1
-> FROM
-> gen_nums
-> WHERE v + 1 <= 100000000
-> )
-> SELECT v from gen_nums;
+------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| -> Table scan on gen_nums (cost=3.87..5.56 rows=3)
-> Materialize recursive CTE gen_nums (cost=3.03..3.03 rows=3)
-> Rows fetched before execution (cost=0.00..0.00 rows=1)
-> Repeat until convergence
-> Filter: ((gen_nums.v + 1) <= 100000000) (cost=2.73 rows=2)
-> Scan new records on gen_nums (cost=2.73 rows=2)
|
+------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.00 sec)


explain analyzeの実時間を確認

mysql> explain analyze
-> WITH RECURSIVE gen_nums(v)
-> AS
-> (
-> SELECT 1
-> UNION ALL
-> SELECT v + 1
-> FROM
-> gen_nums
-> WHERE v + 1 <= 100000000
-> )
-> SELECT v from gen_nums;
+------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| -> Table scan on gen_nums (cost=3.87..5.56 rows=3) (actual time=139082.095..178091.268 rows=100000000 loops=1)
-> Materialize recursive CTE gen_nums (cost=3.03..3.03 rows=3) (actual time=139081.959..139081.959 rows=100000000 loops=1)
-> Rows fetched before execution (cost=0.00..0.00 rows=1) (actual time=0.000..0.000 rows=1 loops=1)
-> Repeat until convergence
-> Filter: ((gen_nums.v + 1) <= 100000000) (cost=2.73 rows=2) (actual time=0.003..29453.367 rows=50000000 loops=2)
-> Scan new records on gen_nums (cost=2.73 rows=2) (actual time=0.002..21852.139 rows=50000000 loops=2)
|
+------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (3 min 8.46 sec)

Actual timeは、178秒なので、60秒後にキャンセルすればよさそうですね。

| -> Table scan on gen_nums  (cost=3.87..5.56 rows=3) (actual time=139082.095..178091.268 rows=100000000 loops=1)

では、キャンセルでどうなるか検証!!

mysql> explain analyze
-> WITH RECURSIVE gen_nums(v)
-> AS
-> (
-> SELECT 1
-> UNION ALL
-> SELECT v + 1
-> FROM
-> gen_nums
-> WHERE v + 1 <= 100000000
-> )
-> SELECT v from gen_nums;
^C^C -- query aborted
+------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| -> Table scan on gen_nums (cost=3.87..5.56 rows=3) (never executed)
-> Materialize recursive CTE gen_nums (cost=3.03..3.03 rows=3) (never executed)
-> Rows fetched before execution (cost=0.00..0.00 rows=1) (actual time=0.000..0.000 rows=1 loops=1)
-> Repeat until convergence
-> Filter: ((gen_nums.v + 1) <= 100000000) (cost=2.73 rows=2) (actual time=0.004..27789.355 rows=47924088 loops=1)
-> Scan new records on gen_nums (cost=2.73 rows=2) (actual time=0.002..20555.788 rows=47924088 loops=1)
|
+------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set, 1 warning (1 min 3.94 sec)


60行でキャンセルしたので、ほぼ同じ。

1 row in set, 1 warning (1 min 3.94 sec

以下の行ソースを見ると、Actual timeが21秒ほど、rowsも47924088行。


-> Scan new records on gen_nums (cost=2.73 rows=2) (actual time=0.002..20555.788 rows=47924088 loops=1)

興味深い部分は、以下の2行の行ソース。never execute と表示されている。

| -> Table scan on gen_nums  (cost=3.87..5.56 rows=3) (never executed)
-> Materialize recursive CTE gen_nums (cost=3.03..3.03 rows=3) (never executed)
¥


これを見る限り、MySQLのexplain analyzeは、SQL文の実行フェース中にキャンセルすると、その時点までの Actual Planを出力してくれる。これ、結構嬉しいよね。長時間実行で、仕方なくキャンセルするにしても途中の状態。運が良ければ、詰まっている部分が見えるかもしれないわけで。。。


いや、他にも出力されないケースがあるとか、その検証方法だから出力されているだけどか、MySQLのexplain analyzeのディープなツッコミがありましたら、よろしくお願いします。:) (大事なので、2度書いておくw)


以上、explain analyze のブラックボックステスト。 実行途中でキャンセルできるみたいだけど、キャンセルしたら、Actual Plan、途中まで出るの? の巻。終わり。


似てるようで、似てない。それぞれのRDBMSの世界w。 ではまた。

 

参考)
EXPLAIN ANALYZE による情報の取得
https://dev.mysql.com/doc/refman/8.0/ja/explain.html#explain-analyze


| | | コメント (0)

2023年2月 2日 (木)

join elimination(結合の排除)のバリエーション / FAQ

Previously on Mac De Oracle
SQL*Plus -Fastオプション / FAQでした。

 

今日は、結合排除のバリエーションをいくつか紹介しておこうと思います。
なんどか説明していた無駄に結合してないですよね? の例は、参照整合性制約を頼りに結合排除が行われるものでした。

今日はそれに加えて、典型的な例を2つ紹介しておきたいと思います。(ここ” ”、試験に出ませんよ!w でないけど大切:)

一つ目は、結合列がそれぞれユニークキーかプライマリキーで一意で、1 : 0..1 で外部結合されるケース
結合の排除は、その結合を排除しても結果に影響しなことが自明な場合に発動するので、条件を満たしています。ただ、参照整合性制約のパターン同様に、SQL文を見ただけでは気づけないですよね。
あれ、実行計画にSQL文に記載されている表がない!? で制約を見てみて、あ”〜〜〜〜理解。みたいなw

実行計画ではSQLモニターも含め、Join Eliminationしたことを明示的に示すコメント等はありません。結合が消えていることで気づくことが多いわけですw(よーく考えたら、その結合イラねーじゃんというわけですけどもw0

  1* CREATE TABLE foo (id NUMBER PRIMARY KEY, note VARCHAR2(100))

表が作成されました。

1* CREATE TABLE bar (id NUMBER PRIMARY KEY, memo VARCHAR2(100))

表が作成されました。

1 SELECT
2 foo.id
3 , foo.note
4 FROM
5 foo
6 LEFT OUTER JOIN bar
7 ON
8* foo.id = bar.id

実行計画
----------------------------------------------------------
Plan hash value: 1245013993

--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 65 | 2 (0)| 00:00:01 |
| 1 | TABLE ACCESS FULL| FOO | 1 | 65 | 2 (0)| 00:00:01 |
--------------------------------------------------------------------------

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

 

この手の制約による結合の排除は、前提になる制約が結合排除の条件からはずれると排除されなくなります。
たとえば、前述の例で、それぞれの表を主キーで 1:0..1 で外部結合していましたよね?
それが仕様変更で、片方の方の主キーが複合主キーになってしまい。1:* の外部結合になってしまうと。。。。結合排除できなくなります。排除した場合、クエリー結果に影響するから。

  1  CREATE TABLE foo2 (
2 id NUMBER
3 , note VARCHAR2(100)
4 , PRIMARY KEY (id)
5* )

表が作成されました。

1 CREATE TABLE bar2 (
2 id NUMBER
3 , sq NUMBER NOT NULL
4 , memo VARCHAR2(100)
5 , PRIMARY KEY (id, sq)
6* )

表が作成されました。

1 SELECT
2 foo2.id
3 , foo2.note
4 FROM
5 foo2
6 LEFT OUTER JOIN bar2
7 ON
8* foo2.id = bar2.id

実行計画
----------------------------------------------------------
Plan hash value: 3679270243

----------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
----------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 78 | 3 (0)| 00:00:01 |
| 1 | NESTED LOOPS OUTER| | 1 | 78 | 3 (0)| 00:00:01 |
| 2 | TABLE ACCESS FULL| FOO2 | 1 | 65 | 2 (0)| 00:00:01 |
|* 3 | INDEX RANGE SCAN | SYS_C009222 | 1 | 13 | 1 (0)| 00:00:01 |
----------------------------------------------------------------------------------

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

3 - access("FOO2"."ID"="BAR2"."ID"(+))

 

では、結合の排除のもう一つの例。これは制約というより、排除を狙った意図的な方法です。
動的なSQlが利用されているアプリケーションで使われていることが多い印象ですが、この方法は好き嫌い激しいかもしれないですね。私は好きじゃないですw が偶にみます。

このクエリは、1:* の外部結合なのですが、WHERE 1=0 によって結果を返しません。なのでクエリー結果に影響しないことが自明なので排除されている。ということになります。

   1  SELECT
2 foo2.id
3 , foo2.note
4 FROM
5 foo2
6 LEFT OUTER JOIN
7 (
8 SELECT * FROM bar2 WHERE 1=0
9 ) bar2
10 ON
11* foo2.id = bar2.id

実行計画
----------------------------------------------------------
Plan hash value: 2844017661

--------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 65 | 2 (0)| 00:00:01 |
| 1 | TABLE ACCESS FULL| FOO2 | 1 | 65 | 2 (0)| 00:00:01 |
--------------------------------------------------------------------------

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

 

ということで、今年も残すところあと11ヶ月ですねw
長いことやってたプロジェクトが一つ形になったみたいな、ならないような、hear through the grapevine.

 

では、また。

 



・Join Elimination(結合の排除)と 参照整合性制約 / FAQ
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 18 / No.53 / Join Elimination

 

| | | コメント (0)

2023年1月27日 (金)

SQL*Plus -Fastオプション / FAQ

Previously on Mac De Oracle.
前回は、アドベントカレンダーのおまけのおまけwでした

今日は、そこで仕込んでおいたネタを使い、SQL*Plusも機能拡張されてたのすっかり忘れていた! ので、
高Fetch圧症の話に絡めてSQL*Plusの-F[ast]オプション書いておこうと思います。

軽めですが。

Fetch回数削減に効果があるので、多数の行をFetchするような時は思い出すと良いですね。
Client/Server間のrount tripが減ることに繋がるわけで、そこが慢性病の原因なら少しでも楽になれたら良いと思いますし。
(ということで、Fetch Sizeも忘れないでね。という気持ちを込めて。)

最初は、-Fastオプションなしで。arraysizeのデフォルトは 15です。なお、この -F[ast]オプションは、Oracle Database 12c 12.2以降でサポートされています。

[oracle@localhost ~]$ sqlplus scott/tiger@orclpdb1

...略...

Oracle Database 21c Enterprise Edition Release 21.0.0.0.0 - Production
Version 21.3.0.0.0
に接続されました。
SCOTT@orclpdb1> @dayx2
1 WITH
2 t1 AS
3 (
4 SELECT
5 pkey
6 , CASE
7 WHEN col2 IS NULL
8 THEN col1
9 ELSE col2
10 END AS join_key
11 ,description
12 FROM
13 nikoichi_mitaina_subtype
14 WHERE
15 col1 IS NOT NULL
16 OR col2 IS NOT NULL
17 )
18 SELECT *
19 FROM
20 supertype st
21 LEFT OUTER JOIN t1
22 ON
23* st.pkey = t1.join_key

1000001行が選択されました。

経過: 00:02:18.83

実行計画
----------------------------------------------------------
Plan hash value: 2223315184

-------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1000K| 1451M| | 119K (1)| 00:00:05 |
|* 1 | HASH JOIN OUTER | | 1000K| 1451M| 497M| 119K (1)| 00:00:05 |
| 2 | TABLE ACCESS FULL| SUPERTYPE | 1000K| 486M| | 19593 (1)| 00:00:01 |
|* 3 | TABLE ACCESS FULL| NIKOICHI_MITAINA_SUBTYPE | 750K| 723M| | 38796 (1)| 00:00:02 |
-------------------------------------------------------------------------------------------------------

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

1 - access("ST"."PKEY"=CASE WHEN (ROWID(+) IS NOT NULL) THEN CASE WHEN ("COL2"(+) IS
NULL) THEN "COL1"(+) ELSE "COL2"(+) END ELSE NULL END )
3 - filter("COL1"(+) IS NOT NULL OR "COL2"(+) IS NOT NULL)


統計
----------------------------------------------------------
1297 recursive calls
0 db block gets
229599 consistent gets
374715 physical reads
0 redo size
1551273711 bytes sent via SQL*Net to client
735146 bytes received via SQL*Net from client
66668 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1000001 rows processed


1 WITH
2 t1 AS
3 (
4 SELECT
5 pkey
6 , CASE
7 WHEN col2 IS NULL
8 THEN col1
9 ELSE col2
10 END AS join_key
11 ,description
12 FROM
13 nikoichi_mitaina_subtype
14 WHERE
15 col1 IS NOT NULL
16 OR col2 IS NOT NULL
17 )
18 SELECT /*+ MONITOR */ *
19 FROM
20 supertype st
21 LEFT OUTER JOIN t1
22 ON
23* st.pkey = t1.join_key

DBMS_SQLTUNE.REPORT_SQL_MONITOR(SQL_ID=>NULL,TYPE=>'TEXT')
---------------------------------------------------------------------
SQL Monitoring Report

SQL Text
------------------------------
WITH t1 AS ( SELECT pkey , CASE WHEN col2 IS NULL THEN col1 ELSE col2 END AS join_key ,description
FROM nikoichi_mitaina_subtype WHERE col1 IS NOT NULL OR col2 IS NOT NULL ) SELECT /*+ MONITOR */ *
FROM supertype st LEFT OUTER JOIN t1 ON st.pkey = t1.join_key

Global Information
------------------------------

...略...

Duration : 173s

...略...

Fetch Calls : 66668

Global Stats
===========================================================================================
| Elapsed | Cpu | IO | Other | Fetch | Buffer | Read | Read | Write | Write |
| Time(s) | Time(s) | Waits(s) | Waits(s) | Calls | Gets | Reqs | Bytes | Reqs | Bytes |
===========================================================================================
| 17 | 15 | 1.59 | 0.84 | 66668 | 230K | 6878 | 3GB | 5169 | 1GB |
===========================================================================================

SQL Plan Monitoring Details (Plan Hash Value=2223315184)
==============================================================================================================================================================================================================
| Id | Operation | Name | Rows | Cost | Time | Start | Execs | Rows | Read | Read | Write | Write | Mem | Temp | Activity | Activity Detail |
| | | | (Estim) | | Active(s) | Active | | (Actual) | Reqs | Bytes | Reqs | Bytes | (Max) | (Max) | (%) | (# samples) |
==============================================================================================================================================================================================================
| 0 | SELECT STATEMENT | | | | 172 | +2 | 1 | 1M | | | | | . | . | | |
| 1 | HASH JOIN OUTER | | 1M | 119K | 173 | +1 | 1 | 1M | 5169 | 1GB | 5169 | 1GB | 184MB | 1GB | 76.92 | Cpu (9) |
| | | | | | | | | | | | | | | | | SQL*Net more data to client (1) |
| 2 | TABLE ACCESS FULL | SUPERTYPE | 1M | 19593 | 1 | +2 | 1 | 1M | 575 | 559MB | | | . | . | | |
| 3 | TABLE ACCESS FULL | NIKOICHI_MITAINA_SUBTYPE | 750K | 38796 | 47 | +2 | 1 | 1M | 1134 | 1GB | | | . | . | 15.38 | Cpu (2) |
==============================================================================================================================================================================================================


次に、-F[ast]オプションで接続します。このオプションにより、ARRAYSIZE = 100に設定されます。それ以外にも3.5.1.5 FASTオプションいくつかの設定が変更されます。

[oracle@localhost ~]$ sqlplus -Fast scott/tiger@orclpdb1

...略...

Oracle Database 21c Enterprise Edition Release 21.0.0.0.0 - Production
Version 21.3.0.0.0
に接続されました。
SCOTT@orclpdb1> @dayx2
1 WITH
2 t1 AS
3 (
4 SELECT
5 pkey
6 , CASE
7 WHEN col2 IS NULL
8 THEN col1
9 ELSE col2
10 END AS join_key
11 ,description
12 FROM
13 nikoichi_mitaina_subtype
14 WHERE
15 col1 IS NOT NULL
16 OR col2 IS NOT NULL
17 )
18 SELECT *
19 FROM
20 supertype st
21 LEFT OUTER JOIN t1
22 ON
23* st.pkey = t1.join_key

1000001行が選択されました。

経過: 00:01:55.03

実行計画
----------------------------------------------------------
Plan hash value: 2223315184

-------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1000K| 1451M| | 119K (1)| 00:00:05 |
|* 1 | HASH JOIN OUTER | | 1000K| 1451M| 497M| 119K (1)| 00:00:05 |
| 2 | TABLE ACCESS FULL| SUPERTYPE | 1000K| 486M| | 19593 (1)| 00:00:01 |
|* 3 | TABLE ACCESS FULL| NIKOICHI_MITAINA_SUBTYPE | 750K| 723M| | 38796 (1)| 00:00:02 |
-------------------------------------------------------------------------------------------------------

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

1 - access("ST"."PKEY"=CASE WHEN (ROWID(+) IS NOT NULL) THEN CASE WHEN ("COL2"(+) IS
NULL) THEN "COL1"(+) ELSE "COL2"(+) END ELSE NULL END )
3 - filter("COL1"(+) IS NOT NULL OR "COL2"(+) IS NOT NULL)


統計
----------------------------------------------------------
1297 recursive calls
0 db block gets
216797 consistent gets
374715 physical reads
0 redo size
1539940308 bytes sent via SQL*Net to client
110262 bytes received via SQL*Net from client
10001 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1000001 rows processed


1 WITH
2 t1 AS
3 (
4 SELECT
5 pkey
6 , CASE
7 WHEN col2 IS NULL
8 THEN col1
9 ELSE col2
10 END AS join_key
11 ,description
12 FROM
13 nikoichi_mitaina_subtype
14 WHERE
15 col1 IS NOT NULL
16 OR col2 IS NOT NULL
17 )
18 SELECT /*+ MONITOR */ *
19 FROM
20 supertype st
21 LEFT OUTER JOIN t1
22 ON
23* st.pkey = t1.join_key

DBMS_SQLTUNE.REPORT_SQL_MONITOR(SQL_ID=>NULL,TYPE=>'TEXT')
----------------------------------------------------------------
SQL Monitoring Report

SQL Text
------------------------------
WITH t1 AS ( SELECT pkey , CASE WHEN col2 IS NULL THEN col1 ELSE col2 END AS join_key ,description
FROM nikoichi_mitaina_subtype WHERE col1 IS NOT NULL OR col2 IS NOT NULL ) SELECT /*+ MONITOR */ *
FROM supertype st LEFT OUTER JOIN t1 ON st.pkey = t1.join_key

Global Information
------------------------------

...略...

Duration : 146s

...略...

Fetch Calls : 10001

Global Stats
============================================================================================
| Elapsed | Cpu | IO | Other | Fetch | Buffer | Read | Read | Write | Write |
| Time(s) | Time(s) | Waits(s) | Waits(s) | Calls | Gets | Reqs | Bytes | Reqs | Bytes |
============================================================================================
| 17 | 14 | 1.76 | 1.72 | 10001 | 216K | 6878 | 3GB | 5169 | 1GB |
============================================================================================

SQL Plan Monitoring Details (Plan Hash Value=2223315184)
==============================================================================================================================================================================================================
| Id | Operation | Name | Rows | Cost | Time | Start | Execs | Rows | Read | Read | Write | Write | Mem | Temp | Activity | Activity Detail |
| | | | (Estim) | | Active(s) | Active | | (Actual) | Reqs | Bytes | Reqs | Bytes | (Max) | (Max) | (%) | (# samples) |
==============================================================================================================================================================================================================
| 0 | SELECT STATEMENT | | | | 145 | +2 | 1 | 1M | | | | | . | . | | |
| 1 | HASH JOIN OUTER | | 1M | 119K | 146 | +1 | 1 | 1M | 5169 | 1GB | 5169 | 1GB | 184MB | 1GB | 100.00 | Cpu (14) |
| | | | | | | | | | | | | | | | | SQL*Net more data to client (4) |
| 2 | TABLE ACCESS FULL | SUPERTYPE | 1M | 19593 | 1 | +2 | 1 | 1M | 575 | 559MB | | | . | . | | |
| 3 | TABLE ACCESS FULL | NIKOICHI_MITAINA_SUBTYPE | 750K | 38796 | 42 | +2 | 1 | 1M | 1134 | 1GB | | | . | . | | |
==============================================================================================================================================================================================================


-Fastオプションなしの場合と比較してみるとSQL*Net roundtrips to/from clientなど減ってますよね:)
このround tripは、待機イベントSQL*Net more data to clientなどで現れます。(SQL監視のActivity Detailsにも現れていますので、覚えておくと良いと思います)

-Fastオプションなし(auto trace)

1551273711  bytes sent via SQL*Net to client
735146 bytes received via SQL*Net from client
66668 SQL*Net roundtrips to/from client

-Fastオプションあり(auto trace)

1539940308  bytes sent via SQL*Net to client
110262 bytes received via SQL*Net from client
10001 SQL*Net roundtrips to/from client

-Fastオプションなし(SQL監視)

Global Information
------------------------------
...略...
Duration : 173s
...略...
Fetch Calls : 66668

-Fastオプションあり(auto trace)

Global Information
------------------------------
...略...
Dration : 146s
...略...
Fetch Calls : 10001

早く、ポカポカ陽気にならないかなぁ。

と思う寒い日々。

では、また。

| | | コメント (0)

2023年1月16日 (月)

実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 / No.61 / ANSI JOINのおまけのおまけ

年を跨いで, ”実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 / No.61 / ANSI JOINのおまけ”のおまけです.

前回の投稿から間隔が空いていたので, まずは, 簡単な復習から.

ANSI構文のON句の結合条件でORが利用されているという, スーパータイプ, サブタイプテーブルの実装崩れというか, 大人の事情に押し切られて負けた感じありありの半端な状態.
あ, そうだ, Oralceの外部結合だとOR使えないけど, ANSIなら使えるじゃん!
という流れを感じるSQL文を, Oracleのオプティマイザは, LATERALへの書き換え(VW_LAT_E87C3AAF)や, OR EXPANDの書き換え(VW_ORE_FDF394AE)を駆使して, 物凄い最適化を行っていました.

この例では, 外部表, 内部表の多重度は, 1:0..1. かつ, スーパータイプ, サブタイプでいうところの不完全なサブタイプ.
さらに, 内部表は, 単純にニコイチにしただけのようなサブタイプテーブルで外部表との結合列が2列(おそらく本来同一列に統合されていただろう. . と思われる)ある. 惜しい!という感じのモデル.
比較的軽度のモデリング障害ではあるので, このまま使うのであれば, LATERAL変換されるのを避けるような書き換え, 比較的単純な HASH JOIN なるようにすればそこそこ改善できそうな感じはしますよね(いわゆるTemp落ちはある程度発生する前提で)

なお, この例で AUTO TRACEでの実行時間と, SQL MONITORの実行時間(DB内部)に差異があることに気づいた方もいると思いますが. これ, クライアントがデータをFETCHしている時間ですね. 行数が多いので. SQL*Plusの場合, デフォルトのFETCH SIZEが15なので行数が多いと, FETCHの際, サーバーからの受信で時間がかかります.
(この症状は以前, 高フェッチ圧症として紹介したこともあるので, 覚えている方も多いのではないかと思います. この例では1行の行サイズも大きめかつ, 行数も多めにして SELECT * にしているのでそこそこ目立つ時間になるようにしています. これも別のエントリーでネタにするための仕込みではあるのですが, 今回の記事では気にしないでください. SQLモニターのサーバー内部での純粋な処理時間だけで, 書き換え前後での差を見て行きます!)

SCOTT@orclpdb1> @dayx
1 SELECT *
2 FROM
3 supertype st
4 LEFT OUTER JOIN nikoichi_mitaina_subtype nmst
5 ON
6 st.pkey = nmst.col1
7* OR st.pkey = nmst.col2

10001行が選択されました.

経過: 00:00:01.41

実行計画
----------------------------------------------------------
Plan hash value: 2133431102

-------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 20002 | 20M| 40226 (1)| 00:00:02 |
| 1 | MERGE JOIN OUTER | | 20002 | 20M| 40226 (1)| 00:00:02 |
| 2 | TABLE ACCESS FULL | SUPERTYPE | 10001 | 4971K| 204 (0)| 00:00:01 |
| 3 | BUFFER SORT | | 2 | 1082 | 40021 (1)| 00:00:02 |
| 4 | VIEW | VW_LAT_E87C3AAF | 2 | 1082 | 4 (0)| 00:00:01 |
| 5 | VIEW | VW_ORE_FDF394AE | 2 | 1082 | 4 (0)| 00:00:01 |
| 6 | UNION-ALL | | | | | |
| 7 | TABLE ACCESS BY INDEX ROWID| NIKOICHI_MITAINA_SUBTYPE | 1 | 1009 | 2 (0)| 00:00:01 |
|* 8 | INDEX UNIQUE SCAN | UK1 | 1 | | 1 (0)| 00:00:01 |
|* 9 | TABLE ACCESS BY INDEX ROWID| NIKOICHI_MITAINA_SUBTYPE | 1 | 1009 | 2 (0)| 00:00:01 |
|* 10 | INDEX UNIQUE SCAN | UK2 | 1 | | 1 (0)| 00:00:01 |
-------------------------------------------------------------------------------------------------------------

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

8 - access("ST"."PKEY"="NMST"."COL1")
filter("NMST"."COL1" IS NOT NULL)
9 - filter(LNNVL("ST"."PKEY"="NMST"."COL1"))
10 - access("ST"."PKEY"="NMST"."COL2")
filter("NMST"."COL2" IS NOT NULL)

統計
----------------------------------------------------------
0 recursive calls
0 db block gets
32231 consistent gets
0 physical reads
0 redo size
15493776 bytes sent via SQL*Net to client (別エントリ向け)
7378 bytes received via SQL*Net from client
668 SQL*Net roundtrips to/from client (別エントリ向け)
10001 sorts (memory)
0 sorts (disk)
10001 rows processed


1 SELECT /*+ MONITOR */ *
2 FROM
3 supertype st
4 LEFT OUTER JOIN nikoichi_mitaina_subtype nmst
5 ON
6 st.pkey = nmst.col1
7* OR st.pkey = nmst.col2

DBMS_SQLTUNE.REPORT_SQL_MONITOR(SQL_ID=>NULL,TYPE=>'TEXT')
--------------------------------------------------------------------------------------------------------------------------------------
SQL Monitoring Report

SQL Text
------------------------------
SELECT /*+ MONITOR */ * FROM supertype st LEFT OUTER JOIN nikoichi_mitaina_subtype nmst ON st.pkey = nmst.col1 OR st.pkey = nmst.col2

Global Information
------------------------------
Status : DONE (ALL ROWS)

...略...

Duration : 3s(別ネタ向け仕込み)

...略...

Global Stats
=================================================
| Elapsed | Cpu | Other | Fetch | Buffer |
| Time(s) | Time(s) | Waits(s) | Calls | Gets |
=================================================
| 0.37 | 0.31 | 0.06 | 668 | 32231 |
=================================================

SQL Plan Monitoring Details (Plan Hash Value=2133431102)
=====================================================================================================================================================================
| Id | Operation | Name | Rows | Cost | Time | Start | Execs | Rows | Mem | Activity | Activity Detail |
| | | | (Estim) | | Active(s) | Active | | (Actual) | (Max) | (%) | (# samples) |
=====================================================================================================================================================================
| 0 | SELECT STATEMENT | | | | 4 | +0 | 1 | 10001 | . | | |
| 1 | MERGE JOIN OUTER | | 20002 | 40226 | 4 | +0 | 1 | 10001 | . | | |
| 2 | TABLE ACCESS FULL | SUPERTYPE | 10001 | 204 | 4 | +0 | 1 | 10001 | . | | |
| 3 | BUFFER SORT | | 2 | 40021 | 4 | +0 | 10001 | 10000 | 2048 | | |
| 4 | VIEW | VW_LAT_E87C3AAF | 2 | 4 | 4 | +0 | 10001 | 10000 | . | | |
| 5 | VIEW | VW_ORE_FDF394AE | 2 | 4 | 4 | +0 | 10001 | 10000 | . | | |
| 6 | UNION-ALL | | | | 4 | +0 | 10001 | 10000 | . | | |
| 7 | TABLE ACCESS BY INDEX ROWID | NIKOICHI_MITAINA_SUBTYPE | 1 | 2 | 4 | +0 | 10001 | 5000 | . | | |
| 8 | INDEX UNIQUE SCAN | UK1 | 1 | 1 | 4 | +0 | 10001 | 5000 | . | | |
| 9 | TABLE ACCESS BY INDEX ROWID | NIKOICHI_MITAINA_SUBTYPE | 1 | 2 | 4 | +0 | 10001 | 5000 | . | | |
| 10 | INDEX UNIQUE SCAN | UK2 | 1 | 1 | 4 | +0 | 10001 | 5000 | . | | |
=====================================================================================================================================================================

では, 書き換えて, LATERAL変換を避け, HASH JOINになるようにしてみましょう. (WITH句を利用していますが, 再利用ではなく読みやすさ狙いです. Oracleもそれを理解できるのでインラインビューとして扱われます)
今回のようなデータモデル障害の場合は, 治療もシンプルで良いのですがw(例に取り上げるのがメンドクサイやつだと, 解説するのもメンドクサイし良いことないので)

現場どのようになっているかを理解する必要があります. この例では, col1列とcol2列は実は同一列で良いだろうということになるので, 以下のように書き換えれば, JOIN ON ... OR なんて現時点のオプティマイザでは, ほぼ危険な感じしかしない実行計画になるようなSQLへの書き換えも回避できるのではないでしょうか?
結果は見ての通り, 別エントリ向けの仕込みであるFETCH時間を除いたデータベース内部のみの処理時間は, 0.37sec から 0.15secと62%ほど改善しています(ただ, このデータ量で私の環境だとPGA内に収まっているのでTemp落ちの影響は見えないですね. オンメモリなら勝ちは確実ですが)

  1  WITH
2 t1 AS
3 (
4 SELECT
5 pkey
6 , CASE
7 WHEN col2 IS NULL
8 THEN col1
9 ELSE col2
10 END AS join_key
11 ,description
12 FROM
13 nikoichi_mitaina_subtype
14 WHERE
15 col1 IS NOT NULL
16 OR col2 IS NOT NULL
17 )
18 SELECT *
19 FROM
20 supertype st
21 LEFT OUTER JOIN t1
22 ON
23* st.pkey = t1.join_key

10001行が選択されました.

経過: 00:00:01.28

実行計画
----------------------------------------------------------
Plan hash value: 2223315184

-------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 10001 | 14M| | 1224 (1)| 00:00:01 |
|* 1 | HASH JOIN OUTER | | 10001 | 14M| 5096K| 1224 (1)| 00:00:01 |
| 2 | TABLE ACCESS FULL| SUPERTYPE | 10001 | 4971K| | 204 (0)| 00:00:01 |
|* 3 | TABLE ACCESS FULL| NIKOICHI_MITAINA_SUBTYPE | 7500 | 7390K| | 410 (1)| 00:00:01 |
-------------------------------------------------------------------------------------------------------

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

1 - access("ST"."PKEY"=CASE WHEN (ROWID(+) IS NOT NULL) THEN CASE WHEN ("COL2"(+) IS
NULL) THEN "COL1"(+) ELSE "COL2"(+) END ELSE NULL END )
3 - filter("COL1"(+) IS NOT NULL OR "COL2"(+) IS NOT NULL)

統計
----------------------------------------------------------
0 recursive calls
0 db block gets
2748 consistent gets
0 physical reads
0 redo size
15483707 bytes sent via SQL*Net to client(別ネタ向け仕込み)
7378 bytes received via SQL*Net from client
668 SQL*Net roundtrips to/from client(別ネタ向け仕込み)
0 sorts (memory)
0 sorts (disk)
10001 rows processed


1 WITH
2 t1 AS
3 (
4 SELECT
5 pkey
6 , CASE
7 WHEN col2 IS NULL
8 THEN col1
9 ELSE col2
10 END AS join_key
11 ,description
12 FROM
13 nikoichi_mitaina_subtype
14 WHERE
15 col1 IS NOT NULL
16 OR col2 IS NOT NULL
17 )
18 SELECT /*+ MONITOR */ *
19 FROM
20 supertype st
21 LEFT OUTER JOIN t1
22 ON
23* st.pkey = t1.join_key

DBMS_SQLTUNE.REPORT_SQL_MONITOR(SQL_ID=>NULL,TYPE=>'TEXT')
----------------------------------------------------------------------------------------------------------------
SQL Monitoring Report

SQL Text
------------------------------
WITH t1 AS ( SELECT pkey , CASE WHEN col2 IS NULL THEN col1 ELSE col2 END AS join_key ,description
FROM nikoichi_mitaina_subtype WHERE col1 IS NOT NULL OR col2 IS NOT NULL ) SELECT /*+ MONITOR */ *
FROM supertype st LEFT OUTER JOIN t1 ON st.pkey = t1.join_key

Global Information
------------------------------
Status : DONE (ALL ROWS)

...略...

Duration : 2s(別ネタ向け仕込み)

...略...

Global Stats
=================================================
| Elapsed | Cpu | Other | Fetch | Buffer |
| Time(s) | Time(s) | Waits(s) | Calls | Gets |
=================================================
| 0.15 | 0.13 | 0.02 | 668 | 2748 |
=================================================

SQL Plan Monitoring Details (Plan Hash Value=2223315184)
======================================================================================================================================================
| Id | Operation | Name | Rows | Cost | Time | Start | Execs | Rows | Mem | Activity | Activity Detail |
| | | | (Estim) | | Active(s) | Active | | (Actual) | (Max) | (%) | (# samples) |
======================================================================================================================================================
| 0 | SELECT STATEMENT | | | | 3 | +0 | 1 | 10001 | . | | |
| 1 | HASH JOIN OUTER | | 10001 | 1224 | 3 | +0 | 1 | 10001 | 7MB | | |
| 2 | TABLE ACCESS FULL | SUPERTYPE | 10001 | 204 | 1 | +0 | 1 | 10001 | . | | |
| 3 | TABLE ACCESS FULL | NIKOICHI_MITAINA_SUBTYPE | 7500 | 410 | 3 | +0 | 1 | 10000 | . | | |
======================================================================================================================================================

将来のデータ量が100倍だとして. . . . HASH JOIN化してTemp落ちの影響も含めて見ておきましょう

SCOTT@orclpdb1> @dayx-1
1* DROP TABLE supertype

表が削除されました.

経過: 00:00:00.47
1 CREATE TABLE supertype
2 (
3 pkey NUMBER PRIMARY KEY
4 , attr1 NUMBER NOT NULL
5 , attr2 NUMBER NOT NULL
6 , note VARCHAR2(500)
7* )

表が作成されました.

経過: 00:00:00.15
1* DROP TABLE nikoichi_mitaina_subtype

表が削除されました.

経過: 00:00:00.05
1 CREATE TABLE nikoichi_mitaina_subtype
2 (
3 pkey NUMBER PRIMARY KEY
4 , col1 NUMBER
5 , col2 NUMBER
6 , description VARCHAR2(1000)
7 , CONSTRAINT uk1 unique (col1) USING INDEX
8 , CONSTRAINT uk2 unique (col2) USING INDEX
9* )

表が作成されました.

経過: 00:00:00.03
1 DECLARE
2 cMAX_ROWS CONSTANT NUMBER := 1000000;
3 BEGIN
4 FOR i IN 1..cMAX_ROWS LOOP
5 INSERT INTO supertype VALUES(i,0,0,LPAD(i,500,'*'));
6 INSERT INTO nikoichi_mitaina_subtype VALUES(
7 i
8 , CASE WHEN MOD(i,2) = 0 THEN i ELSE null END
9 , CASE WHEN MOD(i,2) = 1 THEN i ELSE null END
10 , LPAD(i,1000,'*')
11 );
12 IF MOD(i,100) = 0 THEN COMMIT; END IF;
13 END LOOP;
14 INSERT INTO supertype VALUES(cMAX_ROWS+1,0,0,null);
15 COMMIT;
16 DBMS_STATS.GATHER_TABLE_STATS(ownname=>'SCOTT',tabname=>'SUPERTYPE',no_invalidate=>false,cascade=>true);
17 DBMS_STATS.GATHER_TABLE_STATS(ownname=>'SCOTT',tabname=>'NIKOICHI_MITAINA_SUBTYPE',no_invalidate=>false,cascade=>true);
18* END;

PL/SQLプロシージャが正常に完了しました.

経過: 00:04:37.66


まず元ネタのLATERALとOR EXPAND書き換えされている方はどうか. . SQLモニターのExecが綺麗に増加(当然ですが)

SCOTT@orclpdb1> @dayx
1 SELECT *
2 FROM
3 supertype st
4 LEFT OUTER JOIN nikoichi_mitaina_subtype nmst
5 ON
6 st.pkey = nmst.col1
7* OR st.pkey = nmst.col2

1000001行が選択されました.

経過: 00:02:23.03

実行計画
----------------------------------------------------------
Plan hash value: 2133431102

-------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 2000K| 2004M| 6022K (1)| 00:03:56 |
| 1 | MERGE JOIN OUTER | | 2000K| 2004M| 6022K (1)| 00:03:56 |
| 2 | TABLE ACCESS FULL | SUPERTYPE | 1000K| 486M| 19524 (1)| 00:00:01 |
| 3 | BUFFER SORT | | 2 | 1082 | 6002K (1)| 00:03:55 |
| 4 | VIEW | VW_LAT_E87C3AAF | 2 | 1082 | 6 (0)| 00:00:01 |
| 5 | VIEW | VW_ORE_FDF394AE | 2 | 1082 | 6 (0)| 00:00:01 |
| 6 | UNION-ALL | | | | | |
| 7 | TABLE ACCESS BY INDEX ROWID| NIKOICHI_MITAINA_SUBTYPE | 1 | 1012 | 3 (0)| 00:00:01 |
|* 8 | INDEX UNIQUE SCAN | UK1 | 1 | | 2 (0)| 00:00:01 |
|* 9 | TABLE ACCESS BY INDEX ROWID| NIKOICHI_MITAINA_SUBTYPE | 1 | 1012 | 3 (0)| 00:00:01 |
|* 10 | INDEX UNIQUE SCAN | UK2 | 1 | | 2 (0)| 00:00:01 |
-------------------------------------------------------------------------------------------------------------

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

8 - access("ST"."PKEY"="NMST"."COL1")
filter("NMST"."COL1" IS NOT NULL)
9 - filter(LNNVL("ST"."PKEY"="NMST"."COL1"))
10 - access("ST"."PKEY"="NMST"."COL2")
filter("NMST"."COL2" IS NOT NULL)

統計
----------------------------------------------------------
0 recursive calls
0 db block gets
3561836 consistent gets
71485 physical reads
0 redo size
1552273780 bytes sent via SQL*Net to client(別ネタ向け仕込み)
734925 bytes received via SQL*Net from client
66668 SQL*Net roundtrips to/from client(別ネタ向け仕込み)
1000001 sorts (memory)
0 sorts (disk)
1000001 rows processed


1 SELECT /*+ MONITOR */ *
2 FROM
3 supertype st
4 LEFT OUTER JOIN nikoichi_mitaina_subtype nmst
5 ON
6 st.pkey = nmst.col1
7* OR st.pkey = nmst.col2

DBMS_SQLTUNE.REPORT_SQL_MONITOR(SQL_ID=>NULL,TYPE=>'TEXT')
-----------------------------------------------------------------------------------------
SQL Monitoring Report

SQL Text
------------------------------
SELECT /*+ MONITOR */ * FROM supertype st LEFT OUTER JOIN nikoichi_mitaina_subtype nmst
ON st.pkey = nmst.col1 OR st.pkey = nmst.col2

Global Information
------------------------------
Status : DONE (ALL ROWS)

...略...

Duration : 219s(別ネタ向け仕込み)

...略...

Global Stats
===========================================================================
| Elapsed | Cpu | IO | Other | Fetch | Buffer | Read | Read |
| Time(s) | Time(s) | Waits(s) | Waits(s) | Calls | Gets | Reqs | Bytes |
===========================================================================
| 37 | 32 | 0.22 | 4.20 | 66668 | 4M | 573 | 558MB |
===========================================================================

SQL Plan Monitoring Details (Plan Hash Value=2133431102)
====================================================================================================================================================================================
| Id | Operation | Name | Rows | Cost | Time | Start | Execs | Rows | Read | Read | Mem | Activity | Activity Detail |
| | | | (Estim) | | Active(s) | Active | | (Actual) | Reqs | Bytes | (Max) | (%) | (# samples) |
====================================================================================================================================================================================
| 0 | SELECT STATEMENT | | | | 220 | +0 | 1 | 1M | | | . | | |
| 1 | MERGE JOIN OUTER | | 2M | 6M | 220 | +0 | 1 | 1M | | | . | 6.90 | Cpu (2) |
| 2 | TABLE ACCESS FULL | SUPERTYPE | 1M | 19524 | 220 | +0 | 1 | 1M | 573 | 558MB | . | 10.34 | Cpu (3) |
| 3 | BUFFER SORT | | 2 | 6M | 220 | +0 | 1M | 1M | | | 2048 | 17.24 | Cpu (5) |
| 4 | VIEW | VW_LAT_E87C3AAF | 2 | 6 | 220 | +0 | 1M | 1M | | | . | | |
| 5 | VIEW | VW_ORE_FDF394AE | 2 | 6 | 220 | +0 | 1M | 1M | | | . | | |
| 6 | UNION-ALL | | | | 220 | +0 | 1M | 1M | | | . | 6.90 | Cpu (2) |
| 7 | TABLE ACCESS BY INDEX ROWID | NIKOICHI_MITAINA_SUBTYPE | 1 | 3 | 220 | +0 | 1M | 500K | | | . | | |
| 8 | INDEX UNIQUE SCAN | UK1 | 1 | 2 | 220 | +0 | 1M | 500K | | | . | 17.24 | Cpu (5) |
| 9 | TABLE ACCESS BY INDEX ROWID | NIKOICHI_MITAINA_SUBTYPE | 1 | 3 | 220 | +0 | 1M | 500K | | | . | | |
| 10 | INDEX UNIQUE SCAN | UK2 | 1 | 2 | 220 | +0 | 1M | 500K | | | . | 6.90 | Cpu (2) |
====================================================================================================================================================================================


LATERAL変換を避け, HASH JOINにしてくれるような書き換えを行った方はどうかというと.
やはり, PGA内に収まっていたHASH JOINと比較して, Temp落ち(1GBほど)の影響で改善幅は減っていますが, 37sec が 32secと, 15%程度は勝っていますね. Temp落ちは避けられないですからね.

であれば, Temp落ちの落ちている先を速くすれば良いではないか. . . ということで, メモリにさえ余裕があれば, 使いすぎないようにした上で, tmpfs を使ってみましょうか. (一時表領域はなければOracleが再作成してくれるので)

  1  WITH
2 t1 AS
3 (
4 SELECT
5 pkey
6 , CASE
7 WHEN col2 IS NULL
8 THEN col1
9 ELSE col2
10 END AS join_key
11 ,description
12 FROM
13 nikoichi_mitaina_subtype
14 WHERE
15 col1 IS NOT NULL
16 OR col2 IS NOT NULL
17 )
18 SELECT *
19 FROM
20 supertype st
21 LEFT OUTER JOIN t1
22 ON
23* st.pkey = t1.join_key

1000001行が選択されました.

経過: 00:02:24.20

実行計画
----------------------------------------------------------
Plan hash value: 2223315184

-------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes |TempSpc| Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1000K| 1451M| | 120K (1)| 00:00:05 |
|* 1 | HASH JOIN OUTER | | 1000K| 1451M| 497M| 120K (1)| 00:00:05 |
| 2 | TABLE ACCESS FULL| SUPERTYPE | 1000K| 486M| | 19524 (1)| 00:00:01 |
|* 3 | TABLE ACCESS FULL| NIKOICHI_MITAINA_SUBTYPE | 750K| 723M| | 39559 (1)| 00:00:02 |
-------------------------------------------------------------------------------------------------------

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

1 - access("ST"."PKEY"=CASE WHEN (ROWID(+) IS NOT NULL) THEN CASE WHEN ("COL2"(+) IS
NULL) THEN "COL1"(+) ELSE "COL2"(+) END ELSE NULL END )
3 - filter("COL1"(+) IS NOT NULL OR "COL2"(+) IS NOT NULL)

統計
----------------------------------------------------------
1297 recursive calls
0 db block gets
229696 consistent gets
374740 physical reads
0 redo size
1551273711 bytes sent via SQL*Net to client(別ネタ向け仕込み)
734925 bytes received via SQL*Net from client
66668 SQL*Net roundtrips to/from client(別ネタ向け仕込み)
0 sorts (memory)
0 sorts (disk)
1000001 rows processed


1 WITH
2 t1 AS
3 (
4 SELECT
5 pkey
6 , CASE
7 WHEN col2 IS NULL
8 THEN col1
9 ELSE col2
10 END AS join_key
11 ,description
12 FROM
13 nikoichi_mitaina_subtype
14 WHERE
15 col1 IS NOT NULL
16 OR col2 IS NOT NULL
17 )
18 SELECT /*+ MONITOR */ *
19 FROM
20 supertype st
21 LEFT OUTER JOIN t1
22 ON
23* st.pkey = t1.join_key

DBMS_SQLTUNE.REPORT_SQL_MONITOR(SQL_ID=>NULL,TYPE=>'TEXT')
----------------------------------------------------------------------------------------------
SQL Monitoring Report

SQL Text
------------------------------
WITH t1 AS ( SELECT pkey , CASE WHEN col2 IS NULL THEN col1 ELSE col2 END AS join_key
,description FROM nikoichi_mitaina_subtype WHERE col1 IS NOT NULL OR col2 IS NOT NULL )
SELECT /*+ MONITOR */ * FROM supertype st LEFT OUTER JOIN t1 ON st.pkey = t1.join_key

Global Information
------------------------------
Status : DONE (ALL ROWS)

...略...

Duration : 223s

...略...

Global Stats
================================================================================
| Elapsed | Cpu | IO | Fetch | Buffer | Read | Read | Write | Write |
| Time(s) | Time(s) | Waits(s) | Calls | Gets | Reqs | Bytes | Reqs | Bytes |
================================================================================
| 32 | 17 | 15 | 66668 | 230K | 6887 | 3GB | 5169 | 1GB |
================================================================================

SQL Plan Monitoring Details (Plan Hash Value=2223315184)
==============================================================================================================================================================================================================
| Id | Operation | Name | Rows | Cost | Time | Start | Execs | Rows | Read | Read | Write | Write | Mem | Temp | Activity | Activity Detail |
| | | | (Estim) | | Active(s) | Active | | (Actual) | Reqs | Bytes | Reqs | Bytes | (Max) | (Max) | (%) | (# samples) |
==============================================================================================================================================================================================================
| 0 | SELECT STATEMENT | | | | 222 | +2 | 1 | 1M | | | | | . | . | | |
| 1 | HASH JOIN OUTER | | 1M | 120K | 223 | +1 | 1 | 1M | 5169 | 1GB | 5169 | 1GB | 184MB | 1GB | 90.62 | Cpu (9) |
| | | | | | | | | | | | | | | | | SQL*Net more data to client (7) |
| | | | | | | | | | | | | | | | | direct path write temp (13) |
| 3 | TABLE ACCESS FULL | NIKOICHI_MITAINA_SUBTYPE | 750K | 39559 | 67 | +7 | 1 | 1M | 1145 | 1GB | | | . | . | 6.25 | Cpu (2) |
==============================================================================================================================================================================================================


ということで, メモリに余裕があるので tmpfsを使って遊んでみましょう. 2GB固定サイズの一時表領域を作成して, SCOTTユーザーのデフォルト一時表領域にしました

[master@localhost ~]$ df -TH
ファイルシス タイプ サイズ 使用 残り 使用% マウント位置
tmpfs tmpfs 9.3G 17k 9.3G 1% /dev/shm
tmpfs tmpfs 9.3G 9.7M 9.3G 1% /run
tmpfs tmpfs 9.3G 0 9.3G 0% /sys/fs/cgroup
/dev/mapper/ol-root xfs 48G 44G 4.3G 92% /
/dev/mapper/ol-work xfs 11G 109M 11G 2% /work

...略...

[master@localhost ~]$ sudo mkdir /oratemp
...略...
[master@localhost ~]$ ls -l / | grep oratemp
drwxrwxrwt. 2 root root 60 1月 14 12:03 oratemp
[master@localhost ~]$ sudo mount -t tmpfs tmpfs /oratemp
[sudo] master のパスワード:
[master@localhost ~]$ df -TH
ファイルシス タイプ サイズ 使用 残り 使用% マウント位置
devtmpfs devtmpfs 9.3G 0 9.3G 0% /dev
tmpfs tmpfs 9.3G 17k 9.3G 1% /dev/shm
tmpfs tmpfs 9.3G 9.7M 9.3G 1% /run
tmpfs tmpfs 9.3G 0 9.3G 0% /sys/fs/cgroup
/dev/mapper/ol-root xfs 48G 44G 4.3G 92% /
/dev/mapper/ol-work xfs 11G 109M 11G 2% /work

...略...

tmpfs tmpfs 9.3G 0 9.3G 0% /oratemp
一時表領域を作成して, scottのデフォルト一時表領域にする
...略...
SYS@orclpdb1> create temporary tablespace hogetemp tempfile '/oratemp/hogetmp.dbf' size 2g;

表領域が作成されました.

SYS@orclpdb1> alter user scott temporary tablespace hogetemp;

ユーザーが変更されました.


では, オリジナルから. こちらそもそもTemp落ちしないので, LATERALビューへのアクセス回数が積み上がるだけなので, 該当表の物理読み込みが影響しなければほぼCPUタイムですね

SCOTT@orclpdb1> @dayx
1 SELECT /*+ MONITOR */ *
2 FROM
3 supertype st
4 LEFT OUTER JOIN nikoichi_mitaina_subtype nmst
5 ON
6 st.pkey = nmst.col1
7* OR st.pkey = nmst.col2

DBMS_SQLTUNE.REPORT_SQL_MONITOR(SQL_ID=>NULL,TYPE=>'TEXT')
------------------------------------------------------------------------------------------
SQL Monitoring Report

SQL Text
------------------------------
SELECT /*+ MONITOR */ * FROM supertype st LEFT OUTER JOIN nikoichi_mitaina_subtype nmst
ON st.pkey = nmst.col1 OR st.pkey = nmst.col2

Global Information
------------------------------
Status : DONE (ALL ROWS)

...略...

Duration : 188s(別ネタ向け仕込み)

...略...

Global Stats
===========================================================================
| Elapsed | Cpu | IO | Other | Fetch | Buffer | Read | Read |
| Time(s) | Time(s) | Waits(s) | Waits(s) | Calls | Gets | Reqs | Bytes |
===========================================================================
| 39 | 34 | 0.21 | 4.62 | 66668 | 4M | 575 | 559MB |
===========================================================================

SQL Plan Monitoring Details (Plan Hash Value=2133431102)
=========================================================================================================================================================================================
| Id | Operation | Name | Rows | Cost | Time | Start | Execs | Rows | Read | Read | Mem | Activity | Activity Detail |
| | | | (Estim) | | Active(s) | Active | | (Actual) | Reqs | Bytes | (Max) | (%) | (# samples) |
=========================================================================================================================================================================================
| 0 | SELECT STATEMENT | | | | 189 | +0 | 1 | 1M | | | . | | |
| 1 | MERGE JOIN OUTER | | 2M | 6M | 189 | +0 | 1 | 1M | | | . | 2.78 | Cpu (1) |
| 2 | TABLE ACCESS FULL | SUPERTYPE | 1M | 19593 | 189 | +0 | 1 | 1M | 575 | 559MB | . | 13.89 | Cpu (4) |
| | | | | | | | | | | | | | direct path read (1) |
| 3 | BUFFER SORT | | 2 | 6M | 189 | +0 | 1M | 1M | | | 2048 | 22.22 | Cpu (8) |
| 4 | VIEW | VW_LAT_E87C3AAF | 2 | 6 | 189 | +0 | 1M | 1M | | | . | | |
| 5 | VIEW | VW_ORE_FDF394AE | 2 | 6 | 189 | +0 | 1M | 1M | | | . | | |
| 6 | UNION-ALL | | | | 189 | +0 | 1M | 1M | | | . | 2.78 | Cpu (1) |
| 7 | TABLE ACCESS BY INDEX ROWID | NIKOICHI_MITAINA_SUBTYPE | 1 | 3 | 189 | +0 | 1M | 500K | | | . | 8.33 | Cpu (3) |
| 8 | INDEX UNIQUE SCAN | UK1 | 1 | 2 | 189 | +0 | 1M | 500K | | | . | 5.56 | Cpu (2) |
| 9 | TABLE ACCESS BY INDEX ROWID | NIKOICHI_MITAINA_SUBTYPE | 1 | 3 | 189 | +0 | 1M | 500K | | | . | 8.33 | Cpu (3) |
| 10 | INDEX UNIQUE SCAN | UK2 | 1 | 2 | 189 | +0 | 1M | 500K | | | . | 13.89 | Cpu (5) |
=========================================================================================================================================================================================


では, HASH JOINになるように書き換えた方のTemp落ちの時間は...想定通り短縮していますね. 15secほどあったIO Waits(s)ものが1/15程度まで減っています.
結果的に, 39sec -> 21secとなりました. Temp落ちする前提だから落ちた先のIOレイテンシーが小さければこうなるわけですけども. 逆に落ちた先のIOレイテンシーが大きければ影響も大きくなりますよね.

  1  WITH
2 t1 AS
3 (
4 SELECT
5 pkey
6 , CASE
7 WHEN col2 IS NULL
8 THEN col1
9 ELSE col2
10 END AS join_key
11 ,description
12 FROM
13 nikoichi_mitaina_subtype
14 WHERE
15 col1 IS NOT NULL
16 OR col2 IS NOT NULL
17 )
18 SELECT /*+ MONITOR */ *
19 FROM
20 supertype st
21 LEFT OUTER JOIN t1
22 ON
23* st.pkey = t1.join_key

DBMS_SQLTUNE.REPORT_SQL_MONITOR(SQL_ID=>NULL,TYPE=>'TEXT')
--------------------------------------------------------------------------------------------
SQL Monitoring Report

SQL Text
------------------------------
WITH t1 AS ( SELECT pkey , CASE WHEN col2 IS NULL THEN col1 ELSE col2 END AS join_key
,description FROM nikoichi_mitaina_subtype WHERE col1 IS NOT NULL OR col2 IS NOT NULL )
SELECT /*+ MONITOR */ * FROM supertype st LEFT OUTER JOIN t1 ON st.pkey = t1.join_key

Global Information
------------------------------
Status : DONE (ALL ROWS)

...略...

Duration : 211s(別ネタ向け仕込み)

...略...

Global Stats
===========================================================================================
| Elapsed | Cpu | IO | Other | Fetch | Buffer | Read | Read | Write | Write |
| Time(s) | Time(s) | Waits(s) | Waits(s) | Calls | Gets | Reqs | Bytes | Reqs | Bytes |
===========================================================================================
| 21 | 16 | 1.65 | 3.28 | 66668 | 247K | 5566 | 3GB | 3855 | 934MB |
===========================================================================================

SQL Plan Monitoring Details (Plan Hash Value=2223315184)
==============================================================================================================================================================================================================
| Id | Operation | Name | Rows | Cost | Time | Start | Execs | Rows | Read | Read | Write | Write | Mem | Temp | Activity | Activity Detail |
| | | | (Estim) | | Active(s) | Active | | (Actual) | Reqs | Bytes | Reqs | Bytes | (Max) | (Max) | (%) | (# samples) |
==============================================================================================================================================================================================================
| 0 | SELECT STATEMENT | | | | 210 | +2 | 1 | 1M | | | | | . | . | | |
| 1 | HASH JOIN OUTER | | 1M | 119K | 211 | +1 | 1 | 1M | 3855 | 934MB | 3855 | 934MB | 367MB | 1GB | 85.71 | sort segment request (1) |
| | | | | | | | | | | | | | | | | Cpu (10) |
| | | | | | | | | | | | | | | | | SQL*Net message to client (1) |
| | | | | | | | | | | | | | | | | SQL*Net more data to client (6) |
| 2 | TABLE ACCESS FULL | SUPERTYPE | 1M | 19593 | 3 | +2 | 1 | 1M | 575 | 559MB | | | . | . | | |
| 3 | TABLE ACCESS FULL | NIKOICHI_MITAINA_SUBTYPE | 750K | 38796 | 120 | +4 | 1 | 1M | 1134 | 1GB | | | . | . | 14.29 | Cpu (2) |
| | | | | | | | | | | | | | | | | direct path read (1) |
==============================================================================================================================================================================================================


既存表定義を変えないでということになるとこの辺りが限界でしょうね.

オプティマイザは進化し続けていますが, モデル起因だったり構文起因だったり, まだまだ頑張っているけど, 何でもかんでもい感じに最適化できるわけではないので, モデリング頑張りましょうね. というのは不変ですよね. と思います.
今回の例は比較的単純かしていますが, 多重度が 1:* で結合カーディナリティが多くなるタイプや, スーパータイプ, サブタイプの共存的サブタイプだとさらに結合カーディナリティが増加するので, UNIONに分割してあげるなど別の手を駆使しないと対応しにくいタイプもあるので, 頭の片隅に置いておくと良さそうです.

2022年分のおまけのおまけ. これで, おしまい.

では, また.



Related article on Mac De Oracle

・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 1 / TABLE FULL SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 2 / INDEX UNIQUE SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 3 / INDEX RANGE SCAN, Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 4 / INDEX RANGE SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 5 / INDEX RANGE SCAN, INLIST ITERATOR
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 6 / INDEX FAST SCAN, Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 7 / INDEX FULL SCAN,Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 8 / INDEX SKIP SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 9 / TABLE ACCESS INMEMORY FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 10 / NESTED LOOP JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 11 / MERGE JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 12 / HASH JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 13 / HASH JOIN OUTER
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 14 / HASH JOIN FULL OUTER
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 15 / PX, TABLE ACCESS FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 16 / CONCATENATION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 17 / SORT UNIQUE, UNION-ALL = UNION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 18 / UNION-ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 19 / INTERSECTION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 20 / MINUS
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 21 / WINDOW NOSORT STOPKEY
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 22 / COUNT STOPKEY
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 23 / HASH JOIN - LEFT-DEEP JOIN vs RIGHT-DEEP JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 24 / CONNECT BY NO FILTERING WITH START-WITH
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 25 / UNION ALL (RECURSIVE WITH) DEPTH FIRST, RECURSIVE WITH PUMP
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#1 / STAR TRANSFORM, VECTOR TRANSFORM (DWH向け)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#2 / MERGE (UPSERT)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#3 / RDFView
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#4 / INDEX FULL SCAN (MIN/MAX) - Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! No.30 - LOAD TABLE CONVENTIONAL vs. LOAD AS SELECT
・実行計画は, SQL文のレントゲン写真だ! No.31 - TEMP TABLE TRANSFORMATION LOAD AS SELECT (CURSOR DURATION MEMORY)
・実行計画は, SQL文のレントゲン写真だ! No.32 - EXTERNAL TABLE ACCESS FULL / INMEMORY FULL
・実行計画は, SQL文のレントゲン写真だ! No.33 - BITMAP CONVERSION TO ROWIDS
・実行計画は, SQL文のレントゲン写真だ! No.34 - 似て非なるもの USE_CONCAT と OR_EXPAND ヒント と 手書きSQLのレントゲンの見分け方
・実行計画は, SQL文のレントゲン写真だ! No.35 - 似て非なるもの USE_CONCAT と OR_EXPANDヒントとパラレルクエリー
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 1 / No.36 / INTERSECT ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 2 / No.37 / MINUS ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 3 / No.38 / EXCEPT and EXCEPT ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 4 / No.39 / In-Memory Hybrid Scans
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 5 / No.40 / PIVOT and UNPIVOT
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 6 / No.41 / In-Memory Vectorized Join
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 7 / No.42 / INDEX RANGE SCAN (MULTI VALUE)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 8 / No.43 / TABLE ACCESS BY INDEX ROWID BATCHED
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 9 / No.44 / COLLECTION ITERATOR PICKLER FETCH
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 10 / No.45 / MAT_VIEW REWRITE ACCESS FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 11 / No.46 / GROUPING SETS, ROLLUP, CUBE
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 12 / No.47 / TEMP TABLE TRANSFORMATION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 13 / No.48 / MULTI-TABLE INSERT
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 14 / No.49 / the DUAL Table
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 15 / No.50 / REMOTE
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 16 / No.51 / Concurrent Execution of Union All and Union
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 17 / No.52 / Order by Elimination
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 18 / No.53 / Join Elimination
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 19 / No.54 / Group by Elimination
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 20 / No.55 / DISTINCT Elimination
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 21 / No.56 / INLIST ITERATOR と Sub Query と STATISTICS COLLECTOR
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 22 / No.57 / Subquery Unnesting
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 23 / No.58 / ANTI JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 24 / No.59 / SQL MACRO (19.7〜)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 25 / No.60 / ANSI JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 / No.60 / ANSI JOINのおまけ

| | | コメント (0)

2022年12月26日 (月)

実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 / No.60 / ANSI JOINのおまけ

さて, 恒例のアドベントカレンダーのおまけw

Day25で, 性能面で気になる部分のコメントしていた点を思い出してください. INDEX UNIQUE SCANを繰り返している点. 問題にならないなら良いのですが, (データ件数がそれ以上増加しなければ, 安定した処理時間にはなるわけですが....)
データ量次第の危さを感じますとコメントしていた点を思い出してください.

では, 私が, 懸念しているINDEX UNIQUE SCANの回数をSQLモニターという名の内視鏡的な方法で診ていきましょう.
VW_LAT_E87C3AAFのLATERALビューに変換されている操作以降がSUPERTYPE表のヒット件数分繰り返されています. この部分こそ, "データ量次第の危さ”と言った理由です.

 

1  SELECT /*+ MONITOR */ *
2 FROM
3 supertype st
4 LEFT OUTER JOIN nikoichi_mitaina_subtype nmst
5 ON
6 st.pkey = nmst.col1
7* OR st.pkey = nmst.col2

DBMS_SQLTUNE.REPORT_SQL_MONITOR(SQL_ID=>NULL,TYPE=>'TEXT')
--------------------------------------------------------------
SQL Monitoring Report

SQL Text
------------------------------
SELECT /*+ MONITOR */ * FROM supertype st LEFT OUTER JOIN nikoichi_mitaina_subtype nmst ON st.pkey = nmst.col1 OR st.pkey = nmst.col2

...略...

Global Stats
=================================================
| Elapsed | Cpu | Other | Fetch | Buffer |
| Time(s) | Time(s) | Waits(s) | Calls | Gets |
=================================================
| 0.42 | 0.37 | 0.06 | 668 | 32231 |
=================================================

SQL Plan Monitoring Details (Plan Hash Value=2133431102)
=====================================================================================================================================================================
| Id | Operation | Name | Rows | Cost | Time | Start | Execs | Rows | Mem | Activity | Activity Detail |
| | | | (Estim) | | Active(s) | Active | | (Actual) | (Max) | (%) | (# samples) |
=====================================================================================================================================================================
| 0 | SELECT STATEMENT | | | | 3 | +0 | 1 | 10001 | . | | |
| 1 | MERGE JOIN OUTER | | 20002 | 40226 | 3 | +0 | 1 | 10001 | . | | |
| 2 | TABLE ACCESS FULL | SUPERTYPE | 10001 | 204 | 3 | +0 | 1 | 10001 | . | | |
| 3 | BUFFER SORT | | 2 | 40021 | 3 | +0 | 10001 | 10000 | 2048 | | |
| 4 | VIEW | VW_LAT_E87C3AAF | 2 | 4 | 3 | +0 | 10001 | 10000 | . | | |
| 5 | VIEW | VW_ORE_FDF394AE | 2 | 4 | 3 | +0 | 10001 | 10000 | . | | |
| 6 | UNION-ALL | | | | 3 | +0 | 10001 | 10000 | . | | |
| 7 | TABLE ACCESS BY INDEX ROWID | NIKOICHI_MITAINA_SUBTYPE | 1 | 2 | 3 | +0 | 10001 | 5000 | . | | |
| 8 | INDEX UNIQUE SCAN | UK1 | 1 | 1 | 3 | +0 | 10001 | 5000 | . | | |
| 9 | TABLE ACCESS BY INDEX ROWID | NIKOICHI_MITAINA_SUBTYPE | 1 | 2 | 3 | +0 | 10001 | 5000 | . | | |
| 10 | INDEX UNIQUE SCAN | UK2 | 1 | 1 | 3 | +0 | 10001 | 5000 | . | | |
=====================================================================================================================================================================

 

今回のケースでは, CPUバウンドになっていますが, キャッシュヒット率が高ければ, CPUバンドでしょうし, 乗り切らないほど巨大であれば, IOバウンドになって現れそうですよね.

どちらの表のデータ件数も今以上に増加(現時点の処理時間が想定範囲内であることを前提としています)する可能性があり, 読みきれない部分があるのであれば, LATERALビュー変換されたこの実行計画は避けた方が良いだろうという意見に反対される方は少ないのではないでしょうか.

であれば, 方法は一つ.
そう, 自分で, 書き換えれば良いですね. データ量が増加しても安定して, 多少無駄なアクセスがあっても処理時間が安定しやすい方向へ最適化が行われるSQL構文へ. (多少, SQL文が長くなってたとしても)

ということで, どう構文変更して書き換えたらいい感じになりそうか考えてみてね, 冬休みの頭の体操になるのではないかと思います:)
(答え合わせはしませんよ)

 

ではまた. (Oracleネタとしては, 今年はここまで) また来年お会いしましょう. みなさん, 良いお年を!


Related article on Mac De Oracle
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 1 / TABLE FULL SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 2 / INDEX UNIQUE SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 3 / INDEX RANGE SCAN, Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 4 / INDEX RANGE SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 5 / INDEX RANGE SCAN, INLIST ITERATOR
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 6 / INDEX FAST SCAN, Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 7 / INDEX FULL SCAN,Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 8 / INDEX SKIP SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 9 / TABLE ACCESS INMEMORY FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 10 / NESTED LOOP JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 11 / MERGE JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 12 / HASH JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 13 / HASH JOIN OUTER
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 14 / HASH JOIN FULL OUTER
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 15 / PX, TABLE ACCESS FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 16 / CONCATENATION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 17 / SORT UNIQUE, UNION-ALL = UNION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 18 / UNION-ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 19 / INTERSECTION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 20 / MINUS
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 21 / WINDOW NOSORT STOPKEY
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 22 / COUNT STOPKEY
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 23 / HASH JOIN - LEFT-DEEP JOIN vs RIGHT-DEEP JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 24 / CONNECT BY NO FILTERING WITH START-WITH
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 25 / UNION ALL (RECURSIVE WITH) DEPTH FIRST, RECURSIVE WITH PUMP
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#1 / STAR TRANSFORM, VECTOR TRANSFORM (DWH向け)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#2 / MERGE (UPSERT)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#3 / RDFView
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#4 / INDEX FULL SCAN (MIN/MAX) - Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! No.30 - LOAD TABLE CONVENTIONAL vs. LOAD AS SELECT
・実行計画は, SQL文のレントゲン写真だ! No.31 - TEMP TABLE TRANSFORMATION LOAD AS SELECT (CURSOR DURATION MEMORY)
・実行計画は, SQL文のレントゲン写真だ! No.32 - EXTERNAL TABLE ACCESS FULL / INMEMORY FULL
・実行計画は, SQL文のレントゲン写真だ! No.33 - BITMAP CONVERSION TO ROWIDS
・実行計画は, SQL文のレントゲン写真だ! No.34 - 似て非なるもの USE_CONCAT と OR_EXPAND ヒント と 手書きSQLのレントゲンの見分け方
・実行計画は, SQL文のレントゲン写真だ! No.35 - 似て非なるもの USE_CONCAT と OR_EXPANDヒントとパラレルクエリー
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 1 / No.36 / INTERSECT ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 2 / No.37 / MINUS ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 3 / No.38 / EXCEPT and EXCEPT ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 4 / No.39 / In-Memory Hybrid Scans
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 5 / No.40 / PIVOT and UNPIVOT
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 6 / No.41 / In-Memory Vectorized Join
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 7 / No.42 / INDEX RANGE SCAN (MULTI VALUE)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 8 / No.43 / TABLE ACCESS BY INDEX ROWID BATCHED
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 9 / No.44 / COLLECTION ITERATOR PICKLER FETCH
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 10 / No.45 / MAT_VIEW REWRITE ACCESS FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 11 / No.46 / GROUPING SETS, ROLLUP, CUBE
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 12 / No.47 / TEMP TABLE TRANSFORMATION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 13 / No.48 / MULTI-TABLE INSERT
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 14 / No.49 / the DUAL Table
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 15 / No.50 / REMOTE
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 16 / No.51 / Concurrent Execution of Union All and Union
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 17 / No.52 / Order by Elimination
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 18 / No.53 / Join Elimination
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 19 / No.54 / Group by Elimination
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 20 / No.55 / DISTINCT Elimination
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 21 / No.56 / INLIST ITERATOR と Sub Query と STATISTICS COLLECTOR
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 22 / No.57 / Subquery Unnesting
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 23 / No.58 / ANTI JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 24 / No.59 / SQL MACRO (19.7〜)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 25 / No.60 / ANSI JOIN

 

| | | コメント (0)

2022年12月25日 (日)

実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 25 / No.60 / ANSI JOIN

Previously on Mac De Oracle...
Day 24は, SQL MACROにフォーカスをあてました. SEMI JOINだろうと思ってた方々 m(_ _)m 元々今日のネタの伏線をはろうとしていたので, いずれにしても, SEMI JOINではなかったのですがw

と, いうことで Day 25!
ついに, アドベントカレンダー 実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022も千秋楽です

最終日ですので, 大人の事情に金縛りに会いながらも, 頑張って作ってしまったと思われる, 稀に, よくみるタイプのモデルを, それぞれのバージョンのOracleオプティマイザが, どう最適化しようと苦労しているのか, 実行計画というレントゲンを通し, 生暖か, いや, 熱い眼差しでワイン片手に, 観察しつつ, アドベントカレンダー 実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022を締めくくりたいと思います. (オチはないかもw)

当医院に担ぎ込まれてきたSQL文を問診してみると, こんな感じに見えました. (スーパータイプ, サブタイプテーブルを大人の事情で無理強いされて出来上がってしまったような危うい雰囲気であります. 本題はモデルの良し悪しではないので, これ以上, ツッコまないよ)

ということで, 稀によく見るタイプの患者さんを図に起こしてみました. ん〜, かなり複雑な事情がありそうですね!
参考までに, スーパータイプ, サブタイプの概念モデルの実装方法についてはいくつかのパターンがあります. サブタイプがニコイチになっている割に, 結合キーが2個というところが大人の事情を強く感じますが.
(スーパータイプ, サブタイプよくわからんという方は, 斜め読みするならスーパータイプ/サブタイプのテーブルへの実装 / hmatsu47が良いかなと思います)

Sql_20221224173301

 

他に, 外傷がないか SQL文 を診ておきましょう, SQL文はこんなでした. ANSI構文でなければ, こうはならないですよね. Oracleの方言では結合条件のORは書けないので, UNIONで書くことになるのですが, Oracleの方言でこれを書こうとすると割と大変です. UNIONで書いた方が割と無難な実行計画に最適化されやすいというのもありますが, さて, ANSI構文ではどうなりますか. .
(SQLは雰囲気が伝わるように作ってありますw)

SELECT *
FROM
supertype st
LEFT OUTER JOIN nikoichi_mitaina_subtype nmst
ON
st.pkey = nmst.col1
OR st.pkey = nmst.col2
;

 

では, 実行計画という名のレントゲンを11gR1, 11gR2, 12cR1, 12cR2, 18c, 19c, 21cで, 最適化の違いを診ていきましょう. なお, 隠しパラメータ含めパラメータはデフォルト設定です.
データの準備

SCOTT@orclpdb1> @day25-0
1* DROP TABLE supertype

表が削除されました.

経過: 00:00:00.05
1 CREATE TABLE supertype
2 (
3 pkey NUMBER PRIMARY KEY
4 , attr1 NUMBER NOT NULL
5 , attr2 NUMBER NOT NULL
6 , note VARCHAR2(500)
7* )

表が作成されました.

経過: 00:00:00.03
1* DROP TABLE nikoichi_mitaina_subtype

表が削除されました.

経過: 00:00:00.05
1 CREATE TABLE nikoichi_mitaina_subtype
2 (
3 pkey NUMBER PRIMARY KEY
4 , col1 NUMBER
5 , col2 NUMBER
6 , description VARCHAR2(1000)
7 , CONSTRAINT uk1 unique (col1) USING INDEX
8 , CONSTRAINT uk2 unique (col2) USING INDEX
9* )

表が作成されました.

経過: 00:00:00.03
1 BEGIN
2 FOR i IN 1..10000 LOOP
3 INSERT INTO supertype VALUES(i,0,0,LPAD(i,500,'*'));
4 INSERT INTO nikoichi_mitaina_subtype VALUES(
5 i
6 , CASE WHEN MOD(i,2) = 0 THEN i ELSE null END
7 , CASE WHEN MOD(i,2) = 1 THEN i ELSE null END
8 , LPAD(i,1000,'*')
9 );
10 IF MOD(i,100) = 0 THEN COMMIT; END IF;
11 END LOOP;
12 INSERT INTO supertype VALUES(10001,0,0,null);
13 COMMIT;
14 DBMS_STATS.GATHER_TABLE_STATS(ownname=>'SCOTT',tabname=>'SUPERTYPE',no_invalidate=>false,cascade=>true);
15 DBMS_STATS.GATHER_TABLE_STATS(ownname=>'SCOTT',tabname=>'NIKOICHI_MITAINA_SUBTYPE',no_invalidate=>false,cascade=>true);
16* END;

PL/SQLプロシージャが正常に完了しました.

経過: 00:00:02.83

 

現時点でリリースされている最新版, 21cから順に, 11gR2まで遡って診ていきます

やはり, 最新のオプティマイザは, なかなかやりますね.

結合条件のORId=5, VW_ORE_FDF394AEで分かるように, OR_EXPANDしているようです.
さらに, その結果を, Id=6, VW_LAT_E87C3AAFで分かるように, LATERALビューに変換し, MERGE JOIN OUTERしています. LATERALが使えるようになったリリースであることも大きく影響しているように見えます.
(実は, 昨日のネタは, LATERALして伏線にしようと思ってたのですが, SQL MACROの面白さ先にしたくなったのでしたw)

さらに, 興味深いのは, NIKOICHI_MITAINA_SUBTYPEのユニーク索引をIS NOT NULLでフィルタリングしながらアクセスしているところ. 流石です. とは言っても, INDEX UNIQUE SCANしているのでデータ量次第の危さも感じますよね. キャッシュに乗ってたら早そうですが.

SCOTT@orclpdb1> select banner from v$version;

BANNER
------------------------------------------------------------------------
Oracle Database 21c Enterprise Edition Release 21.0.0.0.0 - Production

SCOTT@orclpdb1> @day25
1 SELECT *
2 FROM
3 supertype st
4 LEFT OUTER JOIN nikoichi_mitaina_subtype nmst
5 ON
6 st.pkey = nmst.col1
7* OR st.pkey = nmst.col2

10001行が選択されました.

実行計画
----------------------------------------------------------
Plan hash value: 2133431102

-------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 20002 | 20M| 40226 (1)| 00:00:02 |
| 1 | MERGE JOIN OUTER | | 20002 | 20M| 40226 (1)| 00:00:02 |
| 2 | TABLE ACCESS FULL | SUPERTYPE | 10001 | 4971K| 204 (0)| 00:00:01 |
| 3 | BUFFER SORT | | 2 | 1082 | 40021 (1)| 00:00:02 |
| 4 | VIEW | VW_LAT_E87C3AAF | 2 | 1082 | 4 (0)| 00:00:01 |
| 5 | VIEW | VW_ORE_FDF394AE | 2 | 1082 | 4 (0)| 00:00:01 |
| 6 | UNION-ALL | | | | | |
| 7 | TABLE ACCESS BY INDEX ROWID| NIKOICHI_MITAINA_SUBTYPE | 1 | 1009 | 2 (0)| 00:00:01 |
|* 8 | INDEX UNIQUE SCAN | UK1 | 1 | | 1 (0)| 00:00:01 |
|* 9 | TABLE ACCESS BY INDEX ROWID| NIKOICHI_MITAINA_SUBTYPE | 1 | 1009 | 2 (0)| 00:00:01 |
|* 10 | INDEX UNIQUE SCAN | UK2 | 1 | | 1 (0)| 00:00:01 |
-------------------------------------------------------------------------------------------------------------

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

8 - access("ST"."PKEY"="NMST"."COL1")
filter("NMST"."COL1" IS NOT NULL)
9 - filter(LNNVL("ST"."PKEY"="NMST"."COL1"))
10 - access("ST"."PKEY"="NMST"."COL2")
filter("NMST"."COL2" IS NOT NULL)

統計
----------------------------------------------------------
0 recursive calls
0 db block gets
32231 consistent gets
0 physical reads
0 redo size
15493776 bytes sent via SQL*Net to client
7378 bytes received via SQL*Net from client
668 SQL*Net roundtrips to/from client
10001 sorts (memory)
0 sorts (disk)
10001 rows processed

 

冒頭で, ”Oracleの方言でこれを書こうとすると割と大変です. ”と書きましたが, 以下が理由です.
同じような表現はできないのです. 方言でこれを書こうとすると割と大変と言ったのはこの理由からなんです.

  1  SELECT *
2 FROM
3 supertype st
4 , nikoichi_mitaina_subtype nmst
5 WHERE
6 st.pkey = nmst.col1(+)
7* OR st.pkey = nmst.col2(+)
OR st.pkey = nmst.col2(+)
*
行7でエラーが発生しました. :
ORA-01719: ORまたはINオペランドの中で外部結合演算子(+)は使用できません

 

余談はこれぐらいにして, 19cではどうか見てみましょう(結果飲み)
同じ実行計画です. LATERALやOR_EXPANDが実装が利用されていることから, それらが実装されたあたりからはこのような実行計画が生成されている可能性が高いですね.

SCOTT@ORCL> select banner from v$version;

BANNER
--------------------------------------------------------------------------------
Oracle Database 19c Enterprise Edition Release 19.0.0.0.0 - Production

Execution Plan
----------------------------------------------------------
Plan hash value: 2133431102

-------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 20002 | 20M| 40219 (1)| 00:00:02 |
| 1 | MERGE JOIN OUTER | | 20002 | 20M| 40219 (1)| 00:00:02 |
| 2 | TABLE ACCESS FULL | SUPERTYPE | 10001 | 4971K| 204 (0)| 00:00:01 |
| 3 | BUFFER SORT | | 2 | 1082 | 40015 (1)| 00:00:02 |
| 4 | VIEW | VW_LAT_E87C3AAF | 2 | 1082 | 4 (0)| 00:00:01 |
| 5 | VIEW | VW_ORE_FDF394AE | 2 | 1082 | 4 (0)| 00:00:01 |
| 6 | UNION-ALL | | | | | |
| 7 | TABLE ACCESS BY INDEX ROWID| NIKOICHI_MITAINA_SUBTYPE | 1 | 1009 | 2 (0)| 00:00:01 |
|* 8 | INDEX UNIQUE SCAN | UK1 | 1 | | 1 (0)| 00:00:01 |
|* 9 | TABLE ACCESS BY INDEX ROWID| NIKOICHI_MITAINA_SUBTYPE | 1 | 1009 | 2 (0)| 00:00:01 |
|* 10 | INDEX UNIQUE SCAN | UK2 | 1 | | 1 (0)| 00:00:01 |
-------------------------------------------------------------------------------------------------------------

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

8 - access("ST"."PKEY"="NMST"."COL1")
filter("NMST"."COL1" IS NOT NULL)
9 - filter(LNNVL("ST"."PKEY"="NMST"."COL1"))
10 - access("ST"."PKEY"="NMST"."COL2")
filter("NMST"."COL2" IS NOT NULL)

 

18cではどうでしょう(結果のみ)
やはり, 同じ, Plan hash valueになっていますね. なるほどなるほど.

SCOTT> select banner from v$version;

BANNER
--------------------------------------------------------------------------------
Oracle Database 18c Enterprise Edition Release 18.0.0.0.0 - Production

Execution Plan
----------------------------------------------------------
Plan hash value: 2133431102

-------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 20002 | 20M| 40218 (1)| 00:00:02 |
| 1 | MERGE JOIN OUTER | | 20002 | 20M| 40218 (1)| 00:00:02 |
| 2 | TABLE ACCESS FULL | SUPERTYPE | 10001 | 4971K| 204 (0)| 00:00:01 |
| 3 | BUFFER SORT | | 2 | 1082 | 40014 (1)| 00:00:02 |
| 4 | VIEW | VW_LAT_E87C3AAF | 2 | 1082 | 4 (0)| 00:00:01 |
| 5 | VIEW | VW_ORE_FDF394AE | 2 | 1082 | 4 (0)| 00:00:01 |
| 6 | UNION-ALL | | | | | |
| 7 | TABLE ACCESS BY INDEX ROWID| NIKOICHI_MITAINA_SUBTYPE | 1 | 1009 | 2 (0)| 00:00:01 |
|* 8 | INDEX UNIQUE SCAN | UK1 | 1 | | 1 (0)| 00:00:01 |
|* 9 | TABLE ACCESS BY INDEX ROWID| NIKOICHI_MITAINA_SUBTYPE | 1 | 1009 | 2 (0)| 00:00:01 |
|* 10 | INDEX UNIQUE SCAN | UK2 | 1 | | 1 (0)| 00:00:01 |
-------------------------------------------------------------------------------------------------------------

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

8 - access("ST"."PKEY"="NMST"."COL1")
filter("NMST"."COL1" IS NOT NULL)
9 - filter(LNNVL("ST"."PKEY"="NMST"."COL1"))
10 - access("ST"."PKEY"="NMST"."COL2")
filter("NMST"."COL2" IS NOT NULL)

 

お!, 12cR2も同じ実行計画になりますね!

orcl@SCOTT> select banner from v$version;

BANNER
--------------------------------------------------------------------------------
Oracle Database 12c Enterprise Edition Release 12.2.0.1.0 - 64bit Production
PL/SQL Release 12.2.0.1.0 - Production
CORE 12.2.0.1.0 Production
TNS for Linux: Version 12.2.0.1.0 - Production
NLSRTL Version 12.2.0.1.0 - Production

Execution Plan
----------------------------------------------------------
Plan hash value: 2133431102

-------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 20002 | 20M| 40217 (1)| 00:00:02 |
| 1 | MERGE JOIN OUTER | | 20002 | 20M| 40217 (1)| 00:00:02 |
| 2 | TABLE ACCESS FULL | SUPERTYPE | 10001 | 4971K| 204 (0)| 00:00:01 |
| 3 | BUFFER SORT | | 2 | 1082 | 40013 (1)| 00:00:02 |
| 4 | VIEW | VW_LAT_E87C3AAF | 2 | 1082 | 4 (0)| 00:00:01 |
| 5 | VIEW | VW_ORE_FDF394AE | 2 | 1082 | 4 (0)| 00:00:01 |
| 6 | UNION-ALL | | | | | |
| 7 | TABLE ACCESS BY INDEX ROWID| NIKOICHI_MITAINA_SUBTYPE | 1 | 1009 | 2 (0)| 00:00:01 |
|* 8 | INDEX UNIQUE SCAN | UK1 | 1 | | 1 (0)| 00:00:01 |
|* 9 | TABLE ACCESS BY INDEX ROWID| NIKOICHI_MITAINA_SUBTYPE | 1 | 1009 | 2 (0)| 00:00:01 |
|* 10 | INDEX UNIQUE SCAN | UK2 | 1 | | 1 (0)| 00:00:01 |
-------------------------------------------------------------------------------------------------------------

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

8 - access("ST"."PKEY"="NMST"."COL1")
filter("NMST"."COL1" IS NOT NULL)
9 - filter(LNNVL("ST"."PKEY"="NMST"."COL1"))
10 - access("ST"."PKEY"="NMST"."COL2")
filter("NMST"."COL2" IS NOT NULL)

 

12cR1はどうでしょう. おっと, ここで差が出ました. OR_EXPANDではなく, USE_CONCATが行われています. たた, VW_LAT_E87C3AAFがあることからLATERALビューが内部的に利用されていることが見えますね.
性能に影響しそうなのは, NESTED LOOPS OUTERになっているあたりでしょうね. データ量が大きい想定見積もりに倒れている最近のリリースとは明らかに異なり. 危険な感じの実行計画ではあります.

SCOTT@pdborcl12c> select banner from v$version;

BANNER
--------------------------------------------------------------------------------
Oracle Database 12c Enterprise Edition Release 12.1.0.2.0 - 64bit Production
PL/SQL Release 12.1.0.2.0 - Production
CORE 12.1.0.2.0 Production
TNS for Linux: Version 12.1.0.2.0 - Production
NLSRTL Version 12.1.0.2.0 - Production

実行計画
----------------------------------------------------------
Plan hash value: 3799136614

-----------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 20002 | 20M| 40216 (1)| 00:00:02 |
| 1 | NESTED LOOPS OUTER | | 20002 | 20M| 40216 (1)| 00:00:02 |
| 2 | TABLE ACCESS FULL | SUPERTYPE | 10001 | 4971K| 204 (0)| 00:00:01 |
| 3 | VIEW | VW_LAT_E87C3AAF | 2 | 1082 | 4 (0)| 00:00:01 |
| 4 | CONCATENATION | | | | | |
| 5 | TABLE ACCESS BY INDEX ROWID| NIKOICHI_MITAINA_SUBTYPE | 1 | 1009 | 2 (0)| 00:00:01 |
|* 6 | INDEX UNIQUE SCAN | UK2 | 1 | | 1 (0)| 00:00:01 |
|* 7 | TABLE ACCESS BY INDEX ROWID| NIKOICHI_MITAINA_SUBTYPE | 1 | 1009 | 2 (0)| 00:00:01 |
|* 8 | INDEX UNIQUE SCAN | UK1 | 1 | | 1 (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------------------

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

6 - access("ST"."PKEY"="NMST"."COL2")
filter("NMST"."COL2" IS NOT NULL)
7 - filter(LNNVL("NMST"."COL2" IS NOT NULL) OR LNNVL("ST"."PKEY"="NMST"."COL2"))
8 - access("ST"."PKEY"="NMST"."COL1")
filter("NMST"."COL1" IS NOT NULL)

 

11gR2です. LATERALビューも消え, USE_CONCATが行われているだけで, さらに, NESTED LOOPS OUTERなのでデータ量が多いとやはり危険なタイプの実行計画になっているのがわかります.

orcl@SCOTT> select banner from v$version;

BANNER
--------------------------------------------------------------------------------
Oracle Database 11g Enterprise Edition Release 11.2.0.4.0 - 64bit Production
PL/SQL Release 11.2.0.4.0 - Production
CORE 11.2.0.4.0 Production
TNS for Linux: Version 11.2.0.4.0 - Production
NLSRTL Version 11.2.0.4.0 - Production

orcl@SCOTT> @day25
1 SELECT *
2 FROM
3 supertype st
4 LEFT OUTER JOIN nikoichi_mitaina_subtype nmst
5 ON
6 st.pkey = nmst.col1
7* OR st.pkey = nmst.col2

Execution Plan
----------------------------------------------------------
Plan hash value: 2117741269

-----------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 20002 | 20M| 40236 (1)| 00:08:03 |
| 1 | NESTED LOOPS OUTER | | 20002 | 20M| 40236 (1)| 00:08:03 |
| 2 | TABLE ACCESS FULL | SUPERTYPE | 10001 | 4971K| 205 (1)| 00:00:03 |
| 3 | VIEW | | 2 | 1082 | 4 (0)| 00:00:01 |
| 4 | CONCATENATION | | | | | |
| 5 | TABLE ACCESS BY INDEX ROWID| NIKOICHI_MITAINA_SUBTYPE | 1 | 1009 | 2 (0)| 00:00:01 |
|* 6 | INDEX UNIQUE SCAN | UK2 | 1 | | 1 (0)| 00:00:01 |
|* 7 | TABLE ACCESS BY INDEX ROWID| NIKOICHI_MITAINA_SUBTYPE | 1 | 1009 | 2 (0)| 00:00:01 |
|* 8 | INDEX UNIQUE SCAN | UK1 | 1 | | 1 (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------------------

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

6 - access("ST"."PKEY"="NMST"."COL2")
filter("NMST"."COL2" IS NOT NULL)
7 - filter(LNNVL("NMST"."COL2" IS NOT NULL) OR LNNVL("ST"."PKEY"="NMST"."COL2"))
8 - access("ST"."PKEY"="NMST"."COL1")
filter("NMST"."COL1" IS NOT NULL)

 

このケース, 最新のリリースでは, LATERALビューを使い, MERGE JOINも利用するような多くの内部的な書き換えが行われ, いい感じの実行計画が生成されています.

ただ, 稀に, そうでもないケースもあります. (ググってみると結構ある. あった. ことがわかります)
そのような場合, 結合条件でORを利用する構文を書き換え, UNIONを使った構文(このケースではオプティマイザが内部的に変換していますが)へ書き換えてしまった方が良い実行計画にできる場合があります. (その方がヒントでの細かい制御もしやすい場合があります)
その辺りは, 状況に応じ臨機応変に対応すれば良いと思います.
今回のMERGE OUTER JOINで想定より遅い場合には, やはり構文変更してしまった方が処理時間は安定して, 無難な方向になるケースもありそうに思います.

ということで, 今年のアドベントカレンダーは, しゅうーーーりょう!!

参考) Internal Views / Oracle Scratchpad

 

I wish you all a Merry Christmas and a Happy New Year!

ではまた ;)

 


Related article on Mac De Oracle
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 1 / TABLE FULL SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 2 / INDEX UNIQUE SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 3 / INDEX RANGE SCAN, Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 4 / INDEX RANGE SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 5 / INDEX RANGE SCAN, INLIST ITERATOR
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 6 / INDEX FAST SCAN, Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 7 / INDEX FULL SCAN,Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 8 / INDEX SKIP SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 9 / TABLE ACCESS INMEMORY FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 10 / NESTED LOOP JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 11 / MERGE JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 12 / HASH JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 13 / HASH JOIN OUTER
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 14 / HASH JOIN FULL OUTER
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 15 / PX, TABLE ACCESS FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 16 / CONCATENATION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 17 / SORT UNIQUE, UNION-ALL = UNION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 18 / UNION-ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 19 / INTERSECTION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 20 / MINUS
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 21 / WINDOW NOSORT STOPKEY
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 22 / COUNT STOPKEY
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 23 / HASH JOIN - LEFT-DEEP JOIN vs RIGHT-DEEP JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 24 / CONNECT BY NO FILTERING WITH START-WITH
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 25 / UNION ALL (RECURSIVE WITH) DEPTH FIRST, RECURSIVE WITH PUMP
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#1 / STAR TRANSFORM, VECTOR TRANSFORM (DWH向け)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#2 / MERGE (UPSERT)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#3 / RDFView
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#4 / INDEX FULL SCAN (MIN/MAX) - Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! No.30 - LOAD TABLE CONVENTIONAL vs. LOAD AS SELECT
・実行計画は, SQL文のレントゲン写真だ! No.31 - TEMP TABLE TRANSFORMATION LOAD AS SELECT (CURSOR DURATION MEMORY)
・実行計画は, SQL文のレントゲン写真だ! No.32 - EXTERNAL TABLE ACCESS FULL / INMEMORY FULL
・実行計画は, SQL文のレントゲン写真だ! No.33 - BITMAP CONVERSION TO ROWIDS
・実行計画は, SQL文のレントゲン写真だ! No.34 - 似て非なるもの USE_CONCAT と OR_EXPAND ヒント と 手書きSQLのレントゲンの見分け方
・実行計画は, SQL文のレントゲン写真だ! No.35 - 似て非なるもの USE_CONCAT と OR_EXPANDヒントとパラレルクエリー
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 1 / No.36 / INTERSECT ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 2 / No.37 / MINUS ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 3 / No.38 / EXCEPT and EXCEPT ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 4 / No.39 / In-Memory Hybrid Scans
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 5 / No.40 / PIVOT and UNPIVOT
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 6 / No.41 / In-Memory Vectorized Join
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 7 / No.42 / INDEX RANGE SCAN (MULTI VALUE)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 8 / No.43 / TABLE ACCESS BY INDEX ROWID BATCHED
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 9 / No.44 / COLLECTION ITERATOR PICKLER FETCH
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 10 / No.45 / MAT_VIEW REWRITE ACCESS FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 11 / No.46 / GROUPING SETS, ROLLUP, CUBE
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 12 / No.47 / TEMP TABLE TRANSFORMATION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 13 / No.48 / MULTI-TABLE INSERT
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 14 / No.49 / the DUAL Table
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 15 / No.50 / REMOTE
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 16 / No.51 / Concurrent Execution of Union All and Union
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 17 / No.52 / Order by Elimination
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 18 / No.53 / Join Elimination
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 19 / No.54 / Group by Elimination
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 20 / No.55 / DISTINCT Elimination
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 21 / No.56 / INLIST ITERATOR と Sub Query と STATISTICS COLLECTOR
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 22 / No.57 / Subquery Unnesting
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 23 / No.58 / ANTI JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 24 / No.59 / SQL MACRO (19.7〜)

 

| | | コメント (0)

2022年12月24日 (土)

実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 24 / No.59 / SQL MACRO (19.7〜)


Previously on Mac De Oracle...

Day 23は, ANTI JOINにフォーカスをあてました. そうくれば, 次は, SEMI JOINだろう? と思ったあなた! ハズレですw

では, Day 24 クリスマスイブの窓を開けましょう!
実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 9 / No.44 / COLLECTION ITERATOR PICKLER FETCH
で, パイプラインテーブルファンクションだと, ファンクションが実行されていることしかExplain等で見る事はできなくて, COLLECTION ITERATOR PICKLER FETCH だけなんだよね, と言うお話をしました.

1  SELECT
2 *
3 FROM
4* day9_pkg.list_2_latest_sales(2, 20, 5745)

PROD_ID CUST_ID TIME_ID CHANNEL_ID PROMO_ID QUANTITY_SOLD AMOUNT_SOLD
---------- ---------- -------- ---------- ---------- ------------- -----------
20 5745 01-12-31 2 999 1 628.89

実行計画
----------------------------------------------------------
Plan hash value: 457385954

---------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 8168 | 16336 | 29 (0)| 00:00:01 |
| 1 | COLLECTION ITERATOR PICKLER FETCH| LIST_2_LATEST_SALES | 8168 | 16336 | 29 (0)| 00:00:01 |
---------------------------------------------------------------------------------------------------------

レントゲン屋さん的にはw 辛いよね. と. 間接的に見るしかないわけで. 少々面倒, 対応を意識していないと難しいですし.


今日は, クリスマスイブですし, Oracleさんもどんどん新機能提供してくれてて, それファンクションじゃないの? いえ, いえ. ?? 今日は, SQLマクロ(SQM)の実行計画という名のレントゲンです.

SQLマクロ(SQM)を作ります. パイプラインテーブルファンクションと似てますけども, SQL_MACROです.

SCOTT@orclpdb1> @day24
1* DROP FUNCTION sq_macro_sample

ファンクションが削除されました.

1 CREATE FUNCTION sq_macro_sample
2 (
3 p_first_rows NUMBER
4 , p_table_name DBMS_TF.TABLE_T
5 )
6 RETURN VARCHAR2
7 SQL_MACRO
8 IS
9 BEGIN
10 RETURN
11 'SELECT
12 *
13 FROM
14 p_table_name
15 FETCH FIRST sq_macro_sample.p_first_rows ROWS ONLY'
16 ;
17* END;

ファンクションが作成されました.


おおおおおおおおおお〜っと. マクロが展開されて, emp表の直接アクセスしている. (マクロだからそりゃそうだw)
レントゲンを見てみましょう.

パイプラインテーブルファンクションと外見からはあまり違いに気づかないというか, 気づけないですが. レントゲンの結果は全く違います! マクロが展開されて, 普通の SQL文の実行計画という名のレントゲンを見ることができます.

  1  SELECT
2 *
3 FROM
4* sq_macro_sample(3, emp)

EMPNO ENAME JOB MGR HIREDATE SAL COMM DEPTNO
---------- ------------------------------ --------------------------- ---------- -------- ---------- ---------- ----------
7369 SMITH CLERK 7902 80-12-17 800 20
7499 ALLEN SALESMAN 7698 81-02-20 1600 300 30
7521 WARD SALESMAN 7698 81-02-22 1250 500 30

実行計画
----------------------------------------------------------
Plan hash value: 3611411408

-------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 3 | 300 | 2 (0)| 00:00:01 |
|* 1 | VIEW | | 3 | 300 | 2 (0)| 00:00:01 |
|* 2 | WINDOW NOSORT STOPKEY| | 3 | 117 | 2 (0)| 00:00:01 |
| 3 | TABLE ACCESS FULL | EMP | 3 | 117 | 2 (0)| 00:00:01 |
-------------------------------------------------------------------------------

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

1 - filter("from$_subquery$_009"."rowlimit_$$_rownumber"<=3)
2 - filter(ROW_NUMBER() OVER ( ORDER BY NULL )<=3)

統計
----------------------------------------------------------
0 recursive calls
0 db block gets
3 consistent gets
0 physical reads
0 redo size
1262 bytes sent via SQL*Net to client
52 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
3 rows processed


おおおおおおおおおお〜っと. 2回目w  マクロが展開されて, dept表の直接アクセスしている. (マクロだからそりゃそうだw)

  1  SELECT
2 *
3 FROM
4* sq_macro_sample(2, dept)

DEPTNO DNAME LOC
---------- ------------------------------------------ ---------------------------------------
10 ACCOUNTING NEW YORK
20 RESEARCH DALLAS

実行計画
----------------------------------------------------------
Plan hash value: 3452784451

-------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 2 | 86 | 3 (0)| 00:00:01 |
|* 1 | VIEW | | 2 | 86 | 3 (0)| 00:00:01 |
|* 2 | WINDOW NOSORT STOPKEY| | 2 | 40 | 3 (0)| 00:00:01 |
| 3 | TABLE ACCESS FULL | DEPT | 2 | 40 | 3 (0)| 00:00:01 |
-------------------------------------------------------------------------------

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

1 - filter("from$_subquery$_009"."rowlimit_$$_rownumber"<=2)
2 - filter(ROW_NUMBER() OVER ( ORDER BY NULL )<=2)

統計
----------------------------------------------------------
0 recursive calls
0 db block gets
3 consistent gets
0 physical reads
0 redo size
827 bytes sent via SQL*Net to client
52 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
2 rows processed


みなさん, SQL_MACRO, 実は, 物凄く, いいいやつ, っぽい感じがします. 慣れるまでは敷居高そうでもありますがw 
マニュアルを真面目に読んで, 倒れそうになりました. (斜め読みでは理解しきれない深さを感じます)


入門としては, HNakaieさんのエントリがわかりやすいですかね.
Oracle DatabaseのSQLマクロを検証する /

参考)
Oracle Database 21c Database PL/SQL Language Reference / 14.64 SQL_MACRO Clause


ということで,  Happy Holidays中の方も, Hard Wroking Now!な方も, I wish you all a Merry Christmas.

そういえば, もう1週間以上前ですがw, アメリカのOracleの中の人からのメッセージが, Happy Holidays! で締めくくられていました.
日本人の感覚だと, え, と思わなくもないわけですが, アメリカとかだとそうだよねーーー. そう言う時期だよねーーと, 思いつつ,
アドベントカレンダー全部俺のネタ作りに追われているw 俺, なんなの? みたいに思ったりw

では, アドベントカレンダー最終日の担当も, 私です.

ではまた.



Related article on Mac De Oracle

・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 1 / TABLE FULL SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 2 / INDEX UNIQUE SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 3 / INDEX RANGE SCAN, Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 4 / INDEX RANGE SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 5 / INDEX RANGE SCAN, INLIST ITERATOR
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 6 / INDEX FAST SCAN, Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 7 / INDEX FULL SCAN,Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 8 / INDEX SKIP SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 9 / TABLE ACCESS INMEMORY FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 10 / NESTED LOOP JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 11 / MERGE JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 12 / HASH JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 13 / HASH JOIN OUTER
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 14 / HASH JOIN FULL OUTER
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 15 / PX, TABLE ACCESS FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 16 / CONCATENATION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 17 / SORT UNIQUE, UNION-ALL = UNION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 18 / UNION-ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 19 / INTERSECTION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 20 / MINUS
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 21 / WINDOW NOSORT STOPKEY
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 22 / COUNT STOPKEY
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 23 / HASH JOIN - LEFT-DEEP JOIN vs RIGHT-DEEP JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 24 / CONNECT BY NO FILTERING WITH START-WITH
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 25 / UNION ALL (RECURSIVE WITH) DEPTH FIRST, RECURSIVE WITH PUMP
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#1 / STAR TRANSFORM, VECTOR TRANSFORM (DWH向け)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#2 / MERGE (UPSERT)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#3 / RDFView
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#4 / INDEX FULL SCAN (MIN/MAX) - Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! No.30 - LOAD TABLE CONVENTIONAL vs. LOAD AS SELECT
・実行計画は, SQL文のレントゲン写真だ! No.31 - TEMP TABLE TRANSFORMATION LOAD AS SELECT (CURSOR DURATION MEMORY)
・実行計画は, SQL文のレントゲン写真だ! No.32 - EXTERNAL TABLE ACCESS FULL / INMEMORY FULL
・実行計画は, SQL文のレントゲン写真だ! No.33 - BITMAP CONVERSION TO ROWIDS
・実行計画は, SQL文のレントゲン写真だ! No.34 - 似て非なるもの USE_CONCAT と OR_EXPAND ヒント と 手書きSQLのレントゲンの見分け方
・実行計画は, SQL文のレントゲン写真だ! No.35 - 似て非なるもの USE_CONCAT と OR_EXPANDヒントとパラレルクエリー
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 1 / No.36 / INTERSECT ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 2 / No.37 / MINUS ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 3 / No.38 / EXCEPT and EXCEPT ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 4 / No.39 / In-Memory Hybrid Scans
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 5 / No.40 / PIVOT and UNPIVOT
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 6 / No.41 / In-Memory Vectorized Join
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 7 / No.42 / INDEX RANGE SCAN (MULTI VALUE)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 8 / No.43 / TABLE ACCESS BY INDEX ROWID BATCHED
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 9 / No.44 / COLLECTION ITERATOR PICKLER FETCH
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 10 / No.45 / MAT_VIEW REWRITE ACCESS FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 11 / No.46 / GROUPING SETS, ROLLUP, CUBE
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 12 / No.47 / TEMP TABLE TRANSFORMATION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 13 / No.48 / MULTI-TABLE INSERT
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 14 / No.49 / the DUAL Table
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 15 / No.50 / REMOTE
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 16 / No.51 / Concurrent Execution of Union All and Union
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 17 / No.52 / Order by Elimination
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 18 / No.53 / Join Elimination
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 19 / No.54 / Group by Elimination
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 20 / No.55 / DISTINCT Elimination
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 21 / No.56 / INLIST ITERATOR と Sub Query と STATISTICS COLLECTOR
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 22 / No.57 / Subquery Unnesting
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 23 / No.58 / ANTI JOIN

| | | コメント (0)

2022年12月23日 (金)

実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 23 / No.58 / ANTI JOIN

Day 22は, Subquery Unnestingを取り上げました. 最近は積極的にUnnestingしているように思うのですが, 皆さんはどう思いますか?

では, Day 23の窓を開けましょう!

今日は, 昨日話題にした, Subquery Unnesting の発動も必要ですが ANTI JOIN の実行計画という名のレントゲンから, いくつか特徴のある物をピックアップ(全バリエーション取れなかった言い訳w).

ANTI JOIN(結合されなかった主問い合わせの対象行を返す)もJOINのナカーマなであるわけで, Nested Loop Join, Hash Join, Merge joinがあり, さらに, NULLを考慮する必要のある結合列がないか, 一つか, それ以上かで, Null-Awareなし, NA(Null-Aware), SNA(Single Null-Aware) でOperation列に表示されるOperation名にいくつかのバリエーションがあります.
とはいえ, ANTI JOINかNULLを意識する必要があるかないか大きな分類で, JOIN方式は通常のJOINと同じ種類があるのはご存知の通り.

NOT EXISTS演算子や, NOT IN条件かつSubquery Unnestingが発動していると, 直感的にイメージできるようになってればいいかもしれないですね. かつ, NULLを意識してるなどもOperation名から見切れるとなお.
たった1文のOperationにそこまで情報が詰め込まれているんですよね.

参考)

Oracle Database 21c SQL Tuning Guide / 9.3.4 Antijoins

いつもと同じように 21c で確認します.

SCOTT@orclpdb1> select banner from v$version;

BANNER
----------------------------------------------------------------------
Oracle Database 21c Enterprise Edition Release 21.0.0.0.0 - Production


ヒントまで使ってなんとか, NESTED LOOPS ANTI が取れました(なかなか言うこと聞いてくれなかったw)
結合列でNULLを意識する必要のない場合に現れます.

HR@orclpdb1> @day23-2
1 SELECT
2 /*+
3 NO_INDEX(departments DEPT_ID_PK)
4 */
5 department_id
6 , department_name
7 FROM
8 departments
9 WHERE
10 department_id NOT IN
11 (
12 SELECT
13 /*+
14 NL_AJ
15 */
16 department_id
17 FROM
18 employees
19 WHERE
20 department_id IS NOT NULL
21* )

DEPARTMENT_ID DEPARTMENT_NAME
------------- ----------------------
120 Treasury
130 Corporate Tax
140 Control And Credit

...略...

240 Government Sales
250 Retail Sales
260 Recruiting
270 Payroll

16行が選択されました.

実行計画
----------------------------------------------------------
Plan hash value: 3082375452

----------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
----------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 17 | 323 | 3 (0)| 00:00:01 |
| 1 | NESTED LOOPS ANTI | | 17 | 323 | 3 (0)| 00:00:01 |
| 2 | TABLE ACCESS FULL| DEPARTMENTS | 27 | 432 | 3 (0)| 00:00:01 |
|* 3 | INDEX RANGE SCAN | EMP_DEPARTMENT_IX | 41 | 123 | 0 (0)| 00:00:01 |
----------------------------------------------------------------------------------------

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

3 - access("DEPARTMENT_ID"="DEPARTMENT_ID")
filter("DEPARTMENT_ID" IS NOT NULL)

一つ前のSQL文とほぼ同じですが, IS NOT NULL条件を取り除いてあります.
NESTED LOOPS ANTI SNAです, SNAとなっているので, NULLを意識する必要のあることがわかります.

  1  SELECT
2 /*+
3 NO_INDEX(departments DEPT_ID_PK)
4 */
5 department_id
6 , department_name
7 FROM
8 departments
9 WHERE
10 department_id NOT IN
11 (
12 SELECT
13 department_id
14 FROM
15 employees
16* )

レコードが選択されませんでした.

実行計画
----------------------------------------------------------
Plan hash value: 2953329389

--------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 19 | 6 (0)| 00:00:01 |
|* 1 | FILTER | | | | | |
| 2 | NESTED LOOPS ANTI SNA| | 17 | 323 | 6 (50)| 00:00:01 |
| 3 | TABLE ACCESS FULL | DEPARTMENTS | 27 | 432 | 3 (0)| 00:00:01 |
|* 4 | INDEX RANGE SCAN | EMP_DEPARTMENT_IX | 41 | 123 | 0 (0)| 00:00:01 |
|* 5 | TABLE ACCESS FULL | EMPLOYEES | 1 | 3 | 3 (0)| 00:00:01 |
--------------------------------------------------------------------------------------------

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

1 - filter( NOT EXISTS (SELECT 0 FROM "EMPLOYEES" "EMPLOYEES" WHERE
"DEPARTMENT_ID" IS NULL))
4 - access("DEPARTMENT_ID"="DEPARTMENT_ID")
5 - filter("DEPARTMENT_ID" IS NULL)


MERGE JOIN ANTIが現れています. NA, SNAが現れていないことから, NULLは考慮しないANTI JOINであることがわかります.

SCOTT@orclpdb1> @day23

1 SELECT
2 deptno
3 ,dname
4 FROM
5 dept
6 WHERE
7 deptno NOT IN
8 (
9 SELECT
10 deptno
11 FROM
12 emp
13 WHERE
14 deptno IS NOT NULL
15* )

DEPTNO DNAME
---------- ------------------------------------------
40 OPERATIONS

実行計画
----------------------------------------------------------
Plan hash value: 1353548327

----------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
----------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 16 | 6 (17)| 00:00:01 |
| 1 | MERGE JOIN ANTI | | 1 | 16 | 6 (17)| 00:00:01 |
| 2 | TABLE ACCESS BY INDEX ROWID| DEPT | 4 | 52 | 2 (0)| 00:00:01 |
| 3 | INDEX FULL SCAN | PK_DEPT | 4 | | 1 (0)| 00:00:01 |
|* 4 | SORT UNIQUE | | 12 | 36 | 4 (25)| 00:00:01 |
|* 5 | TABLE ACCESS FULL | EMP | 12 | 36 | 3 (0)| 00:00:01 |
----------------------------------------------------------------------------------------

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

4 - access("DEPTNO"="DEPTNO")
filter("DEPTNO"="DEPTNO")
5 - filter("DEPTNO" IS NOT NULL)


MERGE JOIN ANTI NAが出ています. NAがあるので, NULLを考慮したMERGE JOIN ANTIであることがわかります. でもこれ本当は, SNAではないのか?(時間があったら10053でも追ってみようかなぁ)

  1  SELECT
2 deptno
3 ,dname
4 FROM
5 dept
6 WHERE
7 deptno NOT IN
8 (
9 SELECT
10 deptno
11 FROM
12 emp
13* )

DEPTNO DNAME
---------- ------------------------------------------
40 OPERATIONS

実行計画
----------------------------------------------------------
Plan hash value: 218628244

-------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 16 | 6 (17)| 00:00:01 |
| 1 | MERGE JOIN ANTI NA | | 1 | 16 | 6 (17)| 00:00:01 |
| 2 | SORT JOIN | | 4 | 52 | 2 (0)| 00:00:01 |
| 3 | TABLE ACCESS BY INDEX ROWID BATCHED| DEPT | 4 | 52 | 2 (0)| 00:00:01 |
| 4 | INDEX FULL SCAN | PK_DEPT | 4 | | 1 (0)| 00:00:01 |
|* 5 | SORT UNIQUE | | 12 | 36 | 4 (25)| 00:00:01 |
| 6 | TABLE ACCESS FULL | EMP | 12 | 36 | 3 (0)| 00:00:01 |
-------------------------------------------------------------------------------------------------

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

5 - access("DEPTNO"="DEPTNO")
filter("DEPTNO"="DEPTNO")


HASH JOIN ANTIが現れています. NULLを考慮させないために, IS NOT NULL条件を追加しています.

 1  SELECT
2 empno
3 ,ename
4 FROM
5 emp
6 WHERE
7 emp.deptno NOT IN
8 (
9 SELECT
10 /*+
11 HASH_AJ
12 */
13 dept.deptno
14 FROM
15 dept
16 WHERE
17 dept.deptno = 50
18 AND dept.deptno IS NOT NULL
19 )
20* AND emp.deptno IS NOT NULL

EMPNO ENAME
---------- ------------------------------
7499 ALLEN
7521 WARD

...略...

7369 SMITH
7566 JONES
7902 FORD

12行が選択されました.

実行計画
----------------------------------------------------------
Plan hash value: 4131168823

------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 11 | 176 | 3 (0)| 00:00:01 |
|* 1 | HASH JOIN ANTI | | 11 | 176 | 3 (0)| 00:00:01 |
|* 2 | TABLE ACCESS FULL| EMP | 12 | 156 | 3 (0)| 00:00:01 |
|* 3 | INDEX UNIQUE SCAN| PK_DEPT | 1 | 3 | 0 (0)| 00:00:01 |
------------------------------------------------------------------------------

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

1 - access("EMP"."DEPTNO"="DEPT"."DEPTNO")
2 - filter("EMP"."DEPTNO" IS NOT NULL)
3 - access("DEPT"."DEPTNO"=50)


HASH JOIN ANTI SNAが現れています. NULLを考慮する必要のあるSNAが付いた, HASH JOIN ANTIですね.

  1  SELECT
2 empno
3 ,ename
4 FROM
5 emp
6 WHERE
7 deptno NOT IN
8 (
9 SELECT
10 deptno
11 FROM
12 dept
13 WHERE
14 deptno = 50
15* )

EMPNO ENAME
---------- ------------------------------
7499 ALLEN
7521 WARD
7654 MARTIN

...略...

7369 SMITH
7566 JONES
7902 FORD

12行が選択されました.

実行計画
----------------------------------------------------------
Plan hash value: 2782438375

------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 11 | 176 | 3 (0)| 00:00:01 |
|* 1 | HASH JOIN ANTI SNA| | 11 | 176 | 3 (0)| 00:00:01 |
| 2 | TABLE ACCESS FULL| EMP | 12 | 156 | 3 (0)| 00:00:01 |
|* 3 | INDEX UNIQUE SCAN| PK_DEPT | 1 | 3 | 0 (0)| 00:00:01 |
------------------------------------------------------------------------------

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

1 - access("DEPTNO"="DEPTNO")
3 - access("DEPTNO"=50)


次は, HASH JOIN ANTI NAが出ているので, NULLを複数列で考慮してもらう必要があるので, 無理感はありますがw. 一応, 取れたw NA付きのHASH JOIN ANTIのOperationです.

SCOTT@orclpdb1> desc foo
名前 NULL? 型
----------------------------------------- -------- ----------------------------
COL1 NOT NULL NUMBER
COL2 NOT NULL NUMBER
COL3 NUMBER

SCOTT@orclpdb1> desc bar
名前 NULL? 型
----------------------------------------- -------- ----------------------------
COL0 NOT NULL NUMBER
COL1 NUMBER
COL2 NUMBER
COL4 NUMBER
 1  SELECT
2 col1
3 FROM
4 foo
5 WHERE
6 col3 NOT IN
7 (
8 SELECT
9 col2
10 FROM
11 bar
12* )

レコードが選択されませんでした.

実行計画
----------------------------------------------------------
Plan hash value: 3985729167

---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 39 | 4 (0)| 00:00:01 |
|* 1 | HASH JOIN ANTI NA | | 1 | 39 | 4 (0)| 00:00:01 |
| 2 | TABLE ACCESS FULL| FOO | 1 | 26 | 2 (0)| 00:00:01 |
| 3 | TABLE ACCESS FULL| BAR | 1 | 13 | 2 (0)| 00:00:01 |
---------------------------------------------------------------------------

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

1 - access("COL3"="COL2")


狙った通りのOperationを行わせるのって難しいですねwwwwwwww (^^;;;;;;; NA, SNAまで考えるとw 

なかなか, これだけのバリエーション, 現場で見ることはないわけですけどもwww 今回のレントゲンシリーズで一番疲れたw
今日は, ここまで,

残り2日だ.

明日も, 頑張って, 窓を開きますよ. ここまできたらw



Related article on Mac De Oracle

・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 1 / TABLE FULL SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 2 / INDEX UNIQUE SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 3 / INDEX RANGE SCAN, Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 4 / INDEX RANGE SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 5 / INDEX RANGE SCAN, INLIST ITERATOR
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 6 / INDEX FAST SCAN, Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 7 / INDEX FULL SCAN,Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 8 / INDEX SKIP SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 9 / TABLE ACCESS INMEMORY FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 10 / NESTED LOOP JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 11 / MERGE JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 12 / HASH JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 13 / HASH JOIN OUTER
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 14 / HASH JOIN FULL OUTER
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 15 / PX, TABLE ACCESS FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 16 / CONCATENATION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 17 / SORT UNIQUE, UNION-ALL = UNION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 18 / UNION-ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 19 / INTERSECTION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 20 / MINUS
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 21 / WINDOW NOSORT STOPKEY
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 22 / COUNT STOPKEY
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 23 / HASH JOIN - LEFT-DEEP JOIN vs RIGHT-DEEP JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 24 / CONNECT BY NO FILTERING WITH START-WITH
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 25 / UNION ALL (RECURSIVE WITH) DEPTH FIRST, RECURSIVE WITH PUMP
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#1 / STAR TRANSFORM, VECTOR TRANSFORM (DWH向け)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#2 / MERGE (UPSERT)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#3 / RDFView
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#4 / INDEX FULL SCAN (MIN/MAX) - Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! No.30 - LOAD TABLE CONVENTIONAL vs. LOAD AS SELECT
・実行計画は, SQL文のレントゲン写真だ! No.31 - TEMP TABLE TRANSFORMATION LOAD AS SELECT (CURSOR DURATION MEMORY)
・実行計画は, SQL文のレントゲン写真だ! No.32 - EXTERNAL TABLE ACCESS FULL / INMEMORY FULL
・実行計画は, SQL文のレントゲン写真だ! No.33 - BITMAP CONVERSION TO ROWIDS
・実行計画は, SQL文のレントゲン写真だ! No.34 - 似て非なるもの USE_CONCAT と OR_EXPAND ヒント と 手書きSQLのレントゲンの見分け方
・実行計画は, SQL文のレントゲン写真だ! No.35 - 似て非なるもの USE_CONCAT と OR_EXPANDヒントとパラレルクエリー
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 1 / No.36 / INTERSECT ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 2 / No.37 / MINUS ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 3 / No.38 / EXCEPT and EXCEPT ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 4 / No.39 / In-Memory Hybrid Scans
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 5 / No.40 / PIVOT and UNPIVOT
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 6 / No.41 / In-Memory Vectorized Join
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 7 / No.42 / INDEX RANGE SCAN (MULTI VALUE)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 8 / No.43 / TABLE ACCESS BY INDEX ROWID BATCHED
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 9 / No.44 / COLLECTION ITERATOR PICKLER FETCH
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 10 / No.45 / MAT_VIEW REWRITE ACCESS FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 11 / No.46 / GROUPING SETS, ROLLUP, CUBE
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 12 / No.47 / TEMP TABLE TRANSFORMATION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 13 / No.48 / MULTI-TABLE INSERT
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 14 / No.49 / the DUAL Table
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 15 / No.50 / REMOTE
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 16 / No.51 / Concurrent Execution of Union All and Union
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 17 / No.52 / Order by Elimination
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 18 / No.53 / Join Elimination
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 19 / No.54 / Group by Elimination
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 20 / No.55 / DISTINCT Elimination
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 21 / No.56 / INLIST ITERATOR と Sub Query と STATISTICS COLLECTOR
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 22 / No.57 / Subquery Unnesting

| | | コメント (0)

2022年12月21日 (水)

実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 21 / No.56 / INLIST ITERATOR と Sub Query と STATISTICS COLLECTOR

Previously on Mac De Oracle...

Day 20は, DISTINCT Eliminationを取り上げました. 古くから実装されているElimination機能ですが, 知ってる方いましたかね?w 古すぎでしょうかw. とはいえ, この恩恵を得ている方も実は多いかもしれませんよ. クソデカクエリー追いきれてないかもしれないですし.

では, Day 21の窓を開けましょう!

久々に, 実行計画は, SQL文のレントゲン写真だ! っぽく, 実行計画に現れるOperationを楽しんで診ていきましょう!

いつもと同じように 21c で確認します.

SCOTT@orclpdb1> select banner from v$version;

BANNER
----------------------------------------------------------------------
Oracle Database 21c Enterprise Edition Release 21.0.0.0.0 - Production

今日のテーマはIN条件とサブクエリー. IN条件といえば, 昔, 悩ませ過ぎは及ばざるがごとし #1という記事を書いてました. (そのころはまだIT業界に居なかった? 方も多そうですが)
IN条件, 索引を使って少量のデータをアクセスするには, 以下の実行計画にあるように,  INLIST ITERATORで繰り返しアクセスするは問題ないわけですが, 大量にあるとかなり性能面で影響が出ます. (悩ませ過ぎは及ばざるがごとし #1などは, ハードパース時間に影響がでたケースです)

SCOTT@orclpdb1> @day21
1 SELECT *
2 FROM
3 emp
4 WHERE
5* empno IN (7369,7499)

EMPNO ENAME JOB MGR HIREDATE SAL COMM DEPTNO
---------- ------------------------------ --------------------------- ---------- -------- ---------- ---------- ----------
7369 SMITH CLERK 7902 80-12-17 800 20
7499 ALLEN SALESMAN 7698 81-02-20 1600 300 30

実行計画
----------------------------------------------------------
Plan hash value: 2355049923

---------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 2 | 78 | 2 (0)| 00:00:01 |
| 1 | IINLIST ITERATOR | | | | | |
| 2 | TABLE ACCESS BY INDEX ROWID| EMP | 2 | 78 | 2 (0)| 00:00:01 |
|* 3 | INDEX UNIQUE SCAN | PK_EMP | 2 | | 1 (0)| 00:00:01 |
---------------------------------------------------------------------------------------

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

3 - access("EMPNO"=7369 OR "EMPNO"=7499)


性能問題も多かった影響なのか?!, Oracle Database 9i R2頃以降, IN条件が, 結合に書き換えられるように(私の感覚でしかないのですが, 最近は, より積極的に書き換えが行われる傾向があるように感じます)なっています.
以下のようにサブクエリーを利用しているケースが典型例ですね. IN条件だけでなく, EXISTS演算子や, スカラー副問合せなどもこの書き換えの対象です. この書き換えは, Subquery Unnestingと呼ばれています. ご存知の方も多いですよね. 今日の主役はそれでなく, INLIST ITERATOR の方ですが, これ両方話さないとOperationの向き不向きが見えないのでw

  1  SELECT
2 *
3 FROM
4 emp
5 WHERE
6 deptno IN (
7 SELECT
8 deptno
9 FROM
10 dept
11 WHERE
12 dname IN ('SALES','ACCOUNT')
13* )

EMPNO ENAME JOB MGR HIREDATE SAL COMM DEPTNO
---------- ------------------------------ --------------------------- ---------- -------- ---------- ---------- ----------
7499 ALLEN SALESMAN 7698 81-02-20 1600 300 30
7521 WARD SALESMAN 7698 81-02-22 1250 500 30
7654 MARTIN SALESMAN 7698 81-09-28 1250 1400 30
7698 BLAKE MANAGER 7839 81-05-01 2850 30
7844 TURNER SALESMAN 7698 81-09-08 1500 0 30
7900 JAMES CLERK 7698 81-12-03 950 30

6行が選択されました.

実行計画
----------------------------------------------------------
Plan hash value: 4207756064

---------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 8 | 416 | 5 (0)| 00:00:01 |
| 1 | NESTED LOOPS | | 8 | 416 | 5 (0)| 00:00:01 |
| 2 | NESTED LOOPS | | 8 | 416 | 5 (0)| 00:00:01 |
|* 3 | TABLE ACCESS FULL | DEPT | 2 | 26 | 3 (0)| 00:00:01 |
|* 4 | INDEX RANGE SCAN | IX_EMP | 4 | | 0 (0)| 00:00:01 |
| 5 | TABLE ACCESS BY INDEX ROWID| EMP | 4 | 156 | 1 (0)| 00:00:01 |
---------------------------------------------------------------------------------------

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

3 - filter("DNAME"='ACCOUNT' OR "DNAME"='SALES')
4 - access("DEPTNO"="DEPTNO")

Note
-----
- this is an adaptive plan

前述した実行計画の Noteに, - this is an adaptive plan と気になる情報が出ています. これ, 実行計画上は, Subquery Unnestingされて, Nested Loop Join (NLJ) になっていますが, 駆動表のヒット件数に応じて, それ以外の結合メソッドに
へ切り替わる可能性があることを示しています. これは, adaptive planと呼ばれている機能です.
実際, NLJなのかそれ以外なのかをみる方法は, SQL Monitor, Actual Planを利用する方法と, 以下のように, Adaptive Planを表示させ, どちらで動作したのかを確認する方法があります.

ここで登場するのが, Adaptive Planの鍵になる, STATISTICS COLLECTOR というOperationです. ここで駆動表の件数をみつつ, これは! HJ向きのと判断すれば, NLJ から HJ へ切り替えることになります.
今回は, NLJ のままですね. ()

  1* SELECT * FROM TABLE(DBMS_XPLAN.display_cursor(format => 'adaptive'))

PLAN_TABLE_OUTPUT
------------------------------------------------------------------------------
SQL_ID cuxwr51s6gs61, child number 0
-------------------------------------
SELECT * FROM emp WHERE deptno IN ( SELECT deptno
FROM dept WHERE dname IN ('SALES','ACCOUNT') )

Plan hash value: 4207756064

------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | | | 5 (100)| |
|- * 1 | HASH JOIN | | 8 | 416 | 5 (0)| 00:00:01 |
| 2 | NESTED LOOPS | | 8 | 416 | 5 (0)| 00:00:01 |
| 3 | NESTED LOOPS | | 8 | 416 | 5 (0)| 00:00:01 |
|- 4 | STATISTICS COLLECTOR | | | | | |
| * 5 | TABLE ACCESS FULL | DEPT | 2 | 26 | 3 (0)| 00:00:01 |
| * 6 | INDEX RANGE SCAN | IX_EMP | 4 | | 0 (0)| |
| 7 | TABLE ACCESS BY INDEX ROWID| EMP | 4 | 156 | 1 (0)| 00:00:01 |
|- 8 | TABLE ACCESS FULL | EMP | 4 | 156 | 1 (0)| 00:00:01 |
------------------------------------------------------------------------------------------

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

1 - access("DEPTNO"="DEPTNO")
5 - filter(("DNAME"='ACCOUNT' OR "DNAME"='SALES'))
6 - access("DEPTNO"="DEPTNO")

Note
-----
- this is an adaptive plan (rows marked '-' are inactive)

残り, 4日, 最終日はクリスマスで, 日曜日じゃないか!今年は.
大きめのネタをぶん投げて, おまけブログでまとめる感じにするか. 悩みどころだ. それとも軽めのネタで最後まで通すか.

明日も担当は, 私ですよ. (全部俺アドベントカレンダー, 来年はどうしようw なんの苦行だという感じw でも, それ楽しんでる俺は...w)

では, また :)



Related article on Mac De Oracle

・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 1 / TABLE FULL SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 2 / INDEX UNIQUE SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 3 / INDEX RANGE SCAN, Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 4 / INDEX RANGE SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 5 / INDEX RANGE SCAN, INLIST ITERATOR
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 6 / INDEX FAST SCAN, Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 7 / INDEX FULL SCAN,Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 8 / INDEX SKIP SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 9 / TABLE ACCESS INMEMORY FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 10 / NESTED LOOP JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 11 / MERGE JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 12 / HASH JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 13 / HASH JOIN OUTER
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 14 / HASH JOIN FULL OUTER
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 15 / PX, TABLE ACCESS FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 16 / CONCATENATION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 17 / SORT UNIQUE, UNION-ALL = UNION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 18 / UNION-ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 19 / INTERSECTION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 20 / MINUS
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 21 / WINDOW NOSORT STOPKEY
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 22 / COUNT STOPKEY
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 23 / HASH JOIN - LEFT-DEEP JOIN vs RIGHT-DEEP JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 24 / CONNECT BY NO FILTERING WITH START-WITH
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 25 / UNION ALL (RECURSIVE WITH) DEPTH FIRST, RECURSIVE WITH PUMP
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#1 / STAR TRANSFORM, VECTOR TRANSFORM (DWH向け)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#2 / MERGE (UPSERT)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#3 / RDFView
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#4 / INDEX FULL SCAN (MIN/MAX) - Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! No.30 - LOAD TABLE CONVENTIONAL vs. LOAD AS SELECT
・実行計画は, SQL文のレントゲン写真だ! No.31 - TEMP TABLE TRANSFORMATION LOAD AS SELECT (CURSOR DURATION MEMORY)
・実行計画は, SQL文のレントゲン写真だ! No.32 - EXTERNAL TABLE ACCESS FULL / INMEMORY FULL
・実行計画は, SQL文のレントゲン写真だ! No.33 - BITMAP CONVERSION TO ROWIDS
・実行計画は, SQL文のレントゲン写真だ! No.34 - 似て非なるもの USE_CONCAT と OR_EXPAND ヒント と 手書きSQLのレントゲンの見分け方
・実行計画は, SQL文のレントゲン写真だ! No.35 - 似て非なるもの USE_CONCAT と OR_EXPANDヒントとパラレルクエリー
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 1 / No.36 / INTERSECT ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 2 / No.37 / MINUS ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 3 / No.38 / EXCEPT and EXCEPT ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 4 / No.39 / In-Memory Hybrid Scans
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 5 / No.40 / PIVOT and UNPIVOT
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 6 / No.41 / In-Memory Vectorized Join
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 7 / No.42 / INDEX RANGE SCAN (MULTI VALUE)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 8 / No.43 / TABLE ACCESS BY INDEX ROWID BATCHED
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 9 / No.44 / COLLECTION ITERATOR PICKLER FETCH
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 10 / No.45 / MAT_VIEW REWRITE ACCESS FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 11 / No.46 / GROUPING SETS, ROLLUP, CUBE
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 12 / No.47 / TEMP TABLE TRANSFORMATION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 13 / No.48 / MULTI-TABLE INSERT
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 14 / No.49 / the DUAL Table
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 15 / No.50 / REMOTE
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 16 / No.51 / Concurrent Execution of Union All and Union
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 17 / No.52 / Order by Elimination
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 18 / No.53 / Join Elimination
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 19 / No.54 / Group by Elimination
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 20 / No.55 / DISTINCT Elimination

| | | コメント (0)

2022年12月20日 (火)

実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 20 / No.55 / DISTINCT Elimination

Previously on Mac De Oracle... 
Day 19は, かなり地味にデビューしていた機能ですが、変なところで目立った Group by Elimination にフォーカスを当てました。この手のEliminationは、実行計画という名のレントゲンからは見えなくなってしまう。何か悪さをしているときの調査は難易度高めです。基本的に操作が行われなくなるわけですが、例としてお見せしたような単純なSQLなら別ですが、クソデカクエリーだと、キッツイですよねw 結果不正って。

と言うことで、Day 20の窓を開けましょう!

 

今日は、 DISTINCT Eliminationです。またか! と。そういうEliminationもある。ということ知っておいた方が良いと思うので、eliminationネタの最後として、ちょっと飽きてきた感じはありますが取り上げてみました。この機能の提供も古く、機能が提供されていなかった時代を知る人の方が少ないかもしれませんw

ちなみに、今回も該当機能をを局所的に無効化する例を書いていますが、ヒントが提供されていない最適化もあるので、そう言う場合は、隠しパラメータからそれらしいのを探してして、検証して効果の有無を確かめると良いと思います。MOSに書かれてないケースも多いのでBlogなどから情報を集めたりして、最終的には動作確認。もし不具合などと関連しているようであればサポートへ問い合わせても対応してくれるでしょう。コミュニティーにこれどうよ?と投げてみるのもありだと思います。Jonathanもネタもとは、コミュニティーでのやりとりだったりすることも多く、調べてみたら、そうだった!という記事も多く、本当に助かった!って経験は何度もあります!!!。

parameterはこんなあたりから見つけると楽ですよん。
Difference of Initialization Parameters between 19c (19.3.0.0.0) and 21c (21.3.0.0.0) - including hidden params
Difference of Initialization Parameters between 19c (19.3.0.0.0) and 21c (21.3.0.0.0)

 

いつもと同じように 21c で確認します.


SCOTT@orclpdb1> select banner from v$version;

BANNER
----------------------------------------------------------------------
Oracle Database 21c Enterprise Edition Release 21.0.0.0.0 - Production

 

前回と同じデータを使います。(こんな使いまわせるテストデータを作るスクリプト用意しておくと何かと便利ですよ!)


SCOTT@orclpdb1> @day20
1* DROP TABLE business_day_calendar

表が削除されました。

1 CREATE TABLE business_day_calendar AS
2 WITH
3 FUNCTION get_num_of_dates
4 RETURN NUMBER IS
5 l_dummy_date DATE;
6 --
7 eORA01839 exception;
8 pragma exception_init(eORA01839, -1839);
9 BEGIN
10 -- validate leap year
11 l_dummy_date := TO_DATE(TO_CHAR(SYSDATE, 'YYYY') || '0229', 'YYYYMMDD');
12 RETURN 366;
13 EXCEPTION
14 WHEN eORA01839 THEN
15 RETURN 365;
16 END;
17 SELECT
18 TO_DATE(TRUNC(SYSDATE,'YYYY') + level - 1) AS business_date
19 , CASE
20 WHEN TO_CHAR(
21 TO_DATE(TRUNC(SYSDATE,'YYYY') + level - 1)
22 , 'DY'
23 , 'NLS_DATE_LANGUAGE=AMERICAN'
24 ) IN ('SUN','WED')
25 THEN '1'
26 ELSE '0'
27 END AS is_holiday
28 FROM
29 dual
30 CONNECT BY
31* level <= get_num_of_dates

表が作成されました。

1 ALTER TABLE business_day_calendar
2 ADD CONSTRAINT pk_business_day_calendar PRIMARY KEY
3 (
4 business_date
5 )
6* USING INDEX

表が変更されました。

 

はい、主キー列にDISTINCTを使ってますが、無駄ですよね!(いきなり本題w)

DISTINCT操作は見事に実行計画から排除されています!(簡単ですね。とは言っても実行計画を見ただけでは、DISTINCT Eliminationが行われていることには気付けないわけですけども)


  1  SELECT
2 DISTINCT business_date
3 FROM
4* business_day_calendar

BUSINESS
--------
22-01-01
22-01-02
22-01-03

...略...

22-12-29
22-12-30
22-12-31

365行が選択されました。

実行計画
----------------------------------------------------------
Plan hash value: 1786497156

---------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 365 | 2920 | 1 (0)| 00:00:01 |
| 1 | INDEX FULL SCAN | PK_BUSINESS_DAY_CALENDAR | 365 | 2920 | 1 (0)| 00:00:01 |
---------------------------------------------------------------------------------------------

統計
----------------------------------------------------------
0 recursive calls
0 db block gets
26 consistent gets
0 physical reads
0 redo size
8855 bytes sent via SQL*Net to client
316 bytes received via SQL*Net from client
26 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
365 rows processed

 

では、元栓というか、Optimizer自体を、とーーーく昔の状態にしてみましょう(DISTINCT Eliminationがバックポートもされてない時代の10g 10.2.0.3。こういうことができるOracle面白いですよね)
このようなことを本番でやるのはかなりレアで、よっぽど新しい機能を使いたくないか、めちゃめちゃキツイ大人の事情があるんだと思います(知らんけど)

Optimizerを10g R2ぐらいに戻したことで、DISTINCT操作として、SORT UNIQUE NOSORTが合わられましたが、INDEX FULL SCANでユニークキーをアクセスしているのでNOSORTとなり、SORT UNIQUE操作はスキップされていることがわかります。ソート順に索引を全捜査する INDEX FULL SCANだからこそできる動きですね。


  1  SELECT
2 /*+
3 OPTIMIZER_FEATURES_ENABLE('10.2.0.3')
4 */
5 DISTINCT business_date
6 FROM
7* business_day_calendar

BUSINESS
--------
22-01-01
22-01-02
22-01-03

...略...

22-12-29
22-12-30
22-12-31

365行が選択されました。

実行計画
----------------------------------------------------------
Plan hash value: 311283176

-----------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 365 | 2920 | 2 (50)| 00:00:01 |
| 1 | SORT UNIQUE NOSORT| | 365 | 2920 | 2 (50)| 00:00:01 |
| 2 | INDEX FULL SCAN | PK_BUSINESS_DAY_CALENDAR | 365 | 2920 | 1 (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------

統計
----------------------------------------------------------
0 recursive calls
0 db block gets
26 consistent gets
0 physical reads
0 redo size
8855 bytes sent via SQL*Net to client
316 bytes received via SQL*Net from client
26 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
365 rows processed

 

Optimizerの能力を10gに戻してしまうのも勿体ないので、よく使う局所的な方法。特定のSQLだけが影響するのであれば、この方法がおすすめです。
元々の機能に影響を受けていない。逆に恩恵を受けているものあるかもしれません。
前回の結果不正などもそうですが、どの方法で治療するというかチューニングかは、対応するエンジニア考え方や、該当する患者さん(システムや、お客様の大人の事情w)にもよりますが、私は、基本的に局所的な対処で済むのなら、そちらを選ぶ方針です。
なるべく狭い範囲、SQL、セッションあたりで無効、有効にして、経過観察、副作用有無、対処した範囲外で、同一理由による問題が発生していないか。もし狭い範囲の対処では無理なら徐々に広げる。最終系がインスタンス全体で。みたいな流れにすることで、無駄に全て止めてしまうということを避けたい(恩恵を受けているのもあるはずということを前提にしています)。この辺りは考え方次第なので、絶対、こうするのが良いとか悪いという話では無いですが。長い目で考えるとそれが良いのでは無いかと個人的には思います。
この機能、直接利用できるヒントはないので、隠しパラメータでon/offできます。冒頭で紹介したパラメータ一覧には隠しパラメータもリストしているので、こういう時はクエリ投げずに該当ページを検索すると楽ですよん:)


  1  SELECT
2 /*+
3 OPT_PARAM('_optimizer_distinct_elimination','false')
4 */
5 DISTINCT business_date
6 FROM
7* business_day_calendar

BUSINESS
--------
22-01-01
22-01-02
22-01-03

...略...

22-12-29
22-12-30
22-12-31

365行が選択されました。

実行計画
----------------------------------------------------------
Plan hash value: 311283176

-----------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 365 | 2920 | 2 (50)| 00:00:01 |
| 1 | SORT UNIQUE NOSORT| | 365 | 2920 | 2 (50)| 00:00:01 |
| 2 | INDEX FULL SCAN | PK_BUSINESS_DAY_CALENDAR | 365 | 2920 | 1 (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------

統計
----------------------------------------------------------
0 recursive calls
0 db block gets
26 consistent gets
0 physical reads
0 redo size
8855 bytes sent via SQL*Net to client
316 bytes received via SQL*Net from client
26 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
365 rows processed

 

別テストケースで確認してみましょう。


  1* DROP TABLE case2

表が削除されました。

1 CREATE TABLE case2
2 (
3 id NUMBER PRIMARY KEY
4 , col2 NUMBER NOT NULL
5 , col3 NUMBER
6 , col4 VARCHAR2(10) NOT NULL
7 , CONSTRAINT uix_case2 UNIQUE (col2,col3) USING INDEX
8* )

表が作成されました。

1 BEGIN
2 FOR i IN 1..2000 LOOP
3 INSERT INTO case2 VALUES(i, i, NULL, LPAD(TO_CHAR(i),10,'*'));
4 IF MOD(i,100) = 0 THEN COMMIT; END IF;
5 END LOOP;
6* END;

PL/SQLプロシージャが正常に完了しました。

 

このテストケースでは、複合一意キーかつ、第二キーに null を許しています。
Oraleの索引は、null が含まれない!という都市伝説がありましたが、そんなことはないのは以前解説していた通りです。一部の列にnullを許可した一意キーでもDISTINCT Eliminationは発動することを確認するテストケースです)

この例では、第二キー列がnullableで、この状態では、第二キー列全てを null にしています。DISTINCT Eliminationの条件を満たすため、DISTINCT 捜査が排除されていることがわかります!


  1  SELECT
2 DISTINCT col2, col3
3 FROM
4* case2

COL2 COL3
---------- ----------
541 [null]
542 [null]
543 [null]

...略...

1998 [null]
1999 [null]
2000 [null]

2000行が選択されました。

実行計画
----------------------------------------------------------
Plan hash value: 1081843087

----------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
----------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 2000 | 52000 | 5 (0)| 00:00:01 |
| 1 | INDEX FAST FULL SCAN| UIX_CASE2 | 2000 | 52000 | 5 (0)| 00:00:01 |
----------------------------------------------------------------------------------

 

OPT_PARAM('_optimizer_distinct_elimination','false')でDISTINCT Eliminationを無効化すれば、DISTINCT 操作は現れますが、INDEX FAST FULL SCANとなっているため HASH UNIQUE操作が行われていることがわかります。
実際には、ユニーク索引なので、不要ではあるのですが:)


  1  SELECT
2 /*+
3 OPT_PARAM('_optimizer_distinct_elimination','false')
4 */
5 DISTINCT col2, col3
6 FROM
7* case2

COL2 COL3
---------- ----------
555 [null]
585 [null]
586 [null]

...略...

1986 [null]
1989 [null]
1990 [null]

2000行が選択されました。

実行計画
----------------------------------------------------------
Plan hash value: 523836292

-----------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 2000 | 52000 | 6 (17)| 00:00:01 |
| 1 | HASH UNIQUE | | 2000 | 52000 | 6 (17)| 00:00:01 |
| 2 | INDEX FAST FULL SCAN| UIX_CASE2 | 2000 | 52000 | 5 (0)| 00:00:01 |
-----------------------------------------------------------------------------------

 

では、ちょっと初歩的な確認で、1, nullというすでに存在する値をINSERTしてみましょう。当然エラーです。UIX_CASE2は一意キーなので。


  1* INSERT INTO case2 VALUES(2001,1,NULL,'test')
INSERT INTO case2 VALUES(2001,1,NULL,'test')
*
行1でエラーが発生しました。:
ORA-00001: 一意制約(SCOTT.UIX_CASE2)に反しています

 

第二キーがnullではない値を登録しておきます。


  1* INSERT INTO case2 VALUES(2002,1,1,'test')

1行が作成されました。

1* COMMIT

コミットが完了しました。

 

特にどうだということもないのですがw 正しく、DISTINCT EliminationされINDEX FAST FULL SCANだけの実行計画という名のレントゲンが現れています。ニッコリ(どちらかというと、索引に null 入らないという本当の意味を知らずにいる方もいるのではないだろうかという余計な心配をしただけのお節介なテストケース。というだけだったかもしれません)


  1  SELECT
2 DISTINCT col2, col3
3 FROM
4* case2

COL2 COL3
---------- ----------
541 [null]
542 [null]
543 [null]

...略...

1 1
1 [null]
2 [null]
3 [null]

...略...

538 [null]
539 [null]
540 [null]

2001行が選択されました。

実行計画
----------------------------------------------------------
Plan hash value: 1081843087

----------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
----------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 2000 | 52000 | 5 (0)| 00:00:01 |
| 1 | INDEX FAST FULL SCAN| UIX_CASE2 | 2000 | 52000 | 5 (0)| 00:00:01 |
----------------------------------------------------------------------------------

 

ということで、 Day 20はここまで。

 

残り5日。追い込みだーーーーっ
明日も、私が担当なので、よろしくお願いします。


Related article on Mac De Oracle
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 1 / TABLE FULL SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 2 / INDEX UNIQUE SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 3 / INDEX RANGE SCAN, Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 4 / INDEX RANGE SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 5 / INDEX RANGE SCAN, INLIST ITERATOR
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 6 / INDEX FAST SCAN, Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 7 / INDEX FULL SCAN,Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 8 / INDEX SKIP SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 9 / TABLE ACCESS INMEMORY FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 10 / NESTED LOOP JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 11 / MERGE JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 12 / HASH JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 13 / HASH JOIN OUTER
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 14 / HASH JOIN FULL OUTER
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 15 / PX, TABLE ACCESS FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 16 / CONCATENATION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 17 / SORT UNIQUE, UNION-ALL = UNION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 18 / UNION-ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 19 / INTERSECTION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 20 / MINUS
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 21 / WINDOW NOSORT STOPKEY
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 22 / COUNT STOPKEY
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 23 / HASH JOIN - LEFT-DEEP JOIN vs RIGHT-DEEP JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 24 / CONNECT BY NO FILTERING WITH START-WITH
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 25 / UNION ALL (RECURSIVE WITH) DEPTH FIRST, RECURSIVE WITH PUMP
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#1 / STAR TRANSFORM, VECTOR TRANSFORM (DWH向け)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#2 / MERGE (UPSERT)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#3 / RDFView
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#4 / INDEX FULL SCAN (MIN/MAX) - Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! No.30 - LOAD TABLE CONVENTIONAL vs. LOAD AS SELECT
・実行計画は, SQL文のレントゲン写真だ! No.31 - TEMP TABLE TRANSFORMATION LOAD AS SELECT (CURSOR DURATION MEMORY)
・実行計画は, SQL文のレントゲン写真だ! No.32 - EXTERNAL TABLE ACCESS FULL / INMEMORY FULL
・実行計画は, SQL文のレントゲン写真だ! No.33 - BITMAP CONVERSION TO ROWIDS
・実行計画は, SQL文のレントゲン写真だ! No.34 - 似て非なるもの USE_CONCAT と OR_EXPAND ヒント と 手書きSQLのレントゲンの見分け方
・実行計画は, SQL文のレントゲン写真だ! No.35 - 似て非なるもの USE_CONCAT と OR_EXPANDヒントとパラレルクエリー
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 1 / No.36 / INTERSECT ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 2 / No.37 / MINUS ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 3 / No.38 / EXCEPT and EXCEPT ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 4 / No.39 / In-Memory Hybrid Scans
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 5 / No.40 / PIVOT and UNPIVOT
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 6 / No.41 / In-Memory Vectorized Join
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 7 / No.42 / INDEX RANGE SCAN (MULTI VALUE)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 8 / No.43 / TABLE ACCESS BY INDEX ROWID BATCHED
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 9 / No.44 / COLLECTION ITERATOR PICKLER FETCH
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 10 / No.45 / MAT_VIEW REWRITE ACCESS FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 11 / No.46 / GROUPING SETS, ROLLUP, CUBE
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 12 / No.47 / TEMP TABLE TRANSFORMATION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 13 / No.48 / MULTI-TABLE INSERT
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 14 / No.49 / the DUAL Table
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 15 / No.50 / REMOTE
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 16 / No.51 / Concurrent Execution of Union All and Union
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 17 / No.52 / Order by Elimination
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 18 / No.53 / Join Elimination
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 19 / No.54 / Group by Elimination

| | | コメント (0)

2022年12月19日 (月)

実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 19 / No.54 / Group by Elimination

このポストは, JPOUG Advent Calendar 2022 Day 19帰ってきた! 実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺) Advent Calendar 2022へクロスポストしています.

 

JPOUG Advent Calendar 2022 Day 18は, tomoさんのSPM使ってる?でした. 使ってますか?ヒント埋込派, パッケージなんでSPMっす!とか大人の事情で色々ありそうですね.

Previously on Mac De Oracle... Day 18は, Join Elimination にフォーカスをあてました. Join Eliminationも他のElimination系書き換えは, 実行計画上存在が消されてしまうので, 実際行われたかどうか気づきにくいケースが多いです. 以前も書きましたが, Explain plan等で, Elimination Informationとかリストしてくれると便利かもしれません.

ということで, 今日, Day 19は, そんなElimination InformationがExplain plan等で表示されたら嬉しいかもね, という気持ちが強くなるネタにしました.

 

では, Day 19のお題, Group by Eliminationです.
この機能, 目立たないですが, 変なところで目立ったので意外に有名かもしれません. 機能として登場したのは, 12cR2ごろでした. Jonathanも書いてるから間違いない!

変なところで目立ってしまった. そんなに目立たない存在だった, Group by Eliminationの確認
目立ってしまった理由は, 結果不正.  できれば7445の方が分かりやすくて良かったわけですが, 出会いたくないですよねー. 結果不正.
(なお, すでに修正されており, 想定する結果が返されるようになっているようです. 詳細は本ページ末の参考リンクのJonathanのエントリー参照のこと)

まず, 検証用のデータ準備から(ちょいと凝ったことやってますが, 単純に当年のカレンダーで, その日が営業日か休業日かをフラグで持たせるだけのでデータで, 2022年の一年分のデータを作りました. day18.sqlを実行していますが, こちらの都合w公開日を入れ替えた影響なので, 気にしないでくださいw

SCOTT@orclpdb1> @day18
1* DROP TABLE business_day_calendar

表が削除されました.

1 CREATE TABLE business_day_calendar AS
2 WITH
3 FUNCTION get_num_of_dates
4 RETURN NUMBER IS
5 l_dummy_date DATE;
6 --
7 eORA01839 exception;
8 pragma exception_init(eORA01839, -1839);
9 BEGIN
10 -- validate leap year
11 l_dummy_date := TO_DATE(TO_CHAR(SYSDATE, 'YYYY') || '0229', 'YYYYMMDD');
12 RETURN 366;
13 EXCEPTION
14 WHEN eORA01839 THEN
15 RETURN 365;
16 END;
17 SELECT
18 TO_DATE(TRUNC(SYSDATE,'YYYY') + level - 1) AS business_date
19 , CASE
20 WHEN TO_CHAR(
21 TO_DATE(TRUNC(SYSDATE,'YYYY') + level - 1)
22 , 'DY'
23 , 'NLS_DATE_LANGUAGE=AMERICAN'
24 ) IN ('SUN','WED')
25 THEN '1'
26 ELSE '0'
27 END AS is_holiday
28 FROM
29 dual
30 CONNECT BY
31* level <= get_num_of_dates

表が作成されました.

1 ALTER TABLE business_day_calendar
2 ADD CONSTRAINT pk_business_day_calendar PRIMARY KEY
3 (
4 business_date
5 )
6* USING INDEX

表が変更されました.

 

今回は, この Order BY Elimination をちょっと有名にしてしまった, 結果不正から. 12cR2で試してみます. CASE 2の結果が想定と異なっています.

orcl@SCOTT> select banner from v$version;

BANNER
--------------------------------------------------------------------------------
Oracle Database 12c Enterprise Edition Release 12.2.0.1.0 - 64bit Production

CASE 1特に問題は起きていません. 想定通りの結果が帰り, Group By Eliminationの発動条件にはなっていないため, Group By操作が行われています.

  1  SELECT
2 EXTRACT(MONTH from business_date)
3 , COUNT(
4 CASE
5 WHEN TO_CHAR(
6 business_date
7 , 'DY'
8 , 'NLS_DATE_LANGUAGE=AMERICAN'
9 ) IN ('SUN','WED')
10 THEN 1
11 END
12 ) AS holidays
13 FROM
14 business_day_calendar
15 GROUP BY
16* EXTRACT(MONTH from business_date)

EXTRACT(MONTHFROMBUSINESS_DATE) HOLIDAYS
------------------------------- ----------
1 9
6 9
11 9
2 8
4 8
5 9
8 9
3 9
7 9
9 8
10 9
12 8

12 rows selected.

Execution Plan
----------------------------------------------------------
Plan hash value: 100882575

---------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 365 | 2920 | 2 (50)| 00:00:01 |
| 1 | HASH GROUP BY | | 365 | 2920 | 2 (50)| 00:00:01 |
| 2 | INDEX FULL SCAN| PK_BUSINESS_DAY_CALENDAR | 365 | 2920 | 1 (0)| 00:00:01 |
---------------------------------------------------------------------------------------------

 

CASE 2 おやおやおや, おかしなことになってます. 実行計画をみると, 動作対象ではないはずなのに, GROUP BY 操作が排除されているのが分かります! 結果不正です. HASH GROUP BYが排除されています! その影響で, GROUP BYが行われていません!!!!

  1  SELECT
2 TO_CHAR(business_date, 'YYYYMM')
3 , COUNT(1)
4 FROM
5 business_day_calendar
6 GROUP BY
7* TO_CHAR(business_date, 'YYYYMM')

TO_CHA COUNT(1)
------ ----------
202201 1
202201 1
202201 1

...略...

202212 1
202212 1
202212 1

365 rows selected.

Execution Plan
----------------------------------------------------------
Plan hash value: 1786497156

---------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 365 | 2920 | 1 (0)| 00:00:01 |
| 1 | INDEX FULL SCAN | PK_BUSINESS_DAY_CALENDAR | 365 | 2920 | 1 (0)| 00:00:01 |
---------------------------------------------------------------------------------------------

 

CASE 2 の結果不正回避 では, 結果不正回避策を取ってみましょう. 何度か利用している, no_elim_groupbyヒントで回避できるか試します!
結果不正の理由は, 本来発動しない条件であるにもかかわらず発動してしまった Group By Eliminationにる影響であることが分かります.

これをみると, なおさら, Explain Plan等でどのような書き換えが動作したのかしないのか一覧できるような情報が欲しいなと思ったりするわけです. .

orcl@SCOTT> @day18-2
1 SELECT
2 /*+
3 no_elim_groupby
4 */
5 TO_CHAR(business_date, 'YYYYMM')
6 , COUNT(1)
7 FROM
8 business_day_calendar
9 GROUP BY
10* TO_CHAR(business_date, 'YYYYMM')

TO_CHA COUNT(1)
------ ----------
202205 31
202209 30
202211 30
202212 31
202202 28
202208 31
202207 31
202210 31
202201 31
202204 30
202203 31
202206 30

12 rows selected.

Execution Plan
----------------------------------------------------------
Plan hash value: 100882575

---------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 365 | 2920 | 2 (50)| 00:00:01 |
| 1 | HASH GROUP BY | | 365 | 2920 | 2 (50)| 00:00:01 |
| 2 | INDEX FULL SCAN| PK_BUSINESS_DAY_CALENDAR | 365 | 2920 | 1 (0)| 00:00:01 |
---------------------------------------------------------------------------------------------

 

CASE 3 こちらは正しく GROUP BY Eliminationが発動していますね. GROUP BYしなくても問題ないクエリですから!

  1  SELECT
2 business_date
3 , COUNT(1)
4 FROM
5 business_day_calendar
6 GROUP BY
7* business_date

BUSINESS_ COUNT(1)
--------- ----------
01-JAN-22 1
02-JAN-22 1
03-JAN-22 1

...略...

29-DEC-22 1
30-DEC-22 1
31-DEC-22 1

365 rows selected.

Execution Plan
----------------------------------------------------------
Plan hash value: 1786497156

---------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 365 | 2920 | 1 (0)| 00:00:01 |
| 1 | INDEX FULL SCAN | PK_BUSINESS_DAY_CALENDAR | 365 | 2920 | 1 (0)| 00:00:01 |
---------------------------------------------------------------------------------------------

 

CASE 4 GROUP BY Eliminationを無効化しても他のバージョンと結果同じで, 想定した結果通りです. CASE 3と結果は同じなのは当たり前.

  1  SELECT
2 /*+
3 no_elim_groupby
4 */
5 business_date
6 , COUNT(1)
7 FROM
8 business_day_calendar
9 GROUP BY
10* business_date

BUSINESS_ COUNT(1)
--------- ----------
01-JAN-22 1
02-JAN-22 1
03-JAN-22 1

...略...

29-DEC-22 1
30-DEC-22 1
31-DEC-22 1

365 rows selected.

Execution Plan
----------------------------------------------------------
Plan hash value: 3672056694

-------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 365 | 2920 | 1 (0)| 00:00:01 |
| 1 | SORT GROUP BY NOSORT| | 365 | 2920 | 1 (0)| 00:00:01 |
| 2 | INDEX FULL SCAN | PK_BUSINESS_DAY_CALENDAR | 365 | 2920 | 1 (0)| 00:00:01 |
-------------------------------------------------------------------------------------------------

 

では, 私が確認したテストケースの範囲で結果不正が起きていない, 21cは全テストケース, それ以外のバージョンでは, 結果不正の発生していた CASE 2 の実行計画という名のレントゲンを並べておきたいと思います. 頭に浮かんだイメージは, レントゲン写真を光るボードに沢山貼り付けてる感じw

Oracle Database 21cのCASE 1 - CASE 4まで全て

SCOTT@orclpdb1> select banner from v$version;

BANNER
----------------------------------------------------------------------
Oracle Database 21c Enterprise Edition Release 21.0.0.0.0 - Production

 

CASE 1:Group By Eliminationは発生しないケース

  1  SELECT
2 EXTRACT(MONTH from business_date)
3 , COUNT(
4 CASE
5 WHEN TO_CHAR(
6 business_date
7 , 'DY'
8 , 'NLS_DATE_LANGUAGE=AMERICAN'
9 ) IN ('SUN','WED')
10 THEN 1
11 END
12 ) AS holidays
13 FROM
14 business_day_calendar
15 GROUP BY
16* EXTRACT(MONTH from business_date)

EXTRACT(MONTHFROMBUSINESS_DATE) HOLIDAYS
------------------------------- ----------
1 9
2 8
3 9
4 8
5 9
6 9
7 9
8 9
9 8
10 9
11 9
12 8

12行が選択されました.

実行計画
----------------------------------------------------------
Plan hash value: 100882575

---------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 365 | 2920 | 2 (50)| 00:00:01 |
| 1 | HASH GROUP BY | | 365 | 2920 | 2 (50)| 00:00:01 |
| 2 | INDEX FULL SCAN| PK_BUSINESS_DAY_CALENDAR | 365 | 2920 | 1 (0)| 00:00:01 |
---------------------------------------------------------------------------------------------

 

CASE 2:Group By Eliminationは発生しないケース(12cR2でGroup By Eliminationの誤作動で結果不正となったテストケース)

  1  SELECT
2 TO_CHAR(business_date, 'YYYYMM')
3 , COUNT(1)
4 FROM
5 business_day_calendar
6 GROUP BY
7* TO_CHAR(business_date, 'YYYYMM')

TO_CHAR(BUSINESS_D COUNT(1)
------------------ ----------
202201 31
202202 28
202203 31
202204 30
202205 31
202206 30
202207 31
202208 31
202209 30
202210 31
202211 30
202212 31

12行が選択されました.

実行計画
----------------------------------------------------------
Plan hash value: 100882575

---------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 365 | 2920 | 2 (50)| 00:00:01 |
| 1 | HASH GROUP BY | | 365 | 2920 | 2 (50)| 00:00:01 |
| 2 | INDEX FULL SCAN| PK_BUSINESS_DAY_CALENDAR | 365 | 2920 | 1 (0)| 00:00:01 |
---------------------------------------------------------------------------------------------

 

CASE 3:Group By Eliminationが発生するケース

  1  SELECT
2 business_date
3 , COUNT(1)
4 FROM
5 business_day_calendar
6 GROUP BY
7* business_date

BUSINESS_DATE COUNT(1)
--------------- ----------
20220101 000000 1
20220102 000000 1
20220103 000000 1

...略...

20221228 000000 1
20221229 000000 1
20221230 000000 1
20221231 000000 1

365行が選択されました.

経過: 00:00:00.01

実行計画
----------------------------------------------------------
Plan hash value: 1786497156

---------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 365 | 2920 | 1 (0)| 00:00:01 |
| 1 | INDEX FULL SCAN | PK_BUSINESS_DAY_CALENDAR | 365 | 2920 | 1 (0)| 00:00:01 |
---------------------------------------------------------------------------------------------

 

CASE 4:Group By Eliminationが発生するケースで, no_elim_groupby ヒントでGroup By Eliminationを抑止したケース

  1  SELECT
2 /*+
3 no_elim_groupby
4 */
5 business_date
6 , COUNT(1)
7 FROM
8 business_day_calendar
9 GROUP BY
10* business_date

BUSINESS_DATE COUNT(1)
--------------- ----------
20220101 000000 1
20220102 000000 1
20220103 000000 1
20220104 000000 1

...略...

20221228 000000 1
20221229 000000 1
20221230 000000 1
20221231 000000 1

365行が選択されました.

実行計画
----------------------------------------------------------
Plan hash value: 3672056694

-------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 365 | 2920 | 1 (0)| 00:00:01 |
| 1 | SORT GROUP BY NOSORT| | 365 | 2920 | 1 (0)| 00:00:01 |
| 2 | INDEX FULL SCAN | PK_BUSINESS_DAY_CALENDAR | 365 | 2920 | 1 (0)| 00:00:01 |
-------------------------------------------------------------------------------------------------

 

Oracle Database 19cです. CASE 2だけ確認します.

SCOTT@orcl> select banner from v$version;

BANNER
--------------------------------------------------------------------------------
Oracle Database 19c Enterprise Edition Release 19.0.0.0.0 - Production

 

CASE 2:このリリースでも問題は無さそうですね

  1  SELECT
2 TO_CHAR(business_date, 'YYYYMM')
3 , COUNT(1)
4 FROM
5 business_day_calendar
6 GROUP BY
7* TO_CHAR(business_date, 'YYYYMM')

TO_CHA COUNT(1)
------ ----------
202206 30
202203 31
202210 31
202212 31
202202 28
202211 30
202204 30
202208 31
202209 30
202201 31
202205 31
202207 31

12 rows selected.

Execution Plan
----------------------------------------------------------
Plan hash value: 100882575

---------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 365 | 2920 | 2 (50)| 00:00:01 |
| 1 | HASH GROUP BY | | 365 | 2920 | 2 (50)| 00:00:01 |
| 2 | INDEX FULL SCAN| PK_BUSINESS_DAY_CALENDAR | 365 | 2920 | 1 (0)| 00:00:01 |
---------------------------------------------------------------------------------------------

 

Oracle Database 18c です. 同様に, CASE 2のみ確認しています.

SCOTT> select banner from v$version;

BANNER
--------------------------------------------------------------------------------
Oracle Database 18c Enterprise Edition Release 18.0.0.0.0 - Production

CASE 2:このリリースでも問題は無さそう

  1  SELECT
2 TO_CHAR(business_date, 'YYYYMM')
3 , COUNT(1)
4 FROM
5 business_day_calendar
6 GROUP BY
7* TO_CHAR(business_date, 'YYYYMM')

TO_CHA COUNT(1)
------ ----------
202206 30
202203 31
202210 31
202212 31
202202 28
202211 30
202204 30
202208 31
202209 30
202201 31
202205 31
202207 31

12 rows selected.

Execution Plan
----------------------------------------------------------
Plan hash value: 100882575

---------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 365 | 2920 | 2 (50)| 00:00:01 |
| 1 | HASH GROUP BY | | 365 | 2920 | 2 (50)| 00:00:01 |
| 2 | INDEX FULL SCAN| PK_BUSINESS_DAY_CALENDAR | 365 | 2920 | 1 (0)| 00:00:01 |
---------------------------------------------------------------------------------------------

 

ちょっと思ったのですが, 現状, 10053トレース等をみるしかないタイプの書き換えでも, OTHER_XML列からOUTLINEを抜き出すと比較的簡単に発動有無がわかるものもあるかもしれないですね. 今回のように固有のヒントでON/OFFできるタイプだとOUTLINEには, elim_groupby のようなヒントが含まれているだろうし. (全てではないとは思いますが)

ふ〜〜〜っ. JPOUG Advent Calendar 2022 の自分のターンも終わって, 自分の分をなんとかするだけだーーーーー.

 

ということで,
帰ってきた! 実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺) Advent Calendar 2022は, 明日も私が担当です. よろしくお願いします.
そして, JPOUG Advent Calendar 2022 Day 20は, 凌直孝さんの担当です. よろしくお願いします!

 

では, また:)

参考)

Group by Elimination / Oracle Scratchpad
A Look at the Oracle Group-by Bug / Database Journal

 


Related article on Mac De Oracle
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 1 / TABLE FULL SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 2 / INDEX UNIQUE SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 3 / INDEX RANGE SCAN, Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 4 / INDEX RANGE SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 5 / INDEX RANGE SCAN, INLIST ITERATOR
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 6 / INDEX FAST SCAN, Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 7 / INDEX FULL SCAN,Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 8 / INDEX SKIP SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 9 / TABLE ACCESS INMEMORY FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 10 / NESTED LOOP JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 11 / MERGE JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 12 / HASH JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 13 / HASH JOIN OUTER
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 14 / HASH JOIN FULL OUTER
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 15 / PX, TABLE ACCESS FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 16 / CONCATENATION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 17 / SORT UNIQUE, UNION-ALL = UNION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 18 / UNION-ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 19 / INTERSECTION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 20 / MINUS
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 21 / WINDOW NOSORT STOPKEY
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 22 / COUNT STOPKEY
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 23 / HASH JOIN - LEFT-DEEP JOIN vs RIGHT-DEEP JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 24 / CONNECT BY NO FILTERING WITH START-WITH
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 25 / UNION ALL (RECURSIVE WITH) DEPTH FIRST, RECURSIVE WITH PUMP
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#1 / STAR TRANSFORM, VECTOR TRANSFORM (DWH向け)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#2 / MERGE (UPSERT)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#3 / RDFView
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#4 / INDEX FULL SCAN (MIN/MAX) - Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! No.30 - LOAD TABLE CONVENTIONAL vs. LOAD AS SELECT
・実行計画は, SQL文のレントゲン写真だ! No.31 - TEMP TABLE TRANSFORMATION LOAD AS SELECT (CURSOR DURATION MEMORY)
・実行計画は, SQL文のレントゲン写真だ! No.32 - EXTERNAL TABLE ACCESS FULL / INMEMORY FULL
・実行計画は, SQL文のレントゲン写真だ! No.33 - BITMAP CONVERSION TO ROWIDS
・実行計画は, SQL文のレントゲン写真だ! No.34 - 似て非なるもの USE_CONCAT と OR_EXPAND ヒント と 手書きSQLのレントゲンの見分け方
・実行計画は, SQL文のレントゲン写真だ! No.35 - 似て非なるもの USE_CONCAT と OR_EXPANDヒントとパラレルクエリー
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 1 / No.36 / INTERSECT ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 2 / No.37 / MINUS ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 3 / No.38 / EXCEPT and EXCEPT ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 4 / No.39 / In-Memory Hybrid Scans
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 5 / No.40 / PIVOT and UNPIVOT
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 6 / No.41 / In-Memory Vectorized Join
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 7 / No.42 / INDEX RANGE SCAN (MULTI VALUE)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 8 / No.43 / TABLE ACCESS BY INDEX ROWID BATCHED
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 9 / No.44 / COLLECTION ITERATOR PICKLER FETCH
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 10 / No.45 / MAT_VIEW REWRITE ACCESS FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 11 / No.46 / GROUPING SETS, ROLLUP, CUBE
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 12 / No.47 / TEMP TABLE TRANSFORMATION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 13 / No.48 / MULTI-TABLE INSERT
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 14 / No.49 / the DUAL Table
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 15 / No.50 / REMOTE
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 16 / No.51 / Concurrent Execution of Union All and Union
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 17 / No.52 / Order by Elimination
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 18 / No.53 / Join Elimination

 

| | | コメント (0)

2022年12月18日 (日)

実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 18 / No.53 / Join Elimination

Previously on Mac De Oracle...
Day 17は, order by Elimination にフォーカスをあてました. チューニングの現場で気づいたのが最初だったと思いますが, SQL文を見ていて, これ無駄なソートだなぁなんて思いながら実行計画という名のレントゲンをみていたら, おお! NOSORTとかではなく, ORDER BY自体が消されてる! 賢い! と.

では, Day 18のお題, Join Eliminationです. (登場したのは10gR2 ぐらいのはずですが, 間違ってたらツッコミ歓迎)
この排除系書き換えも, 実行計画という名のレントゲンシリーズでは, まだ紹介していなかったので, 今回は, ヒントで無効化する例も含め, 軽めの内容でw, 診ていきたいと思います. (参照整合性制約アレルギーネタを思い出すw)


いつもと同じように 21c で確認します.

SCOTT@orclpdb1> select banner from v$version;

BANNER
----------------------------------------------------------------------
Oracle Database 21c Enterprise Edition Release 21.0.0.0.0 - Production

対象となる表には外部参照整合制約があることが前提です.
20190321-144842

customers表を結合していますが, 参照整合性制約でcustomers表に存在する顧客しか注文できないという制約があります. つまり, この参照整合性制約があるため, INNER JOINやEXISTSを利用して存在チェックする必要はないということを意味しています.
Join EliminationによりSQLが書き換えられ結合が排除されていることがわかります. 実行計画には, ORDER_PKをIndex Only Scanしているだけで, customers表やcustomersの索引を結合しているOperationは含まれていない!!!
結合しないので, 結合のコスト及び, customers表やcustomersの索引へのアクセスコストが削減されています. 実行統計からは, Pysical Readや, Buffer Getsの低下という形で現れてきます.

とはいえ, 参照整合性制約アレルギーのお持ちの方も多く, 一生目にすることのない方々も, 残念ながら多いのも事実です.
参照整合性制約アレルギーが発症してしまうと, 一生付き合っていくことになちゃいますからね(大抵の場合)


Day19の内容とDay18のネタを入れ替えたので, day19.sqlを実行しているところは気にしないでくださいw

OE@orclpdb1> @day19
1 SELECT
2 order_id
3 FROM
4 orders o
5 INNER JOIN customers c
6 ON
7 o.customer_id = c.customer_id
8 WHERE
9* order_id < 2400

ORDER_ID
----------
2354
2355
2356
2357
2358

...略...

2396
2397
2398
2399

実行計画
----------------------------------------------------------
Plan hash value: 1653993310

-----------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 46 | 184 | 1 (0)| 00:00:01 |
|* 1 | INDEX RANGE SCAN| ORDER_PK | 46 | 184 | 1 (0)| 00:00:01 |
-----------------------------------------------------------------------------

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

1 - access("O"."ORDER_ID"<2400)

統計
----------------------------------------------------------
0 recursive calls
0 db block gets
5 consistent gets
0 physical reads
0 redo size
1482 bytes sent via SQL*Net to client
85 bytes received via SQL*Net from client
5 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
46 rows processed
/code>


NO_ELIMINATE_JOINヒントを利用し, Join Eliminationを抑止してみましょう.
NO_ELIMINATE_JOINは, 使うことがあるのか? と思う方もいるかもしれませんが, 例えば, 不具合で7445とか, その他結果不正などに当たった時, かつ, 局所的対応で回避できそうなケースでは, 該当SQLにヒントを埋め込み, Join Eliminationの抑止で回避したりします.
どこで起きるかわからんので, インスタンスレベルで止めるケースもなくなないですが, そういう場合は, 隠しパラメータで無効化するのが一般的です. (ほぼ使わないと思いますが, ELIMINATE_JOIN でJoin Eliminationを利用できます. インスタンスレベルで無効化している状態で, 特定のSQLだけはJoin Eliminationしたいという場合に使うぐらいですね. 滅多にないと思いますが)

  1  SELECT
2 /*+
3 NO_ELIMINATE_JOIN(c)
4 */
5 order_id
6 FROM
7 orders o
8 INNER JOIN customers c
9 ON
10 o.customer_id = c.customer_id
11 WHERE
12* order_id < 2400

ORDER_ID
----------
2354
2355
2356
2357
2358

...略...

2392
2393
2394
2395
2396
2397
2398
2399

実行計画
----------------------------------------------------------
Plan hash value: 875022219

-----------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
-----------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 46 | 552 | 2 (0)| 00:00:01 |
| 1 | NESTED LOOPS | | 46 | 552 | 2 (0)| 00:00:01 |
|* 2 | TABLE ACCESS BY INDEX ROWID BATCHED| ORDERS | 46 | 368 | 2 (0)| 00:00:01 |
|* 3 | INDEX RANGE SCAN | ORDER_PK | 46 | | 1 (0)| 00:00:01 |
|* 4 | INDEX UNIQUE SCAN | CUSTOMERS_PK | 1 | 4 | 0 (0)| 00:00:01 |
-----------------------------------------------------------------------------------------------------

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

2 - filter("O"."CUSTOMER_ID">0)
3 - access("O"."ORDER_ID"<2400)
4 - access("O"."CUSTOMER_ID"="C"."CUSTOMER_ID")

統計
----------------------------------------------------------
0 recursive calls
0 db block gets
15 consistent gets
0 physical reads
0 redo size
1482 bytes sent via SQL*Net to client
85 bytes received via SQL*Net from client
5 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
46 rows processed
/code>

アドベントカレンダー終わったら, もう, 今年も残りわずか....

ということで, 出口が見えつつある, アドベントカレンダー全部俺, 明日も, 俺が書きますw
では, また.


参考)
Join Elimination(結合の排除)と 参照整合性制約 / FAQ
db tech showcase Tokyo 2013 - A35 特濃JPOUG:潮溜まりでジャブジャブ, SQLチューニングの「参照整合性制約アレルギー」を参照のこと



Related article on Mac De Oracle

・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 1 / TABLE FULL SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 2 / INDEX UNIQUE SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 3 / INDEX RANGE SCAN, Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 4 / INDEX RANGE SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 5 / INDEX RANGE SCAN, INLIST ITERATOR
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 6 / INDEX FAST SCAN, Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 7 / INDEX FULL SCAN,Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 8 / INDEX SKIP SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 9 / TABLE ACCESS INMEMORY FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 10 / NESTED LOOP JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 11 / MERGE JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 12 / HASH JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 13 / HASH JOIN OUTER
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 14 / HASH JOIN FULL OUTER
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 15 / PX, TABLE ACCESS FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 16 / CONCATENATION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 17 / SORT UNIQUE, UNION-ALL = UNION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 18 / UNION-ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 19 / INTERSECTION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 20 / MINUS
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 21 / WINDOW NOSORT STOPKEY
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 22 / COUNT STOPKEY
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 23 / HASH JOIN - LEFT-DEEP JOIN vs RIGHT-DEEP JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 24 / CONNECT BY NO FILTERING WITH START-WITH
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 25 / UNION ALL (RECURSIVE WITH) DEPTH FIRST, RECURSIVE WITH PUMP
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#1 / STAR TRANSFORM, VECTOR TRANSFORM (DWH向け)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#2 / MERGE (UPSERT)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#3 / RDFView
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#4 / INDEX FULL SCAN (MIN/MAX) - Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! No.30 - LOAD TABLE CONVENTIONAL vs. LOAD AS SELECT
・実行計画は, SQL文のレントゲン写真だ! No.31 - TEMP TABLE TRANSFORMATION LOAD AS SELECT (CURSOR DURATION MEMORY)
・実行計画は, SQL文のレントゲン写真だ! No.32 - EXTERNAL TABLE ACCESS FULL / INMEMORY FULL
・実行計画は, SQL文のレントゲン写真だ! No.33 - BITMAP CONVERSION TO ROWIDS
・実行計画は, SQL文のレントゲン写真だ! No.34 - 似て非なるもの USE_CONCAT と OR_EXPAND ヒント と 手書きSQLのレントゲンの見分け方
・実行計画は, SQL文のレントゲン写真だ! No.35 - 似て非なるもの USE_CONCAT と OR_EXPANDヒントとパラレルクエリー
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 1 / No.36 / INTERSECT ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 2 / No.37 / MINUS ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 3 / No.38 / EXCEPT and EXCEPT ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 4 / No.39 / In-Memory Hybrid Scans
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 5 / No.40 / PIVOT and UNPIVOT
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 6 / No.41 / In-Memory Vectorized Join
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 7 / No.42 / INDEX RANGE SCAN (MULTI VALUE)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 8 / No.43 / TABLE ACCESS BY INDEX ROWID BATCHED
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 9 / No.44 / COLLECTION ITERATOR PICKLER FETCH
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 10 / No.45 / MAT_VIEW REWRITE ACCESS FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 11 / No.46 / GROUPING SETS, ROLLUP, CUBE
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 12 / No.47 / TEMP TABLE TRANSFORMATION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 13 / No.48 / MULTI-TABLE INSERT
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 14 / No.49 / the DUAL Table
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 15 / No.50 / REMOTE
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 16 / No.51 / Concurrent Execution of Union All and Union
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 17 / No.52 / Order by Elimination

| | | コメント (0)

2022年12月17日 (土)

実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 17 / No.52 / Order by Elimination

Previously on Mac De Oracle... Day 16は, Concurrent Execution of Union All and Union にフォーカスをあてました. 使う機会があったら試してみてくださいね. 私は, この機能が登場した頃, Spatialな機能でRDFを使った際, Exadataでバリバリ活用してたというか, されていたという記憶があります.

では, Day 17のお題, Order by Eliminationです. 文字通り, Order byが排除される, 書き換え機能の一種です. だからと言って, 無駄なOrder by句書いても大丈夫ってことではないですから, 注意しましょうw

 

10gR2ぐらいから実装されている最適化の一つですね. 多少無駄なOrder byを書いちゃっても無駄だと判断されれば, 行われません. (だからと言って, 気にしないでOrder By書いちゃってもいいという話ではありませんがw)

いつもと同じように 21c で確認します.

SCOTT@orclpdb1> select banner from v$version;

BANNER
----------------------------------------------------------------------
Oracle Database 21c Enterprise Edition Release 21.0.0.0.0 - Production

 

emp表の索引などはいかの通り(scottのemp表を知らない方向け. いるのか?)

SCOTT@orclpdb1> desc emp
名前 NULL? 型
----------------------------------------- -------- ----------------------------
EMPNO NOT NULL NUMBER(4)
ENAME VARCHAR2(10)
JOB VARCHAR2(9)
MGR NUMBER(4)
HIREDATE DATE
SAL NUMBER(7,2)
COMM NUMBER(7,2)
DEPTNO NUMBER(2)

SCOTT@orclpdb1> select index_name,column_name from user_ind_columns where
2 table_name = 'EMP' order by index_name,column_position;

INDEX_NAME COLUMN_NAME
------------------------------ ------------------------------
IX01_EMP DEPTNO
PK_EMP EMPNO

 

SQL中に ORDER BY句がありますが, NOSORT操作もないのにソートされていません. そもそも書き換えられて, ORDER BY句が削除されているんですよーーーー.
このケースでは, empno順にソートされた結果返ってきているかのう様に見えますが, それは索引をempno順に読無ことで, ソートを排除できるからなんですよね. 索引がなければ, ソートは必要なのです. ちなみに, INDEX FULL SCAN はソートされたキー順に索引を全てスキャンするところがポイント, INDEX FAST FULL SCANはとは違うのはみなさんご存知のはずなのでコマけーことは省略.

SCOTT@orclpdb1> @day17
1 SELECT
2 *
3 FROM
4 emp
5 ORDER BY
6* empno

EMPNO ENAME JOB MGR HIREDATE SAL COMM DEPTNO
---------- ------------------------------ --------------------------- ---------- -------- ---------- ---------- ----------
7369 SMITH CLERK 7902 80-12-17 800 20
7499 ALLEN SALESMAN 7698 81-02-20 1600 300 30
7521 WARD SALESMAN 7698 81-02-22 1250 500 30
7566 JONES MANAGER 7839 81-04-02 2975 20
7654 MARTIN SALESMAN 7698 81-09-28 1250 1400 30
7698 BLAKE MANAGER 7839 81-05-01 2850 30
7782 CLARK MANAGER 7839 81-06-09 2450 10
7839 KING PRESIDENT 81-11-17 5000 10
7844 TURNER SALESMAN 7698 81-09-08 1500 0 30
7900 JAMES CLERK 7698 81-12-03 950 30
7902 FORD ANALYST 7566 81-12-03 3000 20
7934 MILLER CLERK 7782 82-01-23 1300 10

実行計画
----------------------------------------------------------
Plan hash value: 4170700152

--------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
--------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 12 | 468 | 2 (0)| 00:00:01 |
| 1 | TABLE ACCESS BY INDEX ROWID| EMP | 12 | 468 | 2 (0)| 00:00:01 |
| 2 | INDEX FULL SCAN | PK_EMP | 12 | | 1 (0)| 00:00:01 |
--------------------------------------------------------------------------------------

統計
----------------------------------------------------------
0 recursive calls
0 db block gets
4 consistent gets
0 physical reads
0 redo size
1669 bytes sent via SQL*Net to client
52 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
12 rows processed

 

こちらも, 無用なORDER BY句が削除されています. 賢い. オプティマイザー!
以下のORDER BYは, みるからに無駄なソートですが, オプティマイザーは気づいて排除しています.

  1  SELECT
2 COUNT(ename)
3 FROM
4 (
5 SELECT
6 *
7 FROM
8 emp
9 ORDER BY
10 ename
11* )

COUNT(ENAME)
------------
12

実行計画
----------------------------------------------------------
Plan hash value: 2083865914

---------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time |
---------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | 6 | 3 (0)| 00:00:01 |
| 1 | SORT AGGREGATE | | 1 | 6 | | |
| 2 | TABLE ACCESS FULL| EMP | 12 | 72 | 3 (0)| 00:00:01 |
---------------------------------------------------------------------------

統計
----------------------------------------------------------
0 recursive calls
0 db block gets
6 consistent gets
0 physical reads
0 redo size
595 bytes sent via SQL*Net to client
52 bytes received via SQL*Net from client
2 SQL*Net roundtrips to/from client
0 sorts (memory)
0 sorts (disk)
1 rows processed

 

EXPLAIN等改善してほしいなと思うのはこれらのElimination関連情報, 今回のように無駄なORDER BYを排除したという情報を, Elimination Informationとしてリストしたら良いのではないかと思っています. 最近, SQLヒントに関する情報をリストするようになったのでできるのではないだろうかと. ただ, 諸々内部でSQL文を書き換えたりするから, 実は面倒なのかもねw  10053トレースなら見える訳ですが, 毎回取得するわけにもいかないしね.

ついに,  Day 17 done. あと少しだ頑張れ, 俺w

 

ということで, 残念ですが, 明日も私です.
ではまた.

 


Related article on Mac De Oracle
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 1 / TABLE FULL SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 2 / INDEX UNIQUE SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 3 / INDEX RANGE SCAN, Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 4 / INDEX RANGE SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 5 / INDEX RANGE SCAN, INLIST ITERATOR
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 6 / INDEX FAST SCAN, Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 7 / INDEX FULL SCAN,Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 8 / INDEX SKIP SCAN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 9 / TABLE ACCESS INMEMORY FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 10 / NESTED LOOP JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 11 / MERGE JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 12 / HASH JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 13 / HASH JOIN OUTER
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 14 / HASH JOIN FULL OUTER
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 15 / PX, TABLE ACCESS FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 16 / CONCATENATION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 17 / SORT UNIQUE, UNION-ALL = UNION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 18 / UNION-ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 19 / INTERSECTION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 20 / MINUS
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 21 / WINDOW NOSORT STOPKEY
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 22 / COUNT STOPKEY
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 23 / HASH JOIN - LEFT-DEEP JOIN vs RIGHT-DEEP JOIN
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 24 / CONNECT BY NO FILTERING WITH START-WITH
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - Day 25 / UNION ALL (RECURSIVE WITH) DEPTH FIRST, RECURSIVE WITH PUMP
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#1 / STAR TRANSFORM, VECTOR TRANSFORM (DWH向け)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#2 / MERGE (UPSERT)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#3 / RDFView
・実行計画は, SQL文のレントゲン写真だ! Oracle Database編 (全部俺)Advent Calendar 2019 - おまけ#4 / INDEX FULL SCAN (MIN/MAX) - Index Only Scan
・実行計画は, SQL文のレントゲン写真だ! No.30 - LOAD TABLE CONVENTIONAL vs. LOAD AS SELECT
・実行計画は, SQL文のレントゲン写真だ! No.31 - TEMP TABLE TRANSFORMATION LOAD AS SELECT (CURSOR DURATION MEMORY)
・実行計画は, SQL文のレントゲン写真だ! No.32 - EXTERNAL TABLE ACCESS FULL / INMEMORY FULL
・実行計画は, SQL文のレントゲン写真だ! No.33 - BITMAP CONVERSION TO ROWIDS
・実行計画は, SQL文のレントゲン写真だ! No.34 - 似て非なるもの USE_CONCAT と OR_EXPAND ヒント と 手書きSQLのレントゲンの見分け方
・実行計画は, SQL文のレントゲン写真だ! No.35 - 似て非なるもの USE_CONCAT と OR_EXPANDヒントとパラレルクエリー
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 1 / No.36 / INTERSECT ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 2 / No.37 / MINUS ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 3 / No.38 / EXCEPT and EXCEPT ALL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 4 / No.39 / In-Memory Hybrid Scans
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 5 / No.40 / PIVOT and UNPIVOT
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 6 / No.41 / In-Memory Vectorized Join
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 7 / No.42 / INDEX RANGE SCAN (MULTI VALUE)
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 8 / No.43 / TABLE ACCESS BY INDEX ROWID BATCHED
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 9 / No.44 / COLLECTION ITERATOR PICKLER FETCH
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 10 / No.45 / MAT_VIEW REWRITE ACCESS FULL
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 11 / No.46 / GROUPING SETS, ROLLUP, CUBE
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 12 / No.47 / TEMP TABLE TRANSFORMATION
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 13 / No.48 / MULTI-TABLE INSERT
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 14 / No.49 / the DUAL Table
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 15 / No.50 / REMOTE
・実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 16 / No.51 / Concurrent Execution of Union All and Union

 

| | | コメント (0)

2022年12月16日 (金)

実行計画は, SQL文のレントゲン写真だ! Oracle Database (全部俺)Advent Calendar 2022 Day 16 / No.51 / Concurrent Execution of Union All and Union

Previously on Mac De Oracle... Day 15は, 分散, リモートクエリーにフォーカスをあてました. 好き嫌い多いネタだったかもしれませんねぇw

では, Day 16のお題, Concurrent Execution of Union All and Union

これは何かというと, 12c R1以降だったか?(定かでないw)に, UNION/UNION ALLの各分岐がパラレルで実行されるという機能です. それまではシリアルに処理されていたので, 分岐が多いほど処理時間も増加していた訳ですが, 分岐がそれぞれパラレルで実行される分処理時間は短くなるというやつですね.

Concurrent Execution of Union All and Unionが実行されている時の特徴は, PX SELECTOR があるかどうかで見分けます.

いつもと同じように 21c で確認します.


SCOTT@orclpdb1> select banner from v$version;

BANNER
----------------------------------------------------------------------
Oracle Database 21c Enterprise Edition Release 21.0.0.0.0 - Production

 

準備として, 12c R1以前ではシリアルに処理される代表格, リモート表を用意します. (昨日のネタはこの準備のためでもあったり)



SCOTT@orclpdb2> @day16
1* CREATE DATABASE LINK remote_scott CONNECT TO scott IDENTIFIED BY tiger USING 'orclpdb1'

データベース・リンクが作成されました.

1 CREATE TABLE local_emp
2 AS
3 SELECT *
4 FROM
5 emp@remote_scott
6 ORDER BY
7 empno
8* FETCH FIRST 5 ROWS ONLY

表が作成されました.

1* INSERT INTO local_emp(empno, ename) VALUES(1, 'NULL')

1行が作成されました.

 

クエリーの結果、たまたま、empno順に並んでいるように見える結果もありますが、ソートしたい場合は、必ず!!! ORDER BY句が必要ですからねw(誰となくw)

シリアルで実行してみます. よく見る実行計画という名のレントゲンです. リモート表は Operationが REMOTE として現れています. 2つある分岐は, シリアルに実行されます.

  1  SELECT
2 *
3 FROM
4 local_emp
5 UNION ALL
6 SELECT
7 *
8 FROM
9* emp@remote_scott

EMPNO ENAME JOB MGR HIREDATE SAL COMM DEPTNO
---------- ------------------------------ --------------------------- ---------- -------- ---------- ---------- ----------
7369 SMITH CLERK 7902 80-12-17 800 20
7499 ALLEN SALESMAN 7698 81-02-20 1600 300 30
7521 WARD SALESMAN 7698 81-02-22 1250 500 30
7566 JONES MANAGER 7839 81-04-02 2975 20
7654 MARTIN SALESMAN 7698 81-09-28 1250 1400 30
1 NULL
7369 SMITH CLERK 7902 80-12-17 800 20
7499 ALLEN SALESMAN 7698 81-02-20 1600 300 30
7521 WARD SALESMAN 7698 81-02-22 1250 500 30
7566 JONES MANAGER 7839 81-04-02 2975 20
7654 MARTIN SALESMAN 7698 81-09-28 1250 1400 30
7698 BLAKE MANAGER 7839 81-05-01 2850 30
7782 CLARK MANAGER 7839 81-06-09 2450 10
7839 KING PRESIDENT 81-11-17 5000 10
7844 TURNER SALESMAN 7698 81-09-08 1500 0 30
7900 JAMES CLERK 7698 81-12-03 950 30
7902 FORD ANALYST 7566 81-12-03 3000 20
7934 MILLER CLERK 7782 82-01-23 1300 10

実行計画
----------------------------------------------------------
Plan hash value: 1607333107

------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | Inst |IN-OUT|
------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 18 | 990 | 5 (0)| 00:00:01 | | |
| 1 | UNION-ALL | | | | | | | |
| 2 | TABLE ACCESS FULL| LOCAL_EMP | 6 | 522 | 2 (0)| 00:00:01 | | |
| 3 | REMOTE | EMP | 12 | 468 | 3 (0)| 00:00:01 | REMOT~ | R->S |
------------------------------------------------------------------------------------------------

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

3 - SELECT "EMPNO","ENAME","JOB","MGR","HIREDATE","SAL","COMM","DEPTNO" FROM "EMP"
"EMP" (accessing 'REMOTE_SCOTT' )

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

 

次に, パラレルクエリーにして, かつ, Concurrent Execution を抑止する NO_PQ_CONCURRENT_UNION  を付加して実行してみました.
PX SELECTOR は現れていません. パラレルクエリーではありますが, 分岐は, 順に処理されていきます.

  1  SELECT
2 /*+
3 PARALLEL(3)
4 NO_PQ_CONCURRENT_UNION
5 */
6 *
7 FROM
8 local_emp
9 UNION ALL
10 SELECT
11 *
12 FROM
13* emp@remote_scott

EMPNO ENAME JOB MGR HIREDATE SAL COMM DEPTNO
---------- ------------------------------ --------------------------- ---------- -------- ---------- ---------- ----------
7369 SMITH CLERK 7902 80-12-17 800 20
7499 ALLEN SALESMAN 7698 81-02-20 1600 300 30
7521 WARD SALESMAN 7698 81-02-22 1250 500 30
7566 JONES MANAGER 7839 81-04-02 2975 20
7654 MARTIN SALESMAN 7698 81-09-28 1250 1400 30
1 NULL
7369 SMITH CLERK 7902 80-12-17 800 20
7499 ALLEN SALESMAN 7698 81-02-20 1600 300 30
7521 WARD SALESMAN 7698 81-02-22 1250 500 30
7566 JONES MANAGER 7839 81-04-02 2975 20
7654 MARTIN SALESMAN 7698 81-09-28 1250 1400 30
7698 BLAKE MANAGER 7839 81-05-01 2850 30
7782 CLARK MANAGER 7839 81-06-09 2450 10
7839 KING PRESIDENT 81-11-17 5000 10
7844 TURNER SALESMAN 7698 81-09-08 1500 0 30
7900 JAMES CLERK 7698 81-12-03 950 30
7902 FORD ANALYST 7566 81-12-03 3000 20
7934 MILLER CLERK 7782 82-01-23 1300 10

実行計画
----------------------------------------------------------
Plan hash value: 3305250800

----------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | TQ/Ins |IN-OUT| PQ Distrib |
----------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 18 | 990 | 5 (0)| 00:00:01 | | | |
| 1 | UNION-ALL | | | | | | | | |
| 2 | PX COORDINATOR | | | | | | | | |
| 3 | PX SEND QC (RANDOM)| :TQ10000 | 6 | 522 | 2 (0)| 00:00:01 | Q1,00 | P->S | QC (RAND) |
| 4 | PX BLOCK ITERATOR | | 6 | 522 | 2 (0)| 00:00:01 | Q1,00 | PCWC | |
| 5 | TABLE ACCESS FULL| LOCAL_EMP | 6 | 522 | 2 (0)| 00:00:01 | Q1,00 | PCWP | |
| 6 | REMOTE | EMP | 12 | 468 | 3 (0)| 00:00:01 | REMOT~ | R->S | |
----------------------------------------------------------------------------------------------------------------

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

6 - SELECT /*+ SHARED (3) */ "EMPNO","ENAME","JOB","MGR","HIREDATE","SAL","COMM","DEPTNO" FROM "EMP"
"EMP" (accessing 'REMOTE_SCOTT' )

Note
-----
- dynamic statistics used: dynamic sampling (level=2)
- Degree of Parallelism is 3 because of hint

 

本日の主役 PX SELECTORREMOTE 操作の上に出てきました. この状態であれば, 各分岐はパラレルに処理されることになり, 分岐が順に処理されるより処理時間は短縮されることになります(REMOTE処理が重かったら, その処理時間に引きづら訳ですが, 順に処理するよりは早く終わると予想できますよね)

  1  SELECT
2 /*+
3 PARALLEL(3)
4 */
5 *
6 FROM
7 local_emp
8 UNION ALL
9 SELECT
10 *
11 FROM
12* emp@remote_scott

EMPNO ENAME JOB MGR HIREDATE SAL COMM DEPTNO
---------- ------------------------------ --------------------------- ---------- -------- ---------- ---------- ----------
1 NULL
7369 SMITH CLERK 7902 80-12-17 800 20
7499 ALLEN SALESMAN 7698 81-02-20 1600 300 30
7521 WARD SALESMAN 7698 81-02-22 1250 500 30
7566 JONES MANAGER 7839 81-04-02 2975 20
7654 MARTIN SALESMAN 7698 81-09-28 1250 1400 30
7369 SMITH CLERK 7902 80-12-17 800 20
7499 ALLEN SALESMAN 7698 81-02-20 1600 300 30
7521 WARD SALESMAN 7698 81-02-22 1250 500 30
7566 JONES MANAGER 7839 81-04-02 2975 20
7654 MARTIN SALESMAN 7698 81-09-28 1250 1400 30
7698 BLAKE MANAGER 7839 81-05-01 2850 30
7782 CLARK MANAGER 7839 81-06-09 2450 10
7839 KING PRESIDENT 81-11-17 5000 10
7844 TURNER SALESMAN 7698 81-09-08 1500 0 30
7900 JAMES CLERK 7698 81-12-03 950 30
7902 FORD ANALYST 7566 81-12-03 3000 20
7934 MILLER CLERK 7782 82-01-23 1300 10

実行計画
----------------------------------------------------------
Plan hash value: 2359762278

----------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | TQ |IN-OUT| PQ Distrib |
----------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 18 | 990 | 5 (0)| 00:00:01 | | | |
| 1 | PX COORDINATOR | | | | | | | | |
| 2 | PX SEND QC (RANDOM) | :TQ10000 | | | | | Q1,00 | P->S | QC (RAND) |
| 3 | UNION-ALL | | | | | | Q1,00 | PCWP | |
| 4 | PX BLOCK ITERATOR | | 6 | 522 | 2 (0)| 00:00:01 | Q1,00 | PCWC | |
| 5 | TABLE ACCESS FULL| LOCAL_EMP | 6 | 522 | 2 (0)| 00:00:01 | Q1,00 | PCWP | |
| 6 | PX SELECTOR | | | | | | Q1,00 | PCWP | |
| 7 | REMOTE | EMP | 12 | 468 | 3 (0)| 00:00:01 | Q1,00 | PCWP | |
----------------------------------------------------------------------------------------------------------------

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

7 - SELECT /*+ SHARED (3) */ "EMPNO","ENAME","JOB","MGR","HIREDATE","SAL","COMM","DEPTNO" FROM "EMP"
"EMP" (accessing ':Q1000' )

Note
-----
- dynamic statistics used: dynamic sampling (level=2)
- Degree of Parallelism is 3 because of hint

 

以下, UNIONでも同様に, 実行計画というレントゲンを確認しておきましょう. まずはシリアル.


  1  SELECT
2 *
3 FROM
4 local_emp
5 UNION
6 SELECT
7 *
8 FROM
9* emp@remote_scott

EMPNO ENAME JOB MGR HIREDATE SAL COMM DEPTNO
---------- ------------------------------ --------------------------- ---------- -------- ---------- ---------- ----------
7369 SMITH CLERK 7902 80-12-17 800 20
7499 ALLEN SALESMAN 7698 81-02-20 1600 300 30
7521 WARD SALESMAN 7698 81-02-22 1250 500 30
7566 JONES MANAGER 7839 81-04-02 2975 20
7654 MARTIN SALESMAN 7698 81-09-28 1250 1400 30
1 NULL
7698 BLAKE MANAGER 7839 81-05-01 2850 30
7782 CLARK MANAGER 7839 81-06-09 2450 10
7839 KING PRESIDENT 81-11-17 5000 10
7844 TURNER SALESMAN 7698 81-09-08 1500 0 30
7900 JAMES CLERK 7698 81-12-03 950 30
7902 FORD ANALYST 7566 81-12-03 3000 20
7934 MILLER CLERK 7782 82-01-23 1300 10

実行計画
----------------------------------------------------------
Plan hash value: 2189749816

-------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | Inst |IN-OUT|
-------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 18 | 990 | 7 (29)| 00:00:01 | | |
| 1 | HASH UNIQUE | | 18 | 990 | 7 (29)| 00:00:01 | | |
| 2 | UNION-ALL | | | | | | | |
| 3 | TABLE ACCESS FULL| LOCAL_EMP | 6 | 522 | 2 (0)| 00:00:01 | | |
| 4 | REMOTE | EMP | 12 | 468 | 3 (0)| 00:00:01 | REMOT~ | R->S |
-------------------------------------------------------------------------------------------------

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

4 - SELECT "EMPNO","ENAME","JOB","MGR","HIREDATE","SAL","COMM","DEPTNO" FROM "EMP"
"EMP" (accessing 'REMOTE_SCOTT' )

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

 

NO_PQ_CONCURRENT_UNIONでConcurrent Execution を抑止してみると, 順に処理されていることがわかります.

  1  SELECT
2 /*+
3 PARALLEL(3)
4 NO_PQ_CONCURRENT_UNION 5 */
6 *
7 FROM
8 local_emp
9 UNION
10 SELECT
11 *
12 FROM
13* emp@remote_scott

EMPNO ENAME JOB MGR HIREDATE SAL COMM DEPTNO
---------- ------------------------------ --------------------------- ---------- -------- ---------- ---------- ----------
7902 FORD ANALYST 7566 81-12-03 3000 20
7566 JONES MANAGER 7839 81-04-02 2975 20
7654 MARTIN SALESMAN 7698 81-09-28 1250 1400 30
7369 SMITH CLERK 7902 80-12-17 800 20
1 NULL
7900 JAMES CLERK 7698 81-12-03 950 30
7934 MILLER CLERK 7782 82-01-23 1300 10
7839 KING PRESIDENT 81-11-17 5000 10
7782 CLARK MANAGER 7839 81-06-09 2450 10
7499 ALLEN SALESMAN 7698 81-02-20 1600 300 30
7844 TURNER SALESMAN 7698 81-09-08 1500 0 30
7521 WARD SALESMAN 7698 81-02-22 1250 500 30
7698 BLAKE MANAGER 7839 81-05-01 2850 30

実行計画
----------------------------------------------------------
Plan hash value: 622056383

-----------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | TQ/Ins |IN-OUT| PQ Distrib |
-----------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 18 | 990 | 7 (29)| 00:00:01 | | | |
| 1 | PX COORDINATOR | | | | | | | | |
| 2 | PX SEND QC (RANDOM) | :TQ10002 | 18 | 990 | 7 (29)| 00:00:01 | Q1,02 | P->S | QC (RAND) |
| 3 | HASH UNIQUE | | 18 | 990 | 7 (29)| 00:00:01 | Q1,02 | PCWP | |
| 4 | PX RECEIVE | | 18 | 990 | 7 (29)| 00:00:01 | Q1,02 | PCWP | |
| 5 | PX SEND HASH | :TQ10001 | 18 | 990 | 7 (29)| 00:00:01 | Q1,01 | P->P | HASH |
| 6 | HASH UNIQUE | | 18 | 990 | 7 (29)| 00:00:01 | Q1,01 | PCWP | |
| 7 | UNION-ALL | | | | | | Q1,01 | PCWP | |
| 8 | PX BLOCK ITERATOR | | 6 | 522 | 2 (0)| 00:00:01 | Q1,01 | PCWC | |
| 9 | TABLE ACCESS FULL | LOCAL_EMP | 6 | 522 | 2 (0)| 00:00:01 | Q1,01 | PCWP | |
| 10 | BUFFER SORT | | | | | | Q1,01 | PCWC | |
| 11 | PX RECEIVE | | 12 | 468 | 3 (0)| 00:00:01 | Q1,01 | PCWP | |
| 12 | PX SEND ROUND-ROBIN| :TQ10000 | 12 | 468 | 3 (0)| 00:00:01 | | S->P | RND-ROBIN |
| 13 | REMOTE | EMP | 12 | 468 | 3 (0)| 00:00:01 | REMOT~ | R->S | |
-----------------------------------------------------------------------------------------------------------------------

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

13 - SELECT /*+ SHARED (3) */ "EMPNO","ENAME","JOB","MGR","HIREDATE","SAL","COMM","DEPTNO" FROM "EMP" "EMP"
(accessing 'REMOTE_SCOTT' )

Note
-----
- dynamic statistics used: dynamic sampling (level=2)
- Degree of Parallelism is 3 because of hint

 

UNIONでもPX SELECTORが現れ, 分岐がパラレルに処理されていることが見えます. :)

  1  SELECT
2 /*+
3 PARALLEL(3)
4 */
5 *
6 FROM
7 local_emp
8 UNION
9 SELECT
10 *
11 FROM
12* emp@remote_scott

EMPNO ENAME JOB MGR HIREDATE SAL COMM DEPTNO
---------- ------------------------------ --------------------------- ---------- -------- ---------- ---------- ----------
7499 ALLEN SALESMAN 7698 81-02-20 1600 300 30
7844 TURNER SALESMAN 7698 81-09-08 1500 0 30
7521 WARD SALESMAN 7698 81-02-22 1250 500 30
7698 BLAKE MANAGER 7839 81-05-01 2850 30
7902 FORD ANALYST 7566 81-12-03 3000 20
7566 JONES MANAGER 7839 81-04-02 2975 20
7654 MARTIN SALESMAN 7698 81-09-28 1250 1400 30
7369 SMITH CLERK 7902 80-12-17 800 20
1 NULL
7900 JAMES CLERK 7698 81-12-03 950 30
7934 MILLER CLERK 7782 82-01-23 1300 10
7839 KING PRESIDENT 81-11-17 5000 10
7782 CLARK MANAGER 7839 81-06-09 2450 10

実行計画
----------------------------------------------------------
Plan hash value: 3647789528

--------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Rows | Bytes | Cost (%CPU)| Time | TQ |IN-OUT| PQ Distrib |
--------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 18 | 990 | 7 (29)| 00:00:01 | | | |
| 1 | PX COORDINATOR | | | | | | | | |
| 2 | PX SEND QC (RANDOM) | :TQ10001 | 18 | 990 | 7 (29)| 00:00:01 | Q1,01 | P->S | QC (RAND) |
| 3 | HASH UNIQUE | | 18 | 990 | 7 (29)| 00:00:01 | Q1,01 | PCWP | |
| 4 | PX RECEIVE | | 18 | 990 | 7 (29)| 00:00:01 | Q1,01 | PCWP | |
| 5 | PX SEND HASH | :TQ10000 | 18 | 990 | 7 (29)| 00:00:01 | Q1,00 | P->P | HASH |
| 6 | HASH UNIQUE | | 18 | 990 | 7 (29)| 00:00:01 | Q1,00 | PCWP | |
| 7 | UNION-ALL | | | | | | Q1,00 | PCWP | |
| 8 | PX BLOCK ITERATOR | | 6 | 522 | 2 (0)| 00:00:01 | Q1,00 | PCWC | |
| 9 | TABLE ACCESS FULL| LOCAL_EMP | 6 | 522 | 2 (0)| 00:00:01 | Q1,00 | PCWP