SQLの構文は種類が多く、「JOINの4種類の違い」「WHEREとHAVINGの使い分け」「ウィンドウ関数の書き方」などは、使うたびに検索し直している人も多いはずだ。本記事は、一貫した2つのサンプルテーブル(employees と departments)だけを使い、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_id | dept_name | location |
|---|---|---|
| 1 | Engineering | Tokyo |
| 2 | Sales | Osaka |
| 3 | Marketing | Tokyo |
| 4 | HR | Fukuoka |
employees
| emp_id | name | dept_id | salary | hire_year |
|---|---|---|---|---|
| 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 |
Frankはまだ部署配属前(dept_id が NULL)、HR部署(dept_id = 4)はまだ誰も配属されていない、という状態を意図的に作っている。
2. SELECTの基本 — 列選択とWHERE
列を選ぶ
SELECT name, salary FROM employees;
employees の7行から name と salary の2列だけを抜き出す。行数は変わらず7行のままである。
比較演算子で絞り込む
SELECT name, salary FROM employees WHERE salary >= 600000;
| name | salary |
|---|---|
| Alice | 720000 |
| Carol | 810000 |
| Eve | 610000 |
| Grace | 690000 |
AND / OR で条件を組み合わせる
SELECT name, dept_id, salary FROM employees
WHERE dept_id = 1 AND salary >= 700000;
| name | dept_id | salary |
|---|---|---|
| Alice | 1 | 720000 |
| Carol | 1 | 810000 |
AND は両方の条件を満たす行だけ、OR はどちらか一方を満たす行を残す。優先順位は AND が OR より強いため、混在させるときは (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で終わる)は Alice・Dave・Eve の3行がヒットする。% は0文字以上の任意文字列、_ は任意の1文字に対応するワイルドカードである。
IN でリスト一致
SELECT name, dept_id FROM employees WHERE dept_id IN (1, 3);
| name | dept_id |
|---|---|
| Alice | 1 |
| Carol | 1 |
| Dave | 3 |
| Grace | 1 |
dept_id = 1 OR dept_id = 3 と同じ意味だが、IN の方が候補が多いときに読みやすい。
BETWEEN で範囲指定
SELECT name, salary FROM employees
WHERE salary BETWEEN 550000 AND 700000;
| name | salary |
|---|---|
| Bob | 580000 |
| Eve | 610000 |
| Grace | 690000 |
BETWEEN A AND B は salary >= 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;
| name | salary |
|---|---|
| Carol | 810000 |
| Alice | 720000 |
| Grace | 690000 |
| Eve | 610000 |
| Bob | 580000 |
| Dave | 540000 |
| Frank | 500000 |
ASC(昇順、省略時のデフォルト)と DESC(降順)を指定できる。複数列を指定すると、先頭のキーが優先される。
SELECT name, dept_id, salary FROM employees
ORDER BY dept_id ASC, salary DESC;
| name | dept_id | salary |
|---|---|---|
| Carol | 1 | 810000 |
| Alice | 1 | 720000 |
| Grace | 1 | 690000 |
| Eve | 2 | 610000 |
| Bob | 2 | 580000 |
| Dave | 3 | 540000 |
| Frank | NULL | 500000 |
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;
| name | salary |
|---|---|
| Carol | 810000 |
| Alice | 720000 |
| Grace | 690000 |
SELECT name, salary FROM employees
ORDER BY salary DESC
LIMIT 3 OFFSET 3;
| name | salary |
|---|---|
| Eve | 610000 |
| Bob | 580000 |
| Dave | 540000 |
先頭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;
| cnt | total | avg_salary | max_salary | min_salary |
|---|---|---|---|---|
| 7 | 4450000 | 635714.29… | 810000 | 500000 |
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_id | cnt | avg_salary |
|---|---|---|
| 1 | 3 | 740000.00 |
| 2 | 2 | 595000.00 |
| 3 | 1 | 540000.00 |
| NULL | 1 | 500000.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_id | cnt |
|---|---|
| 1 | 3 |
| 2 | 2 |
dept_id = 3 と NULL はどちらもグループの件数が1件のため除外される。
WHEREとHAVINGの違いは「どの段階でフィルタするか」に尽きる。WHERE は集計前の個々の行を絞り込み、HAVING は GROUP 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_id | avg_salary |
|---|---|
| 1 | 740000.00 |
処理順序は「WHERE でNULL部署のFrankを先に除外 → 残り6行を dept_id でグループ化 → 各グループの平均給与が60万円以上のものだけ HAVING で残す」となり、部署2(平均595,000円)は基準未満のため落ちる。HAVING の条件に COUNT(*) や AVG(salary) のような集計関数を書けるのに対し、WHERE には書けない(集計前なので集計値がまだ存在しない)という制約の違いも覚えておくとよい。
5. JOIN — 複数テーブルの結合
employees と departments を dept_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行)
| name | dept_name | location |
|---|---|---|
| Alice | Engineering | Tokyo |
| Bob | Sales | Osaka |
| Carol | Engineering | Tokyo |
| Dave | Marketing | Tokyo |
| Eve | Sales | Osaka |
| Grace | Engineering | Tokyo |
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 JOIN(employees 全件+一致分、7行)
| name | dept_name |
|---|---|
| Alice | Engineering |
| Bob | Sales |
| Carol | Engineering |
| Dave | Marketing |
| Eve | Sales |
| Frank | NULL |
| Grace | Engineering |
左側(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 JOIN(departments 全件+一致分、7行)
| name | dept_name |
|---|---|
| Alice | Engineering |
| Carol | Engineering |
| Grace | Engineering |
| Bob | Sales |
| Eve | Sales |
| Dave | Marketing |
| NULL | HR |
今度は右側(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行)
| name | dept_name |
|---|---|
| Alice | Engineering |
| Bob | Sales |
| Carol | Engineering |
| Dave | Marketing |
| Eve | Sales |
| Frank | NULL |
| Grace | Engineering |
| NULL | HR |
FrankもHRも両方残る。LEFTとRIGHTの結果を単純に足し合わせて重複(一致した6行)を1回にまとめたものがFULLだとイメージすればよい。

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);
| name | salary |
|---|---|
| Alice | 720000 |
| Carol | 810000 |
| Grace | 690000 |
内側の (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_id | avg_salary |
|---|---|
| 1 | 740000.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
);
| name | salary |
|---|---|
| Carol | 810000 |
| Eve | 610000 |
相関サブクエリは外側の行が1行変わるたびに内側が再評価される。CarolはEngineering部署の平均740,000円より高いので該当、AliceとGraceは部署平均に届かないので除外される。Frankは dept_id がNULLのため、e2.dept_id = e1.dept_id が NULL = 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;
| name | salary | salary_band |
|---|---|---|
| Carol | 810000 | High |
| Alice | 720000 | High |
| Grace | 690000 | Mid |
| Eve | 610000 | Mid |
| Bob | 580000 | Standard |
| Dave | 540000 | Standard |
| Frank | 500000 | Standard |
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_cnt | mid_cnt | standard_cnt |
|---|---|---|
| 2 | 2 | 3 |
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;
| name | dept_id | salary | rn |
|---|---|---|---|
| Carol | 1 | 810000 | 1 |
| Alice | 1 | 720000 | 2 |
| Grace | 1 | 690000 | 3 |
| Eve | 2 | 610000 | 1 |
| Bob | 2 | 580000 | 2 |
| Dave | 3 | 540000 | 1 |
| Frank | NULL | 500000 | 1 |
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;
| name | salary | rnk |
|---|---|---|
| Carol | 810000 | 1 |
| Alice | 720000 | 2 |
| Grace | 690000 | 3 |
| Eve | 610000 | 4 |
| Bob | 580000 | 5 |
| Dave | 540000 | 6 |
| Frank | 500000 | 7 |
今回のサンプルデータは給与がすべて異なるため 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;
| name | dept_id | salary | dept_total |
|---|---|---|---|
| Alice | 1 | 720000 | 2220000 |
| Carol | 1 | 810000 | 2220000 |
| Grace | 1 | 690000 | 2220000 |
| Bob | 2 | 580000 | 1190000 |
| Eve | 2 | 610000 | 1190000 |
| Dave | 3 | 540000 | 540000 |
| Frank | NULL | 500000 | 500000 |
「自分の給与が部署合計の何%か」を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;
| name | salary | running_total |
|---|---|---|
| Carol | 810000 | 810000 |
| Alice | 720000 | 1530000 |
| Grace | 690000 | 2220000 |
| Eve | 610000 | 2830000 |
| Bob | 580000 | 3410000 |
| Dave | 540000 | 3950000 |
| Frank | 500000 | 4450000 |
最終行の累積合計(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;
| name | salary(更新前) | salary(更新後) |
|---|---|---|
| Alice | 720000 | 756000 |
| Carol | 810000 | 850500 |
| Grace | 690000 | 724500 |
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 を実行して対象行数を確認する、トランザクション(BEGIN ~ COMMIT/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 NULL | NULL判定 | 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 は集計前の個々の行をフィルタし、HAVING は GROUP BY で集計した後のグループをフィルタする。処理順序は FROM/JOIN → WHERE → GROUP BY → HAVING → SELECT → ORDER BY であり、WHERE の段階では COUNT(*) のような集計値はまだ存在しないため、集計関数の条件は HAVING に書く必要がある。
Q. JOINで行が増えるのはなぜ?
JOINは「両テーブルで条件に一致する行の組み合わせをすべて列挙する」演算だからである。例えばEngineering部署に3人の社員がいれば、employees と departments を結合した結果にEngineeringの行が3回登場する。1対多の関係にあるテーブルを結合すると、多い側の件数だけ結果行が増えるのは仕様であり、想定より結果が多いときは結合条件やテーブルの関係(1対1か1対多か)を疑うとよい。
Q. サブクエリとJOINはどちらを使うべき?
「結合先の列を結果に含めたい」ならJOIN、「存在確認や集計値との比較だけしたい(結合先の列は不要)」ならサブクエリ(特にEXISTS)が向いている。本記事の例で言えば、部署名まで表示したい場合はJOIN一択だが、「社員が1人もいない部署」を判定するだけならNOT EXISTSの方が意図が明確で、実行計画上もセミジョインとして効率的に処理されやすい。多くのRDBMSのオプティマイザは両者を同等のクエリプランに変換できるため、最終的には「読みやすさ」で選んで問題ないことが多い。
関連書籍
SQLを体系的に学び直したい読者には、入門書の定番として次の一冊を挙げておく。本記事の構文を一通り手を動かしながら学べる。
他の分野の定番書は エンジニアにおすすめの技術書10選 にまとめている。
関連記事
- データベースのコネクションプールサイズを待ち行列理論(M/M/c)でモデル化する - 本記事の「1本のクエリを正しく書く」話の先にある、「同時に何本のクエリを捌けるようにプールを設計するか」をM/M/c待ち行列モデルとPythonシミュレーションで定量化しています。