この記事では、MySQLでINSERT文を実行する際に、既存データとの重複を避けてスキップする方法を詳しく解説します。
3つの主要な手法の特徴と使い分けから、実践的な活用例まで幅広くカバーしていきます。
重複データ問題の背景

なぜ重複スキップが必要か
データベースへのデータ挿入では、以下のような場面で重複チェックが重要になります:
よくある重複発生シーン
- バッチ処理での大量データ登録
- 複数のシステムから同時データ投入
- 外部システムからのデータ同期
- ユーザーの重複送信による二重登録
- データマイグレーション時の重複回避
重複による問題
- アプリケーションエラーの発生
- データ整合性の破綻
- パフォーマンスの低下
- 不正確な集計結果
MySQLの制約とエラー
一意制約違反時のエラー
-- 通常のINSERT文で重複が発生した場合
INSERT INTO users (id, email) VALUES (1, 'test@example.com');
-- Error: Duplicate entry '1' for key 'PRIMARY'
このようなエラーを回避するために、MySQLでは複数の解決手法が提供されています。
サンプルテーブルの準備

実際のコード例を理解するために、以下のサンプルテーブルを使用します:
-- ユーザーテーブルの作成
CREATE TABLE users (
id INT PRIMARY KEY,
email VARCHAR(100) UNIQUE,
name VARCHAR(50) NOT NULL,
age INT,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 初期データの挿入
INSERT INTO users (id, email, name, age) VALUES
(1, 'tanaka@example.com', '田中太郎', 28),
(2, 'sato@example.com', '佐藤花子', 32),
(3, 'suzuki@example.com', '鈴木一郎', 45);
-- テーブル内容の確認
SELECT * FROM users;
初期データの状態
+----+--------------------+-----------+-----+---------------------+
| id | email | name | age | created_at |
+----+--------------------+-----------+-----+---------------------+
| 1 | tanaka@example.com | 田中太郎 | 28 | 2024-01-15 10:30:00 |
| 2 | sato@example.com | 佐藤花子 | 32 | 2024-01-15 10:30:01 |
| 3 | suzuki@example.com | 鈴木一郎 | 45 | 2024-01-15 10:30:02 |
+----+--------------------+-----------+-----+---------------------+
方法1:INSERT IGNORE文
基本構文と動作
基本構文
INSERT IGNORE INTO テーブル名 (列名1, 列名2, ...)
VALUES (値1, 値2, ...);
動作原理
- 一意制約(PRIMARY KEY、UNIQUE)に違反する場合、そのレコードを無視
- エラーを発生させずに処理を継続
- 重複しないレコードのみが挿入される
実用例
単一レコードの挿入
-- 既存IDでの挿入試行(スキップされる)
INSERT IGNORE INTO users (id, email, name, age)
VALUES (1, 'new_tanaka@example.com', '新田中', 30);
-- 新しいIDでの挿入(成功する)
INSERT IGNORE INTO users (id, email, name, age)
VALUES (4, 'yamada@example.com', '山田次郎', 35);
-- 結果確認
SELECT * FROM users WHERE id IN (1, 4);
実行結果
+----+--------------------+-----------+-----+---------------------+
| id | email | name | age | created_at |
+----+--------------------+-----------+-----+---------------------+
| 1 | tanaka@example.com | 田中太郎 | 28 | 2024-01-15 10:30:00 |
| 4 | yamada@example.com | 山田次郎 | 35 | 2024-01-15 10:35:00 |
+----+--------------------+-----------+-----+---------------------+
複数レコードの一括挿入
INSERT IGNORE INTO users (id, email, name, age) VALUES
(1, 'duplicate1@example.com', '重複1', 25), -- スキップ(IDが重複)
(5, 'watanabe@example.com', '渡辺三郎', 40), -- 挿入成功
(2, 'duplicate2@example.com', '重複2', 27), -- スキップ(IDが重複)
(6, 'ito@example.com', '伊藤四郎', 50); -- 挿入成功
-- 影響を受けた行数の確認
-- Query OK, 2 rows affected, 2 warnings (0.01 sec)
重複チェックの詳細動作
-- EMAIL列での重複チェック
INSERT IGNORE INTO users (id, email, name, age)
VALUES (10, 'tanaka@example.com', '別の田中', 40);
-- スキップされる(emailが重複)
-- 複合的な重複チェック
INSERT IGNORE INTO users (id, email, name, age) VALUES
(7, 'unique1@example.com', 'ユニーク1', 30),
(8, 'sato@example.com', 'ユニーク2', 35), -- emailで重複
(9, 'unique3@example.com', 'ユニーク3', 40);
-- 1番目と3番目のみ挿入される
メリットとデメリット
メリット
- シンプルで理解しやすい構文
- 高いパフォーマンス
- バッチ処理に適している
- エラーハンドリングが不要
デメリット
- 重複理由の詳細がわからない
- 部分的な挿入失敗が警告でしか確認できない
- データの更新はできない
方法2:INSERT … ON DUPLICATE KEY UPDATE

基本構文と動作
基本構文
INSERT INTO テーブル名 (列名1, 列名2, ...)
VALUES (値1, 値2, ...)
ON DUPLICATE KEY UPDATE
列名1 = VALUES(列名1),
列名2 = VALUES(列名2);
動作原理
- 重複がない場合:通常のINSERTを実行
- 重複がある場合:指定されたUPDATE文を実行
- 常に1つの結果(INSERT または UPDATE)が得られる
実用例
重複時に更新を行う場合
INSERT INTO users (id, email, name, age)
VALUES (1, 'updated_tanaka@example.com', '更新田中', 29)
ON DUPLICATE KEY UPDATE
email = VALUES(email),
name = VALUES(name),
age = VALUES(age);
-- 結果確認
SELECT * FROM users WHERE id = 1;
実行結果
+----+---------------------------+-----------+-----+---------------------+
| id | email | name | age | created_at |
+----+---------------------------+-----------+-----+---------------------+
| 1 | updated_tanaka@example.com| 更新田中 | 29 | 2024-01-15 10:30:00 |
+----+---------------------------+-----------+-----+---------------------+
重複時にスキップする場合
-- 実質的なスキップ(何も更新しない)
INSERT INTO users (id, email, name, age)
VALUES (2, 'new_sato@example.com', '新佐藤', 33)
ON DUPLICATE KEY UPDATE id = id;
-- または
ON DUPLICATE KEY UPDATE id = VALUES(id);
-- 元のデータが維持される
SELECT * FROM users WHERE id = 2;
条件付き更新
INSERT INTO users (id, email, name, age)
VALUES (3, 'conditional@example.com', '条件付き', 50)
ON DUPLICATE KEY UPDATE
email = CASE
WHEN age < VALUES(age) THEN VALUES(email)
ELSE email
END,
name = CASE
WHEN age < VALUES(age) THEN VALUES(name)
ELSE name
END,
age = GREATEST(age, VALUES(age));
高度な活用例
カウンターの更新
-- アクセスカウンターテーブル
CREATE TABLE page_views (
page_id INT PRIMARY KEY,
view_count INT DEFAULT 1,
last_viewed TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- ページビューの記録(重複時はカウント増加)
INSERT INTO page_views (page_id, view_count)
VALUES (1, 1)
ON DUPLICATE KEY UPDATE
view_count = view_count + 1,
last_viewed = CURRENT_TIMESTAMP;
在庫管理での活用
-- 在庫テーブル
CREATE TABLE inventory (
product_id INT PRIMARY KEY,
quantity INT DEFAULT 0,
last_updated TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP
);
-- 在庫の追加(既存の場合は加算)
INSERT INTO inventory (product_id, quantity)
VALUES (101, 50)
ON DUPLICATE KEY UPDATE
quantity = quantity + VALUES(quantity);
VALUES()関数の詳細
VALUES()関数の使用例
INSERT INTO users (id, email, name, age)
VALUES (10, 'values_test@example.com', 'VALUES関数テスト', 35)
ON DUPLICATE KEY UPDATE
email = VALUES(email), -- 新しい値
name = CONCAT(name, ' (更新)'), -- 既存値を変更
age = GREATEST(age, VALUES(age)); -- 大きい方を選択
方法3:INSERT SELECT + NOT EXISTS
基本構文と動作
基本構文
INSERT INTO テーブル名 (列名1, 列名2, ...)
SELECT 値1, 値2, ...
FROM DUAL
WHERE NOT EXISTS (
SELECT 1
FROM テーブル名
WHERE 条件
);
動作原理
- サブクエリで重複チェックを実施
- 重複がない場合のみINSERTを実行
- より複雑な条件での重複判定が可能
実用例
単純な重複チェック
INSERT INTO users (id, email, name, age)
SELECT 11, 'not_exists@example.com', 'NOT EXISTS', 40
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM users WHERE id = 11
);
複数条件での重複チェック
INSERT INTO users (id, email, name, age)
SELECT 12, 'complex@example.com', '複雑チェック', 45
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM users
WHERE email = 'complex@example.com'
OR (name = '複雑チェック' AND age = 45)
);
条件付きチェック
-- 年齢が30歳以上の同名ユーザーがいない場合のみ挿入
INSERT INTO users (id, email, name, age)
SELECT 13, 'conditional@example.com', '田中太郎', 25
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM users
WHERE name = '田中太郎' AND age >= 30
);
複雑な業務ロジックでの活用
関連テーブルとの整合性チェック
-- 部署テーブル
CREATE TABLE departments (
dept_id INT PRIMARY KEY,
dept_name VARCHAR(50),
active BOOLEAN DEFAULT TRUE
);
-- 社員テーブル
CREATE TABLE employees (
emp_id INT PRIMARY KEY,
dept_id INT,
emp_name VARCHAR(50),
FOREIGN KEY (dept_id) REFERENCES departments(dept_id)
);
-- アクティブな部署にのみ社員を追加
INSERT INTO employees (emp_id, dept_id, emp_name)
SELECT 1001, 10, '新入社員'
FROM DUAL
WHERE NOT EXISTS (
SELECT 1 FROM employees WHERE emp_id = 1001
) AND EXISTS (
SELECT 1 FROM departments
WHERE dept_id = 10 AND active = TRUE
);
REPLACE INTOとの比較

