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期連続の黒字決算。

ABOUT RJC RJCがどんな会社か知る 考え方、研修、働き方、福利厚生、社員の前職まで。RJCのことが丸わかり! RJC丸わかりページへ JOB DESCRIPTION 仕事内容・待遇を見てみる 仕事内容、給与・待遇、選考の流れ。経験者も未経験も!応募前に知りたいこと、まとめました! 募集要項を見る ENTRY エントリーする エントリーは1〜2分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

RJCで一緒に開発しながら、 成長を楽しみませんか?