SQL基本構文チートシート【保存版】SELECT・JOIN・GROUP BYから副問合せ・ウィンドウ関数まで

SELECT・WHERE・JOIN・GROUP BY・HAVING・副問合せ(サブクエリ)・CASE式・ウィンドウ関数まで、共通のサンプルテーブルに対する実行結果つきでSQLの基本構文をひと通り総復習できる保存版チートシート。標準SQL準拠で方言差も適宜注記する。

SQLの構文は種類が多く、「JOINの4種類の違い」「WHEREとHAVINGの使い分け」「ウィンドウ関数の書き方」などは、使うたびに検索し直している人も多いはずだ。本記事は、一貫した2つのサンプルテーブル(employeesdepartments)だけを使い、SELECTの基本から副問合せ・ウィンドウ関数までを「クエリ+実行結果」のセットで一気に総復習できる保存版チートシートである。構文は標準SQLに準拠し、MySQL/PostgreSQLで挙動が分かれる箇所だけ注記する。

1. サンプルテーブルの準備

以降すべての例は、次の2テーブルに対して実行する。employees(社員)は7行、departments(部署)は4行で、あえて「部署未設定の社員(Frank)」と「社員がいない部署(HR)」を混ぜてある。これがJOINや副問合せの結果の違いを際立たせる仕込みになっている。

CREATE TABLE departments (
    dept_id   INTEGER PRIMARY KEY,
    dept_name TEXT NOT NULL,
    location  TEXT NOT NULL
);

CREATE TABLE employees (
    emp_id    INTEGER PRIMARY KEY,
    name      TEXT NOT NULL,
    dept_id   INTEGER REFERENCES departments(dept_id),
    salary    INTEGER NOT NULL,
    hire_year INTEGER NOT NULL
);

INSERT INTO departments (dept_id, dept_name, location) VALUES
  (1, 'Engineering', 'Tokyo'),
  (2, 'Sales',       'Osaka'),
  (3, 'Marketing',   'Tokyo'),
  (4, 'HR',          'Fukuoka');

INSERT INTO employees (emp_id, name, dept_id, salary, hire_year) VALUES
  (101, 'Alice', 1,    720000, 2019),
  (102, 'Bob',   2,    580000, 2020),
  (103, 'Carol', 1,    810000, 2018),
  (104, 'Dave',  3,    540000, 2021),
  (105, 'Eve',   2,    610000, 2022),
  (106, 'Frank', NULL, 500000, 2023),
  (107, 'Grace', 1,    690000, 2021);

departments

dept_iddept_namelocation
1EngineeringTokyo
2SalesOsaka
3MarketingTokyo
4HRFukuoka

employees

emp_idnamedept_idsalaryhire_year
101Alice17200002019
102Bob25800002020
103Carol18100002018
104Dave35400002021
105Eve26100002022
106FrankNULL5000002023
107Grace16900002021

Frankはまだ部署配属前(dept_idNULL)、HR部署(dept_id = 4)はまだ誰も配属されていない、という状態を意図的に作っている。

2. SELECTの基本 — 列選択とWHERE

列を選ぶ

SELECT name, salary FROM employees;

employees の7行から namesalary の2列だけを抜き出す。行数は変わらず7行のままである。

比較演算子で絞り込む

SELECT name, salary FROM employees WHERE salary >= 600000;
namesalary
Alice720000
Carol810000
Eve610000
Grace690000

AND / OR で条件を組み合わせる

SELECT name, dept_id, salary FROM employees
WHERE dept_id = 1 AND salary >= 700000;
namedept_idsalary
Alice1720000
Carol1810000

AND は両方の条件を満たす行だけ、OR はどちらか一方を満たす行を残す。優先順位は ANDOR より強いため、混在させるときは (dept_id = 1 OR dept_id = 3) AND salary >= 700000 のように括弧で明示するのが安全である。

LIKE によるパターン一致

SELECT name FROM employees WHERE name LIKE 'A%';   -- 前方一致
SELECT name FROM employees WHERE name LIKE '%e';    -- 後方一致

'A%'(Aで始まる)は Alice の1行のみ。'%e'(eで終わる)は AliceDaveEve の3行がヒットする。% は0文字以上の任意文字列、_ は任意の1文字に対応するワイルドカードである。