REPLACE INTO文の動作
基本構文
REPLACE INTO テーブル名 (列名1, 列名2, ...)
VALUES (値1, 値2, ...);
動作の違い
-- REPLACEの場合:削除→挿入
REPLACE INTO users (id, email, name, age)
VALUES (1, 'replace@example.com', 'REPLACE', 50);
-- ON DUPLICATE KEY UPDATEの場合:更新のみ
INSERT INTO users (id, email, name, age)
VALUES (1, 'update@example.com', 'UPDATE', 51)
ON DUPLICATE KEY UPDATE
email = VALUES(email),
name = VALUES(name),
age = VALUES(age);
重要な違い
項目 | REPLACE INTO | ON DUPLICATE KEY UPDATE |
---|---|---|
動作 | DELETE → INSERT | UPDATE |
AUTO_INCREMENT | リセットされる | 保持される |
トリガー | DELETE + INSERT | UPDATE のみ |
パフォーマンス | やや低い | 高い |
パフォーマンス比較と最適化
性能テストの実施
大量データでの性能比較
-- テスト用の大きなテーブル作成
CREATE TABLE performance_test (
id INT PRIMARY KEY,
data VARCHAR(100),
value INT,
INDEX idx_data (data)
);
-- 初期データの準備
INSERT INTO performance_test (id, data, value)
SELECT
n,
CONCAT('data_', n),
n * 10
FROM (
SELECT a.N + b.N * 10 + c.N * 100 + 1 as n
FROM
(SELECT 0 as N UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) a,
(SELECT 0 as N UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) b,
(SELECT 0 as N UNION SELECT 1 UNION SELECT 2 UNION SELECT 3 UNION SELECT 4 UNION SELECT 5 UNION SELECT 6 UNION SELECT 7 UNION SELECT 8 UNION SELECT 9) c
) numbers
WHERE n <= 1000;
インデックスの最適化
適切なインデックス設計
-- 重複チェック対象列にインデックス
CREATE INDEX idx_users_email ON users(email);
-- 複合インデックスの活用
CREATE INDEX idx_users_name_age ON users(name, age);
-- NOT EXISTS クエリの最適化
EXPLAIN SELECT * FROM users
WHERE NOT EXISTS (
SELECT 1 FROM users u2
WHERE u2.email = 'test@example.com'
);
エラーハンドリングと監視
ワーニングの確認
INSERT IGNORE のワーニング確認
INSERT IGNORE INTO users (id, email, name, age) VALUES
(1, 'warning_test@example.com', 'ワーニングテスト', 30);
-- ワーニング内容の確認
SHOW WARNINGS;
出力例
+---------+------+---------------------------------------+
| Level | Code | Message |
+---------+------+---------------------------------------+
| Warning | 1062 | Duplicate entry '1' for key 'PRIMARY'|
+---------+------+---------------------------------------+
影響行数の取得
ROW_COUNT()関数の活用
INSERT IGNORE INTO users (id, email, name, age)
VALUES (20, 'rowcount@example.com', 'RowCount', 35);
SELECT ROW_COUNT() as affected_rows;
-- 結果: 1 (成功) または 0 (スキップ)
詳細な結果の取得
-- プロシージャでの実装例
DELIMITER //
CREATE PROCEDURE insert_with_result(
IN p_id INT,
IN p_email VARCHAR(100),
IN p_name VARCHAR(50),
IN p_age INT,
OUT p_result VARCHAR(20)
)
BEGIN
DECLARE row_count INT;
INSERT IGNORE INTO users (id, email, name, age)
VALUES (p_id, p_email, p_name, p_age);
SET row_count = ROW_COUNT();
IF row_count = 1 THEN
SET p_result = 'INSERTED';
ELSE
SET p_result = 'SKIPPED';
END IF;
END //
DELIMITER ;
-- 使用例
CALL insert_with_result(21, 'procedure@example.com', 'プロシージャ', 40, @result);
SELECT @result;
実際の業務での活用パターン

