コマンド道場

WITH 句(共通表式・CTE)の使い方

名前を付けた表を、本体から使う

shain元の表

shain_idnamesalary
1鈴木980000
2佐藤720000
3田中520000
…

heikinWITH で作った表

h
542003

結果平均より上の人

namesalary
鈴木980000
佐藤720000
山田700000
…
  • 元の表 18 行 → WITH で作った表 1 行 → 平均より上の人 5 行
  • 前の段の結果が次の段の入力になります。表が 1 つ増えたつもりで読んでください

真ん中の heikin が WITH で作った表です。1 行 1 列しかありませんが、これも立派な表として本体から参照できます。

WITH は問い合わせに名前を付けて、そのあとの本体から表として使う書き方です。 共通表式(CTE)とも呼びます。同じ集計を 2 か所で使うとき、段階を分けて上から読めるようにするときに効きます。

主要 7 製品で通用 そのまま使える: PostgreSQL / MySQL / MariaDB / SQLite / SQL Server / Oracle Database / IBM Db2

製品ごとの対応表(根拠つき)を見る

このページの実行結果と図は、すべて本物のデータベースに同じ SQL を流して得たものです。手で書いた「たぶんこうなる」ではありません。

01先に名前を付けてから本体を書く

結論: WITH 名前 AS (問い合わせ) 本体 の形です。 本体からは、その名前をふつうの表のように使えます。

読む順番が「材料 → 本体」になるのが特徴です。派生表は FROM の中に問い合わせが入り込むので、外側を読むために内側を先に読む必要がありますが、WITH なら上から順に読めます。

名前はカンマで区切っていくつでも定義できます。あとの定義から前の定義を参照することもできます。

⚠️ WITH で作った表は、その文の中だけのものです。文が終われば消えるので、表を作る(CREATE TABLE)のとは違います。

1 行ずつ、何をしているか
  1. WITH zaiseki AS (★ここで名前を付ける
  2. SELECT * FROM shain中身はふつうの問い合わせ
  3. WHERE taishoku_on IS NULL)在籍中だけ
  4. SELECT b.busho_name, count(*) AS ninzu★ここから本体
  5. FROM zaiseki z付けた名前を表として使う
  6. JOIN busho b ON b.busho_id = z.busho_id結合の相手にもできる
  7. GROUP BY b.busho_nameまとめる
  8. ORDER BY count(*) DESC, b.busho_name;結果の並べ方

上から読めば「在籍中の人を zaiseki と呼ぶ。その zaiseki を部署ごとに数える」と、そのまま日本語になります。

在籍中の社員を部署ごとに数える
WITH zaiseki AS (
  SELECT * FROM shain WHERE taishoku_on IS NULL
)
SELECT b.busho_name, count(*) AS ninzu
FROM zaiseki z
  JOIN busho b ON b.busho_id = z.busho_id
GROUP BY b.busho_name
ORDER BY count(*) DESC, b.busho_name;
busho_nameninzu
開発4
営業3
マーケ2
基盤2
管理2
経営1

(6 行)

6 行です。派生表で書いた場合とまったく同じ結果で、違うのは読む順番だけです。

02同じ結果を 2 か所で使える

結論: WITH の本当の効きどころは、同じ結果を 2 回以上使うときです。

派生表は書いた場所でしか使えないので、2 か所で必要なら同じ SQL を 2 回書くことになります。片方だけ直して食い違う事故が起きます。

WITH なら定義は 1 か所です。直す場所が 1 つなので、食い違いようがありません。

⚠️ 「2 回書いてあるのは同じものか」を読む人に確かめさせないで済む、というのが本質です。速度の話ではありません。

平均を 2 か所で使う
WITH heikin AS (
  SELECT round(avg(salary)) AS h FROM shain
)
SELECT (SELECT h FROM heikin) AS zentai_heikin,
       count(*) AS ue_no_ninzu
FROM shain
WHERE salary > (SELECT h FROM heikin);
zentai_heikinue_no_ninzu
5420035

(1 行)

平均 542003 円を上回るのは 5 人でした。平均を出す式は 1 回しか書いていないので、片方だけ直してずれる心配がありません。

⚠️ この書き方は SQL Server / IBM Db2 では使えません(round(x)(引数 1 つ))。製品ごとの対応表

03段階を分けて上から読めるようにする

結論: 名前をいくつも定義して、処理を段に分けられます。

1 つの SELECT に副問い合わせを何重にも入れると、内側から読まないと分からない文になります。WITH で段に分ければ、上から順に「これを作って、次にこれを作って、最後にこう出す」と読めます。

名前の付け方がそのまま説明になるので、コメントを書かなくても意図が伝わるのが利点です。

⚠️ 分けすぎると今度は行き来が増えます。1 つの段が 1 つの意味になるところで切ってください。