IN でリスト一致

SELECT name, dept_id FROM employees WHERE dept_id IN (1, 3);
namedept_id
Alice1
Carol1
Dave3
Grace1

dept_id = 1 OR dept_id = 3 と同じ意味だが、IN の方が候補が多いときに読みやすい。

BETWEEN で範囲指定

SELECT name, salary FROM employees
WHERE salary BETWEEN 550000 AND 700000;
namesalary
Bob580000
Eve610000
Grace690000

BETWEEN A AND Bsalary >= A AND salary <= B と同義で、両端の値を含む(閉区間)。

NULL判定

SELECT name FROM employees WHERE dept_id IS NULL;
name
Frank

NULLは「値が存在しない」という特殊な状態のため、= NULL では絶対にヒットしない。必ず IS NULL / IS NOT NULL を使う。

3. 並べ替えと件数の制御

ORDER BY

SELECT name, salary FROM employees ORDER BY salary DESC;
namesalary
Carol810000
Alice720000
Grace690000
Eve610000
Bob580000
Dave540000
Frank500000

ASC(昇順、省略時のデフォルト)と DESC(降順)を指定できる。複数列を指定すると、先頭のキーが優先される。

SELECT name, dept_id, salary FROM employees
ORDER BY dept_id ASC, salary DESC;
namedept_idsalary
Carol1810000
Alice1720000
Grace1690000
Eve2610000
Bob2580000
Dave3540000
FrankNULL500000

dept_id が同じ行の中では salary DESC が効いている。NULLの扱いは方言差があり、PostgreSQLは ASC のときNULLを末尾に置くのがデフォルト、MySQLはNULLを先頭に置くのがデフォルトである(上表はPostgreSQL準拠)。厳密に制御したい場合は NULLS LAST / NULLS FIRST(標準SQL・PostgreSQL対応)を明示する。

LIMIT / OFFSET で件数を絞る

SELECT name, salary FROM employees
ORDER BY salary DESC
LIMIT 3;
namesalary
Carol810000
Alice720000
Grace690000
SELECT name, salary FROM employees
ORDER BY salary DESC
LIMIT 3 OFFSET 3;
namesalary
Eve610000
Bob580000
Dave540000

先頭3行を読み飛ばして次の3行を取る、いわゆる「ページネーション」の基本形である。LIMIT/OFFSET はMySQL・PostgreSQL・SQLiteで共通の書き方だが、標準SQLでは FETCH FIRST 3 ROWS ONLY、SQL Serverでは TOP 3 と書く点は方言差として覚えておきたい。

4. 集計とグループ化 — GROUP BY / HAVING

集計関数

SELECT COUNT(*) AS cnt, SUM(salary) AS total,
       AVG(salary) AS avg_salary, MAX(salary) AS max_salary, MIN(salary) AS min_salary
FROM employees;
cnttotalavg_salarymax_salarymin_salary
74450000635714.29…810000500000

COUNT(*) は行数、AVG は合計を件数で割った値(4,450,000 ÷ 7 ≒ 635,714.29)である。COUNT(dept_id) のように列を指定すると、その列がNULLの行(Frank)はカウントから除外される点に注意する。

GROUP BY

SELECT dept_id, COUNT(*) AS cnt, AVG(salary) AS avg_salary
FROM employees
GROUP BY dept_id;
dept_idcntavg_salary
13740000.00
22595000.00
31540000.00
NULL1500000.00

GROUP BY は指定した列の値が同じ行を1つのグループにまとめ、SELECT 句の集計関数をグループ単位で計算する。NULLも1つのグループとして扱われる(Frankだけの「NULLグループ」ができる)。

HAVING — グループへの絞り込み

SELECT dept_id, COUNT(*) AS cnt
FROM employees
GROUP BY dept_id
HAVING COUNT(*) >= 2;
dept_idcnt
13
22

dept_id = 3NULL はどちらもグループの件数が1件のため除外される。

WHEREとHAVINGの違いは「どの段階でフィルタするか」に尽きる。WHERE は集計前の個々の行を絞り込み、HAVINGGROUP BY で集計した後のグループを絞り込む。両方を併用すると次のようになる。