データ移行での活用
他システムからのデータ移行
-- 移行元データの取得と重複回避挿入
INSERT IGNORE INTO users (id, email, name, age)
SELECT
old_id,
old_email,
old_name,
old_age
FROM legacy_users
WHERE migration_status = 'pending';
-- 移行結果の確認
SELECT
COUNT(*) as total_legacy,
(SELECT COUNT(*) FROM users) as current_users
FROM legacy_users;
API連携での重複回避
外部APIからのデータ取り込み
-- APIから取得したデータの安全な登録
INSERT INTO users (id, email, name, age)
VALUES (?, ?, ?, ?)
ON DUPLICATE KEY UPDATE
name = CASE
WHEN last_updated < VALUES(last_updated) THEN VALUES(name)
ELSE name
END,
age = CASE
WHEN last_updated < VALUES(last_updated) THEN VALUES(age)
ELSE age
END,
last_updated = GREATEST(last_updated, VALUES(last_updated));
ログデータの重複排除
アクセスログの重複排除
CREATE TABLE access_logs (
user_id INT,
page_url VARCHAR(255),
access_date DATE,
access_count INT DEFAULT 1,
PRIMARY KEY (user_id, page_url, access_date)
);
-- 同日同ページアクセスは集約
INSERT INTO access_logs (user_id, page_url, access_date, access_count)
VALUES (1, '/product/123', CURDATE(), 1)
ON DUPLICATE KEY UPDATE
access_count = access_count + 1;
トランザクション制御との組み合わせ
安全なバッチ処理
START TRANSACTION;
-- 複数テーブルへの整合性を保った挿入
INSERT IGNORE INTO users (id, email, name, age)
VALUES (30, 'transaction@example.com', 'トランザクション', 35);
INSERT IGNORE INTO user_profiles (user_id, profile_data)
VALUES (30, 'プロフィールデータ');
-- 両方成功した場合のみコミット
COMMIT;
エラー時のロールバック
DELIMITER //
CREATE PROCEDURE safe_user_insert(
IN p_id INT,
IN p_email VARCHAR(100),
IN p_name VARCHAR(50),
IN p_age INT
)
BEGIN
DECLARE EXIT HANDLER FOR SQLEXCEPTION
BEGIN
ROLLBACK;
RESIGNAL;
END;
START TRANSACTION;
INSERT INTO users (id, email, name, age)
VALUES (p_id, p_email, p_name, p_age)
ON DUPLICATE KEY UPDATE
email = VALUES(email),
name = VALUES(name),
age = VALUES(age);
-- 関連テーブルへの挿入
INSERT INTO user_logs (user_id, action, timestamp)
VALUES (p_id, 'USER_CREATED_OR_UPDATED', NOW());
COMMIT;
END //
DELIMITER ;
よくある質問

