Menu

SQLite CTE入門 - WITH句で読みやすいクエリを書く

SQLiteのCTE(共通テーブル式)の使い方を解説。WITH句でサブクエリに名前を付け、複数のCTEを連結して、上から下に読めるクエリを書くコツをまとめました。

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

CTEは「名前を付けたサブクエリ」

共通テーブル式(CTE: Common Table Expression)とは、サブクエリを切り出して名前を付けたものです。SELECT の中に別の SELECT をネストする代わりに、先頭で WITH を使って定義し、名前を付けて、メインのクエリの中でそれをテーブルのように参照します。

形は常にこのパターンです。

上から順に読めるのがポイントです。まず customer_totals という名前付きの結果セットを作り、その後でそれをクエリする、という流れになっています。CTE は、このステートメントが実行されている間だけ存在する一時的なビューのようなものだと考えてください。

CTE を使わずに同じクエリを書くと

比較のために、同じロジックをサブクエリで書いた例を見てみましょう。CTE が何を置き換えているのかが見えてきます。

答えはまったく同じです。ただし、読む順序に注目してください。サブクエリ版だと、いったん括弧の中に潜って何を計算しているのかを把握し、それからまた外に戻ってくる、という視線の動きが必要になります。一方CTE版は、処理が実際に進む順番どおりに読めます。まず中間結果を定義して、それを使う、というシンプルな流れです。小さなクエリならどちらでも大差ありません。でも処理が3つ4つと積み重なってくると、サッと読めるコードと、頑張って解読しないといけないコードの差として効いてきます。

1つのクエリで複数のCTEを使う

複数のCTEはカンマで区切って数珠つなぎに書けます。それぞれのCTEは、その前に定義したCTEを参照できるので、名前付きのステップを連ねたパイプラインのように組み立てられます。

WITH は1回だけ書いて、あとはカンマで CTE 定義を並べていきます。2番目の CTE(big_spenders)は、最初の CTE(customer_totals)をまるでテーブルのように参照しています。メインの SELECT は、最後の CTE 定義の後ろに続けます。

よくあるミスとして、2つ目の CTE の前にもう一度 WITH を書いてしまうパターンがあります。これは構文エラーになるのでNGです。WITH は最初の1回だけで、すべての CTE をカバーします。

同じ CTE を複数回参照する

CTE がサブクエリより一歩リードするのが、まさにこの場面です。同じ中間結果を2か所で使いたいとき、CTE なら一度計算した結果を2回参照できます。

このCTEは2回参照されています。1回目は平均値の計算用、2回目はメインのデータソースとしてです。もしCTEを使わなければ、GROUP BYのクエリを2か所に書くことになり、変更があるたびに両方を直す必要が出てきます。

INSERT・UPDATE・DELETEでのCTE活用

CTEはSELECT専用というわけではありません。WITH句はINSERTUPDATEDELETEの前にも置けるので、書き込み系の処理でも名前付きサブクエリとして使えます。

CTEで「どの行に印を付けるか」を定義し、INSERT ... SELECT のソースとして使う、という流れです。同じ書き方は DELETE FROM ... WHERE id IN (SELECT id FROM cte) にも応用できるので、削除対象の絞り込みロジックが複雑なときの段階的な削除にも便利です。

CTEを使うべきタイミング

判断の目安をいくつか挙げておきます。

  • クエリが複数の論理的なステップに分かれているとき。 集計して、その集計結果でフィルタして、さらに別のテーブルと結合して……というパイプライン処理は、ステップごとにCTEを切り出すと一気に読みやすくなります。
  • 同じサブクエリを何度も書くことになりそうなとき。 一度定義して、二回参照すればOKです。
  • そのサブクエリに名前を付けたいとき。 「このサブクエリは何をしているのか」をコメントで補足したくなるなら、CTEの名前自体がそのコメント代わりになります。しかも構文として強制されるので、ズレることがありません。
  • 再帰クエリを書こうとしているとき。 これは WITH RECURSIVE でしか実現できません。次ページで詳しく扱います。

逆に、CTEにしなくてよい場面はこちら。

  • 一箇所でしか使わないシンプルなサブクエリ。WHERE id IN (SELECT id FROM ...) のままで十分です。
  • パフォーマンスがシビアなクエリで、ロジックを直接埋め込んだ方が速いと検証済みの場合。SQLiteは他のDBほどCTEを最適化の壁(optimization fence)として強く扱わないのですが、ホットパスの処理なら EXPLAIN QUERY PLAN で実行計画を確認しておくと安心です。

実例で見るCTEの使い方

ここまでの内容を組み合わせて、ちょっとしたレポートを書いてみましょう。各顧客の最大注文額と、その顧客の平均注文額との比較を出すクエリです。

CTEを2つ用意して、それぞれに1つの役割を持たせています。最後の SELECT は結果の整形担当です。クエリを上から順に読むだけで、各ステップが何をしているか自然に追えるはず——これこそがCTEを使う最大のメリットですね。

次のステップ:再帰CTE

ここまで紹介してきたのは、いわゆる通常のCTE、つまり「名前付きサブクエリを1回だけ評価する」パターンでした。SQLiteにはこれに加えて WITH RECURSIVE という構文もあって、CTEが自分自身を参照することで、階層構造をたどったり、連番を生成したり、グラフを探索したりできます。詳しくは次のページで見ていきましょう。

よくある質問

そもそもSQLiteのCTEって何ですか?

CTE(Common Table Expression、共通テーブル式)は、SELECTINSERTUPDATEDELETEの先頭に書ける「名前付きサブクエリ」のことです。WITHキーワードで宣言して名前を付けると、その名前をあとからテーブルのように参照できます。複雑なクエリを段階的に組み立てられるので、ぐっと読みやすくなります。

CTEとサブクエリは何が違うの?

結果としては同じものを返せます。CTEは要するに「外に出して名前を付けたサブクエリ」だからです。違いは可読性と再利用性で、CTEは同じクエリ内で何度でも参照でき、付けた名前そのものが「この中間結果は何者か」というドキュメントになります。単発のフィルタならサブクエリで十分ですが、多段ロジックを組むならCTEが断然有利です。

1つのクエリに複数のCTEは書けますか?

書けます。最初のWITHのあと、2つ目以降はカンマで区切って続けます(WITHを繰り返さないのがポイント)。各CTEは前に定義したCTEを参照できるので、名前付きステップのパイプラインのように積み上げられます。メインのSELECTは最後のCTEのあとに書きます。

Coddy programming languages illustration

Coddyでコードを学ぼう

始める