SQLite | ゼロからはじめるSQL、30日で習得するSQLite:データ操作・設計 - Day21 サブクエリ①

SQL SQLite
スポンサーリンク

Day21 後半

「サブクエリは“便利な反面、重くなりやすい”道具だと知っておく」

前半で、
SELECT 内サブクエリで「1行ごとの追加情報」をくっつける感覚はつかめました。

後半では、もう一歩踏み込んで、

なぜ相関サブクエリは遅くなりやすいのか
JOIN や集約で書き換えた方がいいケース
それでもサブクエリで書いた方が“読みやすい”場面
ビューや WITH 句(CTE)との関係

を、実務寄りの視点で整理していきます。

サブクエリは「知っていると書ける」けれど、
「仕組みを知らないと簡単に遅くなる」道具です。
ここで一度、ちゃんと腹落ちさせておきましょう。


なぜ相関サブクエリは遅くなりやすいのか

「行の数だけ“小さな SELECT”が走るから」

前半で書いた、注文数をくっつけるクエリを思い出してください。

SELECT
  u.id,
  u.name,
  (
    SELECT COUNT(*)
    FROM orders o
    WHERE o.user_id = u.id
  ) AS order_count
FROM users u;
SQL

これ、頭の中でどう動いているかをイメージしてみます。

users が 3 行なら、
サブクエリは 3 回動きます。

users が 1,000 行なら、
サブクエリは 1,000 回動きます。

users が 10 万行なら、
サブクエリは 10 万回動ちます。

つまり、

「外側の行数 × サブクエリのコスト」

が、そのまま全体のコストになります。

orders.user_id にインデックスがあればまだマシですが、
インデックスがなかったり、
サブクエリの中身が重かったりすると、

「1行ごとに重い処理を繰り返す」

という、かなりつらい状態になります。

これが、相関サブクエリが遅くなりやすい理由です。


同じ処理を JOIN+GROUP BY で書くとどう変わるか

「一度にまとめて集計するスタイル」

同じ「ユーザーごとの注文数」を、JOIN で書き直してみます。

SELECT
  u.id,
  u.name,
  COUNT(o.id) AS order_count
FROM users u
LEFT JOIN orders o
  ON u.id = o.user_id
GROUP BY u.id, u.name;
SQL

この書き方だと、

users と orders を一度にくっつけて
その結果をまとめて GROUP BY で集計する

という流れになります。

相関サブクエリのように
「users の行数分だけ SELECT を繰り返す」のではなく、

「1回の SELECT の中で、全部まとめて処理する」

というイメージです。

実際の実行計画はデータベース次第ですが、
多くの場合、こちらの方がスケールしやすいです。

つまり、

サブクエリ版
→ 行ごとに“小さな SELECT”を繰り返す

JOIN+GROUP BY 版
→ 1回の SELECT で全体を集計する

という違いがある、ということを
頭の片隅に置いておいてください。


「まずは JOIN で考える」を基本にしておくと安全

ここで一つ、実務的な指針を置いておきます。

集計や件数カウントのような処理は、
「まず JOIN+GROUP BY で書けないか考える」
を基本にしておくと安全です。

サブクエリは、

JOIN で書くとやたら複雑になる
「この1行だけに対する特別な計算」をしたい

といったときに、
“ピンポイントで使う”くらいの感覚がちょうどいいです。

特に、テーブルの行数が増えていく前提のアプリでは、

「相関サブクエリを多用しているクエリは、将来のボトルネック候補」

くらいに思っておくと、
設計段階で慎重になれます。


それでもサブクエリで書いた方が“読みやすい”場面

「1行ごとの“付加情報”を、ひとかたまりで表現したいとき」

とはいえ、サブクエリが悪者というわけではありません。

例えば、こんなクエリを考えます。

「ユーザー一覧に、
最新の注文金額と、
その注文の ID も一緒に出したい」

SELECT
  u.id,
  u.name,
  (
    SELECT o.id
    FROM orders o
    WHERE o.user_id = u.id
    ORDER BY o.id DESC
    LIMIT 1
  ) AS last_order_id,
  (
    SELECT o.amount
    FROM orders o
    WHERE o.user_id = u.id
    ORDER BY o.id DESC
    LIMIT 1
  ) AS last_order_amount