SELECT dept_id, AVG(salary) AS avg_salary
FROM employees
WHERE dept_id IS NOT NULL
GROUP BY dept_id
HAVING AVG(salary) >= 600000;
dept_idavg_salary
1740000.00

処理順序は「WHERE でNULL部署のFrankを先に除外 → 残り6行を dept_id でグループ化 → 各グループの平均給与が60万円以上のものだけ HAVING で残す」となり、部署2(平均595,000円)は基準未満のため落ちる。HAVING の条件に COUNT(*)AVG(salary) のような集計関数を書けるのに対し、WHERE には書けない(集計前なので集計値がまだ存在しない)という制約の違いも覚えておくとよい。

5. JOIN — 複数テーブルの結合

employeesdepartmentsdept_id で結合する。結合の種類によって、Frank(部署未設定)とHR(社員なし)がどう扱われるかが変わる。

SELECT e.name, d.dept_name, d.location
FROM employees AS e
INNER JOIN departments AS d ON e.dept_id = d.dept_id;

INNER JOIN(一致した行だけ、6行)

namedept_namelocation
AliceEngineeringTokyo
BobSalesOsaka
CarolEngineeringTokyo
DaveMarketingTokyo
EveSalesOsaka
GraceEngineeringTokyo

Frank(部署未設定)とHR(社員なし)はどちらも結合相手が存在しないため、結果に一切現れない。

SELECT e.name, d.dept_name
FROM employees AS e
LEFT JOIN departments AS d ON e.dept_id = d.dept_id;

LEFT JOINemployees 全件+一致分、7行)

namedept_name
AliceEngineering
BobSales
CarolEngineering
DaveMarketing
EveSales
FrankNULL
GraceEngineering

左側(FROM に書いた employees)は必ず全件残る。Frankは一致する部署がないため dept_name がNULLになって残る。一方HRは元々右側にしかいないため現れない。

SELECT e.name, d.dept_name
FROM employees AS e
RIGHT JOIN departments AS d ON e.dept_id = d.dept_id;

RIGHT JOINdepartments 全件+一致分、7行)

namedept_name
AliceEngineering
CarolEngineering
GraceEngineering
BobSales
EveSales
DaveMarketing
NULLHR

今度は右側(departments)が全件残る。HRは社員が一人もいないため name がNULLになって残るが、Frankは(部署が一致しないので)現れない。

SELECT e.name, d.dept_name
FROM employees AS e
FULL JOIN departments AS d ON e.dept_id = d.dept_id;

FULL JOIN(両側全件の和集合、8行)

namedept_name
AliceEngineering
BobSales
CarolEngineering
DaveMarketing
EveSales
FrankNULL
GraceEngineering
NULLHR

FrankもHRも両方残る。LEFTとRIGHTの結果を単純に足し合わせて重複(一致した6行)を1回にまとめたものがFULLだとイメージすればよい。

employeesとdepartmentsのミニ版(各3行)に対するINNER・LEFT・RIGHT・FULL JOINの結果を並べた図。INNER JOINは一致した2行だけ、LEFT JOINはemployees全件に一致分を加えた3行(未配属のCarolはNULLで残る)、RIGHT JOINはdepartments全件に一致分を加えた3行(社員のいないHRはNULLで残る)、FULL JOINは両側全件の和集合で4行になる。灰色のセルがNULLを表す

JOINで行が増減する理由は単純で、JOINは「マッチする行の組み合わせをすべて列挙する」演算だからである。1件の部署に3人の社員がいれば、その部署は3回登場する(Engineeringが3回出てくるのはそのため)。逆に一致相手がいない行は、INNER JOINでは消え、LEFT/RIGHT/FULLではNULLで埋められて残る。

なお、MySQLは FULL JOIN 構文を直接サポートしていない(PostgreSQL・SQL Serverは対応)。MySQLで同等の結果を得るには LEFT JOIN の結果と RIGHT JOIN の結果を UNION で合成する必要がある。

6. 副問合せ(サブクエリ)

WHERE句の中のサブクエリ

平均給与より高い社員を抽出する。

SELECT name, salary FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees);
namesalary
Alice720000
Carol810000
Grace690000

