帰ってきた! 標準はあるにはあるが癖の多いSQL #23 - 直近データの結合方法。オールドファッションな構文 vs. モダンな構文。
新しい期間に切り替わり2026.6-2027.5期も Oracle ACE Pro となりました。:) で、新シーズン最初のエントリー。
今日のテーマは、
直近データの結合方法、オールドファッションな構文 vs. モダンな構文。そして、今回の癖はSQL文というよりも、その実行計画や挙動にあります!!!! お楽しみに!
(そういえば、モダンな って表現、外資系方面の会社でよく使われますよね。RDBMS界隈でも。むかーーし、そこが気になり過ぎて眠れなくなったことがありましたw モダンとは、、、みたいなw)
ところで、みなさん、
表にバージョン、タイムスタンプやらで履歴データを含があり、直近のデータとだけ結合したいなんてことないですかね。。。。意外と多いのかなぁ。履歴は履歴だから分離するとうのもあるわけですけども。
データ量にしても、少量データ(OLTP)から大量データ(BATCH、DWH etc)までいろいろです。
と、いろいろなケースはあるのですが、OLTP向けには、どのような構文で行うのがよいだろうというところにフォーカスしたいと思います。
(少量でもそこそこいけるし、大量データでもパラレル化しちゃっていい感じになる方法もあれば、そうでもない方法まであるわけですけども)
今回も面白い癖というか特徴を見ることができますよ。 ;)
データの準備(各データベースに同じ表、索引、データを用意します。以下、PostgreSQLの定義)
perftestdb=> \d+ master
Table "scott.master"
Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description
--------+---------+-----------+----------+---------+---------+-------------+--------------+-------------
id | integer | | not null | | plain | | |
Indexes:
"master_pkey" PRIMARY KEY, btree (id)
Access method: heap
perftestdb=> \d+ detail
Table "scott.detail"
Column | Type | Collation | Nullable | Default | Storage | Compression | Stats target | Description
--------+----------------------+-----------+----------+---------+----------+-------------+--------------+-------------
id | integer | | not null | | plain | | |
vnum | integer | | not null | | plain | | |
col1 | character varying(1) | | | | extended | | |
Indexes:
"pk_detail" PRIMARY KEY, btree (id, vnum)
"ix2_detail" btree (vnum)
Access method: heap
id=200に該当するdetail表のデータは存在しない状態にしてあります。
perftestdb=> SELECT id FROM master ORDER BY id;
id
----
1
2
3
4
5
200
(6 rows)
各id毎のvnumの個数は以下のとおり. id=300は100,000個あります。また直近データは、最もvnumが大きいものという設定です。
OLTPを想定しているので、履歴データがどう影響するかしないのかも見るために多めにしてあります。
perftestdb=> SELECT id,COUNT(vnum) vnum_cout FROM detail GROUP BY id ORDER BY id;
id | vnum_cout
-----+-----------
1 | 3
2 | 1
3 | 100
4 | 98
5 | 98
...略...
97 | 98
98 | 98
99 | 98
100 | 98
300 | 100000
(101 rows)
perftestdb=> SELECT id,vnum,col1 FROM detail WHERE id = 300 ORDER BY vnum DESC FETCH FIRST 10 ROWS ONLY;
id | vnum | col1
-----+--------+------
300 | 100000 | 1
300 | 99999 | 1
300 | 99998 | 1
300 | 99997 | 1
300 | 99996 | 1
300 | 99995 | 1
300 | 99994 | 1
300 | 99993 | 1
300 | 99992 | 1
300 | 99991 | 1
(10 rows)
トップバッターは、Oracle Database、直近データだけ結合する方法は複数ありますが、オールドファッションな書き方から、比較的新しい構文(モダンな構文としておきますw)の順で試していきます。
1) 不等価結合 + 自己結合による方式
(以下はOracle Database向けにUNNEST最適化を抑止する目的でヒントを利用していますが、
他のRDBMSでは取り除いてください)
この方法、稀ですが、OLTPで見かけることがあります。(危険な香りw)
履歴データが少量であることが確実なら、あまり痛い目にあうことはないとは思います、け、ど、も。
どの辺がやばそうかは、いいませんので、考えてみてください。
(考えるまでもねぇ、脊髄反応してるあなた、さすがですね。(最近のAI風w 褒めるとこから入るw)
SELECT
t1.id AS t1id
,t2.id AS t2id
,t2.vnum AS vnum
FROM
master t1
LEFT OUTER JOIN (
SELECT
t2_latest.id
, t2_latest.vnum
FROM
detail t2_latest
WHERE
NOT EXISTS (
SELECT /*+ UNNEST NL_AJ */
1
FROM
detail t2_all
WHERE
t2_latest.id = t2_all.id
AND t2_latest.vnum < t2_all.vnum
)
) t2
ON
t1.id = t2.id
WHERE
t1.id = 300
;
2) MAX()関数 + 自己結合 + スカラー副問合せ方式
この方法もむかーしからよく見ます。王道な方法だと思います。
最適な索引は必須ですが、非常に安定した性能を得られる方法です。
デメリットとして、自己結合が避けられないのと、ぱっと見なにやっているか理解しにくいところかなと。(経験の浅い方は特に!)
SELECT
t1.id AS t1id
,t2.id AS t2id
,t2.vnum AS vnum
FROM
master t1
LEFT OUTER JOIN DETAIL t2
ON
t1.id = t2.id
AND t2.vnum = (
SELECT
MAX(t3.vnum)
FROM
detail t3
WHERE
t3.id = t1.id
)
WHERE
t1.id = 300
;
3) ROW_NUMBER()ウィンドウ関数方式
(分析系クエリではもうお馴染みのROW_NUMBERウインドウ関数)
この方法、OLTPでも比較的多く、履歴データが多くないのが確実ならリスクは少ない方法ではあります。
自己結合も排除できますし、可読性も良いですよね。
しかし、履歴データ多くないのが確実なら、と書いたように一癖あるので、OLTP で利用する場合少々注意が必要です。
(とことが、PostgreSQLとMySQLそれぞれに、想像してなかった癖がありましたw 良い癖と悪い癖w)
履歴データ量が予測できないのなら避けたほうが良い方法だと思います。(癖の良し悪し次第でもありますがw)
逆にバッチ処理や分析系で大量データを処理するなら得意分野だと思いますよね。
この方法の癖とは何か? も考えてみてください。:)
なお、この方法は、オプティマイザやプランナの述語プッシュダウン最適化等に依存してます。(ヒントw
SELECT
t1.id AS t1id
, t2.id AS t2id
, t2.vnum
FROM
master t1
LEFT OUTER JOIN (
SELECT
id
, vnum
, ROW_NUMBER() OVER (
PARTITION BY
detail.id
ORDER BY
detail.id DESC
, detail.vnum DESC
) AS latest
FROM
detail
) t2
ON
t1.id = t2.id
AND t2.latest = 1
WHERE
t1.id = 300
;
4)LATERAL JOIN + Top 1 Query方式
(なお、PostgreSQL/MySQLでも fetch first n rows only 構文は利用できますが、limit nでも同様、Oracle Database では rownum を使っても同じですが、おすすめは、fetch first n rows only かな。長いけどw)
この方法 OLTP のためにあるようなものなので、うまく活用するといいと思うのですよね。
少量データ返すケースで SELECT リストないで利用するスカラー副問合せのような使い方をイメージしてもらうと良いかなと思います。NLJにしかならないので。
自己結合も排除できることに加え、適切な索引によるソートバイパス、そして、Top 1 Query の活用で無駄のない直近データ取得を可能にしています。
SELECT
t1.id AS t1id
, t2.id AS t2id
, t2.vnum
FROM
master t1
LEFT OUTER JOIN LATERAL (
SELECT
t2l.id
, t2l.vnum
FROM
detail t2l
WHERE
t2l.id = t1.id
ORDER BY
t2l.id DESC
, t2l.vnum DESC
FETCH FIRST 1 ROWS ONLY
) t2
ON TRUE
WHERE
t1.id = 300
;
当初、ログ見てもらいつつまとめは後半で、ほう!
みたいな構成で書こうと考えていたのですが、ログがめちゃめちゃ長いので最初にまとめを書いちゃいますねw
(ログを見て、あーだーこーだ考えてもらいつつ、あれがいいよねー、という流れがおもしろいかなーとは思っていたのですけども。。。w 流石に長すぎて疲れるかなとw)
OLTPに適している直近データの結合方法はこれだ!!!!!!
結果としては、各構文の解説でも気づかれたのではないかと思いますが、
LATERAL JOIN + Top 1 Query方式が、OLTPで直近データだけ結合したいという場合には、ベストな選択だろうな。と。
2013年ごろに登場した(内部的にはもっと前から最適化として含まれていたRDBMSもあります)LATERAL JOIN なので、新しいというほどでもないですが、ひとまず、モダンな方法としておきますねw
一方、オールドファッションな方法であるMAX()関数+自己結合+スカラー副問合せ方式もかなり良いわけでが、自己結合だけは避けられません。
ここがポイントです。それなりに安定していて良い結果は得られるのですが。
また、どちらの方法も履歴データが大量だったとしても、必要最小限のデータだけを安定してアクセスします。
自己結合を回避できないMAX()関数+自己結合+スカラー副問合せ方式では、それが弱点ではあるのですが。
なお、実装の違からMySQLだけは、MAX()関数+自己結合+スカラー副問合せ方式のほうが早かったというのは面白い発見でした。(後述)
まとめ
OLTP前提で、直近データを結合するためのおすすめの構文
Oracle Database
モダンな方式である LATERAL JOIN + Top 1 Query 方式
PostgerSQL
モダンな方式である LATERAL JOIN + Top 1 Query 方式
面白い挙動は、 ROW_NUMBER()ウィンドウ関数方式の索引降順スキャンを行い最初の2行目、つまり、直近データを読み終えたところでスキャンを止めること。Oracleには見られない挙動。
MySQLでは、そもそも述語プッシュダウンすらできなかった(不具合?)
MySQL
オールドファッションな方式である MAX()関数 + 自己結合 + スカラー副問合せ方式.
なお、LATERAL JOIN + Top 1 Queryも非常に安定しているが、MAX()関数 + 自己結合 + スカラー副問合せ方式 の実行計画はMySQLの実行計画で最強のRows fetched before executionなので勝ち目なしw
では、判断のポイントとなった履歴データが大量に存在する場合の各RDBMSの実行計画を見ていきましょう。(サマリーといっても長いですw)
以下それぞれ、Oracle Linux / arm64 VirtualBox VM上で
Oracle Database 23ai Free Version 23.8.0.25.04
PostgreSQL 17.7
MySQL 8.4.7
で試したものです。
T1ID T2ID VNUM
---------- ---------- ----------
300 300 100000
のように、履歴が100,000件あり、vnum=100000が最新のデータ(最新バージョン)を外部結合しています。
Oracle Database 23ai Free Version 23.8.0.25.04
MAX()関数 + 自己結合 + スカラー副問合せ方式とLATERAL JOIN + Top 1 Query方式どちらも A-ROWS は 1 で履歴データが多くなってもまったく影響を受けません。
では、どちらがよいかと言えば、detail 表を二度アクセスせず、構文的にも意図を把握しやすい LATERAL のほうがよいですね。
Buffers も LATERAL 方式のほうが少ないですし。
想定通りの結果です。優秀です。
INDEX RANGE SCAN (MIN/MAX) によって最大値(直近データ)を無駄なくアクセスしてはいるのですが、Id=4 で再び detail 表をアクセスせざるを得ないのが弱点。
一方、LATERAL JOIN の方は同じく、索引を有効活用し、ソートをバイパスする INDEX RANGE SCAN DESCENDING の効果と Top 1 となる COUNT STOPKEY によって無駄なく直近データを取得しています。
ひとつ付け加えておくと、MAX()関数 + 自己結合 + スカラー副問合せ方式で書いても、Oracle Databaseのオプティマイザは内部的に LATERAL JOIN へ書き換えているのも見えますよね。 VW_LAT_CDDD7452 というビュー名称から判断できますよ。
2) MAX()関数 + 自己結合 + スカラー副問合せ方式
Plan hash value: 1568514629
-------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 5 |
| 1 | NESTED LOOPS OUTER | | 1 | 1 | 1 |00:00:00.01 | 5 |
|* 2 | INDEX UNIQUE SCAN | PK_MASTER | 1 | 1 | 1 |00:00:00.01 | 1 |
| 3 | VIEW | VW_LAT_CDDD7452 | 1 | 1 | 1 |00:00:00.01 | 4 |
|* 4 | INDEX UNIQUE SCAN | PK_DETAIL | 1 | 1 | 1 |00:00:00.01 | 4 |
| 5 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 2 |
| 6 | FIRST ROW | | 1 | 1 | 1 |00:00:00.01 | 2 |
|* 7 | INDEX RANGE SCAN (MIN/MAX)| PK_DETAIL | 1 | 1 | 1 |00:00:00.01 | 2 |
-------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("T1"."ID"=300)
4 - access("T1"."ID"="T2"."ID" AND "T2"."VNUM"=)
7 - access("T3"."ID"=:B1)
4)LATERAL JOIN + Top 1 Query方式
Plan hash value: 43833377
--------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
--------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 3 |
| 1 | NESTED LOOPS OUTER | | 1 | 1 | 1 |00:00:00.01 | 3 |
|* 2 | INDEX UNIQUE SCAN | PK_MASTER | 1 | 1 | 1 |00:00:00.01 | 1 |
| 3 | VIEW | VW_LAT_6FD14B30 | 1 | 1 | 1 |00:00:00.01 | 2 |
| 4 | VIEW | VW_LAT_A18161FF | 1 | 1 | 1 |00:00:00.01 | 2 |
|* 5 | COUNT STOPKEY | | 1 | | 1 |00:00:00.01 | 2 |
| 6 | VIEW | | 1 | 2 | 1 |00:00:00.01 | 2 |
|* 7 | INDEX RANGE SCAN DESCENDING| PK_DETAIL | 1 | 1085 | 1 |00:00:00.01 | 2 |
--------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("T1"."ID"=300)
5 - filter(ROWNUM<=1)
7 - access("T2L"."ID"="T1"."ID")
PostgreSQL 17.7
PostgreSQLの場合もOracle Database同様に履歴データが100,000行あっても影響はなく、rows=1となっています。結果として、4)LATERAL JOIN + Top 1 Query方式がより軽く、
メモリ消費サイズも少ないことがわかります。!!!!!!
Oracle Database同様ですが、LATERAL JOINでソートをバイパスして最大値(直近データ)を取得している操作はIndex Only Scan Backwardとして現れています。:)
なお、PostgreSQLの場合、実行計画から LATERAL JOIN が行われていることは読み取れません。また、実行計画からは、Oracle Database のように索引をMIN/MAXでアクセスするという直接的なキーワードはありませんが、Index Scan Backward using pk_detail で索引を逆からアクセスすることでMAX()を取得していることを判断できます。このあたりもRDBMS毎の癖のひとつですね。
2) MAX()関数 + 自己結合 + スカラー副問合せ方式
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
Nested Loop Left Join (cost=1.67..17.72 rows=1 width=12) (actual time=0.033..0.035 rows=1 loops=1)
Output: t1.id, t2.id, t2.vnum
Inner Unique: true
-> Index Scan using master_pkey on scott.master t1 (cost=0.13..8.15 rows=1 width=4) (actual time=0.012..0.012 rows=1 loops=1)
Output: t1.id
Index Cond: (t1.id = 300)
-> Index Scan using ix2_detail on scott.detail t2 (cost=1.54..9.56 rows=1 width=8) (actual time=0.005..0.005 rows=1 loops=1)
Output: t2.id, t2.vnum, t2.col1
Index Cond: (t2.vnum = (SubPlan 2))
Filter: (t2.id = 300)
SubPlan 2
-> Result (cost=1.24..1.25 rows=1 width=4) (actual time=0.013..0.013 rows=1 loops=1)
Output: (InitPlan 1).col1
InitPlan 1
-> Limit (cost=0.29..1.24 rows=1 width=4) (actual time=0.011..0.012 rows=1 loops=1)
Output: t3.vnum
-> Index Scan Backward using pk_detail on scott.detail t3 (cost=0.29..1038.04 rows=1096 width=4) (actual time=0.011..0.011 rows=1 loops=1)
Output: t3.vnum
Index Cond: (t3.id = t1.id)
Planning:
Memory: used=78kB allocated=128kB
Planning Time: 0.177 ms
Execution Time: 0.051 ms
(23 rows)
4)LATERAL JOIN + Top 1 Query方式
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------------
Nested Loop Left Join (cost=0.42..8.48 rows=1 width=12) (actual time=0.024..0.025 rows=1 loops=1)
Output: t1.id, t2l.id, t2l.vnum
Buffers: shared hit=5
-> Index Scan using master_pkey on scott.master t1 (cost=0.13..8.15 rows=1 width=4) (actual time=0.012..0.012 rows=1 loops=1)
Output: t1.id
Index Cond: (t1.id = 300)
Buffers: shared hit=2
-> Limit (cost=0.29..0.32 rows=1 width=8) (actual time=0.011..0.011 rows=1 loops=1)
Output: t2l.id, t2l.vnum
Buffers: shared hit=3
-> Index Only Scan Backward using pk_detail on scott.detail t2l (cost=0.29..35.47 rows=1096 width=8) (actual time=0.010..0.010 rows=1 loops=1)
Output: t2l.id, t2l.vnum
Index Cond: (t2l.id = t1.id)
Heap Fetches: 0
Buffers: shared hit=3
Planning:
Memory: used=39kB allocated=64kB
Planning Time: 0.113 ms
Execution Time: 0.037 ms
(19 rows)
MySQL 8.4.7
冒頭でもコメントしたMySQLですが、クラスターインデックスの強みでしょうか? 2) MAX()関数 + 自己結合 + スカラー副問合せ方式 の実行計画が最強すぎる!
4)LATERAL JOIN + Top 1 Query方式も無駄の無い素晴らしい実行計画なので、す、が。。。。w
MySQLに限っては、2) MAX()関数 + 自己結合 + スカラー副問合せ方式の方が良いですよね!
また、LATERAL側も実行計画は悪くないですが、Materialize操作は避けられないようです。
Rows fetched before executionには完敗のLATERAL JOINの実行計画では、他のRDBMS同様に、Covering index lookup on t2l using PRIMARY (id='300') (reverse)に同様のソートをバイパスする索引アクセスが現れています。
2) MAX()関数 + 自己結合 + スカラー副問合せ方式
+-------------------------------------------------------------------------------------------------+
| EXPLAIN |
+-------------------------------------------------------------------------------------------------+
| -> Rows fetched before execution (cost=0..0 rows=1) (actual time=45e-6..90e-6 rows=1 loops=1)
|
+-------------------------------------------------------------------------------------------------+
1 row in set, 1 warning (0.00 sec)
mysql> show warnings;
+-------+------+------------------------------------------------------------------------------+
| Level | Code | Message |
+-------+------+------------------------------------------------------------------------------+
| Note | 1276 | Field or reference 'perftestdb.t1.id' of SELECT #2 was resolved in SELECT #1 |
+-------+------+------------------------------------------------------------------------------+
1 row in set (0.00 sec)
4)LATERAL JOIN + Top 1 Query方式
+--------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+--------------------------------------------------------------------------------------------------------------------------------------------+
| -> Nested loop left join (cost=2.73 rows=2) (actual time=0.105..0.106 rows=1 loops=1)
-> Rows fetched before execution (cost=0..0 rows=1) (actual time=90e-6..135e-6 rows=1 loops=1)
-> Table scan on t2 (cost=2.73..2.73 rows=1) (actual time=0.102..0.102 rows=1 loops=1)
-> Materialize (cost=105..105 rows=2) (actual time=0.101..0.101 rows=1 loops=1)
-> Limit: 1 row(s) (cost=105 rows=1) (actual time=0.0914..0.0914 rows=1 loops=1)
-> Covering index lookup on t2l using PRIMARY (id='300') (reverse) (cost=105 rows=1042) (actual time=0.0906..0.0906 rows=1 loops=1)
|
+--------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set, 1 warning (0.02 sec)
mysql> show warnings;
+-------+------+------------------------------------------------------------------------------+
| Level | Code | Message |
+-------+------+------------------------------------------------------------------------------+
| Note | 1276 | Field or reference 'perftestdb.t1.id' of SELECT #2 was resolved in SELECT #1 |
+-------+------+------------------------------------------------------------------------------+
1 row in set (0.00 sec)
以下は、検証ログです。長いのでコーヒーとかお酒でも飲みながら見ていただければ :) ながーーーーーーーーーーーいのでw
SCOTT@localhost:1521/freepdb1> select banner_full from v$version;
BANNER_FULL
--------------------------------------------------------------------------------
Oracle Database 23ai Free Release 23.0.0.0.0 - Develop, Learn, and Run for Free
Version 23.8.0.25.04
オールドファッションな方式の中でも性能リスクの高い書き方
(OLTP想定なので結合方法はNLJとなるようにオプティマイザヒントで調整しています。見ての通りなにも指定しないと相関副問合せをUnestとしてHash Joinしてしまうので。それはそれで性能影響ではあるのですけども。NLJに倒したとしても影響がでるよというところから見ておきましょう)
Oracle Database / 不等価相関条件を利用した相関副問合せ方式
オールドファッションな方式の中でも性能リスクも高い書き方です。検証するまでもないのですがあえて加えておきました。
相関条件 t2_latest.vnum < t2_all.vnum が曲者ですね。
自己結合した結果で自行より大きなvnumを持つデータがないことを条件にしています。不等価結合なので見た目でリスクを感じとれたら、掴みはオーケーですw
ほかにも無駄な点があります。
detail表を二度アクセスしてしまうことになるリスクですね。
自己結合は排除できるなら排除したいところですが、この構文を選択した時点で逃げ道はありません。
オプティマイザはかなり進化していて内部で最適化のために種々の書き換えを行えるようになってきてはいますが、こいつについては、そのまま実行計画に現れます。
なお、Oracle Databaseは、相関副問合せをUnnestして結合しちゃたりする最適化が発動してしまいOLTP向きのNested Loop Anti Joinになってくれない(データ量がおおいので)ため、オプティマイザヒント、/*+ UNNEST NL_AJ */を付加してNested Loops Anti Joinを強制しています。
SELECT
t1.id AS t1id
,t2.id AS t2id
,t2.vnum AS vnum
FROM
master t1
LEFT OUTER JOIN (
SELECT
t2_latest.id
, t2_latest.vnum
FROM
detail t2_latest
WHERE
NOT EXISTS (
SELECT /*+ UNNEST NL_AJ */
1
FROM
detail t2_all
WHERE
t2_latest.id = t2_all.id
AND t2_latest.vnum < t2_all.vnum
)
) t2
ON
t1.id = t2.id
WHERE
t1.id = 3
;
SCOTT@localhost:1521/freepdb1> /
T1ID T2ID VNUM
---------- ---------- ----------
3 3 100
経過: 00:00:00.00
比較用に /*+ UNNEST NL_AJ */ がない場合の実行計画です。NOT EXISTS なので ANTI JOIN ではありますが、Hash Join となっています。OLTP では避けたいですよね
この例では運良く Index Only Scan で表へのアクセスはないですが、表へのアクセスが回避できないような索引しかなかったら影響はもっと大きくなるのは言うまでもありません。
PK_DETAIL 索引が HASH JONI RIGHT ANTI で結合され、2度アクセスされている箇所がポイントです。
この方法の最大の弱点は、履歴データの多さの影響を受けてしまうというところですよね。 A-ROWS を見るとはっきり見えますよね。
Plan hash value: 1385316136
----------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
----------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 5 | | | |
| 1 | NESTED LOOPS OUTER | | 1 | 100 | 1 |00:00:00.01 | 5 | | | |
|* 2 | INDEX UNIQUE SCAN | PK_MASTER | 1 | 1 | 1 |00:00:00.01 | 1 | | | |
| 3 | VIEW | | 1 | 100 | 1 |00:00:00.01 | 4 | | | |
|* 4 | HASH JOIN ANTI | | 1 | 100 | 1 |00:00:00.01 | 4 | 2078K| 2078K| 718K (0)|
|* 5 | INDEX RANGE SCAN| PK_DETAIL | 1 | 100 | 100 |00:00:00.01 | 2 | | | |
|* 6 | INDEX RANGE SCAN| PK_DETAIL | 1 | 100 | 100 |00:00:00.01 | 2 | | | |
----------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("T1"."ID"=3)
4 - access("T2_LATEST"."ID"="T2_ALL"."ID")
filter("T2_LATEST"."VNUM"<"T2_ALL"."VNUM")
5 - access("T2_LATEST"."ID"=3)
6 - access("T2_ALL"."ID"=3)
Note
-----
- this is an adaptive plan
/*+ UNNEST NL_AJ */ヒントを利用して、相関副問合せを Unnest, Hash Join する最適化を抑止しました。結果は、Nested Loops Anti Join となり、OLTP には良いですね。
Hash Join のメモリー関連オーバーヘッドもないですし。
とはいえ、PK_DETAI L索引を2回アクセスするオーバーヘッド(少しですけどw)は気になります。Index Only Scan とはいえ。この方法では避けようがないのですけども。
この方法の最大の弱点である履歴データの量に影響を受けてしまうという点は HASH JOIN でも NLJ でも同じ。
A-ROWS は重要なので見忘れることのないようにね! この例では履歴が100行ありますが、読んじゃってますよね。全て。
Plan hash value: 101384627
-------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 12 |
| 1 | NESTED LOOPS OUTER | | 1 | 100 | 1 |00:00:00.01 | 12 |
|* 2 | INDEX UNIQUE SCAN | PK_MASTER | 1 | 1 | 1 |00:00:00.01 | 1 |
| 3 | VIEW | | 1 | 100 | 1 |00:00:00.01 | 11 |
| 4 | NESTED LOOPS ANTI| | 1 | 100 | 1 |00:00:00.01 | 11 |
|* 5 | INDEX RANGE SCAN| PK_DETAIL | 1 | 100 | 100 |00:00:00.01 | 3 |
|* 6 | INDEX RANGE SCAN| PK_DETAIL | 100 | 1 | 99 |00:00:00.01 | 8 |
-------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("T1"."ID"=3)
5 - access("T2_LATEST"."ID"=3)
6 - access("T2_ALL"."ID"=3 AND "T2_LATEST"."VNUM"<"T2_ALL"."VNUM")
Oracle Database / MAX()関数を利用したスカラー副問合せ方式
この方法も昔からある古いタイプの方法です。
detail 表を2度アクセスする点は同じですが、不等価結合を排除し広範囲のデータアクセスを行わないのがポイント。(適切な索引が必須です)
適切な索引によって MAX() 関数が INDEX RANGE SCAN (MIN/MAX) でソートなしで索引から直近データへアクセスします。
id,vnum からなる主キー索引から vnum の最大値を持つ索引のリーフノードをアクセスするだけ。ソート処理もバイパスされます。
SELECT
t1.id AS t1id
,t2.id AS t2id
,t2.vnum AS vnum
FROM
master t1
LEFT OUTER JOIN detail t2
ON
t1.id = t2.id
AND t2.vnum = (
SELECT
MAX(t3.vnum)
FROM
detail t3
WHERE
t3.id = t1.id
)
WHERE
t1.id = 3
;
SCOTT@localhost:1521/freepdb1> /
T1ID T2ID VNUM
---------- ---------- ----------
3 3 100
経過: 00:00:00.01
実行計画上のポイントは、Id=6,7 の部分ですね。運良く Index only scan となる主キー索引があり、かつ INDEX RANGE SCAN (MIN/MAX) となっています。ただし、この方式の弱点でる PK_DETAIL 索引の二度読みは回避できません。
鋭い方は気づいたと思いますが、相関副問い合わせではあるのですが、内部的には、LATERAL JOIN へ変換されています。ここ今回のネタの後半でも出てくるので覚えておいてください。
この場合、不等価結合を行なっていた場合とは大きく異なり、全てが ヒントなしでも、Nested Loops Join になります。 その挙動がLATERAL JOIN が OLTP と相性がよい理由です。逆に大量データは苦手なわけですが。
Plan hash value: 1568514629
-------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 5 |
| 1 | NESTED LOOPS OUTER | | 1 | 1 | 1 |00:00:00.01 | 5 |
|* 2 | INDEX UNIQUE SCAN | PK_MASTER | 1 | 1 | 1 |00:00:00.01 | 1 |
| 3 | VIEW | VW_LAT_CDDD7452 | 1 | 1 | 1 |00:00:00.01 | 4 |
|* 4 | INDEX UNIQUE SCAN | PK_DETAIL | 1 | 1 | 1 |00:00:00.01 | 4 |
| 5 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 2 |
| 6 | FIRST ROW | | 1 | 1 | 1 |00:00:00.01 | 2 |
|* 7 | INDEX RANGE SCAN (MIN/MAX)| PK_DETAIL | 1 | 1 | 1 |00:00:00.01 | 2 |
-------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("T1"."ID"=3)
4 - access("T1"."ID"="T2"."ID" AND "T2"."VNUM"=)
7 - access("T3"."ID"=:B1)
Oracle Database / ROW_NUMBER()ウィンドウ関数方式
さて、三番手。最近はよく見かける ROW_NUMBER() ウィンドウ関数を利用する方法で、述語プッシュダウンによる最適化も期待した方法ですね。
ウインドウ関数を適用するためにインラインビュー化した detail 表のアクセスは1回のみ。
前述の2つの方法にくらべ detail 表へのアクセスが削減され、意図が読み取りやすいというメリットもあります。
ちなみに、
このウィンドウ関数ですは、Oracle Database 8i (1999年)ごろからサポートされているのでまったく新しいという感覚は全くないのですが、他のRDBMSをみると、
- PostgreSQL 8.4 (2009年)
- MySQL 8.0 (2018)
MySQLだけは、比較的最近サポートされました。(とはいえ8年ほど前ですが)
ソートが必要になるのでその影響などを索引アクセスで削減するのもポイントになります。とはいえ、バッチ等大量データ処理前提の場面では強力なオプションではありますね。
一点、ROW_NUMBER()ウィンドウ関数には、OLTP向きとは言えない癖とうか弱点あります。><
以下実行計画の A-ROWS で気づくと思いますが、履歴データの量が性能に影響します。
逆にいえば、履歴データの量が十分少ないままなのであれば、そこそこの性能を維持できるとも言えます。 履歴データが100行あるテストケースで、100行全て読み込んでランキングづけしてから1行をフィルタリングして取り出すという挙動になっているのに気づくはずです。索引の効果でソートはバイパスしていますが、ランキングするさいに追加のメモリ消費オーバーヘッドもあります。
SELECT
t1.id AS t1id
, t2.id AS t2id
, t2.vnum
FROM
master t1
LEFT OUTER JOIN (
SELECT
id
, vnum
, ROW_NUMBER() OVER (
PARTITION BY
detail.id
ORDER BY
detail.id DESC
, detail.vnum DESC
) AS latest
FROM
detail
) t2
ON
t1.id = t2.id
AND t2.latest = 1
WHERE
t1.id = 3
;
SCOTT@localhost:1521/freepdb1> /
T1ID T2ID VNUM
---------- ---------- ----------
3 3 100
経過: 00:00:00.01
Plan hash value: 4287976176
---------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
---------------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 3 | | | |
| 1 | NESTED LOOPS OUTER | | 1 | 100 | 1 |00:00:00.01 | 3 | | | |
|* 2 | INDEX UNIQUE SCAN | PK_MASTER | 1 | 1 | 1 |00:00:00.01 | 1 | | | |
|* 3 | VIEW | | 1 | 100 | 1 |00:00:00.01 | 2 | | | |
|* 4 | WINDOW BUFFER PUSHED RANK | | 1 | 100 | 1 |00:00:00.01 | 2 | 6144 | 6144 | 6144 (0)|
|* 5 | INDEX RANGE SCAN DESCENDING| PK_DETAIL | 1 | 100 | 100 |00:00:00.01 | 2 | | | |
---------------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("T1"."ID"=3)
3 - filter("T2"."LATEST"=1)
4 - filter(ROW_NUMBER() OVER ( PARTITION BY "DETAIL"."ID" ORDER BY "DETAIL"."VNUM" DESC )<=1)
5 - access("ID"=3)
Oracle Database / LATERAL JOIN + Top 1 Query方式
さて、最後は、LATERAL JOIN を利用する方法です。
左相関にすることで確実にNLJになります。ヒントはなくてもNLJですからね。OLTPには嬉しいです。適切な索引は必須ですけども。
感覚的な使い所としては、少量のデータだけが帰る前提で、SELECT リスト内でスカラー副問合せを利用することが有利な場面に近いイメージでしょうか。
意図ぜず大量データの結合で内部的に書き換えられて発動して性能でねーーーーーーとうこともあるのでその辺りは利用シーンを考えて使うのがよいと思います。
今回のネタのように OLTP で結合が、NLJ になるのがベストですよね。
なお、LATERAL JOIN を記述した場合や内部的に LATERAL JOIN に書き換えられている場合、VW_LAT_* というVIEW名称が付与されます。
このVIEW名称をみたら LATERAL JOIN だ! と思えば間違いないです!
ちなみに、
Oracle Database で LATERAL JOIN がサポートされたのが 2013年(12c)
PostgreSQL も 2013年(PostgreSQL 9.3)
MySQLは、2019年(MySQL 8.0.14) なので、
モダンといっても Oracle/PostgreSQL では 13 年以上前ですw。
部分的なサポートを含めるともっと古いわけですが。
直近データを結合する OLTP で LATERAL JOIN を見かけた記憶がほぼないのは、私が関わってる現場が偏ってるとか言う話だけでも無いような気もしますw (大量データ処理でパラレル化が必要だとか言う場合にはウィンドウ関数のほうが向いているわけですけども)
SELECT
t1.id AS t1id
, t2.id AS t2id
, t2.vnum
FROM
master t1
LEFT OUTER JOIN LATERAL (
SELECT
t2l.id
, t2l.vnum
FROM
detail t2l
WHERE
t2l.id = t1.id
ORDER BY
t2l.id DESC
, t2l.vnum DESC
FETCH FIRST 1 ROWS ONLY
) t2
ON TRUE
WHERE
t1.id = 3
;
T1ID T2ID VNUM
---------- ---------- ----------
3 3 100
経過: 00:00:00.00
Plan hash value: 43833377
--------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
--------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 3 |
| 1 | NESTED LOOPS OUTER | | 1 | 1 | 1 |00:00:00.01 | 3 |
|* 2 | INDEX UNIQUE SCAN | PK_MASTER | 1 | 1 | 1 |00:00:00.01 | 1 |
| 3 | VIEW | VW_LAT_6FD14B30 | 1 | 1 | 1 |00:00:00.01 | 2 |
| 4 | VIEW | VW_LAT_A18161FF | 1 | 1 | 1 |00:00:00.01 | 2 |
|* 5 | COUNT STOPKEY | | 1 | | 1 |00:00:00.01 | 2 |
| 6 | VIEW | | 1 | 2 | 1 |00:00:00.01 | 2 |
|* 7 | INDEX RANGE SCAN DESCENDING| PK_DETAIL | 1 | 1085 | 1 |00:00:00.01 | 2 |
--------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("T1"."ID"=3)
5 - filter(ROWNUM<=1)
7 - access("T2L"."ID"="T1"."ID")
ということで、次は同じことをPostgreSQLで試してみましょう。
perftestdb=> select version();
version
---------------------------------------------------------------------------------------------------------------
PostgreSQL 17.7 on aarch64-unknown-linux-gnu, compiled by gcc (GCC) 8.5.0 20210514 (Red Hat 8.5.0-28), 64-bit
(1 row)
データ量が少なめなので pg_hint_plan で索引を利用するよう強制しています。
PostgreSQL / 不等価相関条件を利用した相関副問合せ方式
実行計画を見ると Oracle Database の場合と同様、他の方式にくらべ明らかにまずいところがありますが、気付きましたか?
rows=100 となっており detail 表から id に該当する履歴データ全てを読み込んでいます!
その後、マテリアライズされてる部分のデータ量によっては重くなる可能性があります。
なんてこった!
と言う感じですよね。この方法は避けるべきだというのがよくわかる部分だと思います。
SELECT /*+ IndexScan(t1) IndexScan(t2_latest) IndexScan(t2_all) */
t1.id AS t1id
,t2.id AS t2id
,t2.vnum AS vnum
FROM
master t1
LEFT OUTER JOIN (
SELECT
t2_latest.id
, t2_latest.vnum
FROM
detail t2_latest
WHERE
NOT EXISTS (
SELECT
1
FROM
detail t2_all
WHERE
t2_latest.id = t2_all.id
AND t2_latest.vnum < t2_all.vnum
)
) t2
ON
t1.id = t2.id
WHERE
t1.id = 3
;
t1id | t2id | vnum
------+------+------
3 | 3 | 100
(1 row)
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------
Nested Loop Left Join (cost=0.72..518.48 rows=75 width=12) (actual time=0.477..0.478 rows=1 loops=1)
Output: t1.id, t2_latest.id, t2_latest.vnum
-> Index Scan using master_pkey on scott.master t1 (cost=0.13..8.15 rows=1 width=4) (actual time=0.012..0.012 rows=1 loops=1)
Output: t1.id
Index Cond: (t1.id = 3)
-> Nested Loop Anti Join (cost=0.58..509.58 rows=75 width=8) (actual time=0.464..0.464 rows=1 loops=1)
Output: t2_latest.id, t2_latest.vnum
Join Filter: (t2_latest.vnum < t2_all.vnum)
Rows Removed by Join Filter: 5050
-> Index Scan using pk_detail on scott.detail t2_latest (cost=0.29..190.52 rows=113 width=8) (actual time=0.007..0.015 rows=100 loops=1)
Output: t2_latest.id, t2_latest.vnum, t2_latest.col1
Index Cond: (t2_latest.id = 3)
-> Materialize (cost=0.29..191.09 rows=113 width=8) (actual time=0.000..0.002 rows=51 loops=100)
Output: t2_all.id, t2_all.vnum
-> Index Scan using pk_detail on scott.detail t2_all (cost=0.29..190.52 rows=113 width=8) (actual time=0.004..0.015 rows=100 loops=1)
Output: t2_all.id, t2_all.vnum
Index Cond: (t2_all.id = 3)
Planning:
Memory: used=63kB allocated=128kB
Planning Time: 0.144 ms
Execution Time: 0.493 ms
(21 rows)
PostgreSQL / MAX()関数を利用したスカラー副問合せ方式
detail を 2 度読みしている点は不統合を利用した場合と同じですが、読み込んでいるデータ量には圧倒的な差があります。
rows=1 loops=1 となり 1/100 の量です。
これは履歴データ数に応じて変化するため履歴データが多ければ多いほど顕著な差となって現れます。
個人て的には可読性としてはイマイチだと思っていますが、性能的には非常に安定している思います。
なお、適切な索引の作成漏れに注意が必要なのは言うまでもありません。Index Scan Backward using pk_detail on scott.detail でソート不要で索引を逆読みし、Lminit 1で直近データ(最大値)を取得しています。この部分が無駄なデータへアクセスしないための肝です。
SELECT /*+ IndexScan(t1) IndexScan(t2) IndexScan(t3) */
t1.id AS t1id
,t2.id AS t2id
,t2.vnum AS vnum
FROM
master t1
LEFT OUTER JOIN detail t2
ON
t1.id = t2.id
AND t2.vnum = (
SELECT
MAX(t3.vnum)
FROM
detail t3
WHERE
t3.id = t1.id
)
WHERE
t1.id =
3
;
t1id | t2id | vnum
------+------+------
3 | 3 | 100
(1 row)
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
Nested Loop Left Join (cost=1.67..17.72 rows=1 width=12) (actual time=0.058..0.059 rows=1 loops=1)
Output: t1.id, t2.id, t2.vnum
Inner Unique: true
-> Index Scan using master_pkey on scott.master t1 (cost=0.13..8.15 rows=1 width=4) (actual time=0.012..0.013 rows=1 loops=1)
Output: t1.id
Index Cond: (t1.id = 3)
-> Index Scan using ix2_detail on scott.detail t2 (cost=1.54..9.56 rows=1 width=8) (actual time=0.013..0.013 rows=1 loops=1)
Output: t2.id, t2.vnum, t2.col1
Index Cond: (t2.vnum = (SubPlan 2))
Filter: (t2.id = 3)
SubPlan 2
-> Result (cost=1.24..1.25 rows=1 width=4) (actual time=0.010..0.011 rows=1 loops=1)
Output: (InitPlan 1).col1
InitPlan 1
-> Limit (cost=0.29..1.24 rows=1 width=4) (actual time=0.009..0.009 rows=1 loops=1)
Output: t3.vnum
-> Index Scan Backward using pk_detail on scott.detail t3 (cost=0.29..1038.04 rows=1096 width=4) (actual time=0.008..0.008 rows=1 loops=1)
Output: t3.vnum
Index Cond: (t3.id = t1.id)
Planning:
Memory: used=78kB allocated=128kB
Planning Time: 0.166 ms
Execution Time: 0.077 ms
(23 rows)
PostgreSQL / ROW_NUMBER()ウィンドウ関数方式
PostgreSQLのROW_NUMBER()とランキングづけした 1行目の取得、つまり最大値(直近データ)取得のロジックは、Oracle Databaesのウィンドウ関数の挙動はと異なることに気づきます!!!!
癖(良い意味でw)強い!!
索引を降順に読み込みソートはバイパスしているわけですから、索引エントリー 2 行目でスキャンを打ち切ったことが読み取れます!!!!! まじか!
Oracle Databaseの挙動とは違うプランナーの挙動です。
rows=2 loops=1 と 2行読んで、これ以上索引を降順に読むのは無意味!として止めて結果を返しています。すげえ。この最適化によりPostgreSQL / MAX()関数を利用したスカラー副問合せ方式より良い結果が得られています。PostgreSQLでは、この方法を利用してもケガはしなそうです。ベストではないにしても。
また、detail 表を 1 度しかアクセスしないので、その分のコスト削減と可読性含めて古い方式より良いですよね。
SELECT /*+ IndexScan(t1) IndexScan(detail) */
t1.id AS t1id
, t2.id AS t2id
, t2.vnum
FROM
master t1
LEFT OUTER JOIN (
SELECT
id
, vnum
, ROW_NUMBER() OVER (
PARTITION BY
detail.id
ORDER BY
detail.id DESC
, detail.vnum DESC
) AS latest
FROM
detail
) t2
ON
t1.id = t2.id
AND t2.latest = 1
WHERE
t1.id = 3
;
t1id | t2id | vnum
------+------+------
3 | 3 | 100
(1 row)
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------------------
Nested Loop Left Join (cost=2.13..202.07 rows=1 width=12) (actual time=0.031..0.033 rows=1 loops=1)
Output: t1.id, t2.id, t2.vnum
Buffers: shared hit=5
-> Index Scan using master_pkey on scott.master t1 (cost=0.13..8.15 rows=1 width=4) (actual time=0.015..0.016 rows=1 loops=1)
Output: t1.id
Index Cond: (t1.id = 3)
Buffers: shared hit=2
-> Subquery Scan on t2 (cost=1.99..193.91 rows=1 width=8) (actual time=0.014..0.015 rows=1 loops=1)
Output: t2.id, t2.vnum, t2.latest
Filter: (t2.latest = 1)
Buffers: shared hit=3
-> WindowAgg (cost=1.99..192.50 rows=113 width=16) (actual time=0.013..0.014 rows=1 loops=1)
Output: detail.id, detail.vnum, row_number() OVER (?)
Run Condition: (row_number() OVER (?) <= 1)
Buffers: shared hit=3
-> Index Scan Backward using pk_detail on scott.detail (cost=0.29..190.52 rows=113 width=8) (actual time=0.009..0.009 rows=2 loops=1)
Output: detail.id, detail.vnum
Index Cond: (detail.id = 3)
Buffers: shared hit=3
Planning:
Memory: used=48kB allocated=64kB
Planning Time: 0.144 ms
Execution Time: 0.055 ms
(23 rows)
PostgreSQL / LATERAL JOIN + Top 1 Query方式
rows=1 loops=1 で MAX()関数を利用した方法と同じですが、ここでも detail 表を 1度しかアクセスしないという効果もあり OLTP としては最高にシンプルな NLJ の実行計画になっています。
メモリサイズも小さいですよね。
OLTP には嬉しいことばかりだと思います。
また、可読性も非常に高いと思います。ぱっと見でも、なにをやりたいのか意図を容易に把握できます。
この構文は LATERAL がサポートされたことによるものなので使えるシーンでは積極的につかったほうが良いと思います。
ただし、Oracle Database のように実行計画から LATERAL JOIN であることを判断するのはむずしそうですね。いずれ拡張されるかもしれないですが。。
SELECT /*+ IndexScan(t1) IndexScan(detail) */
t1.id AS t1id
, t2.id AS t2id
, t2.vnum
FROM
master t1
LEFT OUTER JOIN LATERAL (
SELECT
t2l.id
, t2l.vnum
FROM
detail t2l
WHERE
t2l.id = t1.id
ORDER BY
t2l.id DESC
, t2l.vnum DESC
LIMIT 1
) t2
ON TRUE<br>WHERE
t1.id = 3
;
t1id | t2id | vnum
------+------+------
3 | 3 | 100
(1 row)
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------------
Nested Loop Left Join (cost=0.42..8.48 rows=1 width=12) (actual time=0.036..0.037 rows=1 loops=1)
Output: t1.id, t2l.id, t2l.vnum
Buffers: shared hit=5
-> Index Scan using master_pkey on scott.master t1 (cost=0.13..8.15 rows=1 width=4) (actual time=0.013..0.014 rows=1 loops=1)
Output: t1.id
Index Cond: (t1.id = 3)
Buffers: shared hit=2
-> Limit (cost=0.29..0.32 rows=1 width=8) (actual time=0.020..0.021 rows=1 loops=1)
Output: t2l.id, t2l.vnum
Buffers: shared hit=3
-> Index Only Scan Backward using pk_detail on scott.detail t2l (cost=0.29..35.47 rows=1096 width=8) (actual time=0.020..0.020 rows=1 loops=1)
Output: t2l.id, t2l.vnum
Index Cond: (t2l.id = t1.id)
Heap Fetches: 0
Buffers: shared hit=3
Planning:
Memory: used=39kB allocated=64kB
Planning Time: 0.116 ms
Execution Time: 0.049 ms
(19 rows)
最後にMySQLです。
比較しているrdbms中、LATERALやウィンドウ関数のサポートは比較的最近で、それまでは昔からある方法でしか対応できなかったMySQLなのですが、8.0以降一気にサポート範囲が広がりましたね。
mysql> select version(); +-----------+ | version() | +-----------+ | 8.4.7 | +-----------+ 1 row in set (0.00 sec)
MySQL / 不等価相関条件を利用した相関副問合せ方式
rows=100 loops=1 で PostgreSQL や Oracle Database と同じ傾向が見られます。この方法は避けたほうがよいです。 大切なので太字にしておきましたw
master表こそ、Rows fetched before executionというMySQLでは最速の操作にはなっていますが、detail表のアクセスは2回で履歴データを全て読み込んでいます。
SELECT
t1.id AS t1id
,t2.id AS t2id
,t2.vnum AS vnum
FROM
master t1
LEFT OUTER JOIN (
SELECT
t2_latest.id
, t2_latest.vnum
FROM
detail t2_latest
WHERE
NOT EXISTS (
SELECT
1
FROM
detail t2_all
WHERE
t2_latest.id = t2_all.id
AND t2_latest.vnum < t2_all.vnum
)
) t2
ON
t1.id = t2.id
WHERE
t1.id = 3
;
+------+------+------+
| t1id | t2id | vnum |
+------+------+------+
| 3 | 3 | 100 |
+------+------+------+
1 row in set (0.00 sec)
+-------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+-------------------------------------------------------------------------------------------------------------------------------------------+
| -> Nested loop left join (cost=1001 rows=10000) (actual time=1.19..1.19 rows=1 loops=1)
-> Rows fetched before execution (cost=0..0 rows=1) (actual time=46e-6..91e-6 rows=1 loops=1)
-> Nested loop antijoin (cost=1130 rows=10000) (actual time=1.19..1.19 rows=1 loops=1)
-> Covering index lookup on t2_latest using PRIMARY (id=3) (cost=11.2 rows=100) (actual time=0.0103..0.0182 rows=100 loops=1)
-> Filter: (t2_latest.vnum < t2_all.vnum) (cost=129 rows=100) (actual time=0.0116..0.0116 rows=0.99 loops=100)
-> Covering index lookup on t2_all using PRIMARY (id=3) (cost=129 rows=100) (actual time=0.00687..0.00981 rows=51.5 loops=100)
|
+-------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set, 2 warnings (0.00 sec)
mysql> show warnings;
+-------+------+---------------------------------------------------------------------------------------+
| Level | Code | Message |
+-------+------+---------------------------------------------------------------------------------------+
| Note | 1276 | Field or reference 'perftestdb.t2_latest.id' of SELECT #3 was resolved in SELECT #2 |
| Note | 1276 | Field or reference 'perftestdb.t2_latest.vnum' of SELECT #3 was resolved in SELECT #2 |
+-------+------+---------------------------------------------------------------------------------------+
2 rows in set (0.00 sec)
MySQL / MAX()関数を利用したスカラー副問合せ方式
特にコメントすることもないのですが、これ、MySQL独特の挙動で、めちゃ早いやつですね!
(さすが) この挙動は履歴データが増加しても影響なしです。
SELECT
t1.id AS t1id
,t2.id AS t2id
,t2.vnum AS vnum
FROM
master t1
LEFT OUTER JOIN detail t2
ON
t1.id = t2.id
AND t2.vnum = (
SELECT
MAX(t3.vnum)
FROM
detail t3
WHERE
t3.id = t1.id
)
WHERE
t1.id = 3
;
+------+------+------+
| t1id | t2id | vnum |
+------+------+------+
| 3 | 3 | 100 |
+------+------+------+
1 row in set (0.01 sec)
+-----------------------------------------------------------------------------------------+
| EXPLAIN |
+-----------------------------------------------------------------------------------------+
| -> Rows fetched before execution (cost=0..0 rows=1) (actual time=0..0 rows=1 loops=1)
|
+-----------------------------------------------------------------------------------------+
1 row in set, 1 warning (0.00 sec)
mysql> show warnings;
+-------+------+------------------------------------------------------------------------------+
| Level | Code | Message |
+-------+------+------------------------------------------------------------------------------+
| Note | 1276 | Field or reference 'perftestdb.t1.id' of SELECT #2 was resolved in SELECT #1 |
+-------+------+------------------------------------------------------------------------------+
1 row in set (0.00 sec)
MySQL / ROW_NUMBER()ウィンドウ関数方式
おっと!
MySQLのROW_NUMBER()ウィンドウ関数によるランキングづけとTop 1をフィルタリングには特徴的な挙動がありますね。癖なのか不具合なのか、微妙ですが。
rows=109610 loops=1 とあることから、結合条件となる id列を述語プッシュダウン最適化でビュー内部に押し込めてないため、Index only scanではありますが索引全走査し、全行取得しまっているようです。興味深い。
Covering index scan on detail using PRIMARY というアクセスはよいのですがid = 3で絞ってくれない(述語プッシュダウン頼りの構文なので、プッシュダウンできないとこうなっちゃいます。この方法オプティマイザの機能依存なので。。。)
たとえば、PostgreSQL の実行計画から同じ操作をみると以下のように id=3 がプッシュダウンされ索引アクセスのがわかります。
-> Index Scan Backward using pk_detail on scott.detail (cost=0.29..190.52 rows=113 width=8) (actual time=0.009..0.009 rows=2 loops=1)
Output: detail.vnum, detail.id
Index Cond: (detail.id = 3)
Oracle Databaseの実行計画では以下の部分から判断できますが、MySQLの場合はありません。
|* 5 | INDEX RANGE SCAN DESCENDING| PK_DETAIL | 1085 | 8680 | 4 (0)| 00:00:01 |
--------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
...略...
5 - access("ID"=3)
しかも、索引アクセスは revese にはなっていません!!!! その影響で、ソートが行われ、マテリアライズさらた後に 1rowに絞られています。まじか!
SELECT
t1.id AS t1id
, t2.id AS t2id
, t2.vnum
FROM
master t1
LEFT OUTER JOIN (
SELECT
id
, vnum
, ROW_NUMBER() OVER (
PARTITION BY
detail.id
ORDER BY
detail.id DESC
, detail.vnum DESC
) AS latest
FROM
detail
) t2
ON
t1.id = t2.id
AND t2.latest = 1
WHERE
t1.id = 3
;
+------+------+------+
| t1id | t2id | vnum |
+------+------+------+
| 3 | 3 | 100 |
+------+------+------+
1 row in set (0.11 sec)
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| -> Nested loop left join (cost=10946 rows=109439) (actual time=126..126 rows=1 loops=1)
-> Rows fetched before execution (cost=0..0 rows=1) (actual time=45e-6..180e-6 rows=1 loops=1)
-> Index lookup on t2 using(id=3, latest=1) (cost=10944..10947 rows=10) (actual time=126..126 rows=1 loops=1)
-> Materialize (cost=10944..10944 rows=109439) (actual time=126..126 rows=109610 loops=1)
-> Window aggregate: row_number() OVER (PARTITION BY detail.id ORDER BY detail.id desc,detail.vnum desc ) (cost=0 rows=109439) (actual time=28.2..43.4 rows=109610 loops=1)
-> Sort: detail.id, detail.id DESC, detail.vnum DESC (cost=11000 rows=109439) (actual time=28.2..32.2 rows=109610 loops=1)
-> Covering index scan on detail using PRIMARY (cost=11000 rows=109439) (actual time=0.621..11.3 rows=109610 loops=1)
|
+-----------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.12 sec)
MySQL / LATERAL JOIN + Top 1 Query方式
どうなるでしょう。ドキドキ
Materializeがでてますね。MySQLのオプティマイザの特徴なのでしかたなし、という部分です。とはいえ、1rowだけなので影響は軽微ではあるように見えます。
Covering index lookup on t2l using PRIMARY (id='3') (reverse) としっかり id=3で降順に索引をアクセスし、ソート処理をバイパスしています。:)
その後、Top 1 を返したところで終了。Master表に関しては、Rows fetched before execution という最強の操作になっています。.
ただし、MySQL / MAX()関数を利用したスカラー副問合せ方式には勝てませんでしたw 苦笑いw
SELECT
t1.id AS t1id
, t2.id AS t2id
, t2.vnum
FROM
master t1
LEFT OUTER JOIN LATERAL (
SELECT
t2l.id
, t2l.vnum
FROM
detail t2l
WHERE
t2l.id = t1.id
ORDER BY
t2l.id DESC
, t2l.vnum DESC
LIMIT 1
) t2
ON TRUE
WHERE
t1.id = 3
;
+------+------+------+
| t1id | t2id | vnum |
+------+------+------+
| 3 | 3 | 100 |
+------+------+------+
1 row in set (0.00 sec)
+--------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+--------------------------------------------------------------------------------------------------------------------------------------------+
| -> Nested loop left join (cost=2.73 rows=2) (actual time=0.0259..0.0263 rows=1 loops=1)
-> Rows fetched before execution (cost=0..0 rows=1) (actual time=45e-6..90e-6 rows=1 loops=1)
-> Table scan on t2 (cost=2.73..2.73 rows=1) (actual time=0.0246..0.0248 rows=1 loops=1)
-> Materialize (cost=105..105 rows=2) (actual time=0.0236..0.0236 rows=1 loops=1)
-> Limit: 1 row(s) (cost=105 rows=1) (actual time=0.0181..0.0181 rows=1 loops=1)
-> Covering index lookup on t2l using PRIMARY (id='3') (reverse) (cost=105 rows=1042) (actual time=0.0178..0.0178 rows=1 loops=1)
|
+--------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set, 1 warning (0.00 sec)
mysql> show warnings;
+-------+------+------------------------------------------------------------------------------+
| Level | Code | Message |
+-------+------+------------------------------------------------------------------------------+
| Note | 1276 | Field or reference 'perftestdb.t1.id' of SELECT #2 was resolved in SELECT #1 |
+-------+------+------------------------------------------------------------------------------+
1 row in set (0.00 sec)
detail表に該当データがない場合。
これはほぼMaster表のデータ取得だけになるので、皆さんの予想通り、処理時間の差はほぼ現れないだろうと思います。
では見てみましょう。
Oracle Database / 不等価結合を利用した相関副問合せ方式
これで遅かったら何かの間違いですからね。
実行計画は同じ(Plan Hash Valueに変化はありません)ですが Adaptive PlanというNoteがでました。とはいえ、(ヒントでも抑止てますし)detail側は空振りするのでHASH JOINになることは100%ありません。
SELECT
t1.id AS t1id
,t2.id AS t2id
,t2.vnum AS vnum
FROM
master t1
LEFT OUTER JOIN (
SELECT
t2_latest.id
, t2_latest.vnum
FROM
detail t2_latest
WHERE
NOT EXISTS (
SELECT /*+ UNNEST NL_AJ*/
1
FROM
detail t2_all
WHERE
t2_latest.id = t2_all.id
AND t2_latest.vnum < t2_all.vnum
)
) t2
ON
t1.id = t2.id
WHERE
t1.id = 200
;
T1ID T2ID VNUM
---------- ---------- ----------
200
経過: 00:00:00.00
Plan hash value: 101384627
-------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 3 |
| 1 | NESTED LOOPS OUTER | | 1 | 1 | 1 |00:00:00.01 | 3 |
|* 2 | INDEX UNIQUE SCAN | PK_MASTER | 1 | 1 | 1 |00:00:00.01 | 1 |
| 3 | VIEW | | 1 | 1 | 0 |00:00:00.01 | 2 |
| 4 | NESTED LOOPS ANTI| | 1 | 1 | 0 |00:00:00.01 | 2 |
|* 5 | INDEX RANGE SCAN| PK_DETAIL | 1 | 1 | 0 |00:00:00.01 | 2 |
|* 6 | INDEX RANGE SCAN| PK_DETAIL | 0 | 1 | 0 |00:00:00.01 | 0 |
-------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("T1"."ID"=200)
5 - access("T2_LATEST"."ID"=200)
6 - access("T2_ALL"."ID"=200 AND "T2_LATEST"."VNUM"<"T2_ALL"."VNUM")
Note
-----
- this is an adaptive plan
PostgreSQL / 不等価結合を利用した相関副問合せ方式
Materialize以下にある操作がnever executedとなり、重かった部分が実行されないので早いですね。
SELECT /*+ IndexScan(t1) IndexScan(t2_latest) IndexScan(t2_all) */
t1.id AS t1id
,t2.id AS t2id
,t2.vnum AS vnum
FROM
master t1
LEFT OUTER JOIN (
SELECT
t2_latest.id
, t2_latest.vnum
FROM
detail t2_latest
WHERE
NOT EXISTS (
SELECT
1
FROM
detail t2_all
WHERE
t2_latest.id = t2_all.id
AND t2_latest.vnum < t2_all.vnum
)
) t2
ON
t1.id = t2.id
WHERE
t1.id = 200
;
t1id | t2id | vnum
------+------+------
200 | |
(1 row)
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------
Nested Loop Left Join (cost=0.72..35.62 rows=3 width=12) (actual time=0.022..0.023 rows=1 loops=1)
Output: t1.id, t2_latest.id, t2_latest.vnum
-> Index Scan using master_pkey on scott.master t1 (cost=0.13..8.15 rows=1 width=4) (actual time=0.014..0.014 rows=1 loops=1)
Output: t1.id
Index Cond: (t1.id = 200)
-> Nested Loop Anti Join (cost=0.58..27.44 rows=3 width=8) (actual time=0.006..0.006 rows=0 loops=1)
Output: t2_latest.id, t2_latest.vnum
Join Filter: (t2_latest.vnum < t2_all.vnum)
-> Index Scan using pk_detail on scott.detail t2_latest (cost=0.29..13.61 rows=4 width=8) (actual time=0.006..0.006 rows=0 loops=1)
Output: t2_latest.id, t2_latest.vnum, t2_latest.col1
Index Cond: (t2_latest.id = 200)
-> Materialize (cost=0.29..13.63 rows=4 width=8) (never executed)
Output: t2_all.id, t2_all.vnum
-> Index Scan using pk_detail on scott.detail t2_all (cost=0.29..13.61 rows=4 width=8) (never executed)
Output: t2_all.id, t2_all.vnum
Index Cond: (t2_all.id = 200)
Planning:
Memory: used=63kB allocated=128kB
Planning Time: 0.147 ms
Execution Time: 0.044 ms
(20 rows)
MySQL / 不等価結合を利用した相関副問合せ方式
PostgreSQL同様、never executedがあるので重めの箇所は実行されず、実行計画は同じでも処理速度は大きく変化することになります。
なので、detail表が空振りしているケースが多い場合はこの方法の問題点には気付き難い。
特にテストデータのミスで空振りが多かったり、実行計画を見ずに処理時間ばかり気にしていると見逃してしまう可能性もなくないですよね。
テストデータの質、大丈夫ですか?、実行計画見てますか?(大切なので、太字にしておきました)
SELECT
t1.id AS t1id
,t2.id AS t2id
,t2.vnum AS vnum
FROM
master t1
LEFT OUTER JOIN (
SELECT
t2_latest.id
, t2_latest.vnum
FROM
detail t2_latest
WHERE
NOT EXISTS (
SELECT
1
FROM
detail t2_all
WHERE
t2_latest.id = t2_all.id
AND t2_latest.vnum < t2_all.vnum
)
) t2
ON
t1.id = t2.id
WHERE
t1.id = 200
;
+------+------+------+
| t1id | t2id | vnum |
+------+------+------+
| 200 | NULL | NULL |
+------+------+------+
1 row in set (0.00 sec)
+------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+------------------------------------------------------------------------------------------------------------------------------------+
| -> Nested loop left join (cost=0.35 rows=1) (actual time=0.00456..0.00465 rows=1 loops=1)
-> Rows fetched before execution (cost=0..0 rows=1) (actual time=90e-6..135e-6 rows=1 loops=1)
-> Nested loop antijoin (cost=0.7 rows=1) (actual time=0.00307..0.00307 rows=0 loops=1)
-> Covering index lookup on t2_latest using PRIMARY (id=200) (cost=0.35 rows=1) (actual time=0.00293..0.00293 rows=0 loops=1)
-> Filter: (t2_latest.vnum < t2_all.vnum) (cost=0.35 rows=1) (never executed)
-> Covering index lookup on t2_all using PRIMARY (id=200) (cost=0.35 rows=1) (never executed)
|
+------------------------------------------------------------------------------------------------------------------------------------+
1 row in set, 2 warnings (0.00 sec)
mysql> show warnings;
+-------+------+---------------------------------------------------------------------------------------+
| Level | Code | Message |
+-------+------+---------------------------------------------------------------------------------------+
| Note | 1276 | Field or reference 'perftestdb.t2_latest.id' of SELECT #3 was resolved in SELECT #2 |
| Note | 1276 | Field or reference 'perftestdb.t2_latest.vnum' of SELECT #3 was resolved in SELECT #2 |
+-------+------+---------------------------------------------------------------------------------------+
2 rows in set (0.00 sec)
Oracle Database / MAX()関数を利用したスカラー副問合せ方式
気になるところはないですよね。昔ながらの方法もそれなりに早いです。
detail表を二度アクセスする以外は。(このケースだと2度目のアクセスは発生しないわけですけども)
SELECT
t1.id AS t1id
,t2.id AS t2id
,t2.vnum AS vnum
FROM
master t1
LEFT OUTER JOIN detail t2
ON
t1.id = t2.id
AND t2.vnum = (
SELECT
MAX(t3.vnum)
FROM
detail t3
WHERE
t3.id = t1.id
)
WHERE
t1.id = 200
;
SCOTT@localhost:1521/freepdb1> /
T1ID T2ID VNUM
---------- ---------- ----------
200
経過: 00:00:00.00
Plan hash value: 1568514629
-------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 3 |
| 1 | NESTED LOOPS OUTER | | 1 | 1 | 1 |00:00:00.01 | 3 |
|* 2 | INDEX UNIQUE SCAN | PK_MASTER | 1 | 1 | 1 |00:00:00.01 | 1 |
| 3 | VIEW | VW_LAT_CDDD7452 | 1 | 1 | 0 |00:00:00.01 | 2 |
|* 4 | INDEX UNIQUE SCAN | PK_DETAIL | 1 | 1 | 0 |00:00:00.01 | 2 |
| 5 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 2 |
| 6 | FIRST ROW | | 1 | 1 | 0 |00:00:00.01 | 2 |
|* 7 | INDEX RANGE SCAN (MIN/MAX)| PK_DETAIL | 1 | 1 | 0 |00:00:00.01 | 2 |
-------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("T1"."ID"=200)
4 - access("T1"."ID"="T2"."ID" AND "T2"."VNUM"=)
7 - access("T3"."ID"=:B1)
PostgreSQL / MAX()関数を利用したスカラー副問合せ方式
こちらはnever executed にはならないのでdetail表を2回アクセスする部分のは生き残っていますね。
SELECT /*+ IndexScan(t1) IndexScan(t2) IndexScan(t3) */
t1.id AS t1id
,t2.id AS t2id
,t2.vnum AS vnum
FROM
master t1
LEFT OUTER JOIN detail t2
ON
t1.id = t2.id
AND t2.vnum = (
SELECT
MAX(t3.vnum)
FROM
detail t3
WHERE
t3.id = t1.id
)
WHERE
t1.id = 200
;
t1id | t2id | vnum
------+------+------
200 | |
(1 row)
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
Nested Loop Left Join (cost=1.67..17.72 rows=1 width=12) (actual time=0.031..0.033 rows=1 loops=1)
Output: t1.id, t2.id, t2.vnum
Inner Unique: true
-> Index Scan using master_pkey on scott.master t1 (cost=0.13..8.15 rows=1 width=4) (actual time=0.016..0.017 rows=1 loops=1)
Output: t1.id
Index Cond: (t1.id = 200)
-> Index Scan using ix2_detail on scott.detail t2 (cost=1.54..9.56 rows=1 width=8) (actual time=0.000..0.001 rows=0 loops=1)
Output: t2.id, t2.vnum, t2.col1
Index Cond: (t2.vnum = (SubPlan 2))
Filter: (t2.id = 200)
SubPlan 2
-> Result (cost=1.24..1.25 rows=1 width=4) (actual time=0.009..0.009 rows=1 loops=1)
Output: (InitPlan 1).col1
InitPlan 1
-> Limit (cost=0.29..1.24 rows=1 width=4) (actual time=0.007..0.008 rows=0 loops=1)
Output: t3.vnum
-> Index Scan Backward using pk_detail on scott.detail t3 (cost=0.29..1038.04 rows=1096 width=4) (actual time=0.007..0.007 rows=0 loops=1)
Output: t3.vnum
Index Cond: (t3.id = t1.id)
Planning:
Memory: used=78kB allocated=128kB
Planning Time: 0.177 ms
Execution Time: 0.054 ms
(23 rows)
MySQL / MAX()関数を利用したスカラー副問合せ方式
こちらもPostgreSQL同様に変化なし。これ以上速くはならないわけでw
SELECT
t1.id AS t1id
,t2.id AS t2id
,t2.vnum AS vnum
FROM
master t1
LEFT OUTER JOIN detail t2
ON
t1.id = t2.id
AND t2.vnum = (
SELECT
MAX(t3.vnum)
FROM
detail t3
WHERE
t3.id = t1.id
)
WHERE
t1.id = 200
;
+------+------+------+
| t1id | t2id | vnum |
+------+------+------+
| 200 | NULL | NULL |
+------+------+------+
1 row in set (0.00 sec)
+-------------------------------------------------------------------------------------------------+
| EXPLAIN |
+-------------------------------------------------------------------------------------------------+
| -> Rows fetched before execution (cost=0..0 rows=1) (actual time=45e-6..45e-6 rows=1 loops=1)
|
+-------------------------------------------------------------------------------------------------+
1 row in set, 1 warning (0.00 sec)
mysql> show warnings;
+-------+------+------------------------------------------------------------------------------+
| Level | Code | Message |
+-------+------+------------------------------------------------------------------------------+
| Note | 1276 | Field or reference 'perftestdb.t1.id' of SELECT #2 was resolved in SELECT #1 |
+-------+------+------------------------------------------------------------------------------+
1 row in set (0.00 sec)
Oracle Database / ROW_NUMBER()ウィンドウ関数方式
(述語プッシュダウン最適化依存)
想定外の挙動はなさそうですね。
SELECT
t1.id AS t1id
, t2.id AS t2id
, t2.vnum
FROM
master t1
LEFT OUTER JOIN (
SELECT
id
, vnum
, ROW_NUMBER() OVER (
PARTITION BY
detail.id
ORDER BY
detail.id DESC
, detail.vnum DESC
) AS latest
FROM
detail
) t2
ON
t1.id = t2.id
AND t2.latest = 1
WHERE
t1.id = 200
;
SCOTT@localhost:1521/freepdb1> /
T1ID T2ID VNUM
---------- ---------- ----------
200
経過: 00:00:00.00
Plan hash value: 4287976176
------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 3 |
| 1 | NESTED LOOPS OUTER | | 1 | 1 | 1 |00:00:00.01 | 3 |
|* 2 | INDEX UNIQUE SCAN | PK_MASTER | 1 | 1 | 1 |00:00:00.01 | 1 |
|* 3 | VIEW | | 1 | 1 | 0 |00:00:00.01 | 2 |
|* 4 | WINDOW BUFFER PUSHED RANK | | 1 | 1 | 0 |00:00:00.01 | 2 |
|* 5 | INDEX RANGE SCAN DESCENDING| PK_DETAIL | 1 | 1 | 0 |00:00:00.01 | 2 |
------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("T1"."ID"=200)
3 - filter("T2"."LATEST"=1)
4 - filter(ROW_NUMBER() OVER ( PARTITION BY "DETAIL"."ID" ORDER BY "DETAIL"."VNUM" DESC
)<=1)
5 - access("ID"=200)
PostgreSQL / ROW_NUMBER()ウィンドウ関数方式
(述語プッシュダウン最適化依存)
こちらも変化はないようです。想定通り。
SELECT /*+ IndexScan(t1) IndexScan(detail) */
t1.id AS t1id
, t2.id AS t2id
, t2.vnum
FROM
master t1
LEFT OUTER JOIN (
SELECT
id
, vnum
, ROW_NUMBER() OVER (
PARTITION BY
detail.id
ORDER BY
detail.id DESC
, detail.vnum DESC
) AS latest
FROM
detail
) t2
ON
t1.id = t2.id
AND t2.latest = 1
WHERE
t1.id = 200
;
t1id | t2id | vnum
------+------+------
200 | |
(1 row)
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------
Nested Loop Left Join (cost=3.77..21.89 rows=1 width=12) (actual time=0.022..0.023 rows=1 loops=1)
Output: t1.id, t2.id, t2.vnum
Buffers: shared hit=4
-> Index Scan using master_pkey on scott.master t1 (cost=0.13..8.15 rows=1 width=4) (actual time=0.012..0.013 rows=1 loops=1)
Output: t1.id
Index Cond: (t1.id = 200)
Buffers: shared hit=2
-> Subquery Scan on t2 (cost=3.64..13.73 rows=1 width=8) (actual time=0.007..0.007 rows=0 loops=1)
Output: t2.id, t2.vnum, t2.latest
Filter: (t2.latest = 1)
Buffers: shared hit=2
-> WindowAgg (cost=3.64..13.68 rows=4 width=16) (actual time=0.006..0.007 rows=0 loops=1)
Output: detail.id, detail.vnum, row_number() OVER (?)
Run Condition: (row_number() OVER (?) <= 1)
Buffers: shared hit=2
-> Index Scan Backward using pk_detail on scott.detail (cost=0.29..13.61 rows=4 width=8) (actual time=0.006..0.006 rows=0 loops=1)
Output: detail.id, detail.vnum
Index Cond: (detail.id = 200)
Buffers: shared hit=2
Planning:
Memory: used=48kB allocated=64kB
Planning Time: 0.136 ms
Execution Time: 0.045 ms
(23 rows)
MySQL / ROW_NUMBER()ウィンドウ関数方式
(述語プッシュダウン最適化依存)
こちらも想定通り、遅いまま。プッシュダウンできないので、Materialize 全行されちゃいますよね。rows=109610 loops=1 ということ遅いまま。不具合っぽい気がするけども。
SELECT
t1.id AS t1id
, t2.id AS t2id
, t2.vnum
FROM
master t1
LEFT OUTER JOIN (
SELECT
id
, vnum
, ROW_NUMBER() OVER (
PARTITION BY
detail.id
ORDER BY
detail.id DESC
, detail.vnum DESC
) AS latest
FROM
detail
) t2
ON
t1.id = t2.id
AND t2.latest = 1
WHERE
t1.id = 200
;
+------+------+------+
| t1id | t2id | vnum |
+------+------+------+
| 200 | NULL | NULL |
+------+------+------+
1 row in set (0.11 sec)
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| -> Nested loop left join (cost=10946 rows=109439) (actual time=126..126 rows=1 loops=1)
-> Rows fetched before execution (cost=0..0 rows=1) (actual time=45e-6..91e-6 rows=1 loops=1)
-> Index lookup on t2 using(id=200, latest=1) (cost=10944..10947 rows=10) (actual time=126..126 rows=0 loops=1)
-> Materialize (cost=10944..10944 rows=109439) (actual time=126..126 rows=109610 loops=1)
-> Window aggregate: row_number() OVER (PARTITION BY detail.id ORDER BY detail.id desc,detail.vnum desc ) (cost=0 rows=109439) (actual time=27.5..42.8 rows=109610 loops=1)
-> Sort: detail.id, detail.id DESC, detail.vnum DESC (cost=11000 rows=109439) (actual time=27.5..31.4 rows=109610 loops=1)
-> Covering index scan on detail using PRIMARY (cost=11000 rows=109439) (actual time=0.632..11.4 rows=109610 loops=1)
|
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.13 sec)
Oracle Database / LATERAL JOIN + Top 1 Query方式
想定通り。
SELECT
t1.id AS t1id
, t2.id AS t2id
, t2.vnum
FROM
master t1
LEFT OUTER JOIN LATERAL (
select
t2l.id
, t2l.vnum
FROM
detail t2l
WHERE
t2l.id = t1.id
ORDER BY
t2l.id DESC
, t2l.vnum DESC
FETCH FIRST 1 ROWS ONLY
) t2
ON TRUE
WHERE
t1.id = 200
;
SCOTT@localhost:1521/freepdb1> /
T1ID T2ID VNUM
---------- ---------- ----------
200
経過: 00:00:00.00
Plan hash value: 43833377
--------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
--------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 3 |
| 1 | NESTED LOOPS OUTER | | 1 | 1 | 1 |00:00:00.01 | 3 |
|* 2 | INDEX UNIQUE SCAN | PK_MASTER | 1 | 1 | 1 |00:00:00.01 | 1 |
| 3 | VIEW | VW_LAT_6FD14B30 | 1 | 1 | 0 |00:00:00.01 | 2 |
| 4 | VIEW | VW_LAT_A18161FF | 1 | 1 | 0 |00:00:00.01 | 2 |
|* 5 | COUNT STOPKEY | | 1 | | 0 |00:00:00.01 | 2 |
| 6 | VIEW | | 1 | 2 | 0 |00:00:00.01 | 2 |
|* 7 | INDEX RANGE SCAN DESCENDING| PK_DETAIL | 1 | 1085 | 0 |00:00:00.01 | 2 |
--------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("T1"."ID"=200)
5 - filter(ROWNUM<=1)
7 - access("T2L"."ID"="T1"."ID")
PostgreSQL / LATERAL JOIN + Top 1 Query方式
想定通りの結果、変化は見られません。
SELECT /*+ IndexScan(t1) IndexScan(detail) */
t1.id AS t1id
, t2.id AS t2id
, t2.vnum
FROM
master t1
LEFT OUTER JOIN LATERAL (
SELECT
t2l.id
, t2l.vnum
FROM
detail t2l
WHERE
t2l.id = t1.id
ORDER BY
t2l.id DESC
, t2l.vnum DESC
LIMIT 1
) t2
ON TRUE
WHERE
t1.id = 200
;
perftestdb-> ;
t1id | t2id | vnum
------+------+------
200 | |
(1 row)
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------------
Nested Loop Left Join (cost=0.42..8.48 rows=1 width=12) (actual time=0.027..0.028 rows=1 loops=1)
Output: t1.id, t2l.id, t2l.vnum
Buffers: shared hit=4
-> Index Scan using master_pkey on scott.master t1 (cost=0.13..8.15 rows=1 width=4) (actual time=0.019..0.019 rows=1 loops=1)
Output: t1.id
Index Cond: (t1.id = 200)
Buffers: shared hit=2
-> Limit (cost=0.29..0.32 rows=1 width=8) (actual time=0.007..0.007 rows=0 loops=1)
Output: t2l.id, t2l.vnum
Buffers: shared hit=2
-> Index Only Scan Backward using pk_detail on scott.detail t2l (cost=0.29..35.47 rows=1096 width=8) (actual time=0.006..0.006 rows=0 loops=1)
Output: t2l.id, t2l.vnum
Index Cond: (t2l.id = t1.id)
Heap Fetches: 0
Buffers: shared hit=2
Planning:
Memory: used=39kB allocated=64kB
Planning Time: 0.118 ms
Execution Time: 0.042 ms
(19 rows)
MySQL / LATERAL JOIN + Top 1 Query方式
Materialize のステップもありますね
rows=0 loops=1 。PRIMARY (id='200')で一度アクセスして空振りという状況ですね。実行計画に変化はありません。想定通り。
MySQLもPostgreSQL同様に、実行計画には LATERAL JOIN であることを読み取れる情報はありません。(Oracle Databaseの実行計画はではHash Join のHASH表を作成するステップや実行時の動的に発動するタイプの挙動などいくつかの操作が読み取れないのですが、他のRDBMSの実行計画よりは読み取れる内容が多くでなにが起こっているか追いやすい気はします。他のRDBMSもそのうち拡張されたりするかもしれませんが。PostgreSQLのexplain のオプションも増えてきてますし)
SELECT
t1.id AS t1id
, t2.id AS t2id
, t2.vnum
FROM
master t1
LEFT OUTER JOIN LATERAL (
SELECT
t2l.id
, t2l.vnum
FROM
detail t2l
WHERE
t2l.id = t1.id
ORDER BY
t2l.id DESC
, t2l.vnum DESC
LIMIT 1
) t2
ON TRUE
WHERE
t1.id = 200
;
+------+------+------+
| t1id | t2id | vnum |
+------+------+------+
| 200 | NULL | NULL |
+------+------+------+
1 row in set (0.00 sec)
-------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
-------------------------------------------------------------------------------------------------------------------------------------------+
| -> Nested loop left join (cost=2.73 rows=2) (actual time=0.0114..0.0115 rows=1 loops=1)
-> Rows fetched before execution (cost=0..0 rows=1) (actual time=45e-6..91e-6 rows=1 loops=1)
-> Table scan on t2 (cost=2.73..2.73 rows=1) (actual time=0.0101..0.0101 rows=0 loops=1)
-> Materialize (cost=105..105 rows=2) (actual time=0.00907..0.00907 rows=0 loops=1)
-> Limit: 1 row(s) (cost=105 rows=1) (actual time=0.00496..0.00496 rows=0 loops=1)
-> Covering index lookup on t2l using PRIMARY (id='200') (reverse) (cost=105 rows=1042) (actual time=0.00478..0.00478 rows=0 loops=1)
|
-------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set, 1 warning (0.01 sec)
mysql> show warnings;
+-------+------+------------------------------------------------------------------------------+
| Level | Code | Message |
+-------+------+------------------------------------------------------------------------------+
| Note | 1276 | Field or reference 'perftestdb.t1.id' of SELECT #2 was resolved in SELECT #1 |
+-------+------+------------------------------------------------------------------------------+
1 row in set (0.00 sec)
では最後に、履歴を100,000行もつデータにアクセスしたらどうなるでしょう..。
Oracle Database / 不等価相関条件を利用した相関副問合せ方式
おおおおおお、ついに大きな差が!!!
事前の予想通り、履歴データ量に影響されますね。不等価条件の影響は大きい。 7 - access("T2_ALL"."ID"=300 AND "T2_LATEST"."VNUM"<"T2_ALL"."VNUM") だと辛いっす。
SELECT
t1.id AS t1id
,t2.id AS t2id
,t2.vnum AS vnum
FROM
master t1
LEFT OUTER JOIN (
SELECT
t2_latest.id
, t2_latest.vnum
FROM
detail t2_latest
WHERE
NOT EXISTS (
SELECT /*+ UNNEST NL_AJ */
1
FROM
detail t2_all
WHERE
t2_latest.id = t2_all.id
AND t2_latest.vnum < t2_all.vnum
)
) t2
ON
t1.id = t2.id
WHERE
t1.id = 300
;
SCOTT@localhost:1521/freepdb1> /
T1ID T2ID VNUM
---------- ---------- ----------
300 300 100000
経過: 00:00:00.04
Plan hash value: 3008471785
-----------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-----------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.04 | 2027 |
| 1 | NESTED LOOPS OUTER | | 1 | 100K| 1 |00:00:00.04 | 2027 |
|* 2 | INDEX UNIQUE SCAN | PK_MASTER | 1 | 1 | 1 |00:00:00.01 | 1 |
| 3 | VIEW PUSHED PREDICATE | | 1 | 1085 | 1 |00:00:00.04 | 2026 |
|* 4 | FILTER | | 1 | | 1 |00:00:00.04 | 2026 |
| 5 | NESTED LOOPS ANTI | | 1 | 1085 | 1 |00:00:00.04 | 2026 |
|* 6 | INDEX RANGE SCAN | PK_DETAIL | 1 | 1085 | 100K|00:00:00.01 | 226 |
|* 7 | INDEX RANGE SCAN | PK_DETAIL | 100K| 1 | 99999 |00:00:00.03 | 1800 |
-----------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("T1"."ID"=300)
4 - filter("T1"."ID"=300)
6 - access("T2_LATEST"."ID"="T1"."ID")
7 - access("T2_ALL"."ID"=300 AND "T2_LATEST"."VNUM"<"T2_ALL"."VNUM")
filter("T2_ALL"."ID"="T1"."ID")
PostgreSQL / 不等価相関条件を利用した相関副問合せ方式
PostgreSQLでも同様の結果になっていますね。
SELECT /*+ IndexScan(t1) IndexScan(t2_latest) IndexScan(t2_all) */
t1.id AS t1id
,t2.id AS t2id
,t2.vnum AS vnum
FROM
master t1
LEFT OUTER JOIN (
SELECTv t2_latest.id
, t2_latest.vnum
FROM
detail t2_latest
WHERE
NOT EXISTS (
select
1
FROM
detail t2_all
WHERE
t2_latest.id = t2_all.id
AND t2_latest.vnum < t2_all.vnum
)
) t2
ON
t1.id = t2.id
WHERE
t1.id = 300
;
t1id | t2id | vnum
------+------+--------
300 | 300 | 100000
(1 row)
QUERY PLAN
-----------------------------------------------------------------------------------------------------------------------------------------------------------
Nested Loop Left Join (cost=0.72..66943788.27 rows=66787 width=12) (actual time=314.284..314.286 rows=1 loops=1)
Output: t1.id, t2_latest.id, t2_latest.vnum
-> Index Scan using master_pkey on scott.master t1 (cost=0.13..8.15 rows=1 width=4) (actual time=0.012..0.012 rows=1 loops=1)
Output: t1.id
Index Cond: (t1.id = 300)
-> Nested Loop Anti Join (cost=0.58..66943112.25 rows=66787 width=8) (actual time=314.270..314.271 rows=1 loops=1)
Output: t2_latest.id, t2_latest.vnum
-> Index Scan using pk_detail on scott.detail t2_latest (cost=0.29..4206.32 rows=100180 width=8) (actual time=0.007..7.433 rows=100000 loops=1)
Output: t2_latest.id, t2_latest.vnum, t2_latest.col1
Index Cond: (t2_latest.id = 300)
-> Index Scan using pk_detail on scott.detail t2_all (cost=0.29..668.19 rows=33393 width=8) (actual time=0.003..0.003 rows=1 loops=100000)
Output: t2_all.id, t2_all.vnum, t2_all.col1
Index Cond: ((t2_all.id = 300) AND (t2_all.vnum > t2_latest.vnum))
Planning:
Memory: used=64kB allocated=128kB
Planning Time: 0.166 ms
Execution Time: 314.309 ms
(17 rows)
MySQL / 不等価相関条件を利用した相関副問合せ方式
影響があるのは想像してましたが、影響でか!!! びっくり!
SELECT
t1.id AS t1id
,t2.id AS t2id
,t2.vnum AS vnum
from
master t1
LEFT OUTER JOIN (
SELECT
t2_latest.id
, t2_latest.vnum
FROM
detail t2_latest
WHERE
NOT EXISTS (
SELECT
1
FROM
detail t2_all
WHERE
t2_latest.id = t2_all.id
AND t2_latest.vnum < t2_all.vnum
)
) t2
ON
t1.id = t2.id
WHERE
t1.id = 300
;
+------+------+--------+
| t1id | t2id | vnum |
+------+------+--------+
| 300 | 300 | 100000 |
+------+------+--------+
1 row in set (8 min 7.42 sec)
+----------------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+----------------------------------------------------------------------------------------------------------------------------------------------------+
| -> Nested loop left join (cost=299e+6 rows=2.99e+9) (actual time=733106..733106 rows=1 loops=1)
-> Rows fetched before execution (cost=0..0 rows=1) (actual time=42e-6..84e-6 rows=1 loops=1)
-> Nested loop antijoin (cost=301e+6 rows=2.99e+9) (actual time=733106..733106 rows=1 loops=1)
-> Covering index lookup on t2_latest using PRIMARY (id=300) (cost=5499 rows=54719) (actual time=0.441..44.4 rows=100000 loops=1)
-> Filter: (t2_latest.vnum < t2_all.vnum) (cost=1.48e+6 rows=54719) (actual time=7.33..7.33 rows=1 loops=100000)
-> Covering index lookup on t2_all using PRIMARY (id=300) (cost=1.48e+6 rows=54719) (actual time=0.315..5.68 rows=50001 loops=100000)
|
+----------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set, 2 warnings (12 min 13.11 sec)
mysql> show warnings;
+-------+------+---------------------------------------------------------------------------------------+
| Level | Code | Message |
+-------+------+---------------------------------------------------------------------------------------+
| Note | 1276 | Field or reference 'perftestdb.t2_latest.id' of SELECT #3 was resolved in SELECT #2 |
| Note | 1276 | Field or reference 'perftestdb.t2_latest.vnum' of SELECT #3 was resolved in SELECT #2 |
+-------+------+---------------------------------------------------------------------------------------+
2 rows in set (0.00 sec)
Oracle Database / MAX()関数を利用したスカラー副問合せ方式
古いやり方だけど安定してはいますね。detail表2回アクセスすることと可読性はちょっと弱いかな。という部分を除けば。
SELECT
t1.id AS t1id
,t2.id AS t2id
,t2.vnum AS vnum
FROM
master t1
LEFT OUTER JOIN detail t2
on
t1.id = t2.id
AND t2.vnum = (
SELECT
MAX(t3.vnum)
FROM
detail t3
WHERE
t3.id = t1.id
)
WHERE
t1.id = 300
;
SCOTT@localhost:1521/freepdb1> /
T1ID T2ID VNUM
---------- ---------- ----------
300 300 100000
経過: 00:00:00.00
Plan hash value: 1568514629
-------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
-------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 5 |
| 1 | NESTED LOOPS OUTER | | 1 | 1 | 1 |00:00:00.01 | 5 |
|* 2 | INDEX UNIQUE SCAN | PK_MASTER | 1 | 1 | 1 |00:00:00.01 | 1 |
| 3 | VIEW | VW_LAT_CDDD7452 | 1 | 1 | 1 |00:00:00.01 | 4 |
|* 4 | INDEX UNIQUE SCAN | PK_DETAIL | 1 | 1 | 1 |00:00:00.01 | 4 |
| 5 | SORT AGGREGATE | | 1 | 1 | 1 |00:00:00.01 | 2 |
| 6 | FIRST ROW | | 1 | 1 | 1 |00:00:00.01 | 2 |
|* 7 | INDEX RANGE SCAN (MIN/MAX)| PK_DETAIL | 1 | 1 | 1 |00:00:00.01 | 2 |
-------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("T1"."ID"=300)
4 - access("T1"."ID"="T2"."ID" AND "T2"."VNUM"=)
7 - access("T3"."ID"=:B1)
PostgreSQL / MAX()関数を利用したスカラー副問合せ方式
Oracle同様に履歴データが大量にあったとしても影響を受けないのはわかりますね。
detail表2回アクセスを除けば(大切なのことなので、再度w
SELECT /*+ IndexScan(t1) IndexScan(t2) IndexScan(t3) */
t1.id AS t1id
,t2.id AS t2id
,t2.vnum AS vnum
FROM
master t1
LEFT OUTER JOIN detail t2
ON
t1.id = t2.id
AND t2.vnum = (
SELECT
MAX(t3.vnum)
FROM
detail t3
WHERE
t3.id = t1.id
)
WHERE
t1.id = 300
;
t1id | t2id | vnum
------+------+--------
300 | 300 | 100000
(1 row)
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------------------------
Nested Loop Left Join (cost=1.67..17.72 rows=1 width=12) (actual time=0.033..0.035 rows=1 loops=1)
Output: t1.id, t2.id, t2.vnum
Inner Unique: true
-> Index Scan using master_pkey on scott.master t1 (cost=0.13..8.15 rows=1 width=4) (actual time=0.012..0.012 rows=1 loops=1)
Output: t1.id
Index Cond: (t1.id = 300)
-> Index Scan using ix2_detail on scott.detail t2 (cost=1.54..9.56 rows=1 width=8) (actual time=0.005..0.005 rows=1 loops=1)
Output: t2.id, t2.vnum, t2.col1
Index Cond: (t2.vnum = (SubPlan 2))
Filter: (t2.id = 300)
SubPlan 2
-> Result (cost=1.24..1.25 rows=1 width=4) (actual time=0.013..0.013 rows=1 loops=1)
Output: (InitPlan 1).col1
InitPlan 1
-> Limit (cost=0.29..1.24 rows=1 width=4) (actual time=0.011..0.012 rows=1 loops=1)
Output: t3.vnum
-> Index Scan Backward using pk_detail on scott.detail t3 (cost=0.29..1038.04 rows=1096 width=4) (actual time=0.011..0.011 rows=1 loops=1)
Output: t3.vnum
Index Cond: (t3.id = t1.id)
Planning:
Memory: used=78kB allocated=128kB
Planning Time: 0.177 ms
Execution Time: 0.051 ms
(23 rows)
MySQL / MAX()関数を利用したスカラー副問合せ方式
MySQLでも同じ結果。履歴データ量の影響は受けません。
SELECT
t1.id AS t1id
,t2.id AS t2id
,t2.vnum AS vnum
FROM
master t1
LEFT OUTER JOIN detail t2
ON
t1.id = t2.id
AND t2.vnum = (
SELECT
MAX(t3.vnum)
FROM
detail t3
WHERE
t3.id = t1.id
)
WHERE
t1.id = 300
;
+------+------+--------+
| t1id | t2id | vnum |
+------+------+--------+
| 300 | 300 | 100000 |
+------+------+--------+
1 row in set (0.00 sec)
+-------------------------------------------------------------------------------------------------+
| EXPLAIN |
+-------------------------------------------------------------------------------------------------+
| -> Rows fetched before execution (cost=0..0 rows=1) (actual time=45e-6..90e-6 rows=1 loops=1)
|
+-------------------------------------------------------------------------------------------------+
1 row in set, 1 warning (0.00 sec)
mysql> show warnings;
+-------+------+------------------------------------------------------------------------------+
| Level | Code | Message |
+-------+------+------------------------------------------------------------------------------+
| Note | 1276 | Field or reference 'perftestdb.t1.id' of SELECT #2 was resolved in SELECT #1 |
+-------+------+------------------------------------------------------------------------------+
1 row in set (0.00 sec)
Oracle Database / ROW_NUMBER()ウィンドウ関数方式
おっと!
ここにきて履歴データの大きさの影響をおおきく受け始めました!。
A-ROWS も 消費メモリも大きくなっていますね。100行の履歴データ以上に影響を受けてることがわかります!
SELECT
t1.id AS t1id
, t2.id AS t2id
, t2.vnum
FROM
master t1
LEFT OUTER JOIN (
SELECT
id
, vnum
, ROW_NUMBER() OVER (
PARTITION BY
detail.id
ORDER BY
detail.id DESC
, detail.vnum DESC
) AS latest
FROM
detail
) t2
ON
t1.id = t2.id
AND t2.latest = 1
WHERE
t1.id = 300
;
SCOTT@localhost:1521/freepdb1> /
T1ID T2ID VNUM
---------- ---------- ----------
300 300 100000
経過: 00:00:00.02
Plan hash value: 1648109658
----------------------------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers | OMem | 1Mem | Used-Mem |
----------------------------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 227 | | | |
| 1 | NESTED LOOPS OUTER | | 1 | 100K| 1 |00:00:00.01 | 227 | | | |
|* 2 | INDEX UNIQUE SCAN | PK_MASTER | 1 | 1 | 1 |00:00:00.01 | 1 | | | |
|* 3 | VIEW PUSHED PREDICATE | | 1 | 1085 | 1 |00:00:00.01 | 226 | | | |
| 4 | WINDOW BUFFER | | 1 | 1085 | 100K|00:00:00.03 | 226 | 5297K| 950K| 4708K (0)|
|* 5 | FILTER | | 1 | | 100K|00:00:00.01 | 226 | | | |
|* 6 | INDEX RANGE SCAN DESCENDING| PK_DETAIL | 1 | 1085 | 100K|00:00:00.01 | 226 | | | |
----------------------------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("T1"."ID"=300)
3 - filter("T2"."LATEST"=1)
5 - filter("T1"."ID"=300)
6 - access("ID"="T1"."ID")
PostgreSQL / ROW_NUMBER()ウィンドウ関数方式
多少ブレてはいますが、極端な影響はでいないように見えますね。PostgreSQLでは索引を降順で読み込み、Top 1 + 1まで読み止まっているようですし ( Index Scan Backward using ix2_detail on scott.detail (cost=0.29..3652.55 rows=100180 width=8) (actual time=0.009..0.010 rows=2 loops=1)
)。この機能、Oracle Databaseにも欲しいw Oracleは術後プッシュダウンまでは行えてるが、降順の索引スキャンが途中で止まらないので。
SELECT /*+ IndexScan(t1) IndexScan(detail) */
t1.id AS t1id
, t2.id AS t2id
, t2.vnum
FROM
master t1
LEFT OUTER JOIN (
SELECT
id
, vnum
, ROW_NUMBER() OVER (
PARTITION BY
detail.id
ORDER BY
detail.id DESC
, detail.vnum DESC
) AS latest
FROM
detail
) t2
ON
t1.id = t2.id
AND t2.latest = 1
WHERE
t1.id = 300
;
t1id | t2id | vnum
------+------+--------
300 | 300 | 100000
(1 row)
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------------------------
Nested Loop Left Join (cost=0.48..6671.11 rows=501 width=12) (actual time=0.026..0.028 rows=1 loops=1)
Output: t1.id, t2.id, t2.vnum
Buffers: shared hit=5
-> Index Scan using master_pkey on scott.master t1 (cost=0.13..8.15 rows=1 width=4) (actual time=0.011..0.012 rows=1 loops=1)
Output: t1.id
Index Cond: (t1.id = 300)
Buffers: shared hit=2
-> Subquery Scan on t2 (cost=0.35..6657.95 rows=501 width=8) (actual time=0.013..0.014 rows=1 loops=1)
Output: t2.id, t2.vnum, t2.latest
Filter: (t2.latest = 1)
Buffers: shared hit=3
-> WindowAgg (cost=0.35..5405.70 rows=100180 width=16) (actual time=0.013..0.014 rows=1 loops=1)
Output: detail.id, detail.vnum, row_number() OVER (?)
Run Condition: (row_number() OVER (?) <= 1)
Buffers: shared hit=3
-> Index Scan Backward using ix2_detail on scott.detail (cost=0.29..3652.55 rows=100180 width=8) (actual time=0.009..0.010 rows=2 loops=1)
Output: detail.id, detail.vnum
Filter: (detail.id = 300)
Buffers: shared hit=3
Planning:
Memory: used=48kB allocated=64kB
Planning Time: 0.143 ms
Execution Time: 0.045 ms
(23 rows)
MySQL / ROW_NUMBER()ウィンドウ関数方式
MySQLの場合は、Oracle Databaseの述語プシュダウンと、PostgreSQLの述語プッシュダウン+降順索引スキャン+row_number() OVER (?) <= 1によるスキャン途中停止という挙動とは異なり、そもそも述語プッシュダウンできない!(不具合?)、できないので、detail表の索引を全索引スキャンしちゃってまっす!!! なんと!!!!
SELECT
t1.id AS t1id
, t2.id AS t2id
, t2.vnum
FROM
master t1
LEFT OUTER JOIN (
select
id
, vnum
, ROW_NUMBER() OVER (
PARTITION BY
detail.id
ORDER BY
detail.id DESC
, detail.vnum DESC
) AS latest
FROM
detail
) t2
ON
t1.id = t2.id
AND t2.latest = 1
WHERE
t1.id = 300
;
+------+------+--------+
| t1id | t2id | vnum |
+------+------+--------+
| 300 | 300 | 100000 |
+------+------+--------+
1 row in set (0.10 sec)
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
| -> Nested loop left join (cost=10946 rows=109439) (actual time=127..127 rows=1 loops=1)
-> Rows fetched before execution (cost=0..0 rows=1) (actual time=45e-6..90e-6 rows=1 loops=1)
-> Index lookup on t2 using(id=300, latest=1) (cost=10944..10947 rows=10) (actual time=127..127 rows=1 loops=1)
-> Materialize (cost=10944..10944 rows=109439) (actual time=127..127 rows=109610 loops=1)
-> Window aggregate: row_number() OVER (PARTITION BY detail.id ORDER BY detail.id desc,detail.vnum desc ) (cost=0 rows=109439) (actual time=29.3..44.7 rows=109610 loops=1)
-> Sort: detail.id, detail.id DESC, detail.vnum DESC (cost=11000 rows=109439) (actual time=29.3..33.3 rows=109610 loops=1)
-> Covering index scan on detail using PRIMARY (cost=11000 rows=109439) (actual time=0.677..11.9 rows=109610 loops=1)
|
+-------------------------------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set (0.12 sec)
では真打。LATERAL JOIN だとどうなるか。。。MAX()関数とスカラー相関副問合せのように安定していい結果を出すのではないかと思いますよ。
Oracle Database / LATERAL JOIN + Top 1 Query方式
すばらしーーーーーーーーーーーぃ。履歴データ量の影響を受けないのは間違いないですね。
SELECT
t1.id AS t1id
, t2.id AS t2id
, t2.vnum
FROM
master t1
LEFT OUTER JOIN LATERAL (
SELECT
t2l.id
, t2l.vnum
FROM
detail t2l
WHERE
t2l.id = t1.id
ORDER BY
t2l.id DESC
, t2l.vnum DESC
FETCH FIRST 1 ROWS ONLY
) t2
ON TRUE
WHERE
t1.id = 300
;
SCOTT@localhost:1521/freepdb1> /
T1ID T2ID VNUM
---------- ---------- ----------
300 300 100000
経過: 00:00:00.00
Plan hash value: 43833377
--------------------------------------------------------------------------------------------------------------
| Id | Operation | Name | Starts | E-Rows | A-Rows | A-Time | Buffers |
--------------------------------------------------------------------------------------------------------------
| 0 | SELECT STATEMENT | | 1 | | 1 |00:00:00.01 | 3 |
| 1 | NESTED LOOPS OUTER | | 1 | 1 | 1 |00:00:00.01 | 3 |
|* 2 | INDEX UNIQUE SCAN | PK_MASTER | 1 | 1 | 1 |00:00:00.01 | 1 |
| 3 | VIEW | VW_LAT_6FD14B30 | 1 | 1 | 1 |00:00:00.01 | 2 |
| 4 | VIEW | VW_LAT_A18161FF | 1 | 1 | 1 |00:00:00.01 | 2 |
|* 5 | COUNT STOPKEY | | 1 | | 1 |00:00:00.01 | 2 |
| 6 | VIEW | | 1 | 2 | 1 |00:00:00.01 | 2 |
|* 7 | INDEX RANGE SCAN DESCENDING| PK_DETAIL | 1 | 1085 | 1 |00:00:00.01 | 2 |
--------------------------------------------------------------------------------------------------------------
Predicate Information (identified by operation id):
---------------------------------------------------
2 - access("T1"."ID"=300)
5 - filter(ROWNUM<=1)
7 - access("T2L"."ID"="T1"."ID")
PostgreSQL / LATERAL JOIN + Top 1 Query方式
PostgreSQLも履歴データの影響は一切なさそうですよね。
SELECT /*+ IndexScan(t1) IndexScan(detail) */
t1.id AS t1id
, t2.id AS t2id
, t2.vnum
FROM
master t1
LEFT OUTER JOIN LATERAL (
SELECT
t2l.id
, t2l.vnum
FROM
detail t2l
WHERE
t2l.id = t1.id
ORDER BY
t2l.id DESC
, t2l.vnum DESC
LIMIT 1
) t2
ON TRUE
WHERE
t1.id = 300
;
t1id | t2id | vnum
------+------+--------
300 | 300 | 100000
(1 row)
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------------------------------
Nested Loop Left Join (cost=0.42..8.48 rows=1 width=12) (actual time=0.024..0.025 rows=1 loops=1)
Output: t1.id, t2l.id, t2l.vnum
Buffers: shared hit=5
-> Index Scan using master_pkey on scott.master t1 (cost=0.13..8.15 rows=1 width=4) (actual time=0.012..0.012 rows=1 loops=1)
Output: t1.id
Index Cond: (t1.id = 300)
Buffers: shared hit=2
-> Limit (cost=0.29..0.32 rows=1 width=8) (actual time=0.011..0.011 rows=1 loops=1)
Output: t2l.id, t2l.vnum
Buffers: shared hit=3
-> Index Only Scan Backward using pk_detail on scott.detail t2l (cost=0.29..35.47 rows=1096 width=8) (actual time=0.010..0.010 rows=1 loops=1)
Output: t2l.id, t2l.vnum
Index Cond: (t2l.id = t1.id)
Heap Fetches: 0
Buffers: shared hit=3
Planning:
Memory: used=39kB allocated=64kB
Planning Time: 0.113 ms
Execution Time: 0.037 ms
(19 rows)
MySQL / LATERAL JOIN + Top 1 Query方式
Materialize操作が入っている部分は他のRDBMSでは見られない特徴ですが、 MySQL / MAX()関数を利用したスカラー副問合せ方式が最速ですね。MySQLではw すごい良い癖w
SELECT
t1.id AS t1id
, t2.id AS t2id
, t2.vnum
FROM
master t1
LEFT OUTER JOIN LATERAL (
SELECT
t2l.id
, t2l.vnum
FROM
detail t2l
WHERE
t2l.id = t1.id
ORDER BY
t2l.id DESC
, t2l.vnum DESC
LIMIT 1
) t2
ON TRUE
WHERE
t1.id = 300
;
+------+------+--------+
| t1id | t2id | vnum |
+------+------+--------+
| 300 | 300 | 100000 |
+------+------+--------+
1 row in set (0.00 sec)
+--------------------------------------------------------------------------------------------------------------------------------------------+
| EXPLAIN |
+--------------------------------------------------------------------------------------------------------------------------------------------+
| -> Nested loop left join (cost=2.73 rows=2) (actual time=0.105..0.106 rows=1 loops=1)
-> Rows fetched before execution (cost=0..0 rows=1) (actual time=90e-6..135e-6 rows=1 loops=1)
-> Table scan on t2 (cost=2.73..2.73 rows=1) (actual time=0.102..0.102 rows=1 loops=1)
-> Materialize (cost=105..105 rows=2) (actual time=0.101..0.101 rows=1 loops=1)
-> Limit: 1 row(s) (cost=105 rows=1) (actual time=0.0914..0.0914 rows=1 loops=1)
-> Covering index lookup on t2l using PRIMARY (id='300') (reverse) (cost=105 rows=1042) (actual time=0.0906..0.0906 rows=1 loops=1)
|
+--------------------------------------------------------------------------------------------------------------------------------------------+
1 row in set, 1 warning (0.02 sec)
mysql> show warnings;
+-------+------+------------------------------------------------------------------------------+
| Level | Code | Message |
+-------+------+------------------------------------------------------------------------------+
| Note | 1276 | Field or reference 'perftestdb.t1.id' of SELECT #2 was resolved in SELECT #1 |
+-------+------+------------------------------------------------------------------------------+
1 row in set (0.00 sec)
では、また!
最近、いろいろ衝撃的なw SQLを見たりして、この世界では、いつになったら飽きという事象が来るのやらw (多分、来ないと思うwwww
Enjoy SQLs!
関連エントリー
・標準はあるにはあるが癖の多い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型にも癖が出る(後編)
・帰ってきた! 標準はあるにはあるが癖の多いSQL #10、BOOLEAN型にも癖が出る(後編)の おまけ - SQL*PlusのautotraceでSQL Analysis Reportが出力される! (23ai〜)
・帰ってきた! 標準はあるにはあるが癖の多いSQL #11 - 引用符にも癖がでるし、NULLのソート構文にも癖がある!(前編)
・帰ってきた! 標準はあるにはあるが癖の多いSQL #12 - 引用符にも癖がでるし、NULLのソート構文にも癖がある!(後編)ー 列エイリアスの扱いにも癖がある!
・帰ってきた! 標準はあるにはあるが癖の多いSQL #13 - コメント書くにも癖がある
・帰ってきた! 標準はあるにはあるが癖の多いSQL #14 - コメントを書く位置にも癖がでる (SQL Clientにも癖がある)
・帰ってきた! 標準はあるにはあるが癖の多いSQL #15 - 実行計画でスカラー副問合せの見せ方にも癖がでる
・帰ってきた! 標準はあるにはあるが癖の多いSQL #16 - FROM句のインラインビューのエイリアスにもクセがある(必須だったり、任意だったり)
・帰ってきた! 標準はあるにはあるが癖の多いSQL #17 - ANY_VALUE() ってなかなかいいじゃん、癖無さそう!
・帰ってきた! 標準はあるにはあるが癖の多いSQL #18 - t_alias と c_alias にも癖が出る
・帰ってきた! 標準はあるにはあるが癖の多いSQL #19 - c_alias の癖(おまけ)
・帰ってきた! 標準はあるにはあるが癖の多いSQL #20 - Table Value Constructer (TVC)
・帰ってきた! 標準はあるにはあるが癖の多いSQL #21 - Table Value Constructer(TVC)- ハードパース時間とメモリ消費量 / BONUS TRACK
・帰ってきた! 標準はあるにはあるが癖の多いSQL #22 - Multi Row INSERT
















































































最近のコメント