Query Go
大量 INSERT を速くする — multi-row VALUES / COPY / LOAD DATA
ガイド

大量 INSERT を速くする — multi-row VALUES / COPY / LOAD DATA

大量データを高速に投入したいとき。単行 INSERT のループが遅い理由と対処。

大量 INSERT を速くする — multi-row VALUES / COPY / LOAD DATA diagram

問題 — 単一 INSERT ループが極端に遅い

10万件のデータを for ループで INSERT INTO ... VALUES (...) 1行ずつ流す実装は、分単位〜時間単位で詰まります。

理由は主に3つ:

  • ネットワーク往復が1回/行
  • オートコミットで1行ごとに fsync(ディスク同期)
  • 毎回のインデックス更新 / WAL 書き込み / トリガ発火

これをまとめて減らすのが高速化の基本戦略です。

解法1: multi-row VALUES

最もポータブルな高速化。1つの INSERT 文に多数行をまとめる:

ネットワーク往復が一気に減り、パース・プラン作成も1回で済みます。1バッチあたり 500〜1000 行が目安(大きすぎると max_allowed_packet などに引っかかる)。PostgreSQL / MySQL / SQLite / SQL Server すべてで使えます。

sql
INSERT INTO users (name, email) VALUES
  ('Alice', 'a@example.com'),
  ('Bob',   'b@example.com'),
  ('Carol', 'c@example.com'),
  ...  -- 500〜1000 行
  ('Zed',   'z@example.com');

解法2: PostgreSQL の COPY / MySQL の LOAD DATA INFILE

RDBMS 固有のバルクロード専用コマンド。INSERT よりもはるかに速い(PostgreSQL の COPY は INSERT の10倍以上速いことも)。

  • PostgreSQL: COPY table FROM STDIN(psql の \copy や各ドライバの COPY API)
  • MySQL: LOAD DATA INFILE(サーバーファイルシステム読み込み、または LOAD DATA LOCAL INFILE でクライアント側)
  • SQL Server: BULK INSERTbcp ユーティリティ

初期データ投入、大量 ETL、復旧での流し込み、などではまずこれを検討

sql
-- PostgreSQL
COPY users (name, email)
FROM '/tmp/users.csv' WITH (FORMAT csv, HEADER true);

-- psql では \copy でクライアント側ファイルを読ませられる
\copy users (name, email) FROM 'users.csv' CSV HEADER

-- MySQL
LOAD DATA LOCAL INFILE 'users.csv'
INTO TABLE users
FIELDS TERMINATED BY ',' ENCLOSED BY '"'
LINES TERMINATED BY '\n'
IGNORE 1 ROWS
(name, email);

解法3: トランザクションでまとめる

どうしても 1行ずつ INSERT したい場合でも、明示的なトランザクションで包むだけで大幅改善。オートコミットによる fsync を1回にできるため:

ただしトランザクションが長すぎるとロック・WAL・UNDOが膨らむので、適度なバッチ(例: 1万件ごとに COMMIT)で刻むのが実務的。

sql
BEGIN;
INSERT INTO users (name, email) VALUES ('Alice', 'a@example.com');
INSERT INTO users (name, email) VALUES ('Bob',   'b@example.com');
-- ... 1万件
COMMIT;

解法4: インデックス・制約を一時オフにする

初期データ投入のような他からアクセスがないタイミングでは:

  1. 対象テーブルの二次インデックスを DROP
  2. 外部キー・トリガを無効化
  3. バルクロード
  4. 後からインデックス再作成、制約再有効化、ANALYZE(統計更新)

行ごとにインデックスを更新するよりも、データ投入後にまとめて構築する方がはるかに速い。PostgreSQL なら CREATE INDEX CONCURRENTLY ではなく通常の CREATE INDEX(ロックしてよいので)で一気に構築する。

注意点

本番稼働中のテーブルでインデックスを落とさないこと。外部キーやユニーク制約を切った状態で投入すると、途中で失敗したら整合性が壊れたデータが残る。メンテウィンドウや空テーブル時限定の戦術です。

LOAD DATA LOCAL INFILE は MySQL サーバー/クライアント双方の設定が必要(セキュリティ上デフォルト無効になっていることが多い)。動かない時はまず local_infile 設定を確認。

バッチサイズは大きければよい訳ではなく、MySQL の max_allowed_packet、PostgreSQL の WAL バッファ、ドライバのメモリ消費を見ながら 500〜数千行の現実的な値を選ぶこと。

関連トピック