- SQL
SQL入門|基本文法一覧と書き方を実行結果つきで解説【コピペOK】
SQLとは、リレーショナルデータベース(RDBMS)に「このデータを取り出して」「この行を追加して」と指示するための言語です。 文法の中心は SELECT 列 FROM 表 WHERE 条件 の形で、これに「並べ替え(ORDER BY)」「集計(GROUP BY)」「表の結合(JOIN)」を足していけば、日常的に書く SQL の大半をカバーできます。
この記事は、SQL をこれから覚える人と、研修で習ったけれど書き方を毎回検索してしまう人のための「読める・コピペできる・結果が見える」文法一覧です。冒頭の早見表で「やりたいこと→書き方」を引き、本文では MySQL 8.0 で実際に実行した SQL と結果テーブルをそのまま載せています。 3つのサンプル表(部署・社員・売上)は CREATE TABLE と INSERT をそのまま配布しているので、手元で同じ結果を再現できます。さらに、PostgreSQL 16・SQLite 3 でも同じ SQL を流して方言差を表にし、初心者がつまずく典型エラー9種を実際のエラーメッセージつきで再現し、最後に理解度チェック10問を用意しました。
実行環境は MySQL 8.0.46(sql_mode は既定値)・PostgreSQL 16.15・SQLite 3.45.1 です。MySQL 8.0 は 2026年4月21日に Oracle の Sustaining Support へ移行(Premier Support が終了)しており、公式は 8.4 LTS または 9.7 LTS への移行を案内しています。手元で動かせた版が 8.0.46 のためその結果を載せていますが、この記事で扱う文法は標準的なものなので、8.4 以降でも同じ SQL がそのまま使えます。
SQLの基本文法 早見表(やりたいこと→書き方)
まずは「何をしたいか」から書き方を引ける表です。例はすべてこの記事のサンプル表(employees・departments・sales)で動くものを載せています。
| やりたいこと | 書き方 | この記事での例 |
|---|---|---|
| 列を選ぶ | SELECT 列1, 列2 FROM 表 |
SELECT name, salary FROM employees |
| すべての列を見る | SELECT * FROM 表 |
SELECT * FROM departments |
| 列に別名を付ける | SELECT 列 AS 別名 |
SELECT salary * 12 AS annual_salary |
| 計算した列を出す | SELECT 列 * 12, 列 / 10000 |
SELECT salary * 12 AS annual_salary |
| 重複を除く | SELECT DISTINCT 列 |
SELECT DISTINCT department_id FROM employees |
| 条件で絞る | WHERE 条件 |
WHERE salary >= 400000 |
| 複数の条件 | AND / OR / NOT(カッコで優先順位を明示) |
WHERE (department_id = 1 OR department_id = 2) AND salary >= 400000 |
| 範囲で絞る | WHERE 列 BETWEEN a AND b |
WHERE hire_date BETWEEN '2020-01-01' AND '2022-12-31' |
| 候補のどれかに一致 | WHERE 列 IN (a, b, c) |
WHERE department_id IN (1, 3) |
| 部分一致 | WHERE 列 LIKE 'Yama%'(% は任意の文字列、_ は1文字) |
WHERE name LIKE 'Yama%' |
| NULL の行を探す | WHERE 列 IS NULL / IS NOT NULL(= NULL では0件) |
WHERE department_id IS NULL |
| 並べ替える | ORDER BY 列 ASC / DESC(複数キーはカンマ区切り) |
ORDER BY department_id ASC, salary DESC |
| 件数を絞る | LIMIT 件数 OFFSET 開始位置 |
ORDER BY salary DESC LIMIT 3 OFFSET 3 |
| 数える・合計・平均 | COUNT(*) / SUM(列) / AVG(列) / MAX(列) / MIN(列) |
SELECT COUNT(*), SUM(salary), AVG(salary) FROM employees |
| グループごとに集計 | GROUP BY 列 |
SELECT department_id, COUNT(*) FROM employees GROUP BY department_id |
| 集計結果で絞る | GROUP BY 列 HAVING 集計条件 |
HAVING COUNT(*) >= 3 |
| 2表をつなぐ | FROM 表A INNER JOIN 表B ON 表A.列 = 表B.列 |
employees AS e INNER JOIN departments AS d ON e.department_id = d.id |
| 片方に無い行も残す | LEFT JOIN(左の表の行を全部残す) |
departments AS d LEFT JOIN employees AS e ON e.department_id = d.id |
| 自分の表と結合する | 同じ表に別の別名を付けて JOIN | employees AS e LEFT JOIN employees AS m ON e.manager_id = m.id |
| 別の問い合わせ結果で絞る | WHERE 列 IN (SELECT ...) / WHERE EXISTS (SELECT ...) |
WHERE salary > (SELECT AVG(salary) FROM employees) |
| 条件で値を変える | CASE WHEN 条件 THEN 値 ELSE 値 END |
CASE WHEN salary >= 400000 THEN 'A' ... END AS grade |
| 文字列をつなぐ | CONCAT(a, b)(PostgreSQL・SQLite は a || b も可) |
CONCAT(name, ' (', id, ')') |
| NULL を別の値に置き換える | COALESCE(列, 代わりの値) |
COALESCE(d.name, '(unassigned)') |
| 日付の差を求める | MySQL DATEDIFF(a, b)、PostgreSQL a - b、SQLite julianday(a) - julianday(b) |
DATEDIFF('2026-10-02', hire_date) |
| 今日の日付 | CURRENT_DATE(3つのDBで共通) |
SELECT CURRENT_DATE |
| 行を追加する | INSERT INTO 表 (列, ...) VALUES (値, ...) |
INSERT INTO employees (id, name, ...) VALUES (15, 'Matsumoto', ...) |
| 行を更新する | UPDATE 表 SET 列 = 値 WHERE 条件 |
UPDATE employees SET salary = 320000 WHERE id = 15 |
| 行を削除する | DELETE FROM 表 WHERE 条件 |
DELETE FROM sales WHERE employee_id = 15 |
| 表を作る | CREATE TABLE 表 (列 型 制約, ...) |
主キー・NOT NULL・UNIQUE・DEFAULT・外部キーの例を本文に掲載 |
| 表の列を変える | ALTER TABLE 表 ADD COLUMN 列 型 |
ALTER TABLE tasks ADD COLUMN due_date DATE |
| 検索を速くする | CREATE INDEX 名前 ON 表 (列) |
CREATE INDEX idx_sales_employee_date ON sales (employee_id, sale_date) |
| まとめて確定/取り消す | START TRANSACTION; ... COMMIT; または ROLLBACK; |
本文で ROLLBACK により元に戻る様子を掲載 |
| 問い合わせに名前を付ける | CREATE VIEW 名前 AS SELECT ... |
CREATE VIEW employee_summary AS SELECT ... |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「SQL入門|基本文法一覧と書き方を実行結果つきで解説【コピペOK】」
SQLとは:RDBMSとテーブル、命令の4分類
SQL(Structured Query Language)は、データを「表(テーブル)」の形で保存するリレーショナルデータベース管理システム(RDBMS)を操作するための言語です。 MySQL・PostgreSQL・SQLite・SQL Server・Oracle Database など製品は複数ありますが、基本の文法は共通で、覚えた書き方はどの製品でもほぼそのまま使えます(細かい違いは後半の「方言差の早見表」にまとめました)。
テーブルは Excel のシートに似た「行(レコード)× 列(カラム)」の構造です。各列には型(整数・文字列・日付など)があり、行を一意に区別する列を 主キー(PRIMARY KEY) と呼びます。この記事では「部署」「社員」「売上」の3つのテーブルを使い、社員は department_id 列で部署と、売上は employee_id 列で社員とつながっています。
SQL の命令は4つに分類される
| 分類 | 正式名 | 役割 | 代表的な命令 | この記事での扱い |
|---|---|---|---|---|
| DML | Data Manipulation Language | データを読む・書く | SELECT / INSERT / UPDATE / DELETE |
本文の大半。最初に覚えるのはここ |
| DDL | Data Definition Language | 表や索引などの「入れ物」を作る・変える | CREATE / ALTER / DROP |
CREATE TABLE・ALTER TABLE・CREATE INDEX・CREATE VIEW |
| DCL | Data Control Language | 誰が何をできるかを決める | GRANT / REVOKE |
ユーザー作成と権限付与を1例だけ掲載 |
| TCL | Transaction Control Language | 変更をまとめて確定・取り消す | START TRANSACTION(BEGIN)/ COMMIT / ROLLBACK |
ROLLBACK で元に戻る様子を掲載 |
入門段階では DML(特に SELECT)に時間の8割を使うのが近道です。 DDL は「表を作れる」程度、DCL と TCL は「存在と役割を知っている」程度で、まずは十分です。
手元で試す方法
この記事の SQL は、次のどれかを用意すれば全部そのまま試せます。
| 環境 | 用意の仕方 | 向いている人 |
|---|---|---|
| SQLite 3 | 多くの OS に最初から入っています(sqlite3 sample.db で起動。Ubuntu/Debian は apt-get install sqlite3)。ファイル1つがデータベースになり、サーバー起動が不要 |
まず文法だけ覚えたい人、いちばん手軽 |
| MySQL 8.x | 公式サイトのインストーラー、Linux のパッケージ、または Docker の公式イメージ。クライアントは mysql -u root -p |
多くのWebサービスや学習教材で使われており、この記事の主役 |
| PostgreSQL 16/17 | 公式サイトのインストーラー、Linux のパッケージ、Docker の公式イメージ。クライアントは psql -U postgres |
標準SQLに近い書き方を身につけたい人 |
| ブラウザで試す | DB Fiddle、SQLFiddle、SQLite Online、paiza.io などの Web 実行環境にこの記事の CREATE TABLE と INSERT を貼る | インストールせずに今すぐ試したい人 |
どれを選んでも、この記事のサンプル表は同じ CREATE TABLE と INSERT で作れます。 次の章の SQL をそのまま貼ってください。
この記事で使うサンプルテーブル(コピペで同じ結果が出ます)
3つの表を使います。列名・表名はすべて英語で、MySQL・PostgreSQL・SQLite の3つで同じ SQL がそのまま通ることを確認済みです。
erDiagram
departments ||--o{ employees : "department_id"
employees ||--o{ sales : "employee_id"
employees ||--o{ employees : "manager_id"
departments {
int id PK
varchar name
varchar location
}
employees {
int id PK
varchar name
int department_id FK
int manager_id FK
date hire_date
int salary
}
sales {
int id PK
int employee_id FK
date sale_date
int amount
}
| 表 | 内容 | 件数 | 覚えておくポイント |
|---|---|---|---|
departments |
部署 | 5件 | Planning には社員がいない(LEFT JOIN の説明に使う) |
employees |
社員 | 14件 | Yamada は配属前で department_id が NULL。manager_id は上司の社員 id で、上司がいない人は NULL |
sales |
売上 | 28件 | 2026年7〜9月。売上のある社員は7人(集計・JOIN・サブクエリの説明に使う) |
CREATE TABLE departments (
id INTEGER PRIMARY KEY,
name VARCHAR(30) NOT NULL,
location VARCHAR(30) NOT NULL
);
CREATE TABLE employees (
id INTEGER PRIMARY KEY,
name VARCHAR(30) NOT NULL,
department_id INTEGER, -- 配属前の社員は NULL
manager_id INTEGER, -- 上司がいない社員は NULL
hire_date DATE NOT NULL,
salary INTEGER NOT NULL,
FOREIGN KEY (department_id) REFERENCES departments(id)
);
CREATE TABLE sales (
id INTEGER PRIMARY KEY,
employee_id INTEGER NOT NULL,
sale_date DATE NOT NULL,
amount INTEGER NOT NULL,
FOREIGN KEY (employee_id) REFERENCES employees(id)
);
INSERT INTO departments (id, name, location) VALUES
(1, 'Sales', 'Tokyo'),
(2, 'Development', 'Osaka'),
(3, 'Marketing', 'Tokyo'),
(4, 'Support', 'Nagoya'),
(5, 'Planning', 'Tokyo');
INSERT INTO employees (id, name, department_id, manager_id, hire_date, salary) VALUES
(1, 'Sato', 1, NULL, '2015-04-01', 450000),
(2, 'Suzuki', 1, 1, '2018-04-01', 380000),
(3, 'Takahashi', 2, NULL, '2016-10-01', 480000),
(4, 'Tanaka', 2, 3, '2019-04-01', 400000),
(5, 'Ito', 2, 3, '2021-04-01', 350000),
(6, 'Watanabe', 3, 1, '2017-07-01', 420000),
(7, 'Yamamoto', 3, 6, '2020-04-01', 330000),
(8, 'Nakamura', 4, 3, '2018-01-15', 360000),
(9, 'Kobayashi', 4, 8, '2022-04-01', 300000),
(10, 'Kato', 1, 1, '2023-04-01', 310000),
(11, 'Yoshida', 2, 3, '2024-04-01', 290000),
(12, 'Yamada', NULL, NULL, '2026-04-01', 280000),
(13, 'Sasaki', 3, 6, '2024-10-01', 320000),
(14, 'Yamaguchi', 4, 8, '2025-04-01', 295000);
INSERT INTO sales (id, employee_id, sale_date, amount) VALUES
(1, 1, '2026-07-03', 120000),
(2, 2, '2026-07-05', 80000),
(3, 6, '2026-07-08', 95000),
(4, 1, '2026-07-12', 60000),
(5, 10, '2026-07-15', 45000),
(6, 7, '2026-07-20', 70000),
(7, 2, '2026-07-22', 150000),
(8, 13, '2026-07-28', 30000),
(9, 1, '2026-08-02', 200000),
(10, 6, '2026-08-04', 110000),
(11, 10, '2026-08-09', 55000),
(12, 2, '2026-08-11', 90000),
(13, 7, '2026-08-14', 40000),
(14, 1, '2026-08-18', 75000),
(15, 13, '2026-08-21', 65000),
(16, 4, '2026-08-25', 25000),
(17, 2, '2026-08-28', 130000),
(18, 6, '2026-08-30', 85000),
(19, 1, '2026-09-01', 180000),
(20, 10, '2026-09-05', 50000),
(21, 7, '2026-09-08', 95000),
(22, 2, '2026-09-10', 70000),
(23, 13, '2026-09-14', 45000),
(24, 1, '2026-09-17', 90000),
(25, 6, '2026-09-20', 120000),
(26, 10, '2026-09-23', 35000),
(27, 2, '2026-09-26', 110000),
(28, 7, '2026-09-29', 60000);
MySQL では先にデータベースを作って選びます(CREATE DATABASE sqlintro; USE sqlintro;)。PostgreSQL は CREATE DATABASE 後に \c で接続、SQLite は sqlite3 sample.db と起動するだけです。読み込み後の社員表は次のとおりです(MySQL 8.0.46 の出力。部署表は前の表のとおり5行です)。
mysql> SELECT * FROM employees;
+----+-----------+---------------+------------+------------+--------+
| id | name | department_id | manager_id | hire_date | salary |
+----+-----------+---------------+------------+------------+--------+
| 1 | Sato | 1 | NULL | 2015-04-01 | 450000 |
| 2 | Suzuki | 1 | 1 | 2018-04-01 | 380000 |
| 3 | Takahashi | 2 | NULL | 2016-10-01 | 480000 |
| 4 | Tanaka | 2 | 3 | 2019-04-01 | 400000 |
| 5 | Ito | 2 | 3 | 2021-04-01 | 350000 |
| 6 | Watanabe | 3 | 1 | 2017-07-01 | 420000 |
| 7 | Yamamoto | 3 | 6 | 2020-04-01 | 330000 |
| 8 | Nakamura | 4 | 3 | 2018-01-15 | 360000 |
| 9 | Kobayashi | 4 | 8 | 2022-04-01 | 300000 |
| 10 | Kato | 1 | 1 | 2023-04-01 | 310000 |
| 11 | Yoshida | 2 | 3 | 2024-04-01 | 290000 |
| 12 | Yamada | NULL | NULL | 2026-04-01 | 280000 |
| 13 | Sasaki | 3 | 6 | 2024-10-01 | 320000 |
| 14 | Yamaguchi | 4 | 8 | 2025-04-01 | 295000 |
+----+-----------+---------------+------------+------------+--------+
14 rows in set (0.00 sec)
3つの DB で通るように書くための注意点は2つだけです。自動採番(AUTO_INCREMENT など)は製品ごとに書き方が違うので使わず、id を明示的に入れています。また、SQLite は FOREIGN KEY を書いても既定では検査しません(PRAGMA foreign_keys = ON; で有効になります。後述)。
書き方の作法:読みやすい SQL の6つの約束
文法に入る前に、この記事の SQL が従っている書き方の約束をまとめます。SQL は大文字・小文字や改行を区別しないので、約束は「人が読みやすいか」だけで決めます。
| 約束 | 書き方 | 理由 |
|---|---|---|
| 予約語は大文字 | SELECT, FROM, WHERE, AS を大文字、表名・列名は小文字 |
命令と自分の名前が一目で区別できる |
| 句ごとに改行 | SELECT / FROM / WHERE / GROUP BY / ORDER BY を行頭に置く |
長い SQL でも構造が追える。1行に収まる短いものは1行でよい |
| インデントは2〜4文字 | 列の一覧や JOIN の ON を字下げ |
どの句に属するかが見える |
別名(エイリアス)は AS を付ける |
employees AS e、salary * 12 AS annual_salary |
AS は省略できますが、付けたほうが「別名である」と読めます。表の別名は e・d・s など頭文字が定番 |
| コメントを書く | -- 1行コメント、/* 複数行コメント */ |
「なぜこの条件か」を残せる |
| 文末にセミコロン | ...; |
複数の文を続けて実行するときの区切り。クライアントによっては無いと実行されない |
コメントと改行を使った形はこうなります(MySQL で実行済み)。
-- 給与が 400,000 以上の社員を高い順に
SELECT name, salary -- 行末コメントも書けます
FROM employees
/* 複数行コメント
条件はここ */
WHERE salary >= 400000
ORDER BY salary DESC;
結果は Takahashi・Sato・Watanabe・Tanaka の4行です(コメント部分は無視して実行されました)。
SELECT * は手元で表の中身を確認するときには便利ですが、アプリケーションのコードでは列名を書くことをおすすめします。 理由は3つあります。(1) 列が増えたときに取得するデータ量と通信量が黙って増える、(2) 列の順番を前提にしたコードが列追加で壊れる、(3) 必要な列だけを書けば、インデックスだけで答えが返る(表本体を読まずに済む)場合がある。この記事でも、結果を見せたい列だけを書いています。
SELECT:列を選んで取り出す
SELECT 文は「どの列を(SELECT)」「どの表から(FROM)」取り出すかを書く、SQL でいちばん使う文です。 ここから先の実行結果は、すべて MySQL 8.0.46 でそのまま実行して得たものです。
列を指定する・すべての列を出す
SELECT name, salary FROM employees;
+-----------+--------+
| name | salary |
+-----------+--------+
| Sato | 450000 |
| Suzuki | 380000 |
| Takahashi | 480000 |
| Tanaka | 400000 |
| Ito | 350000 |
| Watanabe | 420000 |
| Yamamoto | 330000 |
| Nakamura | 360000 |
…(省略)…
14 rows in set (0.00 sec)
列をカンマ区切りで並べると、その順番で返ります。* を書くとすべての列が表の定義順に返ります(SELECT * FROM departments; の結果は前の章のとおり)。
AS で別名を付ける・計算した列を出す
列には AS で別名(エイリアス)を付けられます。四則演算で計算した列も作れます。
SELECT name AS employee_name, salary / 10000 AS salary_man
FROM employees
WHERE id <= 3;
+---------------+------------+
| employee_name | salary_man |
+---------------+------------+
| Sato | 45.0000 |
| Suzuki | 38.0000 |
| Takahashi | 48.0000 |
+---------------+------------+
3 rows in set (0.00 sec)
整数どうしの割り算は、MySQL では小数(45.0000)、PostgreSQL と SQLite では整数(45)になります(同じ SQL を流して確認しました)。小数が欲しいときは salary / 10000.0 のように小数で割ると、どの DB でも小数になります。
salary * 12 AS annual_salary と書けば年収の列も作れます(Sato は 5400000 になりました)。
DISTINCT で重複を除く
SELECT DISTINCT department_id FROM employees;
+---------------+
| department_id |
+---------------+
| NULL |
| 1 |
| 2 |
| 3 |
| 4 |
+---------------+
5 rows in set (0.00 sec)
NULL も「1種類の値」として1行残ります。 SELECT DISTINCT department_id, manager_id のように複数列を書くと「列の組み合わせ」で重複を除きます(実行すると9行になりました)。
WHERE:条件で行を絞る
WHERE 句には「この条件が真(TRUE)の行だけ返す」という条件式を書きます。 比較演算子・論理演算子・範囲・候補・部分一致・NULL 判定の6種類を押さえれば、たいていの絞り込みが書けます。
| 種類 | 書き方 | 意味 |
|---|---|---|
| 比較 | = <>(または !=)< <= > >= |
等しい・等しくない・大小 |
| 論理 | AND OR NOT |
かつ・または・でない。AND が OR より先に評価される |
| 範囲 | BETWEEN a AND b |
a 以上 b 以下(両端を含む) |
| 候補 | IN (a, b, c) |
どれかに一致。OR を並べるより短い |
| 部分一致 | LIKE 'パターン' |
% は0文字以上の任意の文字列、_ は任意の1文字 |
| NULL | IS NULL / IS NOT NULL |
NULL かどうか。= NULL では判定できない |
比較演算子
SELECT name, salary FROM employees WHERE salary >= 400000;
+-----------+--------+
| name | salary |
+-----------+--------+
| Sato | 450000 |
| Takahashi | 480000 |
| Tanaka | 400000 |
| Watanabe | 420000 |
+-----------+--------+
4 rows in set (0.00 sec)
文字列は 'Sato' のようにシングルクォートで囲みます。日付も '2020-01-01' と文字列の形で書けば、DATE 型の列と比較できます。
AND / OR / NOT と、カッコの大切さ
WHERE department_id = 2 AND salary >= 350000 のように条件をつなぎます(Development の Takahashi・Tanaka・Ito の3行が返りました)。AND は OR より先に評価されます。「部署1か部署2で、かつ給与40万以上」のつもりでカッコなしに書くと、結果が変わります。
-- カッコなし:「部署1の全員」または「部署2で40万以上」と解釈される
SELECT name, department_id, salary
FROM employees
WHERE department_id = 1 OR department_id = 2 AND salary >= 400000;
+-----------+---------------+--------+
| name | department_id | salary |
+-----------+---------------+--------+
| Sato | 1 | 450000 |
| Suzuki | 1 | 380000 |
| Kato | 1 | 310000 |
| Takahashi | 2 | 480000 |
| Tanaka | 2 | 400000 |
+-----------+---------------+--------+
5 rows in set (0.00 sec)
-- カッコあり:意図どおり「(部署1 または 部署2)かつ 40万以上」
SELECT name, department_id, salary
FROM employees
WHERE (department_id = 1 OR department_id = 2) AND salary >= 400000;
+-----------+---------------+--------+
| name | department_id | salary |
+-----------+---------------+--------+
| Sato | 1 | 450000 |
| Takahashi | 2 | 480000 |
| Tanaka | 2 | 400000 |
+-----------+---------------+--------+
3 rows in set (0.00 sec)
AND と OR を混ぜるときは、必ずカッコで意図を明示します。 給与38万の Suzuki と31万の Kato が結果に入るかどうかが変わりました。
BETWEEN:範囲で絞る
SELECT name, hire_date
FROM employees
WHERE hire_date BETWEEN '2020-01-01' AND '2022-12-31';
+-----------+------------+
| name | hire_date |
+-----------+------------+
| Ito | 2021-04-01 |
| Yamamoto | 2020-04-01 |
| Kobayashi | 2022-04-01 |
+-----------+------------+
3 rows in set (0.00 sec)
BETWEEN a AND b は両端を含みます(hire_date >= '2020-01-01' AND hire_date <= '2022-12-31' と同じ)。
IN:候補のどれかに一致
SELECT name, department_id FROM employees WHERE department_id IN (1, 3);
Sales と Marketing の6人(Sato・Suzuki・Kato・Watanabe・Yamamoto・Sasaki)が返りました。department_id = 1 OR department_id = 3 と同じ意味で、候補が多いほど IN のほうが短く書けます。
LIKE:部分一致(% と _)
SELECT name FROM employees WHERE name LIKE 'Yama%'; -- Yama で始まる
+-----------+
| name |
+-----------+
| Yamamoto |
| Yamada |
| Yamaguchi |
+-----------+
3 rows in set (0.00 sec)
'_ato'(任意の1文字 + ato)なら Sato・Kato の2行、'%a%i%'(a の後ろのどこかに i がある)なら Takahashi・Kobayashi・Sasaki・Yamaguchi の4行になりました。先頭が % のパターン('%ato' など)はインデックスが使えず表全体を読むので、大きな表では「前方一致で書けないか」を考えるのが定石です。
IS NULL / IS NOT NULL
SELECT name, department_id FROM employees WHERE department_id IS NULL;
+--------+---------------+
| name | department_id |
+--------+---------------+
| Yamada | NULL |
+--------+---------------+
1 row in set (0.00 sec)
NULL は「値が無い」という印で、= や <> で比較すると結果は真にも偽にもならず、その行は返りません。 WHERE department_id <> 1 を実行すると、Sales 以外の10人が返り、department_id が NULL の Yamada は含まれませんでした(実行して確認)。NULL の行も含めたいときは OR department_id IS NULL を足します。= NULL と書いて0件になる例は、後半の「典型エラー」で実際の結果を載せています。
ORDER BY:並べ替える
ORDER BY 句は結果の並び順を決めます。ASC(昇順、省略時の既定)か DESC(降順)を列ごとに指定できます。 ORDER BY を書かない SELECT の順番は保証されないので、「上位3件」のように順番に意味があるときは必ず書きます。
SELECT name, salary FROM employees ORDER BY salary DESC;
+-----------+--------+
| name | salary |
+-----------+--------+
| Takahashi | 480000 |
| Sato | 450000 |
| Watanabe | 420000 |
| Tanaka | 400000 |
…(省略)…
14 rows in set (0.00 sec)
複数キーで並べる
「部署順、同じ部署の中では給与の高い順」はカンマで並べます。先に書いたキーが優先され、同じ値のときだけ次のキーで並びます。
SELECT name, department_id, salary
FROM employees
ORDER BY department_id ASC, salary DESC;
+-----------+---------------+--------+
| name | department_id | salary |
+-----------+---------------+--------+
| Yamada | NULL | 280000 |
| Sato | 1 | 450000 |
| Suzuki | 1 | 380000 |
| Kato | 1 | 310000 |
| Takahashi | 2 | 480000 |
| Tanaka | 2 | 400000 |
…(省略)…
| Yamaguchi | 4 | 295000 |
+-----------+---------------+--------+
14 rows in set (0.00 sec)
NULL はどこに並ぶか(DB で違う)
上の結果で、department_id が NULL の Yamada は先頭に来ました。MySQL と SQLite は NULL を「いちばん小さい値」として扱うため昇順では先頭、降順では末尾になります。PostgreSQL は逆で、昇順では NULL が末尾、降順では先頭です(同じ SQL を PostgreSQL 16 で実行し、昇順で Yamada が最後の行になることを確認しました。公式ドキュメントにも「ASC では NULLS LAST、DESC では NULLS FIRST が既定」と書かれています)。
NULL の位置を DB に依存せず固定したいときは、「NULL かどうか」を先頭のキーにします。
SELECT name, department_id
FROM employees
ORDER BY department_id IS NULL, department_id; -- NULL を最後に
+-----------+---------------+
| name | department_id |
+-----------+---------------+
| Sato | 1 |
| Suzuki | 1 |
| Kato | 1 |
…(省略)…
| Yamaguchi | 4 |
| Yamada | NULL |
+-----------+---------------+
14 rows in set (0.00 sec)
department_id IS NULL は NULL の行で 1、それ以外で 0 になるので、0 の行が先に並びます。PostgreSQL では ORDER BY department_id NULLS LAST という専用の書き方も使えます。
同じ値の行どうしの順番は保証されません(PostgreSQL で ORDER BY department_id だけを指定したところ、部署1の並びが Sato・Kato・Suzuki と MySQL とは違う順で返りました)。順番を完全に決めたいときは、最後に主キーなど一意な列を足します(ORDER BY department_id, id)。
LIMIT/OFFSET:件数を絞る
LIMIT n で先頭から n 行、OFFSET m で m 行読み飛ばします。 ランキングやページ分けの基本です。
SELECT name, salary FROM employees ORDER BY salary DESC LIMIT 3;
+-----------+--------+
| name | salary |
+-----------+--------+
| Takahashi | 480000 |
| Sato | 450000 |
| Watanabe | 420000 |
+-----------+--------+
3 rows in set (0.00 sec)
-- 4位〜6位(3行飛ばして3行)
SELECT name, salary FROM employees ORDER BY salary DESC LIMIT 3 OFFSET 3;
+----------+--------+
| name | salary |
+----------+--------+
| Tanaka | 400000 |
| Suzuki | 380000 |
| Nakamura | 360000 |
+----------+--------+
3 rows in set (0.00 sec)
| 書き方 | MySQL 8.0 | PostgreSQL 16 | SQLite 3.45 |
|---|---|---|---|
LIMIT 3 OFFSET 3 |
動く | 動く | 動く |
LIMIT 3, 3(オフセット, 件数) |
動く | ERROR: LIMIT #,# syntax is not supported |
動く |
OFFSET 3 ROWS FETCH FIRST 3 ROWS ONLY(標準SQL) |
構文エラー(1064) | 動く | 構文エラー |
3つの DB で共通に動く LIMIT n OFFSET m の形で覚えるのがおすすめです。 LIMIT は ORDER BY とセットで書きます。ORDER BY なしの LIMIT 3 は「どの3件か」が保証されません(実行したところ id 順の Sato・Suzuki・Takahashi が返りましたが、これは偶然そうなっただけです)。
集計関数:数える・合計・平均・最大・最小
集計関数は複数の行から1つの値を作ります。 5つを1回の SELECT で使えます。
SELECT COUNT(*) AS cnt, SUM(salary) AS total, AVG(salary) AS average,
MAX(salary) AS highest, MIN(salary) AS lowest
FROM employees;
+-----+---------+-------------+---------+--------+
| cnt | total | average | highest | lowest |
+-----+---------+-------------+---------+--------+
| 14 | 4965000 | 354642.8571 | 480000 | 280000 |
+-----+---------+-------------+---------+--------+
1 row in set (0.00 sec)
COUNT(*) と COUNT(列) の違い
SELECT COUNT(*) AS all_rows,
COUNT(department_id) AS has_dept,
COUNT(DISTINCT department_id) AS dept_kinds
FROM employees;
+----------+----------+------------+
| all_rows | has_dept | dept_kinds |
+----------+----------+------------+
| 14 | 13 | 4 |
+----------+----------+------------+
1 row in set (0.32 sec)
| 書き方 | 数えるもの | この表での値 |
|---|---|---|
COUNT(*) |
行そのもの(NULL の行も数える) | 14 |
COUNT(列) |
その列が NULL でない行 | 13(Yamada の department_id が NULL) |
COUNT(DISTINCT 列) |
その列の NULL 以外の値の種類 | 4(1〜4。NULL は種類に含めない) |
SUM AVG MAX MIN も NULL を無視して計算します。 「NULL を 0 として平均したい」ときは AVG(COALESCE(列, 0)) と書きます。AVG の結果は MySQL が小数4桁(354642.8571)、PostgreSQL は 354642.857142857143、SQLite は 354642.857142857 と表示桁が違いましたが、ROUND(AVG(salary)) にすれば MySQL・PostgreSQL は 354643、SQLite は 354643.0 と、値として揃います。
GROUP BY/HAVING:グループごとに集計して絞る
GROUP BY 句は「同じ値の行をひとまとめにして」集計関数をグループごとに計算します。 「部署ごとの人数」「社員ごとの売上合計」がこれです。
SELECT department_id, COUNT(*) AS cnt,
ROUND(AVG(salary)) AS avg_salary, MAX(salary) AS max_salary
FROM employees
GROUP BY department_id
ORDER BY department_id;
+---------------+-----+------------+------------+
| department_id | cnt | avg_salary | max_salary |
+---------------+-----+------------+------------+
| NULL | 1 | 280000 | 280000 |
| 1 | 3 | 380000 | 450000 |
| 2 | 4 | 380000 | 480000 |
| 3 | 3 | 356667 | 420000 |
| 4 | 3 | 318333 | 360000 |
+---------------+-----+------------+------------+
5 rows in set (0.00 sec)
NULL も1つのグループになります(1行目)。SELECT に書けるのは「GROUP BY に書いた列」と「集計関数」だけです。 name のような、グループ内で値が1つに決まらない列を書くと MySQL では ERROR 1055 になります(典型エラーの章で実際のメッセージを載せています)。
HAVING:集計した結果で絞る
SELECT department_id, COUNT(*) AS cnt
FROM employees
GROUP BY department_id
HAVING COUNT(*) >= 3;
+---------------+-----+
| department_id | cnt |
+---------------+-----+
| 1 | 3 |
| 2 | 4 |
| 3 | 3 |
| 4 | 3 |
+---------------+-----+
4 rows in set (0.00 sec)
| 句 | いつ絞るか | 書けるもの | 例 |
|---|---|---|---|
WHERE |
グループにまとめる前に、行を絞る | 列の条件(集計関数は書けない) | WHERE hire_date >= '2018-01-01' |
HAVING |
グループにまとめた後に、グループを絞る | 集計関数の条件 | HAVING AVG(salary) >= 320000 |
両方を組み合わせた例です。「2018年以降入社の社員だけを対象に、部署ごとの平均給与を出し、平均32万以上の部署だけ、高い順」を1文で書けます。
SELECT department_id, ROUND(AVG(salary)) AS avg_salary
FROM employees
WHERE hire_date >= '2018-01-01'
GROUP BY department_id
HAVING AVG(salary) >= 320000
ORDER BY avg_salary DESC;
+---------------+------------+
| department_id | avg_salary |
+---------------+------------+
| 2 | 346667 |
| 1 | 345000 |
| 3 | 325000 |
+---------------+------------+
3 rows in set (0.00 sec)
売上表で SELECT employee_id, SUM(amount) AS total FROM sales GROUP BY employee_id ORDER BY total DESC; を実行すると、employee_id 1 が 725000、2 が 630000、6 が 410000 … と7行返ります。このままでは「employee_id = 1 は誰か」がわかりません。社員の名前を付けるには、次の JOIN が必要です。
JOIN:2つ以上の表をつなぐ
JOIN は「社員表の department_id と部署表の id が同じ行どうし」のように、列の値でつないで1つの結果にします。 まず INNER と LEFT の2種類を覚えれば十分です。
| 種類 | 残る行 | 使いどころ |
|---|---|---|
INNER JOIN |
両方の表に相手がいる行だけ | 「部署が決まっている社員」の一覧 |
LEFT JOIN |
左の表の行は全部。相手がいなければ右側は NULL | 「社員がいない部署も0人として出す」 |
INNER JOIN と LEFT JOIN の違いを同じ SQL で見る
SELECT e.name, d.name AS department
FROM employees AS e
INNER JOIN departments AS d ON e.department_id = d.id
ORDER BY e.id;
+-----------+-------------+
| name | department |
+-----------+-------------+
| Sato | Sales |
| Suzuki | Sales |
| Takahashi | Development |
…(省略)…
| Yamaguchi | Support |
+-----------+-------------+
13 rows in set (0.00 sec)
INNER JOIN を LEFT JOIN に変えるだけで、配属前の Yamada が department NULL で加わり 14 行になります。逆に部署表を左にすると、社員のいない Planning が残ります。
SELECT d.name AS department, COUNT(e.id) AS headcount
FROM departments AS d
LEFT JOIN employees AS e ON e.department_id = d.id
GROUP BY d.id, d.name
ORDER BY d.id;
+-------------+-----------+
| department | headcount |
+-------------+-----------+
| Sales | 3 |
| Development | 4 |
| Marketing | 3 |
| Support | 3 |
| Planning | 0 |
+-------------+-----------+
5 rows in set (0.00 sec)
0人の部署を出すには LEFT JOIN と COUNT(e.id) の組み合わせが必要です。 COUNT(*) にすると Planning も「NULL の行が1つ」と数えられて 1 になります。
3表結合:売上 → 社員 → 部署
JOIN は何個でもつなげます。「部署ごとの売上合計」は3表を順につなぎます。
SELECT d.name AS department, SUM(s.amount) AS total_sales
FROM sales AS s
INNER JOIN employees AS e ON s.employee_id = e.id
INNER JOIN departments AS d ON e.department_id = d.id
GROUP BY d.id, d.name
ORDER BY total_sales DESC;
+-------------+-------------+
| department | total_sales |
+-------------+-------------+
| Sales | 1540000 |
| Marketing | 815000 |
| Development | 25000 |
+-------------+-------------+
3 rows in set (0.00 sec)
自己結合:社員と上司を同じ表から
同じ表に別の別名を付ければ、「社員 e」と「その上司 m」を同じ employees から取れます。
SELECT e.name AS employee, m.name AS manager
FROM employees AS e
LEFT JOIN employees AS m ON e.manager_id = m.id
ORDER BY e.id;
+-----------+-----------+
| employee | manager |
+-----------+-----------+
| Sato | NULL |
| Suzuki | Sato |
| Takahashi | NULL |
| Tanaka | Takahashi |
…(省略)…
+-----------+-----------+
14 rows in set (0.00 sec)
上司がいない Sato・Takahashi・Yamada を残すために LEFT JOIN にしています。
USING:両方の表で列名が同じとき
結合する列の名前が両方の表で同じなら USING (列名) と短く書けます。この記事の表は department_id と id で名前が違うので、サブクエリで名前を揃えて動かしました。
SELECT e.name, d.name AS department
FROM employees AS e
INNER JOIN (SELECT id AS department_id, name FROM departments) AS d USING (department_id)
WHERE e.id <= 3;
結果は ON e.department_id = d.id と書いたときと同じ3行(Sato・Suzuki・Takahashi)です。JOIN の列名があいまいだと ambiguous エラーになるので、2表以上では e.name のように表の別名を付けて書く習慣をつけます(典型エラーの章参照)。
サブクエリ:別の問い合わせの結果で絞る
サブクエリは SELECT の中に書くもう1つの SELECT です。 置く場所で4種類に分かれます。
| 置き場所 | 形 | 返すもの | 例 |
|---|---|---|---|
| WHERE(比較) | WHERE 列 > (SELECT ...) |
1行1列(スカラ) | 平均より給与が高い社員 |
| WHERE IN | WHERE 列 IN (SELECT ...) |
1列の一覧 | Tokyo の部署に所属する社員 |
| WHERE EXISTS | WHERE EXISTS (SELECT 1 ... WHERE 外側の列 = 内側の列) |
行があるかどうか | 売上がある社員 |
| FROM | FROM (SELECT ...) AS t |
表 | 社員ごとの売上合計の平均 |
SELECT name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees)
ORDER BY salary DESC;
+-----------+--------+
| name | salary |
+-----------+--------+
| Takahashi | 480000 |
| Sato | 450000 |
| Watanabe | 420000 |
| Tanaka | 400000 |
| Suzuki | 380000 |
| Nakamura | 360000 |
+-----------+--------+
6 rows in set (0.00 sec)
平均 354642.8571 より高い6人です。「平均」を先に求めて数字を手で書き写す必要がありません。
-- IN:Tokyo にある部署の社員(6行:Sato, Suzuki, Watanabe, Yamamoto, Kato, Sasaki)
SELECT name FROM employees
WHERE department_id IN (SELECT id FROM departments WHERE location = 'Tokyo');
-- EXISTS:売上が1件でもある社員(7行)。NOT EXISTS にすると売上のない7行
SELECT name FROM employees AS e
WHERE EXISTS (SELECT 1 FROM sales AS s WHERE s.employee_id = e.id);
-- FROM:社員ごとの売上合計を表にしてから、その平均を取る
SELECT ROUND(AVG(t.total)) AS avg_total_per_employee
FROM (SELECT employee_id, SUM(amount) AS total FROM sales GROUP BY employee_id) AS t;
+------------------------+
| avg_total_per_employee |
+------------------------+
| 340000 |
+------------------------+
1 row in set (0.00 sec)
FROM に置くサブクエリには必ず別名(AS t)が必要です(MySQL で省略すると Every derived table must have its own alias のエラーになります)。SELECT の列としてスカラサブクエリを置き、(SELECT MAX(salary) FROM employees) - salary AS gap_to_top と「最高給与との差」を出すこともできます(Takahashi 0、Sato 30000、Watanabe 60000)。
CASE:条件で値を変える
CASE 式は「この条件ならこの値」を上から順に評価し、最初に当たった値を返します。 列の値をラベルに変える、条件つきで集計する、の2つの使い方が定番です。
SELECT name, salary,
CASE WHEN salary >= 400000 THEN 'A'
WHEN salary >= 330000 THEN 'B'
ELSE 'C' END AS grade
FROM employees
ORDER BY salary DESC;
+-----------+--------+-------+
| name | salary | grade |
+-----------+--------+-------+
| Takahashi | 480000 | A |
| Sato | 450000 | A |
…(省略)…
| Yamamoto | 330000 | B |
| Sasaki | 320000 | C |
…(省略)…
14 rows in set (0.00 sec)
ELSE を省くと、どの条件にも当たらない行は NULL になります。CASE 式を GROUP BY に使えば「ランクごとの人数」になり(A 4人・B 4人・C 6人)、SUM(CASE WHEN ... THEN amount ELSE 0 END) と集計関数の中に置けば「月ごとの売上を横に並べる」こともできます。
SELECT SUM(CASE WHEN sale_date < '2026-08-01' THEN amount ELSE 0 END) AS july,
SUM(CASE WHEN sale_date BETWEEN '2026-08-01' AND '2026-08-31' THEN amount ELSE 0 END) AS august,
SUM(CASE WHEN sale_date >= '2026-09-01' THEN amount ELSE 0 END) AS september
FROM sales;
+--------+--------+-----------+
| july | august | september |
+--------+--------+-----------+
| 650000 | 875000 | 855000 |
+--------+--------+-----------+
1 row in set (0.00 sec)
よく使う関数の最小セット:文字列・数値・NULL・日付
関数は「列の値を加工して別の値にする」道具です。 入門で押さえるのは次の10個程度で十分で、残りは必要になったときに調べれば間に合います。
| 分類 | 関数 | 意味 | MySQL での結果(下の実行結果より) |
|---|---|---|---|
| 文字列 | CONCAT(a, b, ...) |
つなげる | CONCAT(name, ' (', id, ')') → Sato (1) |
| 文字列 | UPPER(s) / LOWER(s) |
大文字/小文字にする | SATO / sato |
| 文字列 | SUBSTRING(s, 開始, 長さ) |
部分文字列を取る(開始は1から数える) | SUBSTRING(name, 1, 3) → Sat |
| 文字列 | CHAR_LENGTH(s) |
文字数(SQLite は LENGTH(s)) |
Takahashi → 9 |
| 文字列 | REPLACE(s, 前, 後) / TRIM(s) |
置き換え/前後の空白を除く | REPLACE(name, 'a', '@') → T@k@h@shi |
| 数値 | ROUND(n, 桁) / FLOOR(n) / CEIL(n) |
四捨五入/切り捨て/切り上げ | 354642.8571 → 354643 / 354642 / 354643 |
| NULL | COALESCE(a, b, ...) |
最初の NULL でない値を返す | COALESCE(manager_id, 0) → 上司なしは 0 |
| 日付 | CURRENT_DATE / CURRENT_TIMESTAMP |
今日/今の日時 | 2026-10-02 / 2026-10-02 15:31:47 |
| 日付 | DATE_FORMAT(d, 書式) |
書式を変える(MySQL) | '%Y年%m月%d日' → 2015年04月01日 |
| 日付 | DATEDIFF(a, b) / TIMESTAMPDIFF(YEAR, b, a) |
日数差/年数差(MySQL) | Sato の勤続 4202 日・11 年 |
| 日付 | DATE_ADD(d, INTERVAL 3 MONTH) / YEAR(d) |
日付の足し算/年を取り出す(MySQL) | 2015-07-01 / 2015 |
SELECT CONCAT(name, ' (', id, ')') AS label, UPPER(name) AS upper_name,
LOWER(name) AS lower_name, CHAR_LENGTH(name) AS len
FROM employees
WHERE id <= 3;
+---------------+------------+------------+-----+
| label | upper_name | lower_name | len |
+---------------+------------+------------+-----+
| Sato (1) | SATO | sato | 4 |
| Suzuki (2) | SUZUKI | suzuki | 6 |
| Takahashi (3) | TAKAHASHI | takahashi | 9 |
+---------------+------------+------------+-----+
3 rows in set (0.00 sec)
CONCAT は数値の id を自動的に文字列にしてつないでくれます。SUBSTRING(name, 1, 3) は Sat・Suz・Tak、TRIM(' padded ') は padded、ROUND(AVG(salary), 1) は 354642.9 になりました。
COALESCE は LEFT JOIN と相性がよく、「相手がいない行の NULL」を読みやすい値に置き換えられます。 COALESCE(d.name, '(unassigned)') を使うと、配属前の Yamada の部署が (unassigned) と表示されました(Sasaki は Marketing、Yamaguchi は Support のまま)。
SELECT name, hire_date, DATEDIFF('2026-10-02', hire_date) AS days_worked,
TIMESTAMPDIFF(YEAR, hire_date, '2026-10-02') AS years_worked
FROM employees
WHERE id <= 3;
+-----------+------------+-------------+--------------+
| name | hire_date | days_worked | years_worked |
+-----------+------------+-------------+--------------+
| Sato | 2015-04-01 | 4202 | 11 |
| Suzuki | 2018-04-01 | 3106 | 8 |
| Takahashi | 2016-10-01 | 3653 | 10 |
+-----------+------------+-------------+--------------+
3 rows in set (0.00 sec)
日付の書式を変える関数は「月ごとの集計」でよく使います。SELECT DATE_FORMAT(sale_date, '%Y-%m') AS month, SUM(amount) AS total, COUNT(*) AS cnt FROM sales GROUP BY DATE_FORMAT(sale_date, '%Y-%m') ORDER BY month; を実行すると、7月 650000(8件)・8月 875000(10件)・9月 855000(10件)の3行になりました(CASE の章で横に並べた数字と一致します)。
日付関数は DB ごとの差がいちばん大きい部分です。 書式変換(PostgreSQL は TO_CHAR、SQLite は strftime)と日数差の書き方は、後半の「方言差の早見表」に、3つの DB で同じ値(2015/04/01・4202 日)が返った形でまとめています。UPPER・SUBSTRING・COALESCE・ROUND は3つとも同じ書き方で動きました(SQLite の文字数は CHAR_LENGTH ではなく LENGTH)。
INSERT/UPDATE/DELETE:SELECT で対象を確かめてから書き換える
データを書き換える3つの文は、「同じ WHERE で SELECT して対象を確かめる → 実行する → もう一度 SELECT で確かめる」の順に書くのが安全な型です。 UPDATE と DELETE は WHERE を書き忘れると全行が対象になります(典型エラーの章で ROLLBACK で戻す手順を載せています)。
| 文 | 形 | 実行後に MySQL が返す行 |
|---|---|---|
| 追加 | INSERT INTO 表 (列, ...) VALUES (値, ...), (値, ...); |
Query OK, 2 rows affected |
| 更新 | UPDATE 表 SET 列 = 値, 列 = 値 WHERE 条件; |
Rows matched: 1 Changed: 1 |
| 削除 | DELETE FROM 表 WHERE 条件; |
Query OK, 2 rows affected |
-- 追加:社員を1人、その社員の売上を2件(複数行は VALUES をカンマで並べる)
INSERT INTO employees (id, name, department_id, manager_id, hire_date, salary)
VALUES (15, 'Matsumoto', 5, NULL, '2026-10-01', 300000);
INSERT INTO sales (id, employee_id, sale_date, amount)
VALUES (29, 15, '2026-10-01', 40000), (30, 15, '2026-10-02', 52000);
-- 更新:先に SELECT で対象を見る → UPDATE → 同じ SELECT で確かめる
SELECT id, name, salary FROM employees WHERE id = 15;
UPDATE employees SET salary = 320000 WHERE id = 15;
SELECT id, name, salary FROM employees WHERE id = 15;
mysql> UPDATE employees SET salary = 320000 WHERE id = 15;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
mysql> SELECT id, name, salary FROM employees WHERE id = 15;
+----+-----------+--------+
| id | name | salary |
+----+-----------+--------+
| 15 | Matsumoto | 320000 |
+----+-----------+--------+
UPDATE 前の SELECT では 300000 でした。SET salary = salary + 10000 WHERE department_id = 4 のように、今の値を使った計算もできます(Support の3人がそれぞれ 1 万円増え、Rows matched: 3 Changed: 3 と返りました。確認後に元へ戻しています)。
-- 削除:売上 → 社員の順に消す(社員を先に消すと外部キーのエラーになる)
SELECT * FROM sales WHERE employee_id = 15; -- 2 行あることを確認
DELETE FROM sales WHERE employee_id = 15;
SELECT * FROM sales WHERE employee_id = 15; -- Empty set
DELETE FROM employees WHERE id = 15;
SELECT COUNT(*) FROM employees; -- 14 に戻った
mysql> DELETE FROM sales WHERE employee_id = 15;
Query OK, 2 rows affected (0.00 sec)
mysql> SELECT * FROM sales WHERE employee_id = 15;
Empty set (0.00 sec)
外部キーで参照されている行は、参照している側を先に消します。 売上が残っている Sato(id 1)を消そうとすると、MySQL は次のように止めてくれます(PostgreSQL も violates foreign key constraint "sales_employee_id_fkey" と同じ趣旨のエラーでした)。
mysql> DELETE FROM employees WHERE id = 1;
ERROR 1451 (23000): Cannot delete or update a parent row: a foreign key constraint fails (`sqlintro`.`sales`, CONSTRAINT `sales_ibfk_1` FOREIGN KEY (`employee_id`) REFERENCES `employees` (`id`))
CREATE TABLE/ALTER TABLE/CREATE INDEX:表を作る・変える・速くする
CREATE TABLE では列ごとに「型」と「制約」を書きます。制約は、入ってほしくないデータを DB が自動で止めてくれる仕組みです。 タスク管理の表を例に、5つの制約を1つの表に入れました(AUTO_INCREMENT は MySQL の書き方です。他の DB は方言差の早見表を参照)。
| 制約 | 意味 | 破ったときの MySQL のエラー(実行結果) |
|---|---|---|
PRIMARY KEY |
行を一意に識別する。NULL 不可 | ERROR 1062 (23000): Duplicate entry '1' for key 'memo2.PRIMARY'/NULL を入れると ERROR 1048 (23000): Column 'id' cannot be null |
NOT NULL |
必ず値を入れる | ERROR 1364 (HY000): Field 'title' doesn't have a default value |
UNIQUE |
同じ値を2行入れられない | ERROR 1062 (23000): Duplicate entry 'T-001' for key 'tasks.code' |
DEFAULT 値 |
省略したときの値 | (エラーではなく既定値が入る) |
FOREIGN KEY ... REFERENCES |
他の表に存在する値だけを許す | ERROR 1452 (23000): Cannot add or update a child row: a foreign key constraint fails ... |
CREATE TABLE tasks (
id INT AUTO_INCREMENT PRIMARY KEY, -- 自動採番の主キー
title VARCHAR(100) NOT NULL,
code VARCHAR(20) NOT NULL UNIQUE,
status VARCHAR(10) NOT NULL DEFAULT 'todo',
assignee_id INT,
created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
FOREIGN KEY (assignee_id) REFERENCES employees(id)
);
INSERT INTO tasks (title, code, assignee_id) VALUES ('Write the manual', 'T-001', 2), ('Review the manual', 'T-002', 1);
INSERT INTO tasks (title, code, status) VALUES ('Plan the workshop', 'T-003', 'doing');
SELECT * FROM tasks;
+----+-------------------+-------+--------+-------------+---------------------+
| id | title | code | status | assignee_id | created_at |
+----+-------------------+-------+--------+-------------+---------------------+
| 1 | Write the manual | T-001 | todo | 2 | 2026-10-02 15:32:23 |
| 2 | Review the manual | T-002 | todo | 1 | 2026-10-02 15:32:23 |
| 3 | Plan the workshop | T-003 | doing | NULL | 2026-10-02 15:32:23 |
+----+-------------------+-------+--------+-------------+---------------------+
3 rows in set (0.00 sec)
id と created_at は書かなくても自動で入り、status を省いた2行は既定値の todo になりました。制約を破る INSERT を実際に流した結果が、上の表の右列です(code に T-001 を重複、title を省略、存在しない社員 999 を担当者に指定。主キーの例は別の小さな表 memo2 で id 1 を2回入れ、NULL も入れて確かめました)。
ALTER TABLE:列を足す・変える・消す
ALTER TABLE tasks ADD COLUMN due_date DATE; -- 列を追加
ALTER TABLE tasks MODIFY COLUMN status VARCHAR(20) NOT NULL DEFAULT 'todo'; -- 型を変更(MySQL の書き方)
ALTER TABLE tasks DROP COLUMN due_date; -- 列を削除
DESCRIBE tasks; -- 表の定義を確認(MySQL)
3つとも Query OK, 0 rows affected で通り、DESCRIBE tasks の status 行は varchar(20) | NO | | todo に変わっていました。列の型変更は PostgreSQL では ALTER COLUMN status TYPE VARCHAR(20)、SQLite は型変更の ALTER がないため表を作り直す、と差があります(この2つは未実行・一般に知られた仕様)。
CREATE INDEX:検索を速くする
インデックスは「WHERE や JOIN でよく使う列」に作る索引です。 売上表を「社員 × 日付」で探す問い合わせのために、2列の複合インデックスを作りました。
CREATE INDEX idx_sales_employee_date ON sales (employee_id, sale_date);
EXPLAIN SELECT * FROM sales WHERE employee_id = 1 AND sale_date >= '2026-09-01';
EXPLAIN の結果は key 列が idx_sales_employee_date、type が range、rows が 2 でした。つまり表全体(28行)ではなく、インデックスで絞った2行だけを読んでいます。「WHERE に使う列にインデックスがあるか」を EXPLAIN で確かめる習慣は、表が大きくなったときに効いてきます。なお、外部キー列 employee_id を先頭に含むこのインデックスを DROP INDEX しようとすると、ERROR 1553 (HY000): Cannot drop index 'idx_sales_employee_date': needed in a foreign key constraint と止められました(MySQL は外部キーの検査にインデックスを使うためです)。
SQLite で外部キーを検査させるには、接続ごとに PRAGMA foreign_keys = ON; を実行します(公式ドキュメントに「互換性のため既定では無効で、接続ごとに有効にする必要がある」と明記されています)。有効にしてから売上のある Sato を消そうとすると Runtime error: FOREIGN KEY constraint failed (19) と止まりましたが、無効のままでは同じ DELETE がエラーなしで受け付けられました。
トランザクション:まとめて確定する・ROLLBACK で元に戻す
トランザクションは「複数の変更をひとまとまりとして、全部確定(COMMIT)するか、全部取り消す(ROLLBACK)か」を選べる仕組みです。 書き換えを試すときの命綱にもなります。実際に UPDATE してから ROLLBACK し、値が戻ることを確かめました。
START TRANSACTION; -- ここから1つのまとまり(BEGIN でも可)
UPDATE employees SET salary = 999999 WHERE id = 2;
SELECT id, name, salary FROM employees WHERE id = 2; -- この接続からは 999999 に見える
ROLLBACK; -- 取り消す
SELECT id, name, salary FROM employees WHERE id = 2; -- 380000 に戻っている
mysql> UPDATE employees SET salary = 999999 WHERE id = 2;
Query OK, 1 row affected (0.00 sec)
Rows matched: 1 Changed: 1 Warnings: 0
…(省略:SELECT は 999999 を返す)…
mysql> ROLLBACK;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT id, name, salary FROM employees WHERE id = 2;
+----+--------+--------+
| id | name | salary |
+----+--------+--------+
| 2 | Suzuki | 380000 |
+----+--------+--------+
1 row in set (0.00 sec)
同じ流れで UPDATE(給与 390000)と INSERT(売上 id 29)を続けて COMMIT; すると、両方が保存されていました(確認後に元へ戻しています)。
| 文 | 役割 | 補足 |
|---|---|---|
START TRANSACTION; / BEGIN; |
まとまりの開始 | SQLite は BEGIN; のみ(START TRANSACTION は Parse error: near "START": syntax error)。PostgreSQL は両方動いた |
COMMIT; |
変更を確定する | 確定後は ROLLBACK できない |
ROLLBACK; |
開始時点まで戻す | MySQL・PostgreSQL・SQLite の3つで戻ることを確認 |
MySQL は既定で autocommit が ON(SELECT @@autocommit; → 1)なので、START TRANSACTION を書かない UPDATE はその場で確定します。 「試してから決めたい」ときは、必ず START TRANSACTION から始めてください。
ビュー:よく使う問い合わせに名前を付ける
ビューは SELECT 文に名前を付けて、表のように使えるようにしたものです。 データの実体は持たず、参照のたびに元の表から計算されます。JOIN を毎回書く手間が減ります。
CREATE VIEW employee_summary AS
SELECT e.id, e.name, d.name AS department, e.salary
FROM employees AS e
LEFT JOIN departments AS d ON e.department_id = d.id;
SELECT * FROM employee_summary WHERE salary >= 400000 ORDER BY salary DESC;
+----+-----------+-------------+--------+
| id | name | department | salary |
+----+-----------+-------------+--------+
| 3 | Takahashi | Development | 480000 |
| 1 | Sato | Sales | 450000 |
| 6 | Watanabe | Marketing | 420000 |
| 4 | Tanaka | Development | 400000 |
+----+-----------+-------------+--------+
4 rows in set (0.00 sec)
ビューに対して GROUP BY department も書けます(Development 4・Sales 3・Marketing 3・Support 3・NULL 1 の5行)。不要になったら DROP VIEW employee_summary; で消します。
DCL:ユーザーを作って権限を付ける(1例だけ)
CREATE USER 'reader'@'localhost' IDENTIFIED BY 'Reader#2026';
GRANT SELECT ON sqlintro.* TO 'reader'@'localhost'; -- 読み取りだけ許可
SHOW GRANTS FOR 'reader'@'localhost';
REVOKE SELECT ON sqlintro.* FROM 'reader'@'localhost'; -- 取り消し
DROP USER 'reader'@'localhost';
SHOW GRANTS には GRANT USAGE ON *.* TO ... と GRANT SELECT ON sqlintro.* TO ... の2行が表示されました(MySQL の出力では名前がバッククォートで囲まれます)。アプリケーションから使うユーザーには、必要な権限だけを GRANT するのが基本です。
SQL の実行順序:書く順と動く順は違う
SQL は SELECT から書き始めますが、DB が評価する順番は FROM からです。 この順番を知っていると、初心者がよく見るエラーの半分は「まだ存在しないものを参照している」と説明できます。
flowchart LR
A[FROM / JOIN] --> B[WHERE] --> C[GROUP BY] --> D[HAVING] --> E[SELECT] --> F[ORDER BY] --> G[LIMIT]
| 順 | 句 | やること | この順番で説明できること |
|---|---|---|---|
| 1 | FROM / JOIN |
表を用意し、つなぐ | 表の別名(AS e)は以降のどの句でも使える |
| 2 | WHERE |
行を絞る | 集計関数は使えない(まだ集計していない)→ Invalid use of group function |
| 3 | GROUP BY |
グループにまとめる | SELECT に書けるのはグループの列と集計だけ → ERROR 1055 |
| 4 | HAVING |
グループを絞る | 集計関数が使える |
| 5 | SELECT |
列を選ぶ・別名を付ける | 別名はここで初めて生まれるので、WHERE では使えない |
| 6 | ORDER BY |
並べ替える | SELECT の後なので別名が使える |
| 7 | LIMIT |
件数を絞る | 並べ替えた後に切る |
mysql> SELECT name, salary * 12 AS annual_salary FROM employees WHERE annual_salary >= 5000000;
ERROR 1054 (42S22): Unknown column 'annual_salary' in 'where clause'
mysql> SELECT name, salary * 12 AS annual_salary FROM employees ORDER BY annual_salary DESC LIMIT 3;
+-----------+---------------+
| name | annual_salary |
+-----------+---------------+
| Takahashi | 5760000 |
| Sato | 5400000 |
| Watanabe | 5040000 |
+-----------+---------------+
3 rows in set (0.00 sec)
WHERE では WHERE salary * 12 >= 5000000 と式を書き直せば動きます(Sato・Takahashi・Watanabe の3行)。PostgreSQL も同じく column "annual_salary" does not exist でした。なお HAVING cnt >= 3 のように HAVING で別名を使う書き方は、MySQL では動きましたが PostgreSQL では column "cnt" does not exist になるので、HAVING には HAVING COUNT(*) >= 3 と集計式をそのまま書くのが無難です。
方言差の早見表:MySQL/PostgreSQL/SQLite(実行)と SQL Server/Oracle(未実行)
ここまでの SELECT・JOIN・サブクエリ・CASE は3つの DB でそのまま動きましたが、次の項目は製品ごとに書き方が違います。 MySQL 8.0.46・PostgreSQL 16.15・SQLite 3.45.1 の列は実際に実行した結果、SQL Server と Oracle の列は未実行(一般に知られた仕様)です。
| 項目 | MySQL 8.0(実行) | PostgreSQL 16(実行) | SQLite 3.45(実行) | SQL Server/Oracle(未実行) |
|---|---|---|---|---|
| 文字列連結 | CONCAT('SQL', ' intro') → SQL intro。'SQL' || ' intro' は OR と解釈され 0(警告 '|| as a synonym for OR' is deprecated ...)。sql_mode に PIPES_AS_CONCAT を足すと SQL intro |
|| と CONCAT の両方 OK |
|| と CONCAT の両方 OK |
SQL Server は + / CONCAT、Oracle は || / CONCAT |
| 件数を絞る | LIMIT 2 OFFSET 1 OK。OFFSET 1 ROWS FETCH FIRST 2 ROWS ONLY は ERROR 1064 |
両方 OK | LIMIT のみ(FETCH は Parse error) |
SQL Server は TOP n または OFFSET ... FETCH NEXT、Oracle は FETCH FIRST n ROWS ONLY(旧来は ROWNUM) |
| 現在日時 | CURRENT_DATE / CURRENT_TIMESTAMP → 2026-10-02 15:33:32(サーバーのタイムゾーン) |
同じ構文 → 2026-10-02 06:33:47.980526+00(タイムゾーン付き) |
同じ構文 → 2026-10-02 06:33:53(UTC。現地時刻は datetime('now', 'localtime')) |
SQL Server は GETDATE() / SYSDATETIME()、Oracle は SYSDATE / CURRENT_TIMESTAMP |
| 自動採番 | id INT AUTO_INCREMENT PRIMARY KEY |
id INT GENERATED ALWAYS AS IDENTITY PRIMARY KEY |
id INTEGER PRIMARY KEY(省略すると自動で採番) |
SQL Server は IDENTITY(1,1)、Oracle は GENERATED AS IDENTITY |
| 識別子の引用符 | バッククォート `name`。"name" は文字列リテラル扱い(結果が name という文字になった) |
"name"。バッククォートは syntax error |
"name" と `name` の両方 OK |
SQL Server は [name]("name" も可)、Oracle は "NAME" |
| 文字列比較の大文字小文字 | name = 'sato' → Sato が返る(既定の照合順序は区別しない) |
= 'sato' も LIKE 'sato' も 0 行。ILIKE 'sato' で Sato |
= 'sato' は 0 行、LIKE 'sato' は Sato(LIKE は ASCII を区別しない) |
SQL Server は照合順序しだい(既定は多くの場合区別しない)、Oracle は区別する |
| UPSERT(あれば更新、なければ追加) | INSERT ... AS new ON DUPLICATE KEY UPDATE location = new.location |
INSERT ... ON CONFLICT (id) DO UPDATE SET location = EXCLUDED.location |
PostgreSQL と同じ ON CONFLICT 構文 |
MERGE 文 |
| 真偽値 | SELECT TRUE, FALSE, 1 = 1 → 1・0・1(数値で返る) |
t・f・t(boolean 型) |
1・0・1 |
SQL Server は BIT 型、Oracle は 23ai で BOOLEAN 型が追加 |
| 日付の書式 | DATE_FORMAT(d, '%Y/%m/%d') |
TO_CHAR(d, 'YYYY/MM/DD') |
strftime('%Y/%m/%d', d) |
SQL Server は FORMAT(d, 'yyyy/MM/dd')、Oracle は TO_CHAR(d, 'YYYY/MM/DD') |
| 日付の差(日数) | DATEDIFF(a, b) → 4202 |
a - b(DATE 型どうし)→ 4202 |
julianday(a) - julianday(b) → 4202 |
SQL Server は DATEDIFF(day, b, a)、Oracle は a - b |
| 外部キーの検査 | 既定で有効 | 既定で有効 | 接続ごとに PRAGMA foreign_keys = ON; が必要 |
既定で有効 |
迷ったら「3つの DB で共通に動いた書き方」(CONCAT、LIMIT n OFFSET m、CURRENT_DATE、COALESCE、CASE)を選び、日付の書式と自動採番だけは使う DB の書き方を確認する、という覚え方で十分です。UPSERT の MySQL 旧構文 ON DUPLICATE KEY UPDATE location = VALUES(location) も動きましたが、1 warning が付いたので、AS new を使う新しい書き方にしています。
初心者の典型エラー9種:実際のメッセージと直し方
エラーメッセージは「どこを直せばよいか」を教えてくれる味方です。 MySQL 8.0.46 で実際に出したメッセージと、同じ SQL を PostgreSQL 16 に流したときのメッセージを並べます。
| # | やってしまうこと | MySQL のメッセージ(実行結果) | PostgreSQL のメッセージ | 直し方 |
|---|---|---|---|---|
| 1 | 列名のつづり違い SELECT nmae ... |
ERROR 1054 (42S22): Unknown column 'nmae' in 'field list' |
column "nmae" does not exist + HINT: Perhaps you meant to reference the column "employees.name". |
DESCRIBE 表 で列名を確認。表名の違いは ERROR 1146 ... Table 'sqlintro.employee' doesn't exist |
| 2 | 文字列を引用符なしで書く WHERE name = Sato |
ERROR 1054 (42S22): Unknown column 'Sato' in 'where clause' |
column "sato" does not exist |
'Sato' とシングルクォートで囲む(列名と解釈されていた) |
| 3 | GROUP BY に無い列を SELECT | ERROR 1055 (42000): Expression #2 of SELECT list is not in GROUP BY clause and contains nonaggregated column 'sqlintro.employees.name' which is not functionally dependent on columns in GROUP BY clause; this is incompatible with sql_mode=only_full_group_by |
column "employees.name" must appear in the GROUP BY clause or be used in an aggregate function |
その列を GROUP BY に足すか、SELECT から外す(SQLite はエラーにならず、グループ内のどれか1人の名前を返した) |
| 4 | WHERE で集計関数 WHERE COUNT(*) >= 3 |
ERROR 1111 (HY000): Invalid use of group function |
aggregate functions are not allowed in WHERE |
HAVING COUNT(*) >= 3 に移す |
| 5 | WHERE department_id = NULL |
Empty set(エラーにならず0件) |
(0 rows) |
IS NULL に書き換える(Yamada が返る)。3つの DB とも0件だった |
| 6 | JOIN で列名があいまい SELECT id, name FROM employees INNER JOIN departments ... |
ERROR 1052 (23000): Column 'id' in field list is ambiguous |
column reference "id" is ambiguous |
employees.id や e.id と表名・別名を付ける。SQLite は ambiguous column name: id |
| 7 | 予約語を列名に使う CREATE TABLE memo (id INT, order INT, desc VARCHAR(50)) |
ERROR 1064 (42000): You have an error in your SQL syntax; ... near 'order INT, desc VARCHAR(50))' at line 1 |
syntax error at or near "order" |
`order`(MySQL)や "order"(PostgreSQL)で囲めば通るが、sort_order・description に改名するほうが毎回の引用が不要 |
| 8 | UPDATE/DELETE の WHERE 忘れ | Query OK, 13 rows affected Rows matched: 14(全員の給与が 300000 に) |
UPDATE 14 |
下の手順のとおり、トランザクション内で実行して ROLLBACK。SET SESSION sql_safe_updates = 1; を使うと MySQL は ERROR 1175 (HY000): You are using safe update mode and you tried to update a table without a WHERE that uses a KEY column. と止めてくれる |
| 9 | 文字列と数値の暗黙変換 | WHERE name = 0 が14行すべてを返し 14 warnings(Truncated incorrect DOUBLE value: 'Sato')。'10' < '9' は 1(文字列比較)、10 < 9 は 0 |
WHERE name = 0 は operator does not exist: character varying = integer |
比較する両側の型を揃える。数値列には数値、文字列列には引用符付きの文字列を書く |
8番の「WHERE を忘れた UPDATE」をトランザクションで守る手順です。書き換えを試すときは、まず START TRANSACTION; を打ってから UPDATE/DELETE を実行し、SELECT で確認してから COMMIT か ROLLBACK を選びます。
mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)
mysql> UPDATE employees SET salary = 300000;
Query OK, 13 rows affected (0.00 sec)
Rows matched: 14 Changed: 13 Warnings: 0
…(省略:SELECT すると全員 300000)…
mysql> ROLLBACK;
Query OK, 0 rows affected (0.00 sec)
mysql> SELECT name, salary FROM employees ORDER BY id LIMIT 4;
+-----------+--------+
| name | salary |
+-----------+--------+
| Sato | 450000 |
| Suzuki | 380000 |
…(省略)…
4 rows in set (0.00 sec)
Changed: 13 なのは、Kobayashi がもともと 300000 だったためです。DELETE FROM sales; も同じ手順で 28 行が消えたあと ROLLBACK で 28 行に戻りました。PostgreSQL(BEGIN; → UPDATE 14 → ROLLBACK)と SQLite(BEGIN; → ROLLBACK;)でも同じように戻ることを確認しています。
9番の暗黙変換は、DB によって振る舞いが大きく違う部分です。
| SQL | MySQL 8.0 | PostgreSQL 16 | SQLite 3.45 |
|---|---|---|---|
WHERE salary = '450000'(数値列に文字列) |
Sato(文字列を数値に変換) | Sato | Sato |
WHERE salary > '400000abc' |
3行返り 1 warning(先頭の数字だけ使われた) |
ERROR: invalid input syntax for type integer: "400000abc" |
エラーなく0行('400000' なら3行) |
整数列に 'abc' を INSERT |
ERROR 1366 (HY000): Incorrect integer value: 'abc' for column 'salary' at row 1 |
ERROR: invalid input syntax for type integer: "abc" |
エラーなく入り、typeof(salary) が text になった(型親和性。公式ドキュメントのとおり、変換できない文字列は TEXT のまま保存される。厳密に検査したい表は STRICT を付ける) |
理解度チェック10問(初級6問・中級4問)
この記事のサンプル表で解ける10問です。 先に自分で書いてから、解答 SQL と実行結果を見てください。実行結果は MySQL 8.0.46 の出力から枠線と見出し行を省いたデータ行だけを載せています(PostgreSQL 16・SQLite 3.45 でも10問とも同じ行が返ることを確認済み。SQLite は Q5 の ROUND が 380000.0 のように小数で表示される点だけ違います)。
Q1(初級) 給与が 350,000 以上の社員の名前と給与を、給与の高い順に出す。期待する結果:7行、先頭は Takahashi。
SELECT name, salary FROM employees WHERE salary >= 350000 ORDER BY salary DESC;
| Takahashi | 480000 |
| Sato | 450000 |
| Watanabe | 420000 |
…(省略)…
7 rows in set (0.00 sec)
Q2(初級) 2020年1月1日以降に入社した社員の人数を数える。期待する結果:1行1列で 8。
SELECT COUNT(*) AS cnt FROM employees WHERE hire_date >= '2020-01-01';
| cnt |
| 8 |
Q3(初級) 部署が決まっていない社員の id・名前・入社日を出す。期待する結果:Yamada の1行(= NULL では0件になる)。
SELECT id, name, hire_date FROM employees WHERE department_id IS NULL;
| 12 | Yamada | 2026-04-01 |
Q4(初級) 名前が Sa で始まる社員の名前を出す。期待する結果:Sato と Sasaki の2行。
SELECT name FROM employees WHERE name LIKE 'Sa%';
| Sato |
| Sasaki |
Q5(初級) 部署ごとの平均給与を四捨五入して、部署 id 順に出す(配属前の社員は除く)。期待する結果:4行。
SELECT department_id, ROUND(AVG(salary)) AS avg_salary
FROM employees
WHERE department_id IS NOT NULL
GROUP BY department_id
ORDER BY department_id;
| 1 | 380000 |
| 2 | 380000 |
| 3 | 356667 |
| 4 | 318333 |
Q6(初級) 2026年8月の売上合計と件数を出す。期待する結果:875000 と 10。
SELECT SUM(amount) AS august_total, COUNT(*) AS cnt
FROM sales
WHERE sale_date BETWEEN '2026-08-01' AND '2026-08-31';
| august_total | cnt |
| 875000 | 10 |
Q7(中級:JOIN+GROUP BY) 部署ごとの社員数を、社員がいない部署も 0 として、人数の多い順(同数なら部署 id 順)に出す。期待する結果:5行、Planning が 0。
SELECT d.name AS department, COUNT(e.id) AS headcount
FROM departments AS d
LEFT JOIN employees AS e ON e.department_id = d.id
GROUP BY d.id, d.name
ORDER BY headcount DESC, d.id;
| Development | 4 |
| Sales | 3 |
| Marketing | 3 |
| Support | 3 |
| Planning | 0 |
Q8(中級:サブクエリ) 全社員の平均給与より高い給与の社員を、高い順に出す。期待する結果:6行(平均は 354642.8571)。
SELECT name, salary
FROM employees
WHERE salary > (SELECT AVG(salary) FROM employees)
ORDER BY salary DESC;
| Takahashi | 480000 |
| Sato | 450000 |
| Watanabe | 420000 |
…(省略)…
6 rows in set (0.00 sec)
Q9(中級:CASE) 給与 400,000 以上を A、330,000 以上を B、それ以外を C として、ランクごとの人数・最低給与・最高給与を出す。期待する結果:A 4人・B 4人・C 6人。
SELECT CASE WHEN salary >= 400000 THEN 'A' WHEN salary >= 330000 THEN 'B' ELSE 'C' END AS grade,
COUNT(*) AS cnt, MIN(salary) AS min_salary, MAX(salary) AS max_salary
FROM employees
GROUP BY CASE WHEN salary >= 400000 THEN 'A' WHEN salary >= 330000 THEN 'B' ELSE 'C' END
ORDER BY grade;
| A | 4 | 400000 | 480000 |
| B | 4 | 330000 | 380000 |
| C | 6 | 280000 | 320000 |
Q10(中級:HAVING) 売上合計が 300,000 を超える社員の名前・合計・件数を、合計の高い順に出す。期待する結果:Sato・Suzuki・Watanabe の3行。
SELECT e.name, SUM(s.amount) AS total_sales, COUNT(*) AS cnt
FROM employees AS e
INNER JOIN sales AS s ON s.employee_id = e.id
GROUP BY e.id, e.name
HAVING SUM(s.amount) > 300000
ORDER BY total_sales DESC;
| Sato | 725000 | 6 |
| Suzuki | 630000 | 6 |
| Watanabe | 410000 | 4 |
10問中7問以上できれば、日常の SELECT は書けます。 Q7〜Q10 で迷ったら、JOIN・サブクエリ・CASE・HAVING の章に戻って実行結果を見比べてください。
よくある質問
Q. SQL の練習はどの DB で始めるのがよいですか? A. 文法だけ覚えたいなら、インストールが簡単で1ファイルで完結する SQLite がいちばん手軽です。この記事の SQL は SQLite 3.45 でも通ることを確認しています。仕事で使う DB が決まっているなら、その DB(MySQL か PostgreSQL が多い)で始めると、日付関数などの方言を最初から身につけられます。
Q. 大文字と小文字は区別されますか?
A. SELECT と select のような予約語は区別されません。文字列の比較は DB ごとに違い、name = 'sato' は MySQL(既定の照合順序)では Sato が返り、PostgreSQL と SQLite では0件でした(方言差の早見表参照)。
Q. JOIN とサブクエリはどちらを使えばよいですか?
A. 「両方の表の列を結果に出したい」なら JOIN、「ある表の列で絞り込みたいだけ」なら IN や EXISTS のサブクエリが読みやすい、という使い分けが基本です。この記事の「Tokyo の部署の社員」は、どちらでも同じ6行になります。
Q. ウィンドウ関数や CTE(WITH 句)は覚えなくてよいですか? A. この記事の範囲(SELECT〜HAVING、JOIN、サブクエリ、CASE)が書けるようになってからで十分です。どちらもここで覚えた SELECT の上に乗る文法なので、基本が固まってから取り組むと短時間で身につきます。
まとめ
- SQL の中心は
SELECT 列 FROM 表 WHERE 条件で、ORDER BY・LIMIT・GROUP BY・HAVING・JOIN を足していけば日常の問い合わせの大半が書けます。 - この記事のサンプル表(CREATE TABLE+INSERT)は MySQL 8.0・PostgreSQL 16・SQLite 3 でそのまま動き、本文の結果をすべて手元で再現できます。
- NULL は
IS NULLで判定し、COUNT(*)とCOUNT(列)の違い、ORDER BY での NULL の位置(MySQL・SQLite は先頭、PostgreSQL は末尾)を押さえておきます。 - UPDATE/DELETE は「同じ WHERE で SELECT → 実行 → SELECT」の型で書き、試すときは
START TRANSACTIONから始めて ROLLBACK で戻せるようにします。 - SELECT の別名を WHERE で使えない、WHERE に集計関数を書けない、といったエラーは「FROM → WHERE → GROUP BY → HAVING → SELECT → ORDER BY → LIMIT」の実行順序で説明できます。
- 文字列連結・LIMIT・自動採番・日付の書式・大文字小文字の扱いは DB ごとに違うので、方言差の早見表で確認してから書きます。
参考(出典)
- https://it-trend.jp/development_tools/article/32-0012
- https://udemy.benesse.co.jp/development/system/intro-sql.html
- https://www.mysql.com/support/eol-notice.html
- https://www.postgresql.org/docs/16/queries-order.html
- https://dev.mysql.com/doc/refman/8.0/en/group-by-handling.html
- https://www.sqlite.org/foreignkeys.html
- https://www.sqlite.org/datatype3.html
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「SQL入門|基本文法一覧と書き方を実行結果つきで解説【コピペOK】」
株式会社RJCでは、SQLの基本文法と書き方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。