Menu

SQLite サブクエリ入門:WHERE/FROM/SELECT で SELECT をネスト

SQLite で SELECT を入れ子にする書き方を実例で解説。スカラーサブクエリ、IN/EXISTS、派生テーブル、相関サブクエリ、そして JOIN との使い分けまで。

このページのコードはエディタで実行できます — 編集してすぐに結果を確認できます。

サブクエリとは「SELECT の中の SELECT」

SQLite のサブクエリは、その名のとおり SELECT 文の中にもう一つ SELECT 文を入れ子にしたもので、内側のクエリを丸かっこで囲んで書きます。SQLite はまず内側のクエリを実行し、その結果を外側のクエリに渡して処理を続けます。

これから繰り返し使えるよう、簡単なサンプルデータを用意しておきましょう。

注文5件、顧客4人、そのうち2人はまだ何も注文していない――このデータをこの先ずっと使い回していきます。

WHERE 句で使う sqlite サブクエリ:リストで絞り込む

一番よく見るパターンがこれ。内側のクエリで id のリストを取ってきて、外側のクエリでそれを使ってフィルタする、という形です。

内側のクエリは、orders に登場するすべての customer_id を返します。外側のクエリは、その一覧に id が含まれる顧客だけを残す、という流れです。結果として Cleo、Boris、Ada が出てきますが、注文のない Dmitri は弾かれます。

IN (SELECT ...) は「A のうち B にマッチがある行」を抽出する定番パターンです。頭の中では「この列の値が、内側のクエリが返す値のいずれかと一致する場合」と読み下すとスッと入ってきます。

NOT IN を使うときは NULL に要注意

逆に「どの顧客が まだ 注文していないか?」を知りたい場合は、ほんの一行書き換えるだけで済みます。

ここまではうまくいきます。ただし NOT IN には落とし穴があって、サブクエリが一度でも NULL を返すと、NOT IN 全体が NULL(つまり TRUE ではない)になり、結果は 0 件 になってしまいます。エラーも出ないので、気づきにくいんですよね。

NULL が混じる可能性のあるカラムに対して NOT IN を使うときの安全な書き方はこちらです:

あるいは、こうした問題が一切起きない NOT EXISTS を使う手もあります。これについては後ほど取り上げます。

スカラーサブクエリ:1行1列だけ返すサブクエリ

スカラーサブクエリは、1行・1列という単一の値を返すサブクエリで、値が必要な場所ならどこでも使えます。

内側の SELECT MAX(total) FROM orders200 を返し、外側のクエリでその値に一致する注文だけを抽出します。集計値と比較したい場面で何かと重宝するパターンです。

スカラーサブクエリは SELECT 句の中に書くこともできて、各行に計算結果をくっつけたいときに便利です。

customers の行ごとに、customers.id を差し込んで内側のクエリが1回ずつ実行されます。これがいわゆる 相関サブクエリ(correlated subquery)で、詳しくは後ほど解説します。今回のように「1行につき1つの値が欲しい」というケースでは、LEFT JOINGROUP BY の組み合わせのほうがパフォーマンス的に有利なことが多いですが、スカラーサブクエリの書き方は何より読みやすいのが魅力です。

EXISTS の使い方:行が存在するかだけを確認する

EXISTSIN の地味な親戚のような存在です。値そのものには関心がなく、サブクエリが何らかの行を返すかどうかだけをチェックします。中身のカラムは何でもよいので、慣例的に SELECT 1 と書くことが多いですね。

これは「100を超える注文を1件でも出している顧客」を取り出すクエリです。内側のクエリで外側の c.id を参照しているので、いわゆる相関サブクエリになります。SQLite はマッチが1件見つかった時点で内側のスキャンを打ち切るため、「関連する行があるか?」を判定したいケースでは EXISTSIN より速くなることが多いです。

否定形の NOT EXISTS は、「関連する行が存在しない」を NULL セーフに表現できる書き方です:

FROM句のサブクエリ:派生テーブルとして使う

SQLiteのサブクエリは、テーブルが書ける場所ならどこにでも置けます。FROM句もその一つです。内側のクエリに別名を付ければ、一時的な「派生テーブル(derived table)」として扱えるので、それに対してJOINしたり、絞り込んだり、集計したりできます。

内側のクエリで顧客ごとの合計を出し、外側のクエリでその合計を国ごとに平均しています。こういう2段構えの集計こそ、派生テーブル(derived table)の出番です。1回の GROUP BY で片付けられないときに使う、まさに定番のパターンですね。