どの方法が最も高速ですか?
パフォーマンス順序(一般的な場合):
- INSERT IGNORE:最も高速
- ON DUPLICATE KEY UPDATE:中程度
- NOT EXISTS:条件によっては低速
ただし、テーブルサイズやインデックス設計により変動するため、実際の環境での測定が重要です。
複数の一意制約がある場合はどうなりますか?
INSERT IGNORE の場合:
-- PRIMARY KEY と UNIQUE制約の両方がある場合
INSERT IGNORE INTO users (id, email, name, age)
VALUES (1, 'existing@example.com', 'テスト', 30);
-- どちらかの制約に違反した場合、レコード全体がスキップ
ON DUPLICATE KEY UPDATE の場合: どの一意制約に違反したかに関わらず、UPDATE句が実行されます。
外部キー制約がある場合の注意点は?
制約違反時の動作:
-- 存在しない部署IDを指定した場合
INSERT IGNORE INTO employees (emp_id, dept_id, emp_name)
VALUES (1001, 999, '新入社員'); -- dept_id=999が存在しない
-- 外部キー制約違反により、このレコードもスキップされる
推奨対応: 事前に参照整合性をチェックするか、NOT EXISTS パターンで条件を明示的に記述する。
AUTO_INCREMENTカラムでの注意点は?
INSERT IGNORE の場合:
CREATE TABLE auto_test (
id INT AUTO_INCREMENT PRIMARY KEY,
data VARCHAR(50) UNIQUE
);
INSERT IGNORE INTO auto_test (data) VALUES
('データ1'), -- id=1で挿入
('データ2'), -- id=2で挿入
('データ1'), -- スキップされるが、id=3は消費される
('データ3'); -- id=4で挿入
-- 結果:id=1,2,4 が使用され、3は欠番になる
この動作を避けたい場合は、事前重複チェックを推奨します。
まとめ
MySQLでINSERT時の重複をスキップする方法は、用途に応じて適切に選択することが重要です。
重要なポイント
- INSERT IGNORE:シンプルで高速、バッチ処理に最適
- ON DUPLICATE KEY UPDATE:更新も含めた柔軟な制御が可能
- NOT EXISTS:複雑な条件指定や他DB互換性重視時に使用
使い分けの指針
用途 | 推奨方法 | 理由 |
---|---|---|
大量データの高速挿入 | INSERT IGNORE | パフォーマンス重視 |
データの更新も含めたい | ON DUPLICATE KEY UPDATE | 柔軟性が高い |
複雑な重複判定 | NOT EXISTS | 条件指定の自由度 |
他DBとの互換性重視 | NOT EXISTS | 標準SQL準拠 |
カウンター更新 | ON DUPLICATE KEY UPDATE | 加算処理に最適 |
ベストプラクティス
- インデックス設計:重複チェック対象列への適切なインデックス
- パフォーマンステスト:実際のデータ量での性能測定
- エラーハンドリング:ワーニングや影響行数の適切な監視
- トランザクション制御:整合性が重要な場合の適切な制御
コメント