タイトル: INSERT
SEOタイトル: SQL INSERT 文の完全ガイド - 複数行 / SELECT / Upsert (ON DUPLICATE / ON CONFLICT / MERGE)
| この記事の要点 |
|
INSERT 文の基本
INSERT 文は SQL の DML (Data Manipulation Language) のうち最も基本的な命令で、テーブルに新しい行を追加します。データの登録に使う中核 SQL であり、ほぼすべてのアプリケーションで使われます。
-- 列名を明示(推奨)
INSERT INTO users (id, name, email)
VALUES (1, 'Alice', 'alice@example.com');
-- 列名省略(全列に値を指定、テーブル定義順)
INSERT INTO users VALUES (1, 'Alice', 'alice@example.com', NOW());
-- DEFAULT で既定値を使う
INSERT INTO users (id, name, created_at)
VALUES (2, 'Bob', DEFAULT);
列名は明示するのが鉄則です。テーブルに列が追加されても既存 INSERT が壊れないため、保守性が大きく向上します。
複数行 INSERT(マルチバリュー)
1 つの INSERT で複数行を挿入できます。1 行ずつ INSERT するより桁違いに高速:
INSERT INTO users (id, name, email) VALUES
(1, 'Alice', 'alice@example.com'),
(2, 'Bob', 'bob@example.com'),
(3, 'Carol', 'carol@example.com'),
(4, 'Dave', 'dave@example.com');
-- 大量データの場合は 500〜1000 行ずつに分割
-- (MySQL の max_allowed_packet、PostgreSQL のパラメータ数制限を考慮)
1 万行を 1 行 INSERT × 1 万回するより、1000 行 × 10 回の方が 50〜100 倍高速になります。バルクロードの基本テクニックです。
INSERT ... SELECT
SELECT の結果を別テーブル(または同テーブル)に流し込めます。ETL やテーブル再構成で多用されます:
-- 別テーブルからコピー
INSERT INTO users_archive (id, name, email, created_at)
SELECT id, name, email, created_at
FROM users
WHERE created_at < '2024-01-01';
-- 集計結果を保存
INSERT INTO daily_sales (date, total)
SELECT DATE(created_at), SUM(amount)
FROM orders
WHERE created_at >= '2026-06-01'
GROUP BY DATE(created_at);
-- 同テーブルへの自己コピー(履歴生成など)
INSERT INTO logs (level, message)
SELECT 'ERROR', message FROM logs WHERE level = 'WARN';
Upsert(あれば更新、無ければ挿入)
「主キー or ユニーク列が既に存在するなら更新、無ければ挿入」を 1 文で行う Upsert は、リトライ可能なデータ取り込み処理で必須です。
MySQL: ON DUPLICATE KEY UPDATE
INSERT INTO user_points (user_id, point)
VALUES (1, 100)
ON DUPLICATE KEY UPDATE
point = point + VALUES(point); -- 既存にあれば加算
-- MySQL 8.0.20+ は VALUES() より new エイリアスが推奨
INSERT INTO user_points (user_id, point) VALUES (1, 100) AS new
ON DUPLICATE KEY UPDATE point = user_points.point + new.point;
PostgreSQL: ON CONFLICT
INSERT INTO user_points (user_id, point)
VALUES (1, 100)
ON CONFLICT (user_id) DO UPDATE
SET point = user_points.point + EXCLUDED.point;
-- EXCLUDED は INSERT しようとした行を指す疑似テーブル
-- 何もしないバージョン(重複なら無視)
INSERT INTO user_points (user_id, point)
VALUES (1, 100)
ON CONFLICT (user_id) DO NOTHING;
SQL 標準: MERGE 文
-- SQL Server / Oracle / DB2 / PostgreSQL 15+ で利用可能
MERGE INTO user_points AS tgt
USING (SELECT 1 AS user_id, 100 AS point) AS src
ON tgt.user_id = src.user_id
WHEN MATCHED THEN
UPDATE SET point = tgt.point + src.point
WHEN NOT MATCHED THEN
INSERT (user_id, point) VALUES (src.user_id, src.point);
RETURNING で採番された ID を取得
-- PostgreSQL / Oracle / MariaDB 10.5+
INSERT INTO users (name, email)
VALUES ('Alice', 'alice@example.com')
RETURNING id, created_at;
-- MySQL は LAST_INSERT_ID() を別途呼ぶ
INSERT INTO users (name, email) VALUES ('Alice', 'alice@example.com');
SELECT LAST_INSERT_ID();
-- SQL Server は OUTPUT
INSERT INTO users (name, email)
OUTPUT inserted.id, inserted.created_at
VALUES ('Alice', 'alice@example.com');
パフォーマンスのコツ
| シーン | 推奨 |
|---|---|
| 1 万行以下の単発投入 | マルチバリュー INSERT を 1000 行ずつ |
| 10 万行以上のバルクロード | MySQL LOAD DATA INFILE / PostgreSQL COPY |
| 大量 INSERT 中に INDEX が遅い | INDEX を一時 DROP → INSERT → 再 CREATE |
| 外部キー制約が遅い | セッション内で一時的に制約 DEFER(PostgreSQL) |
| 同時 INSERT で競合 | Auto-Commit OFF + バッチでまとめて COMMIT |
プレースホルダ(SQL インジェクション対策)
アプリから INSERT する際は必ずプレースホルダを使います。文字列連結は SQL インジェクションの温床:
// ❌ 危険: SQL インジェクション
$pdo->exec("INSERT INTO users (name) VALUES ('$name')");
// ✅ プレースホルダ
$stmt = $pdo->prepare('INSERT INTO users (name, email) VALUES (?, ?)');
$stmt->execute([$name, $email]);
// ✅ Laravel
DB::table('users')->insert(['name' => $name, 'email' => $email]);
// ✅ バルク INSERT
DB::table('users')->insert([
['name' => 'Alice', 'email' => 'alice@example.com'],
['name' => 'Bob', 'email' => 'bob@example.com'],
]);
よくあるトラブル
| 症状 | 原因 | 対処 |
|---|---|---|
Duplicate entry '...' for key 'PRIMARY' | 主キー重複 | ON DUPLICATE KEY / ON CONFLICT 活用 |
Column count doesn't match value count | 列数と値数の不一致 | 列名を明示 |
Field 'xxx' doesn't have a default value | NOT NULL 列に値が無い | 列に値を入れるか DEFAULT 設定 |
Data too long for column 'xxx' | VARCHAR 長超過 | 列長を見直すか文字列短縮 |
| 大量 INSERT が遅い | 1 行ずつ実行 / インデックス過多 | マルチバリュー化 / インデックス見直し |
FAQ
Q: INSERT IGNORE と INSERT ... ON DUPLICATE KEY UPDATE の違いは?
A: INSERT IGNORE は重複時に黙って何もしない(エラー無視)。ON DUPLICATE KEY UPDATE は重複時に更新を行います。意図に応じて使い分け。
Q: AUTO_INCREMENT の値を予測できる?
A: 予測すべきでありません。LAST_INSERT_ID() / RETURNING / OUTPUT で挿入直後に取得するのが正解です。
Q: トランザクション内で INSERT してロールバックすると AUTO_INCREMENT は戻る?
A: 戻りません。MySQL InnoDB の場合、AUTO_INCREMENT カウンタは進んだまま。歯抜けの ID が発生するのは仕様です。