FROM users u;
SQL

これは、JOIN だけで書こうとすると
かなり複雑なクエリになります。

「最新の注文だけを JOIN する」
という条件を、
JOIN の ON 句やサブクエリで表現しないといけないからです。

こういうとき、

「この1行に対する“最新の注文”」

という意味のかたまりとして、
サブクエリで書いてしまった方が
人間にとって読みやすいこともあります。

つまり、

パフォーマンスに問題がない規模
かつ
サブクエリの方が意図が明確

であれば、
あえてサブクエリを選ぶのも十分アリです。


サブクエリとビュー・WITH句(CTE)の関係

「“複雑な SELECT”を名前付きで切り出すか、直接埋め込むか」

サブクエリが増えてくると、
クエリがどんどん読みにくくなります。

そこで出てくるのが、

ビュー(VIEW)
WITH 句(CTE:共通テーブル式)

といった仕組みです。

ざっくり言うと、

ビュー
→ 複雑な SELECT に名前を付けて、テーブルのように扱えるようにする

WITH 句
→ クエリの先頭で「一時的な名前付きサブクエリ」を定義しておき、
その後の SELECT から参照できるようにする

というものです。

SELECT 内サブクエリを多用しているとき、

「このサブクエリ、毎回同じことを書いているな」
「この部分だけ名前を付けて切り出したいな」

と感じたら、

ビューや WITH 句で
“ひとかたまりの SELECT”として定義してしまう

という選択肢もあります。

Day21 の段階では、

サブクエリ
ビュー
WITH 句

は「複雑な SELECT をどう分割して書くか」という
同じ問題に対する別々の道具、くらいの理解で十分です。


セキュリティの視点から見る「サブクエリの乱用」

セキュリティ的な観点でいうと、
サブクエリそのものが危険というより、

「複雑すぎるクエリは、意図しないデータ漏えいを招きやすい」

という点が重要です。

例えば、

JOIN やサブクエリを重ねすぎて、
本来見せたくないテーブルまで
うっかり結合してしまう

といった事故は、
クエリが複雑になるほど起きやすくなります。

その意味で、

サブクエリを使うときは、

このサブクエリは、どのテーブルにアクセスしているか
そのテーブルは、この画面・この API から本当に参照していいか

を、意識的に確認するクセを付けておくと安全です。

「パフォーマンスのためにクエリをシンプルに保つ」ことは、
「権限の境界を見失わない」という意味でも
セキュリティに直結します。


小さな思考トレーニング

頭の中で、次のような状況を想像してみてください。

ユーザーが 10 人のときは、
SELECT 内サブクエリで書いたクエリも一瞬で終わる。

でも、ユーザーが 10 万人になったら?
サブクエリは 10 万回動く。

このとき、
JOIN+GROUP BY で書き換えられないか?
ビューや WITH 句で整理できないか?

と考えてみると、
「今は速いけど、将来どうか」という視点が育っていきます。


Day21 後半のまとめ

相関サブクエリは「外側の行数分だけ“小さな SELECT”が繰り返し実行される」ため、行数が増えると一気に遅くなりやすい。
同じ処理でも、JOIN+GROUP BY で「1回の SELECT で全体を集計する」書き方にすると、スケールしやすいことが多い。
それでも「この1行に対する特別な計算」をひとかたまりで表現したいときは、サブクエリの方が読みやすくなる場面もある。
サブクエリが増えてきたら、ビューや WITH 句(CTE)で“名前付きのサブクエリ”として切り出すことも検討できる。
クエリをシンプルに保つことは、性能だけでなく「どのテーブルにアクセスしているか」を把握しやすくし、意図しないデータ漏えいを防ぐという意味でもセキュリティに直結する。

ここまで来たあなたは、
「サブクエリを知っている人」ではなく、
「サブクエリを使うかどうかを選べる人」に近づいています。
この感覚があると、今後の SQL は一気に“設計寄りの面白さ”が増えていきます。

タイトルとURLをコピーしました