PostgreSQL | SQLite+MySQL経験者向け、30日で習得するPostgreSQL:高度SQL - Day8 CTE(WITH句)

SQL PostgreSQL
スポンサーリンク

Day8 後半のゴール

「“CTEを使うかどうか”を、自分で選べるようになる」

前半で「CTE=途中結果に名前を付ける仕組み」という感覚はつかめました。
後半では、もう一歩踏み込んで「どんなときにCTEを使うと嬉しいか」「どう書くと読みやすくなるか」「MySQLやSQLiteとの違いをどう意識するか」を整理していきます。

ここでのゴールは、
「これはCTEに切り出した方がいいな」と自分で判断できること。
CTEに“いい名前”を付けて、SQL全体の意図を説明できること。
PostgreSQLでのCTEの振る舞いをざっくり理解しつつ、「まずは可読性優先で使う」というスタンスを持てること、です。


どんなときにCTEを使うと嬉しいか

「“1文でやりたいことが3つ以上ある”ときはCTE候補」

CTEを使うべき典型パターンを、感覚でつかんでおきましょう。
キーワードは「1文の中に“段階”が見えるとき」です。

例えば、こんなSQLを書こうとしているとします。

直近30日間の注文だけを対象にする。
その中から、1万円以上買っているユーザーだけを抽出する。
そのユーザーの中で、メールアドレスが特定ドメインの人だけを出す。

これをCTEで書くと、こうなります。

WITH recent_orders AS (
  SELECT *
  FROM orders
  WHERE created_at >= NOW() - INTERVAL '30 days'
),
rich_users AS (
  SELECT
    user_id,
    SUM(amount) AS total_amount
  FROM recent_orders
  GROUP BY user_id
  HAVING SUM(amount) >= 10000
),
target_users AS (
  SELECT u.*
  FROM users u
  JOIN rich_users r ON u.id = r.user_id
  WHERE u.email LIKE '%@example.com'
)
SELECT *
FROM target_users;
SQL

ここでのポイントは、「やりたいことの“段階”とCTEの名前が対応している」ことです。

recent_orders → 直近30日の注文
rich_users → たくさん買っているユーザー
target_users → その中から特定ドメインのユーザー

もしこれをCTEなしで書くと、JOINの中にサブクエリが入り、さらにその中に条件が入り…と、
「どこで何をしているか」が一気に見えにくくなります。

目安として、「1つのSQLで3段階以上の処理をしている」と感じたら、
CTEで分割できないかを考える、という癖をつけるといいです。


CTEに“いい名前”を付けるコツ

「“何をしているか”が一言で分かる名前にする」

CTEは、名前の付け方で読みやすさが大きく変わります。
変数名や関数名と同じで、「中身を見なくても役割が分かる名前」が理想です。

悪い例として、こういう名前を考えてみます。

WITH t1 AS (...),
     t2 AS (...),
     t3 AS (...)
SELECT * FROM t3;
SQL

これだと、「t1って何?」「t2って何?」と、毎回中身を見ないと意味が分かりません。

さっきの例のように、

recent_orders
rich_users
target_users

といった名前にしておくと、SQLを上から読んだときに「ストーリー」が頭に浮かびます。

CTE名を付けるときのコツは、「日本語で説明するときの名詞を、そのまま英語にする」ことです。

「直近30日の注文」→ recent_orders
「売上の多いユーザー」→ rich_users
「対象ユーザー」→ target_users

SQLは“仕様書の翻訳”なので、「仕様書に出てきそうな言葉」をそのまま名前にするのが一番強いです。


MySQL・SQLiteとの違いを軽く押さえておく

「PostgreSQLはCTEが標準装備、“読めるSQL”を書きやすい」

MySQLやSQLiteでも最近はWITH句が使えますが、
歴史的には「PostgreSQLの方が先にCTEをちゃんと実装していた」世界です。

SQLite:バージョンによってはCTEが使えない/機能が限定的なことがある。
MySQL:バージョン8以降でWITH句が使えるようになったが、古い環境だと使えないこともある。

