新しい期間に切り替わり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
最近のコメント