内側の (SELECT AVG(salary) FROM employees) が先に評価されて 635714.29 という1つの値になり、外側のクエリはそれを定数のように使って絞り込む。Eve(610000)は平均未満のため含まれない。

FROM句の中のサブクエリ(派生テーブル)

部署ごとの平均給与を一度集計し、そのうえで60万円以上の部署だけを取り出す。

SELECT dept_id, avg_salary
FROM (
    SELECT dept_id, AVG(salary) AS avg_salary
    FROM employees
    WHERE dept_id IS NOT NULL
    GROUP BY dept_id
) AS dept_avg
WHERE avg_salary >= 600000;
dept_idavg_salary
1740000.00

FROM 句の中に書いたサブクエリは「派生テーブル(derived table)」と呼ばれ、集計結果をあたかも1つのテーブルであるかのように再利用できる。前節の HAVING と同じ結果を得られるが、集計結果に対してさらに複雑な加工をしたい場合はこちらの方が書きやすいことが多い。

相関サブクエリ

「自分の所属部署の平均より高い給与をもらっている社員」を抽出する。外側のクエリの行(e1)を、内側のサブクエリ(e2)が参照している点がポイントである。

SELECT e1.name, e1.salary
FROM employees AS e1
WHERE e1.salary > (
    SELECT AVG(e2.salary)
    FROM employees AS e2
    WHERE e2.dept_id = e1.dept_id
);
namesalary
Carol810000
Eve610000

相関サブクエリは外側の行が1行変わるたびに内側が再評価される。CarolはEngineering部署の平均740,000円より高いので該当、AliceとGraceは部署平均に届かないので除外される。Frankは dept_id がNULLのため、e2.dept_id = e1.dept_idNULL = NULL という比較になり常にUNKNOWN(false扱い)になるので対象外である。

EXISTS / NOT EXISTS

社員が1人以上いる部署だけを取り出す。

SELECT dept_name FROM departments AS d
WHERE EXISTS (
    SELECT 1 FROM employees AS e WHERE e.dept_id = d.dept_id
);
dept_name
Engineering
Sales
Marketing

社員が1人もいない部署(=欠員部署)を取り出したい場合は NOT EXISTS を使う。

SELECT dept_name FROM departments AS d
WHERE NOT EXISTS (
    SELECT 1 FROM employees AS e WHERE e.dept_id = d.dept_id
);
dept_name
HR

EXISTS はサブクエリが1行でも返せば真になる「存在確認」の演算子で、値そのものは見ない。多くの実装では該当行が見つかった時点で探索を打ち切れるため、IN で同じ判定をするより効率的になりやすい。

7. CASE式で条件分岐する

給与を3段階のバンドに分類する。

SELECT name, salary,
    CASE
        WHEN salary >= 700000 THEN 'High'
        WHEN salary >= 600000 THEN 'Mid'
        ELSE 'Standard'
    END AS salary_band
FROM employees
ORDER BY salary DESC;
namesalarysalary_band
Carol810000High
Alice720000High
Grace690000Mid
Eve610000Mid
Bob580000Standard
Dave540000Standard
Frank500000Standard

CASE は上から順に条件を評価し、最初に真になった WHEN の結果を返す(どれにも当てはまらなければ ELSE)。この単純CASE構文(WHEN 式 THEN 値 を並べる形)に対して、CASE 列 WHEN 値1 THEN ... WHEN 値2 THEN ... と書く「単純CASE式」もあるが、範囲判定には上記の「検索CASE式」の方が向いている。

CASE は集計関数と組み合わせて「条件付きカウント」にもよく使われる。

SELECT
    SUM(CASE WHEN salary >= 700000 THEN 1 ELSE 0 END) AS high_cnt,
    SUM(CASE WHEN salary >= 600000 AND salary < 700000 THEN 1 ELSE 0 END) AS mid_cnt,
    SUM(CASE WHEN salary < 600000 THEN 1 ELSE 0 END) AS standard_cnt
FROM employees;
high_cntmid_cntstandard_cnt
223

GROUP BY を使わずに、1行の集計結果としてバンド別の人数を横並びで得られる。ダッシュボード集計でよく使うパターンである。

8. ウィンドウ関数入門

