はじめに
【SQL実践ドリル】 シリーズ第5回では、DML(Data Manipulation Language:データ操作言語) を扱います。SELECT以外のSQL操作、すなわちデータの 追加(INSERT)、更新(UPDATE)、削除(DELETE) を練習します。本番環境で実行する際は必ずトランザクションを活用し、意図しない変更を防ぎましょう。
このシリーズでは共通のサンプルデータベースを使用します。初回(第1回)でCREATE TABLE文を掲載しています。
サンプルデータの概要
| テーブル | 件数 | 主な列 |
|---|---|---|
| departments | 5件 | department_id, department_name, location |
| employees | 10件 | employee_id, name, department_id, hire_date, salary, manager_id |
| customers | 5件 | customer_id, customer_name, email, prefecture, registered_date |
| products | 8件 | product_id, product_name, category, price, stock |
| orders | 10件 | order_id, customer_id, employee_id, order_date |
| order_details | 20件 | detail_id, order_id, product_id, quantity |
注意: DML問題は実行するとデータが変更されます。各問題は独立して解けるように記載していますが、複数の問題を連続して実行する場合はデータの状態にご注意ください。練習時はトランザクションで囲むか、毎回データを再投入することを推奨します。
DML の基本構文
INSERT INTO
-- 単一行の挿入
INSERT INTO テーブル名 (列1, 列2, ...) VALUES (値1, 値2, ...);
-- 複数行の挿入
INSERT INTO テーブル名 (列1, 列2, ...) VALUES
(値1a, 値2a, ...),
(値1b, 値2b, ...),
(値1c, 値2c, ...);
-- SELECT結果からの挿入
INSERT INTO テーブル名 (列1, 列2, ...)
SELECT 列A, 列B, ... FROM 別テーブル WHERE 条件;
UPDATE
-- 基本構文
UPDATE テーブル名
SET 列1 = 新しい値1, 列2 = 新しい値2
WHERE 条件;
-- JOIN を使った更新(MySQL構文)
UPDATE テーブル1 t1
JOIN テーブル2 t2 ON t1.キー = t2.キー
SET t1.列 = 新しい値
WHERE 条件;
DELETE
-- 基本構文
DELETE FROM テーブル名 WHERE 条件;
-- サブクエリを使った削除
DELETE FROM テーブル名
WHERE 列 IN (SELECT 列 FROM 別テーブル WHERE 条件);
-- TRUNCATE(全行削除、WHERE不可、高速、AUTO_INCREMENTリセット)
TRUNCATE TABLE テーブル名;
トランザクション
BEGIN; -- トランザクション開始(MySQL: START TRANSACTION も可)
-- SQL文を実行
COMMIT; -- 確定
-- または
ROLLBACK; -- 取り消し
MySQL と PostgreSQL の違い
- MySQL:
INSERT ... ON DUPLICATE KEY UPDATEで重複時に更新。- PostgreSQL:
INSERT ... ON CONFLICT (...) DO UPDATE SET ...で同様の処理。- MySQL:
UPDATE ... JOIN構文が使える。PostgreSQL:UPDATE ... FROM ... WHERE構文を使う。- MySQL:
DELETE t1 FROM t1 JOIN t2 ON ...でJOIN削除。PostgreSQL:DELETE FROM t1 USING t2 WHERE ...。TRUNCATEはMySQLでは暗黙的にコミットされ、ロールバックできない。PostgreSQLではトランザクション内でロールバック可能。
問題
問1 ⭐(基本):新しい社員を追加する
以下の社員を employees テーブルに追加するINSERT文を書いてください。
- employee_id: 11
- name: '藤田大地'
- department_id: 5(マーケティング部)
- hire_date: '2025-04-01'
- salary: 320000
- manager_id: NULL
期待される結果(挿入後にSELECTで確認):
| employee_id | name | department_id | hire_date | salary | manager_id |
|---|---|---|---|---|---|
| 11 | 藤田大地 | 5 | 2025-04-01 | 320000 | NULL |
模範解答
INSERT INTO employees (employee_id, name, department_id, hire_date, salary, manager_id)
VALUES (11, '藤田大地', 5, '2025-04-01', 320000, NULL);
確認用クエリ:
SELECT * FROM employees WHERE employee_id = 11;
解説:
INSERT文では列名を明示的に指定するのがベストプラクティスです。列名を省略して INSERT INTO employees VALUES (...) と書くこともできますが、テーブル定義の変更に弱くなるため推奨されません。NULLを挿入する場合はそのまま NULL と記述します。
問2 ⭐(基本):複数行を一度に追加する
以下の3つの商品を products テーブルに一括で追加するINSERT文を書いてください。
| product_id | product_name | category | price | stock |
|---|---|---|---|---|
| 9 | ドッキングステーション | 周辺機器 | 15000 | 40 |
| 10 | ポータブルSSD 1TB | ストレージ | 12800 | 60 |
| 11 | ノートPCスタンド | 周辺機器 | 4500 | 90 |
期待される結果(挿入後にSELECTで確認):
| product_id | product_name | category | price | stock |
|---|---|---|---|---|
| 9 | ドッキングステーション | 周辺機器 | 15000 | 40 |
| 10 | ポータブルSSD 1TB | ストレージ | 12800 | 60 |
| 11 | ノートPCスタンド | 周辺機器 | 4500 | 90 |
模範解答
INSERT INTO products (product_id, product_name, category, price, stock)
VALUES
(9, 'ドッキングステーション', '周辺機器', 15000, 40),
(10, 'ポータブルSSD 1TB', 'ストレージ', 12800, 60),
(11, 'ノートPCスタンド', '周辺機器', 4500, 90);
確認用クエリ:
SELECT * FROM products WHERE product_id >= 9 ORDER BY product_id;
解説:
MySQLでは複数行をVALUES句にカンマ区切りで列挙できます。1行ずつINSERTするよりもネットワークラウンドトリップが減り、パフォーマンスが向上します。大量データの挿入時は LOAD DATA INFILE の利用も検討してください。
問3 ⭐(基本):特定条件の社員の給与を更新する
2024年以降に入社した社員(hire_date が '2024-01-01' 以降)の給与を一律10000円昇給するUPDATE文を書いてください。
対象社員(更新前):
| employee_id | name | hire_date | salary |
|---|---|---|---|
| 9 | 小林誠 | 2024-01-15 | 270000 |
期待される結果(更新後):
| employee_id | name | hire_date | salary |
|---|---|---|---|
| 9 | 小林誠 | 2024-01-15 | 280000 |
模範解答
UPDATE employees
SET salary = salary + 10000
WHERE hire_date >= '2024-01-01';
確認用クエリ:
SELECT employee_id, name, hire_date, salary
FROM employees
WHERE hire_date >= '2024-01-01'
ORDER BY employee_id;
解説:
元のデータで hire_date が '2024-01-01' 以降の社員は小林誠(2024-01-15)の1名のみです。salary = salary + 10000 のように現在の値を参照して更新できます。UPDATE文には必ずWHERE句を付けましょう。WHERE句を忘れると全行が更新されてしまいます。
ベストプラクティス: 本番環境でUPDATEを実行する前に、同じWHERE条件でSELECTを実行し、対象行を確認してから実行してください。
問4 ⭐⭐(応用):JOINを使った更新 ― 部署の所在地に基づく給与調整
開発部(department_id = 2)に所属する社員の給与を5%引き上げるUPDATE文を、JOINを使って書いてください。
対象社員(更新前):
| employee_id | name | department_name | salary |
|---|---|---|---|
| 2 | 佐藤花子 | 開発部 | 420000 |
| 5 | 伊藤健太 | 開発部 | 450000 |
| 6 | 渡辺さくら | 開発部 | 280000 |
期待される結果(更新後):
| employee_id | name | department_name | salary |
|---|---|---|---|
| 2 | 佐藤花子 | 開発部 | 441000 |
| 5 | 伊藤健太 | 開発部 | 472500 |
| 6 | 渡辺さくら | 開発部 | 294000 |
模範解答
MySQL:
UPDATE employees e
JOIN departments d ON e.department_id = d.department_id
SET e.salary = FLOOR(e.salary * 1.05)
WHERE d.department_name = '開発部';
PostgreSQL の場合:
UPDATE employees e
SET salary = FLOOR(e.salary * 1.05)
FROM departments d
WHERE e.department_id = d.department_id
AND d.department_name = '開発部';
確認用クエリ:
SELECT e.employee_id, e.name, d.department_name, e.salary
FROM employees e
JOIN departments d ON e.department_id = d.department_id
WHERE d.department_name = '開発部'
ORDER BY e.employee_id;
解説:
給与の計算結果を確認します。
- 佐藤花子: 420000 × 1.05 = 441000
- 伊藤健太: 450000 × 1.05 = 472500
- 渡辺さくら: 280000 × 1.05 = 294000
FLOOR 関数で小数点以下を切り捨てています(今回の値はすべて整数になります)。MySQLでは UPDATE ... JOIN 構文を使い、PostgreSQLでは UPDATE ... FROM ... WHERE 構文を使います。この違いは頻出なので覚えておきましょう。
問5 ⭐⭐(応用):INSERT ... SELECT ― 集計結果をテーブルに保存する
部署別の社員数と平均給与を格納するサマリーテーブル department_summary を作成し、INSERT ... SELECT で集計結果を挿入してください。
手順:
- まず
department_summaryテーブルを作成(department_id INT, department_name VARCHAR(50), employee_count INT, avg_salary INT) - INSERT ... SELECT で集計データを挿入
期待される結果(挿入後にSELECTで確認):
| department_id | department_name | employee_count | avg_salary |
|---|---|---|---|
| 1 | 営業部 | 3 | 320000 |
| 2 | 開発部 | 3 | 383333 |
| 3 | 人事部 | 2 | 325000 |
| 4 | 経理部 | 1 | 330000 |
| 5 | マーケティング部 | 0 | NULL |
模範解答
-- 1. テーブル作成
CREATE TABLE department_summary (
department_id INT PRIMARY KEY,
department_name VARCHAR(50) NOT NULL,
employee_count INT NOT NULL,
avg_salary INT
);
-- 2. INSERT ... SELECT で集計結果を挿入
INSERT INTO department_summary (department_id, department_name, employee_count, avg_salary)
SELECT
d.department_id,
d.department_name,
COUNT(e.employee_id) AS employee_count,
FLOOR(AVG(e.salary)) AS avg_salary
FROM departments d
LEFT JOIN employees e ON d.department_id = e.department_id
GROUP BY d.department_id, d.department_name
ORDER BY d.department_id;
確認用クエリ:
SELECT * FROM department_summary ORDER BY department_id;
解説:
各部署の集計結果を確認します。
- 営業部: 社員3名(田中350000 + 鈴木300000 + 中村310000)、AVG = 960000/3 = 320000
- 開発部: 社員3名(佐藤420000 + 伊藤450000 + 渡辺280000)、AVG = 1150000/3 ≒ 383333.33、FLOOR → 383333
- 人事部: 社員2名(高橋380000 + 小林270000)、AVG = 650000/2 = 325000
- 経理部: 社員1名(山本330000)、AVG = 330000
- マーケティング部: 社員0名、AVG = NULL(LEFT JOINにより社員データなし)
INSERT ... SELECT はETL処理やレポートテーブルの作成で頻繁に使われます。LEFT JOINを使うことで、社員がいないマーケティング部も結果に含まれます。
問6 ⭐⭐(応用):条件付き削除 ― 古い注文を削除する
2024年7月以前(2024-07-31以前)の注文と、その注文に紐づく注文明細を削除するSQL文を書いてください。外部キー制約があるため、先に order_details を削除してから orders を削除してください。
削除対象の注文:
| order_id | order_date |
|---|---|
| 1 | 2024-07-01 |
| 2 | 2024-07-05 |
| 3 | 2024-07-10 |
削除対象の注文明細(上記注文に紐づくもの):
| detail_id | order_id |
|---|---|
| 1 | 1 |
| 2 | 1 |
| 3 | 2 |
| 4 | 2 |
| 5 | 3 |
| 6 | 3 |
模範解答
-- 1. 先に注文明細を削除(外部キー制約のため)
DELETE FROM order_details
WHERE order_id IN (
SELECT order_id FROM orders WHERE order_date <= '2024-07-31'
);
-- 2. 注文を削除
DELETE FROM orders
WHERE order_date <= '2024-07-31';
確認用クエリ:
-- 残っている注文を確認
SELECT * FROM orders ORDER BY order_id;
-- 残っている注文明細を確認
SELECT * FROM order_details ORDER BY detail_id;
解説:
2024-07-31以前の注文は order_id = 1(2024-07-01)、2(2024-07-05)、3(2024-07-10)の3件です。これらの注文に紐づく order_details は detail_id = 1〜6 の6件です。
外部キー制約がある場合、子テーブル(order_details)を先に削除する必要があります。テーブル設計時に ON DELETE CASCADE を設定しておけば、親テーブルの削除時に子テーブルのデータも自動で削除されます。
注意: MySQLでは
DELETE FROM order_details WHERE order_id IN (SELECT order_id FROM orders WHERE ...)のように、同じテーブルを参照しない場合はサブクエリが使えます。同じテーブルを参照する場合は一時テーブルを経由する必要がある場合があります。
問7 ⭐⭐(応用):トランザクション ― 部署間の社員異動
山本大輔(employee_id = 7)を経理部(department_id = 4)からマーケティング部(department_id = 5)に異動させる処理を、トランザクションを使って書いてください。以下の処理を1つのトランザクション内で実行してください。
- 山本大輔の department_id を 5 に更新
- 山本大輔の manager_id を NULL に更新(新部署のマネージャーは未定)
- 異動後の状態を確認するSELECT
期待される結果(異動後):
| employee_id | name | department_id | manager_id |
|---|---|---|---|
| 7 | 山本大輔 | 5 | NULL |
模範解答
START TRANSACTION;
-- 部署と上司を更新
UPDATE employees
SET department_id = 5,
manager_id = NULL
WHERE employee_id = 7;
-- 確認
SELECT employee_id, name, department_id, manager_id
FROM employees
WHERE employee_id = 7;
-- 問題なければ確定
COMMIT;
-- もし問題があれば以下を実行(COMMITの代わりに)
-- ROLLBACK;
解説:
トランザクションは複数のSQL文を「すべて成功」または「すべて取り消し」にするための仕組みです。
-
START TRANSACTION(またはBEGIN)でトランザクションを開始 -
COMMITで変更を確定 -
ROLLBACKで変更を取り消し
今回は1つのUPDATE文で department_id と manager_id を同時に更新していますが、複数のテーブルにまたがる更新(たとえば異動履歴テーブルへの記録など)がある場合にトランザクションが特に重要になります。
MySQL と PostgreSQL の違い
- MySQL:
START TRANSACTIONまたはBEGINでトランザクション開始。- PostgreSQL:
BEGINでトランザクション開始。
問8 ⭐⭐⭐(チャレンジ):INSERT ... ON DUPLICATE KEY UPDATE ― 在庫の更新
商品の入荷処理を想定します。以下の入荷データに対して、product_id が既存の場合は stock に数量を加算し、存在しない場合は新規に挿入するSQL文を書いてください。
入荷データ:
| product_id | product_name | category | price | stock(入荷数) |
|---|---|---|---|---|
| 2 | ワイヤレスマウス | 周辺機器 | 3500 | 50 |
| 5 | メカニカルキーボード | 周辺機器 | 12000 | 30 |
| 9 | ドッキングステーション | 周辺機器 | 15000 | 25 |
期待される結果:
- product_id = 2: 既存(stock 200)→ stock が 250 に更新
- product_id = 5: 既存(stock 80)→ stock が 110 に更新
- product_id = 9: 新規 → stock 25 で挿入
模範解答
MySQL:
INSERT INTO products (product_id, product_name, category, price, stock)
VALUES
(2, 'ワイヤレスマウス', '周辺機器', 3500, 50),
(5, 'メカニカルキーボード', '周辺機器', 12000, 30),
(9, 'ドッキングステーション', '周辺機器', 15000, 25)
ON DUPLICATE KEY UPDATE
stock = stock + VALUES(stock);
注意: MySQL 8.0.20以降では
VALUES(stock)の代わりに以下のエイリアス構文が推奨されています。INSERT INTO products (product_id, product_name, category, price, stock) VALUES (2, 'ワイヤレスマウス', '周辺機器', 3500, 50), (5, 'メカニカルキーボード', '周辺機器', 12000, 30), (9, 'ドッキングステーション', '周辺機器', 15000, 25) AS new_values ON DUPLICATE KEY UPDATE stock = products.stock + new_values.stock;
PostgreSQL の場合:
INSERT INTO products (product_id, product_name, category, price, stock)
VALUES
(2, 'ワイヤレスマウス', '周辺機器', 3500, 50),
(5, 'メカニカルキーボード', '周辺機器', 12000, 30),
(9, 'ドッキングステーション', '周辺機器', 15000, 25)
ON CONFLICT (product_id) DO UPDATE
SET stock = products.stock + EXCLUDED.stock;
確認用クエリ:
SELECT product_id, product_name, stock
FROM products
WHERE product_id IN (2, 5, 9)
ORDER BY product_id;
期待される確認結果:
| product_id | product_name | stock |
|---|---|---|
| 2 | ワイヤレスマウス | 250 |
| 5 | メカニカルキーボード | 110 |
| 9 | ドッキングステーション | 25 |
解説:
- product_id = 2: 元の stock 200 + 入荷 50 = 250
- product_id = 5: 元の stock 80 + 入荷 30 = 110
- product_id = 9: 新規挿入のため stock = 25
ON DUPLICATE KEY UPDATE(MySQL)/ ON CONFLICT ... DO UPDATE(PostgreSQL)は「UPSERT」と呼ばれるパターンです。在庫管理やカウンターの更新など、「あれば更新、なければ挿入」というシナリオで非常に有用です。
問9 ⭐⭐⭐(チャレンジ):サブクエリを使った削除 ― 注文がない顧客の関連データ削除
注文実績が1件もない顧客を customers テーブルから削除するSQL文を書いてください。まず対象の顧客を確認してから、削除を実行してください。
ヒント: サンプルデータでは全顧客に注文実績があるため、この問題では「仮にMNOサービス(customer_id = 5)の注文(order_id = 7)とその明細を先に削除した後」という前提で考えてください。
手順:
- 前提準備: MNOサービスの注文明細と注文を削除
- 注文がない顧客を確認
- 注文がない顧客を削除
期待される結果(最終的に残る顧客):
| customer_id | customer_name |
|---|---|
| 1 | 株式会社ABC |
| 2 | DEFコーポレーション |
| 3 | GHI商事 |
| 4 | JKLテクノロジー |
模範解答
-- 前提準備: MNOサービスの注文関連データを削除
DELETE FROM order_details WHERE order_id = 7;
DELETE FROM orders WHERE order_id = 7;
-- 注文がない顧客を確認
SELECT c.customer_id, c.customer_name
FROM customers c
WHERE NOT EXISTS (
SELECT 1 FROM orders o WHERE o.customer_id = c.customer_id
);
-- 結果: customer_id = 5, MNOサービス
-- 注文がない顧客を削除
DELETE FROM customers
WHERE NOT EXISTS (
SELECT 1 FROM orders o WHERE o.customer_id = customers.customer_id
);
別解(IN サブクエリを使う方法):
DELETE FROM customers
WHERE customer_id NOT IN (
SELECT DISTINCT customer_id FROM orders
);
確認用クエリ:
SELECT customer_id, customer_name FROM customers ORDER BY customer_id;
解説:
NOT EXISTS を使う方法は、NULL安全で確実です。NOT IN を使う方法はより簡潔ですが、サブクエリの結果にNULLが含まれると意図しない結果になる場合があるため注意が必要です(orders.customer_id にNULLがある場合、NOT IN は全行がFALSEになります)。
本番環境では、まずSELECTで対象を確認してからDELETEを実行するのが安全です。
問10 ⭐⭐⭐(チャレンジ):トランザクションとDELETE/INSERT ― 注文の差し替え
注文8(order_id = 8、株式会社ABC、2024-10-01)の明細を全て差し替える処理を、トランザクションを使って安全に実行してください。
現在の注文明細(order_id = 8):
| detail_id | order_id | product_id | quantity |
|---|---|---|---|
| 15 | 8 | 1 | 1 |
| 16 | 8 | 4 | 2 |
新しい注文明細:
| product_id | quantity |
|---|---|
| 6 | 2 |
| 7 | 1 |
| 3 | 3 |
期待される結果(差し替え後の注文明細):
| order_id | product_id | quantity |
|---|---|---|
| 8 | 6 | 2 |
| 8 | 7 | 1 |
| 8 | 3 | 3 |
模範解答
START TRANSACTION;
-- 1. 既存の明細を削除
DELETE FROM order_details WHERE order_id = 8;
-- 2. 新しい明細を挿入
INSERT INTO order_details (detail_id, order_id, product_id, quantity)
VALUES
(21, 8, 6, 2),
(22, 8, 7, 1),
(23, 8, 3, 3);
-- 3. 確認
SELECT order_id, product_id, quantity
FROM order_details
WHERE order_id = 8
ORDER BY detail_id;
-- 4. 問題なければ確定
COMMIT;
差し替え後の注文合計金額の変化:
- 変更前: ノートPC Pro(198000×1) + 4Kモニター(45000×2) = 288000
- 変更後: ノートPC Light(89000×2) + Webカメラ HD(6500×1) + USBハブ(4800×3) = 178000 + 6500 + 14400 = 198900
解説:
トランザクション内で DELETE → INSERT の順序で実行することで、途中でエラーが発生した場合に ROLLBACK で元の状態に戻せます。
detail_id は既存データの最大値(20)の次から採番しています。AUTO_INCREMENT を使用している場合は detail_id の指定を省略できます。
ベストプラクティス: 本番環境では、DELETE前に既存データをバックアップするか、論理削除(deleted_at カラム)を検討してください。物理削除は取り消しが困難です。
まとめ
| 操作 | 構文 | 注意点 |
|---|---|---|
| INSERT | INSERT INTO ... VALUES |
列名を明示する |
| INSERT ... SELECT | INSERT INTO ... SELECT ... |
型の一致を確認 |
| UPDATE | UPDATE ... SET ... WHERE |
WHERE句を忘れない |
| UPDATE (JOIN) | MySQL: UPDATE ... JOIN、PG: UPDATE ... FROM
|
DB間で構文が異なる |
| DELETE | DELETE FROM ... WHERE |
WHERE句を忘れない |
| TRUNCATE | TRUNCATE TABLE |
ロールバック不可(MySQL) |
| UPSERT | MySQL: ON DUPLICATE KEY UPDATE、PG: ON CONFLICT
|
DB間で構文が異なる |
| トランザクション |
BEGIN / COMMIT / ROLLBACK
|
変更操作は常にトランザクション内で |
DML操作はデータを変更するため、本番環境では以下を心がけましょう。
- UPDATE/DELETE実行前に同じWHERE条件でSELECTして対象行を確認する
- トランザクションで囲み、確認後にCOMMITする
- バックアップを取ってから実行する
参考
- MySQL 8.0 リファレンスマニュアル - INSERT ステートメント
- MySQL 8.0 リファレンスマニュアル - UPDATE ステートメント
- MySQL 8.0 リファレンスマニュアル - DELETE ステートメント
- MySQL 8.0 リファレンスマニュアル - INSERT ... ON DUPLICATE KEY UPDATE
- MySQL 8.0 リファレンスマニュアル - START TRANSACTION, COMMIT, ROLLBACK
- PostgreSQL 16 ドキュメント - INSERT
- PostgreSQL 16 ドキュメント - UPDATE
@kotaro_ai_lab
AI活用や開発効率化について発信しています。フォローお気軽にどうぞ!