PostgreSQLを前提にしているなら、「CTEは遠慮なく使っていい標準機能」と考えてOKです。

ここで大事なのは、「MySQL時代に“サブクエリ地獄”で頑張っていたSQL」を、
PostgreSQLに来たタイミングで「CTEで読みやすく書き直す」という発想です。

移植やリファクタリングのときに、

サブクエリをCTEに引き上げる。
ネストを浅くして、上から順に読めるようにする。

という作業をすると、「同じ処理なのに、SQLの理解コストが一気に下がる」体験ができます。


CTEとパフォーマンスの話を“軽く”触れておく

「まずは“可読性優先”、そのあと必要ならチューニング」

CTEには、パフォーマンスの話もあります。
PostgreSQLのバージョンによっては、「CTEは一度必ずマテリアライズ(途中結果を一時的に確定させる)」という挙動をすることがあり、
それが速度に影響するケースもあります。

ただ、Day8の段階では、ここを深追いしなくて大丈夫です。
まずは「CTEでSQLを読みやすく書けるようになる」ことが優先です。

現実的な順番としては、こう考えるのが健全です。

最初は、CTEを使って“意味が分かるSQL”を書く。
そのSQLがボトルネックになったら、EXPLAINで実行計画を見て、必要ならCTEをサブクエリに戻したり、インデックスを調整したりする。

つまり、「最初からパフォーマンスを恐れてCTEを封印する」のではなく、
「まずは読みやすく書いて、必要になったらチューニングする」という順番で考えるのがいいです。

PostgreSQLはプランナが賢いので、「CTEを使ったから即遅い」ということはほとんどありません。
“読みやすさ”というメリットを、まずはしっかり取りに行きましょう。


CTEを使った“リファクタリング練習”のイメージ

「今あるSQLを、WITH句で分割し直してみる」

最後に、実際の練習方法のイメージを共有しておきます。

もしあなたが既にMySQLやSQLiteで、こんなSQLを書いたことがあるなら、

FROMの中にSELECTがネストしている。
さらにその中にGROUP BYやHAVINGが入っている。
WHEREやJOIN条件が長くて、どこで何をしているか分かりにくい。

それをPostgreSQL環境に持ってきて、「CTEで分割し直す」というのが、最高の練習になります。

やることはシンプルです。

サブクエリを1つ抜き出して、WITH句にする。
そのサブクエリに、意味のある名前を付ける。
元の場所では、その名前をテーブルのように使う。

これを繰り返していくと、「あ、ここは“フィルタの段階”」「ここは“集計の段階”」と、
自分のSQLの中に“構造”が見えてきます。

CTEは、ただの機能ではなく、「自分のSQL思考を整理するための道具」です。
Day8のうちに、「1つでいいから、自分のSQLをCTEでリファクタリングしてみる」イメージを持っておいてほしいなと思います。


Day8 後半のまとめ

CTE(WITH句)は、「1つのSQLの中にある“段階”を上に引き上げて名前を付ける」ための仕組みであり、直近30日注文→高額ユーザー→対象ドメインユーザーのような処理を recent_ordersrich_userstarget_users といった名前付きCTEに分けることで、「SQL全体のストーリー」を上から順に読める形にできる。
CTE名には“仕様書に出てきそうな名詞”をそのまま使い、t1t2のような意味のない名前を避けることで、SQLを見ただけで「何をしているか」が伝わるようになり、MySQLやSQLite時代のサブクエリ地獄をPostgreSQLでは“WITH句でリファクタリングする”という発想に切り替える。
パフォーマンスの細かい話は一旦置いておき、「まずはCTEで可読性の高いSQLを書く → 本当に遅くなったときだけEXPLAINを見て調整する」という順番を意識しつつ、既存の複雑なSQLをWITH句で分割し直す練習を通して、「CTEを使うかどうかを自分で選べる」状態に近づく――これが Day8 後半の着地点になる。

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