ウィンドウ関数は GROUP BY と違って行を集約せず、各行に「同じグループ内での順位や合計」を追加情報として付与できる。OVER (PARTITION BY ... ORDER BY ...) の形で書く。

ROW_NUMBER — 部署内の給与順位

SELECT name, dept_id, salary,
    ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC) AS rn
FROM employees
ORDER BY dept_id, rn;
namedept_idsalaryrn
Carol18100001
Alice17200002
Grace16900003
Eve26100001
Bob25800002
Dave35400001
FrankNULL5000001

PARTITION BY dept_id が部署ごとに「区画」を作り、区画の中だけで ORDER BY salary DESC に基づく連番を振る。GROUP BY と違って元の7行はそのまま残る点が最大の違いである。

RANK — 同順位の扱い

SELECT name, salary,
    RANK() OVER (ORDER BY salary DESC) AS rnk
FROM employees
ORDER BY rnk;
namesalaryrnk
Carol8100001
Alice7200002
Grace6900003
Eve6100004
Bob5800005
Dave5400006
Frank5000007

今回のサンプルデータは給与がすべて異なるため ROW_NUMBER と同じ結果になるが、両者の違いは同順位(タイ)があるときに出る。例えば2人が同じ給与で1位タイだった場合、RANK は両者に1位を与えたうえで次の人を3位に飛ばす(2位が欠番になる)。同じ状況で欠番を作りたくない場合は DENSE_RANK(両者1位、次の人は2位)を使う。用途に応じて3つを使い分けるとよい。

SUM() OVER — 集約せずに合計を添える

部署ごとの合計給与を、行を減らさずに各行へ添える。

SELECT name, dept_id, salary,
    SUM(salary) OVER (PARTITION BY dept_id) AS dept_total
FROM employees
ORDER BY dept_id;
namedept_idsalarydept_total
Alice17200002220000
Carol18100002220000
Grace16900002220000
Bob25800001190000
Eve26100001190000
Dave3540000540000
FrankNULL500000500000

「自分の給与が部署合計の何%か」を1クエリで出したいときなど、GROUP BY して再度JOINし直すより簡潔に書ける。

ORDER BY を付けたウィンドウでは、累積合計(ランニングトータル)も作れる。

SELECT name, salary,
    SUM(salary) OVER (ORDER BY salary DESC
        ROWS BETWEEN UNBOUNDED PRECEDING AND CURRENT ROW) AS running_total
FROM employees
ORDER BY salary DESC;
namesalaryrunning_total
Carol810000810000
Alice7200001530000
Grace6900002220000
Eve6100002830000
Bob5800003410000
Dave5400003950000
Frank5000004450000

最終行の累積合計(4,450,000)が、4節で計算した SUM(salary) の全体合計と一致することも確認できる。

9. データ操作 — INSERT / UPDATE / DELETE

INSERT — 行を追加する

INSERT INTO employees (emp_id, name, dept_id, salary, hire_year)
VALUES (108, 'Henry', 2, 550000, 2026);

departments のSalesチーム(dept_id = 2)にHenryが加わり、employees は8行になる。

UPDATE — 既存の行を書き換える

Engineering部署(dept_id = 1)全員の給与を5%引き上げる。

UPDATE employees
SET salary = salary * 1.05
WHERE dept_id = 1;
namesalary(更新前)salary(更新後)
Alice720000756000
Carol810000850500
Grace690000724500

DELETE — 行を削除する

2019年より前に入社した社員を削除する。

DELETE FROM employees WHERE hire_year < 2019;

該当するのは hire_year = 2018 のCarol1件のみで、これが削除される。

WHEREを忘れる危険は、UPDATE/DELETE で最も事故が起きやすいポイントである。

-- 危険:WHEREがないので全行が対象になる
DELETE FROM employees;

WHERE を書き忘れた DELETE FROM employees; は、テーブルの全行(Henryの追加とCarolの削除を経て、この時点で7行)をひとつ残らず削除する。同様に WHERE なしの UPDATE はテーブル全行の値を書き換えてしまう。本番環境で実行する前には、まず同じ条件で SELECT を実行して対象行数を確認する、トランザクション(BEGINCOMMIT/ROLLBACK)で囲んで結果を見てからコミットする、といった手順を徹底したい。