なお、AS per_customer のエイリアスは省略できません。派生テーブルには必ず名前を付ける必要があります。

相関サブクエリ:外側の行ごとに実行される

外側のクエリの列を参照しているサブクエリのことを「相関サブクエリ(correlated subquery)」と呼びます。SQLite は外側の行が1行進むたびに内側のクエリを評価し直すので、柔軟に書ける反面、コストがかさみやすいので注意が必要です。

各顧客について、それぞれの最大注文額を取得します。内側のクエリは customers.id に依存しているため、顧客 1 人につき 1 回ずつ実行されます。注文がない顧客は NULL になりますが、たいていはこれが望ましい挙動です。

「テーブル A の各行に対して、テーブル B から何かを計算する」という用途には、相関サブクエリがぴったりハマります。テーブルが小さい場合や、検索対象にインデックスが張られている場合は問題ありません。ただし、インデックスのない大きなテーブルではリリース前にプロファイリングしておきましょう。多くのケースでは JOINGROUP BY の組み合わせの方が速くなります。

sqlite サブクエリと JOIN の使い分け

次の 2 つのクエリは、同じ問いに対する答えを返します。

どちらを書いても返ってくる行は同じです。SQLite のオプティマイザは内部で片方をもう片方に書き換えることもよくあります。なので選ぶ基準は 読みやすさ です。

  • 単に絞り込みたいだけで、内側のテーブルのカラムを結果に混ぜたくないときは サブクエリ を使う。
  • 両方のテーブルからカラムが欲しいときは JOIN を使う。
  • 「関連する行が1件でも存在するか?」を聞きたいときは EXISTS を使う。意図が明確で、IN / NOT IN にありがちな NULL のワナも避けられます。

迷ったら、声に出して読んだときに意味がそのまま伝わる方を選びましょう。

よくある落とし穴: 複数行を返すサブクエリ

= と一緒に使うサブクエリは、返す行が最大でも1行でなければなりません。複数行返ってしまうと、SQLite は(実質的にランダムに)そのうちの1つを採用し、エラーも出さずに静かに間違った結果を返します。

内側のクエリが複数行を返す可能性がある場合は、IN を使います:

もし「結果は1行のはず」と前提を置いて、それをきちんと担保したいなら、LIMIT 1ORDER BY を組み合わせて、せめて取得される行が決定的になるようにしておきましょう。さらに望ましいのは、データ側で1行に絞られることが保証されるようにクエリを書くこと(ユニークなカラムで絞り込む、など)です。

次回:共通テーブル式(CTE)

FROM 句のサブクエリは、すぐに見通しが悪くなります。同じ派生テーブルを2回使いたい場合や、ネストが3階層にもなる場合は特にそうです。共通テーブル式(WITH ... AS (...))を使えば、サブクエリにあらかじめ名前を付けておき、以降のステートメントからその名前で参照できます。次のページで詳しく見ていきましょう。

よくある質問

SQLite のサブクエリとは何ですか?

サブクエリは、別の SQL 文の中にカッコで囲んで埋め込んだ SELECT 文のことです。SQLite はまず内側のクエリを実行し、その結果を外側のクエリに渡して処理します。WHEREFROMSELECT のリストなど、いろいろな場所に書けます。

SQLite の IN と EXISTS は何が違うの?

IN (SELECT ...) は、サブクエリが返す値の中に一致するものがあるかをチェックします。一方 EXISTS (SELECT ...) は値そのものは見ず、サブクエリが「行を1つでも返すかどうか」だけを判定します。外側の行を参照する相関サブクエリでは、EXISTS の方が素直に書けることが多いです。

サブクエリと JOIN、どちらを使うべき?

両方のテーブルから列を取り出して結果に並べたいなら JOIN、単に絞り込みや1つの値の計算だけが目的ならサブクエリ、というのが基本の使い分けです。SQLite のオプティマイザは内部で書き換えてくれることも多いので、最終的には「読みやすい方」を選んで構いません。

SQLite の相関サブクエリ(correlated subquery)とは?

外側のクエリの列を参照するサブクエリのことです。外側の1行ごとに再実行されるため、行数が増えると遅くなりやすい性質があります。プロファイルでボトルネックになっていたら、JOIN や CTE(WITH 句)に書き換えると改善することが多いです。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める