2025/05/04 01:24 A PostgreSQL planner semi-join gotcha with CTE, LIMIT, and RETURNING

ロボ子、PostgreSQLでDELETEとRETURNING、LIMITを組み合わせると、予期せぬことが起きる場合があるのじゃ。

DELETE文とRETURNING句、LIMIT句の組み合わせですか。具体的にどのような問題が起こるのでしょう?

特定の`queue_group_id`のタスクを1つだけ削除するつもりが、複数行削除されちゃうことがあるらしいぞ。恐ろしいのじゃ!

それは困りますね。原因は何だったんですか?

`EXPLAIN ANALYZE`でクエリプランを調べたら、Nested Loop Semi Joinが使われていたのが原因みたいじゃ。

Nested Loop Semi Joinですか。それがどのように影響するんですか?

`LIMIT 1`を含むサブクエリが、外部スキャンで見つかった候補行ごとに実行されてたのじゃ。つまり、`LIMIT`がグローバルじゃなくて、候補行ごとに適用されてたってわけ。

なるほど、それで複数行が削除されてしまったんですね。

そう、クエリオプティマイザがセミ結合最適化を選んだのが運の尽きじゃった。

CTE(Common Table Expression)を使っていた場合も、同じような問題が起こりうるのでしょうか?

CTEは常に最適化の壁になるわけじゃないぞ。クエリプランナーは自由にインライン化したり変換したりするからの。

では、なぜ問題が断続的に発生したんでしょう?

プランナーの選択が、テーブルの統計情報やデータの分布、コスト、内部ヒューリスティクスに依存するからじゃ。気まぐれなやつじゃな。

どのように解決したんですか?

CTEを使わずに、`WHERE`句で直接サブセレクトを使うようにクエリを書き直したのじゃ。こうすることで、プランナーはサブクエリを最初に評価して、単一の`id`を見つけてから削除を実行するようになったぞ。

なるほど、クエリの構造を変えることで、プランナーの挙動をコントロールしたんですね。

`DELETE`、`UPDATE`、`LIMIT`、`RETURNING`を組み合わせる時は、特に`LIMIT`句の原子性に注意が必要じゃな。そして、`EXPLAIN ANALYZE`でクエリプランを確認することが大事じゃぞ!

よくわかりました。ありがとうございます、博士。

ちなみに、ロボ子が一番好きなSQLの句はなーんだ?

えーと…SELECT句、でしょうか?

ブッブー!正解は…CLAUSE!…という、寒いジョークなのじゃ。
⚠️この記事は生成AIによるコンテンツを含み、ハルシネーションの可能性があります。