Q1WITH 名前 AS (SELECT ...) SELECT ... FROM 名前;における CTE の説明として正しいものはどれですか。
WITH 句 — 名前付き中間結果でクエリを段階化する
この記事は、基礎から複雑なSQL,SQLチューニングまでSQLの実践的なスキルを1からマスターする「SQL入門講座の一部」です。
WITHでhigh_earner CTEを作りdepartmentと結合、dept_avgとtop_earnerを連結した多段集計までを、employeeとdepartmentデータで実行しながら確かめます。
本記事で使うデータ — employee と department
CTE(Common Table Expression、共通テーブル式)は、WITH 名前 AS (SELECT ...)の形でサブクエリに名前を付け、後続のクエリで参照する仕組みです。
深くネストしたサブクエリを段階に分けて書けるため、可読性が上がります。
題材は社員テーブルemployee(30 行。
manager_id列が上司のemp_idを指す自己参照構造)と、部署テーブルdepartment(6 行)です。
CTE で多段の集計を読みやすく書いていく演習を順に試します。
WITH で名前付き中間結果を作る — 多段の処理を読みやすく
WITH 名前 AS (SELECT ...)と書くと、カッコ内の SELECT 結果に名前が付き、続く本体クエリのFROMやJOINで参照できます。
「まず中間結果を作り、それを使って最終結果を出す」という 2 段階の処理を、ネストしたサブクエリで書く代わりに、上から下へ読める形に分解できます。
CTE は本体クエリの実行が終わると消える一時的な名前で、テーブルを作るわけではありません。
書き方はWITH 名前 AS ( ... ) SELECT ... FROM 名前 ...;です。
-- 高給社員を CTE high_earner にまとめ、
-- department と結合して部署名を付ける
WITH high_earner AS (
SELECT emp_id, name, dept_id, salary
FROM employee
WHERE salary >= 7000000
)
SELECT h.name, h.salary, d.dept_name
FROM high_earner h
LEFT JOIN department d ON h.dept_id = d.dept_id
ORDER BY h.salary DESC;
上のコードではhigh_earnerという名前で「給与 7,000,000 以上の社員」の中間結果を切り出し、続く本体クエリでFROM high_earnerとしてテーブルのように参照しています。
図にすると次のような流れです。
同じ処理はサブクエリ(SELECTのカッコの中にさらにSELECTを入れる書き方)でも書けます。
試しに、先ほどの WITH 版を、CTE ではなくサブクエリで組み立て直してみます。
-- 同じ処理を 副問い合わせ(サブクエリ)で書いた版
-- FROM のカッコの中に SELECT が入り、外→中→外の順で読むことになる
SELECT h.name, h.salary, d.dept_name
FROM (
SELECT emp_id, name, dept_id, salary
FROM employee
WHERE salary >= 7000000
) AS h
LEFT JOIN department d ON h.dept_id = d.dept_id
ORDER BY h.salary DESC;
結果は WITH 版とまったく同じですが、サブクエリ版は「中のSELECTが何をしているか」を読むのに FROMの中にネストした SELECT へ目線を潜らせる 必要があります。
WITH 版なら中間結果にhigh_earnerという名前を付けて切り出すので、「① 高給社員を絞る → ② 部署名を付ける」の 2 段階を上から下へ素直に読めます。
中間結果が増えるほどこの差は大きくなり、3 段・4 段の処理になるとサブクエリの入れ子は追いにくくなります。
複数の WITH を連結する — 多段の集計を 1 段ずつに分ける
WITHのあとにカンマで区切って複数の CTEを並べられます。
WITH a AS (...), b AS (...) SELECT ...の形で、後ろの CTE は前の CTE を参照できます。
多段の集計(「部署ごとの平均を出す」→「その平均を超える社員を抽出する」→「部署名を付ける」など)を、1 つずつ名前を付けた段に分けて書けるため、長いクエリでも各段の役割を分けることができます。
-- ① dept_avg: 部署ごとの平均給与
-- ② top_earner: 自部署平均を上回る社員
-- ③ 本体: 部署名を付けて出力
WITH dept_avg AS (
SELECT dept_id, AVG(salary) AS avg_salary
FROM employee
WHERE dept_id IS NOT NULL
GROUP BY dept_id
),
top_earner AS (
SELECT e.name, e.dept_id, e.salary
FROM employee e
JOIN dept_avg da ON e.dept_id = da.dept_id
WHERE e.salary > da.avg_salary
)
SELECT d.dept_name, t.name, t.salary
FROM top_earner t
JOIN department d ON t.dept_id = d.dept_id
ORDER BY d.dept_name, t.salary DESC;
理解度チェック
まずは1問ずつ答えてみましょう。
Q2SELECT h.name FROM (SELECT name FROM employee WHERE salary >= 7000000) AS h JOIN department d ON h.dept_id = d.dept_id;と同じ処理を WITH CTE で書き直したときの違いとして正しいものはどれですか。
Q3WITH a AS (...), b AS (...) SELECT ... FROM b;のように複数の CTE を並べたときの説明として正しいものはどれですか。