構文早見表

構文用途
SELECT列を選択して取得SELECT name, salary FROM employees;
WHERE行を条件で絞り込むWHERE salary >= 600000
LIKE文字列パターン一致WHERE name LIKE 'A%'
INリストとの一致WHERE dept_id IN (1, 3)
BETWEEN範囲指定(閉区間)WHERE salary BETWEEN 550000 AND 700000
IS NULLNULL判定WHERE dept_id IS NULL
ORDER BY並べ替えORDER BY salary DESC
LIMIT / OFFSET件数制御・ページネーションLIMIT 3 OFFSET 3
COUNT/SUM/AVG/MAX/MIN集計SELECT COUNT(*), AVG(salary) FROM employees;
GROUP BY行をグループ化して集計GROUP BY dept_id
HAVING集計後のグループを絞り込むHAVING COUNT(*) >= 2
INNER JOIN一致した行だけ結合... INNER JOIN departments d ON e.dept_id = d.dept_id
LEFT JOIN左テーブル全件+一致分... LEFT JOIN departments d ON ...
RIGHT JOIN右テーブル全件+一致分... RIGHT JOIN departments d ON ...
FULL JOIN両テーブル全件の和集合... FULL JOIN departments d ON ...
WHERE内サブクエリ定数のように使う副問合せWHERE salary > (SELECT AVG(salary) FROM employees)
FROM内サブクエリ集計結果を派生テーブルとして再利用FROM (SELECT dept_id, AVG(salary) ... ) AS t
相関サブクエリ外側の行を内側が参照する副問合せWHERE e2.dept_id = e1.dept_id
EXISTS該当行の存在確認WHERE EXISTS (SELECT 1 FROM employees e WHERE ...)
CASE条件分岐CASE WHEN salary >= 700000 THEN 'High' ELSE 'Standard' END
ROW_NUMBER()区画内の連番ROW_NUMBER() OVER (PARTITION BY dept_id ORDER BY salary DESC)
RANK()区画内の順位(同順位で欠番あり)RANK() OVER (ORDER BY salary DESC)
SUM() OVER集約せずに合計を付与SUM(salary) OVER (PARTITION BY dept_id)
INSERT行を追加INSERT INTO employees (...) VALUES (...);
UPDATE既存行を更新UPDATE employees SET salary = salary * 1.05 WHERE ...;
DELETE行を削除DELETE FROM employees WHERE hire_year < 2019;

よくある質問

Q. WHEREとHAVINGの違いは?

WHERE は集計前の個々の行をフィルタし、HAVINGGROUP BY で集計した後のグループをフィルタする。処理順序は FROM/JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY であり、WHERE の段階では COUNT(*) のような集計値はまだ存在しないため、集計関数の条件は HAVING に書く必要がある。

Q. JOINで行が増えるのはなぜ?

JOINは「両テーブルで条件に一致する行の組み合わせをすべて列挙する」演算だからである。例えばEngineering部署に3人の社員がいれば、employeesdepartments を結合した結果にEngineeringの行が3回登場する。1対多の関係にあるテーブルを結合すると、多い側の件数だけ結果行が増えるのは仕様であり、想定より結果が多いときは結合条件やテーブルの関係(1対1か1対多か)を疑うとよい。

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

「結合先の列を結果に含めたい」ならJOIN、「存在確認や集計値との比較だけしたい(結合先の列は不要)」ならサブクエリ(特にEXISTS)が向いている。本記事の例で言えば、部署名まで表示したい場合はJOIN一択だが、「社員が1人もいない部署」を判定するだけならNOT EXISTSの方が意図が明確で、実行計画上もセミジョインとして効率的に処理されやすい。多くのRDBMSのオプティマイザは両者を同等のクエリプランに変換できるため、最終的には「読みやすさ」で選んで問題ないことが多い。

関連書籍

SQLを体系的に学び直したい読者には、入門書の定番として次の一冊を挙げておく。本記事の構文を一通り手を動かしながら学べる。

SQL 第2版 ゼロからはじめるデータベース操作(ミック、翔泳社)

他の分野の定番書は エンジニアにおすすめの技術書10選 にまとめている。

関連記事