2 つの名前を定義して段に分ける
WITH zaiseki AS (
  SELECT * FROM shain WHERE taishoku_on IS NULL
),
busho_goukei AS (
  SELECT busho_id, sum(salary) AS goukei, count(*) AS ninzu
  FROM zaiseki
  GROUP BY busho_id
)
SELECT b.busho_name, g.ninzu, g.goukei
FROM busho_goukei g
  JOIN busho b ON b.busho_id = g.busho_id
ORDER BY g.goukei DESC;
busho_nameninzugoukei
開発42215050
営業31651000
管理21105000
基盤21078000
経営1980000
マーケ2903000

(6 行)

6 行です。あとの定義(busho_goukei)から前の定義(zaiseki)を参照しています。上から順に読めば、何をしているかが名前だけで分かります。

04⚠️ よくある間違い:中で付けなかった列名は、外から使えない

結論: WITH の外から使う列には、中で名前を付けてください。 count(*) のような式に名前を付けないまま外で ninzu と書くと、その名前の列は無いというエラーになります。

WITH で作った表の列名は、中の SELECT が出した名前がそのまま使われます。式には自動で名前が付きますが、それは外で書こうとした名前とは違います。外から見えるのは中で決まった名前だけです。

直し方は 2 つあります。

  • 中で AS を付ける … count(*) AS ninzu
  • 名前の後ろに列名を並べる … WITH ninzu_hyo (busho_id, ninzu) AS (…)。中の列に左から順に名前が付きます

⚠️ 後ろに並べる書き方では、並べる数を中の列の数と揃えてください。 中の列より多く並べるとエラーになります。

⚠️ 中で名前を付けないまま、外で ninzu と書く
WITH ninzu_hyo AS (
  SELECT busho_id, count(*)
  FROM shain
  WHERE busho_id IS NOT NULL
  GROUP BY busho_id
)
SELECT busho_id, ninzu
FROM ninzu_hyo
ORDER BY busho_id;
ERROR: column "ninzu" does not exist

エラーになります。中の count(*) の列には自動の名前が付いていて、ninzu という列はどこにもありません。

名前の後ろに列名を並べると通る
WITH ninzu_hyo (busho_id, ninzu) AS (
  SELECT busho_id, count(*)
  FROM shain
  WHERE busho_id IS NOT NULL
  GROUP BY busho_id
)
SELECT busho_id, ninzu
FROM ninzu_hyo
ORDER BY busho_id;
busho_idninzu
11
24
34
43
52
72

(6 行)

中の 2 列に、左から busho_id・ninzu の名前が付きます。中で count(*) AS ninzu と書いても同じ結果です。

⚠️ 中の列より多く名前を並べる
WITH ninzu_hyo (busho_id, ninzu, heikin) AS (
  SELECT busho_id, count(*)
  FROM shain
  WHERE busho_id IS NOT NULL
  GROUP BY busho_id
)
SELECT busho_id, ninzu
FROM ninzu_hyo
ORDER BY busho_id;
ERROR: WITH query "ninzu_hyo" has 2 columns available but 3 columns specified

中の列は 2 つなのに 3 つ並べたので、エラーになります。並べる数は中の列の数と揃えます。

05派生表とどちらを使うか

結論: 1 か所でしか使わないなら派生表、2 か所以上か段に分けたいなら WITH が目安です。

どちらでも書けることがほとんどなので、読みやすいほうを選んでください。

  • 派生表 … その場に書いてある。短い処理なら目で追いやすい
  • WITH … 名前が付く。長い処理や、同じものを何度も使う場合に強い

⚠️ 速さは製品と状況によります。 「WITH は遅い」「派生表のほうが速い」といった一般論は、いまの製品では当てになりません。遅いと感じたら両方書いて実際に測ってください。

⚠️ WITH には再帰という別の使い方もあります。階層をたどるための書き方で、こちらは派生表では代用できません。

同じことを派生表で書く
SELECT b.busho_name, count(*) AS ninzu
FROM (SELECT * FROM shain WHERE taishoku_on IS NULL) t
  JOIN busho b ON b.busho_id = t.busho_id
GROUP BY b.busho_name
ORDER BY count(*) DESC, b.busho_name;
busho_nameninzu
開発4
営業3
マーケ2
基盤2
管理2
経営1

(6 行)

6 行で、WITH で書いた最初の例と完全に同じ結果です。違うのは名前が付いているかどうかだけです。

自分で打ってみる

このページの例と同じデータが入った本物のデータベースを、この場(あなたのブラウザの中)で起動できます。打った SQL がサーバーへ送られることはありません。

押すと初回だけ約 9MB を読み込みます(2 回目以降はキャッシュされます)。

解いてみる

読んだだけでは書けるようになりません。実行結果で採点します。

関連するトピック

根拠(一次情報)