SQLのINSERTで複数行をまとめて登録するには、INSERT INTO テーブル (列1, 列2) VALUES (値1, 値2), (値3, 値4), ...; のように、VALUES の後ろに括弧をカンマで並べます。 別のテーブルの検索結果をそのまま登録したいときは、VALUES の代わりに SELECT を書く INSERT INTO ... SELECT ... を使います。

この記事では、INSERT の書き方を一通り実行結果つきで確認したうえで、「1行ずつ」「トランザクションでまとめる」「複数行VALUES」「INSERT SELECT」「バルクロード」の5通りを、1万行と10万行で実測しました。MySQL 8.0.46 と PostgreSQL 16.15 の両方の数字を載せています。列数の不一致や型の不一致など、つまずきやすいエラーも実際に出して並べました。

MySQL 8.0 は2026年4月に公式サポートが終了しています。この記事の構文は MySQL 8.4/9.7 でも同じように使えます(違いがある箇所は本文で明記します)。検証は手元の 8.0.46 で行いました。

INSERTの早見表

書き方の早見表

やりたいこと 書き方 MySQL 8.0 PostgreSQL 16
1行を登録 INSERT INTO items (name, price) VALUES ('ノート', 300); ○ ○
複数行を登録 INSERT INTO items (name, price) VALUES ('ペン', 120), ('定規', 150); ○ ○
検索結果を登録 INSERT INTO b (id, name) SELECT id, name FROM a WHERE ...; ○ ○
一部の列を初期値に VALUES ('付箋', 200, DEFAULT) ○ ○
全列を初期値で登録 INSERT INTO logs DEFAULT VALUES; ×(INSERT INTO logs () VALUES ();) ○
登録した行を受け取る INSERT ... RETURNING id; ×(LAST_INSERT_ID()) ○
列名=値の形で書く INSERT INTO items SET name = 'A', price = 10; ○(MySQL独自) ×
ファイルから一括登録 バルクロード LOAD DATA COPY/\copy

速度の早見表(10万行・実測)

方法 MySQL 8.0 PostgreSQL 16
1行ずつ INSERT(自動コミット) 47.89秒 16.40秒
1行ずつ INSERT を1トランザクションにまとめる 5.94秒 3.35秒
複数行VALUES(1,000行ずつ) 0.54秒 0.38秒
INSERT SELECT 0.45秒 0.25秒
バルクロード(LOAD DATA/COPY) 0.38秒 0.17秒

1行ずつを複数行VALUESに変えるだけで、数十倍速くなりました。 測定条件は後半の「速度比較」で説明します。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「SQL INSERT複数行の書き方|INSERT SELECTと速度比較つき」

検証に使うテーブル

以降の例で使うテーブルです。そのまま実行できます。

-- MySQL 8.0
CREATE TABLE items (
  id    BIGINT      NOT NULL AUTO_INCREMENT PRIMARY KEY,
  name  VARCHAR(10) NOT NULL,
  price INT         NOT NULL,
  stock INT         NOT NULL DEFAULT 0
);
CREATE TABLE items_archive (
  id    BIGINT      PRIMARY KEY,
  name  VARCHAR(10) NOT NULL,
  price INT         NOT NULL
);
CREATE TABLE logs (
  id         BIGINT   NOT NULL AUTO_INCREMENT PRIMARY KEY,
  created_at DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP
);
-- PostgreSQL 16
CREATE TABLE items (
  id    BIGINT      GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  name  VARCHAR(10) NOT NULL,
  price INT         NOT NULL,
  stock INT         NOT NULL DEFAULT 0
);
CREATE TABLE items_archive (
  id    BIGINT      PRIMARY KEY,
  name  VARCHAR(10) NOT NULL,
  price INT         NOT NULL
);
CREATE TABLE logs (
  id         BIGINT    GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  created_at TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP
);

INSERTの基本:1行を登録する

INSERT INTO items (name, price) VALUES ('ノート', 300);

構造は次の3つです。

部分 意味
INSERT INTO items どのテーブルに
(name, price) どの列に(列リスト)
VALUES ('ノート', 300) 何の値を(列リストと同じ順番・同じ個数)

列リストに書かなかった列には、DEFAULT があればその値、連番の列なら次の番号、どちらもなく NULL を許す列なら NULL が入ります。上の例では id に連番、stock に DEFAULT の 0 が入ります。

文字列と日付はシングルクォートで囲み、数値は囲みません。文字列の中にシングルクォートを含めたいときは、'It''s' のように2つ重ねます。

複数行をまとめて登録する(複数行VALUES)

VALUES の後ろに、括弧をカンマで区切って並べます。

INSERT INTO items (name, price) VALUES
  ('ペン', 120),
  ('消しゴム', 80),
  ('定規', 150);

SELECT id, name, price, stock FROM items ORDER BY id;

実行結果(PostgreSQL 16)です。INSERT の直後には INSERT 0 3(3行を登録)と表示されます。

 id |   name   | price | stock
----+----------+-------+-------
  1 | ノート   |   300 |     0
  2 | ペン     |   120 |     0
  3 | 消しゴム |    80 |     0
  4 | 定規     |   150 |     0
(4 rows)

MySQL 8.0 でも同じ文で同じ結果になりました。

複数行VALUESは「全部入るか、1行も入らないか」

複数行VALUESは1つの文です。途中の1行が制約に違反すると、その文で入れようとした行はすべて登録されません。

SELECT COUNT(*) AS before_cnt FROM items;                             -- 6
INSERT INTO items (name, price) VALUES ('A', 1), ('B', NULL), ('C', 3);
SELECT COUNT(*) AS after_cnt FROM items;                              -- 6 のまま
-- MySQL 8.0
ERROR 1048 (23000): Column 'price' cannot be null
-- PostgreSQL 16
ERROR:  null value in column "price" of relation "items" violates not-null constraint
DETAIL:  Failing row contains (10, B, null, 0).

2行目の 'B' が NOT NULL に当たり、1行目の 'A' も含めて登録されませんでした(件数は 6 のまま)。中途半端な状態が残らないので、やり直しが簡単です。PostgreSQL の DETAIL にはどの行が原因かが表示されるので、行数が多いときの手がかりになります。

最後のカンマに注意

複数行VALUESで多い書き間違いは、最後の括弧の後ろにカンマを残すことと、行ごとに値の個数が違うことです。後者は次のように表示されます。

INSERT INTO items (name, price) VALUES ('のり', 100), ('はさみ');
-- MySQL 8.0
ERROR 1136 (21S01): Column count doesn't match value count at row 2
-- PostgreSQL 16
ERROR:  VALUES lists must all be the same length

MySQL は at row 2 と、何行目の括弧が合っていないかを教えてくれます。

INSERT SELECT:検索結果をそのまま登録する

VALUES の代わりに SELECT 文を書くと、その検索結果が登録されます。 テーブル間のコピー、条件に合う行だけの退避、集計結果の保存などに使います。

INSERT INTO items_archive (id, name, price)
SELECT id, name, price
FROM items
WHERE price >= 150;

SELECT * FROM items_archive ORDER BY id;

実行結果です(MySQL 8.0・PostgreSQL 16 とも同じ)。

 id |  name  | price
----+--------+-------
  1 | ノート |   300
  4 | 定規   |   150
(2 rows)

INSERT SELECT の書き方のポイント

ポイント 説明
VALUES は書かない INSERT INTO b (...) SELECT ... と、列リストのすぐ後ろに SELECT を置く
対応は「順番」で決まる 列名が同じかどうかではなく、列リストの1番目と SELECT の1番目、2番目と2番目…が対応する
列の個数を揃える 列リストが3列なら SELECT も3列
固定値や式も混ぜられる SELECT id, name, price * 2, 'copied' FROM ...
JOIN や GROUP BY も使える SELECT として正しければそのまま登録できる

よく使う形をまとめます。

-- テーブルを丸ごとコピー(同じ構造のテーブルへ)
INSERT INTO items_archive (id, name, price)
SELECT id, name, price FROM items;

-- 集計結果を登録
INSERT INTO price_summary (price, item_count)
SELECT price, COUNT(*)
FROM items
GROUP BY price;

-- まだ登録されていない行だけを登録
INSERT INTO items_archive (id, name, price)
SELECT i.id, i.name, i.price
FROM items i
WHERE NOT EXISTS (SELECT 1 FROM items_archive a WHERE a.id = i.id);

テーブルごと新しく作りながらコピーしたい場合は CREATE TABLE 新テーブル AS SELECT ... が使えます。INSERT SELECT はすでにあるテーブルに追加するときの書き方です。

「すでにあれば更新、なければ登録」をしたいときは、ON CONFLICT(PostgreSQL)や ON DUPLICATE KEY UPDATE(MySQL)を使います。こちらは別記事「SQL UPSERTの書き方」で比較しています。

DEFAULT を使う:初期値で登録する

一部の列だけ初期値にする

値の位置に DEFAULT と書くと、その列の初期値が入ります。

INSERT INTO items (name, price, stock) VALUES ('付箋', 200, DEFAULT);

列リストから stock を外しても同じ結果です。複数行VALUESの中で「この行だけ初期値」としたいときに DEFAULT キーワードが役立ちます。

すべての列を初期値で登録する(DEFAULT VALUES)

連番と作成日時だけのテーブルなど、指定する値が1つもない場合の書き方です。ここは MySQL と PostgreSQL で書き方が分かれます。

-- PostgreSQL 16(標準SQLの書き方)
INSERT INTO logs DEFAULT VALUES;

-- MySQL 8.0
INSERT INTO logs () VALUES ();

相手の書き方で実行すると、どちらも構文エラーになりました。

-- MySQL 8.0 で DEFAULT VALUES
ERROR 1064 (42000): You have an error in your SQL syntax; ... near 'DEFAULT VALUES' at line 1
-- PostgreSQL 16 で () VALUES ()
ERROR:  syntax error at or near ")"
LINE 1: INSERT INTO logs () VALUES ();
                          ^

RETURNING:登録した行をその場で受け取る(PostgreSQL)

連番で振られた id をすぐ使いたい場面は多くあります。PostgreSQL では INSERT の末尾に RETURNING を付けると、登録した行の値が SELECT の結果のように返ります。

INSERT INTO items (name, price) VALUES ('X', 1), ('Y', 2)
RETURNING id, name;
 id | name
----+------
 11 | X
 12 | Y
(2 rows)

INSERT 0 2

RETURNING * と書けば、DEFAULT で入った値も含めた全列が返ります。

INSERT INTO items (name, price) VALUES ('Z', 1) RETURNING *;
 id | name | price | stock
----+------+-------+-------
 13 | Z    |     1 |     0

MySQL では LAST_INSERT_ID() を使う

MySQL 8.0 に RETURNING はなく、書くと構文エラーになります。

ERROR 1064 (42000): You have an error in your SQL syntax; ... near 'RETURNING id' at line 1

代わりに、同じ接続で LAST_INSERT_ID() を呼びます。

INSERT INTO items (name, price) VALUES ('X', 1), ('Y', 2);
SELECT LAST_INSERT_ID(), ROW_COUNT();
+------------------+-------------+
| LAST_INSERT_ID() | ROW_COUNT() |
+------------------+-------------+
|               10 |           2 |
+------------------+-------------+

複数行を登録したとき、LAST_INSERT_ID() が返すのは「最初の行」の番号です(ここでは X が 10、Y が 11)。最後の行の番号ではない点に気を付けてください。

やりたいこと MySQL 8.0 PostgreSQL 16
登録した行の連番を知る SELECT LAST_INSERT_ID();(複数行なら最初の番号) INSERT ... RETURNING id;(全行分)
登録した件数を知る SELECT ROW_COUNT(); INSERT 0 n の n
登録した行の全列を知る 連番を使って SELECT し直す RETURNING *

速度比較:5つの方法を1万行・10万行で実測

ここからは、同じデータを5通りの方法で登録して所要時間を比べます。

測定条件

項目 内容
DB MySQL 8.0.46(InnoDB)、PostgreSQL 16.15。どちらも設定は初期値のまま
テーブル id(連番の主キー)、name VARCHAR(50)、price INT、created_at(DEFAULT で現在時刻)
データ ('item000001', 101) のような行を1万行・10万行
実行方法 SQL ファイルを mysql コマンド/psql -f に読み込ませ、開始から終了までの時間を計測(同じマシン上のソケット接続)
回数 毎回テーブルを作り直して3回測り、中央値を採用(10万行の「1行ずつ」だけは1回)
環境 Linux コンテナ(ほかの処理も動いている共有環境)

数字は環境で大きく変わります。絶対値ではなく、方法ごとの差の大きさを見てください。

5つの方法

-- A. 1行ずつ INSERT(自動コミット。1文ごとにコミットされる)
INSERT INTO items (name, price) VALUES ('item000001', 101);
INSERT INTO items (name, price) VALUES ('item000002', 102);
-- … 行数ぶん続く

-- B. 1行ずつ INSERT を、1つのトランザクションにまとめる
BEGIN;
INSERT INTO items (name, price) VALUES ('item000001', 101);
INSERT INTO items (name, price) VALUES ('item000002', 102);
-- … 行数ぶん続く
COMMIT;

-- C. 複数行VALUES(1,000行ずつ1文にする)
INSERT INTO items (name, price) VALUES ('item000001',101),('item000002',102), … ;

-- D. INSERT SELECT(同じデータが入った staging テーブルから)
INSERT INTO items (name, price) SELECT name, price FROM staging;

-- E. バルクロード(CSV ファイルから)
-- MySQL
LOAD DATA LOCAL INFILE '/tmp/t152b/100000.csv' INTO TABLE items
  FIELDS TERMINATED BY ',' (name, price);
-- PostgreSQL(psql のメタコマンド)
\copy items (name, price) FROM '/tmp/t152b/100000.csv' WITH (FORMAT csv)

実測結果

方法 MySQL 1万行 MySQL 10万行 PostgreSQL 1万行 PostgreSQL 10万行
A. 1行ずつ(自動コミット) 5.18秒 47.89秒 1.71秒 16.40秒
B. 1行ずつ+1トランザクション 1.44秒 5.94秒 0.51秒 3.35秒
C. 複数行VALUES(1,000行ずつ) 0.13秒 0.54秒 0.08秒 0.38秒
D. INSERT SELECT 0.12秒 0.45秒 0.06秒 0.25秒
E. バルクロード 0.06秒 0.38秒 0.05秒 0.17秒

10万行で A を基準にした倍率です。

方法 MySQL PostgreSQL
A → B(トランザクションにまとめる) 約8倍 約5倍
A → C(複数行VALUESにする) 約89倍 約44倍
A → E(バルクロードにする) 約125倍 約94倍

なぜここまで差が出るのか

A が遅い主な理由は、1行ごとにコミットが走ることです。 コミットのたびに、データベースは変更をディスクに確実に書き出します。10万行なら10万回の書き出しになります。B はコミットを1回にまとめたので、それだけで数倍速くなりました。

B と C の差は、文の数です。 B は10万個の文を1つずつ送り、解析し、実行しています。C は100個の文で済むため、やり取りと解析の回数が1,000分の1になります。

D と E は、SQL 文としての値の解析もほぼなくなるため、さらに短くなります。

方法 コミット回数 文の数 向いている場面
A. 1行ずつ 行数ぶん 行数ぶん 画面からの1件登録など、もともと1行の処理
B. トランザクション 1回 行数ぶん 既存の1行INSERTの並びを手早く速くしたいとき
C. 複数行VALUES 文の数ぶん 行数÷まとめる行数 アプリからの数百〜数万行の登録
D. INSERT SELECT 1回 1 DB内にあるデータのコピー・加工
E. バルクロード 1回 1 CSV などファイルからの大量登録

1文に何行まとめるとよいか

1文にまとめる行数を増やすほど文の数は減りますが、1文が大きくなりすぎると、メモリの使用量が増えたり、次に説明する上限に当たったりします。まずは500〜1,000行ずつに区切るのが扱いやすい目安です。今回、10万行すべてを1文(約1.9MB)にまとめた場合も試しましたが、1回の計測で MySQL は 0.62秒、PostgreSQL は 0.44秒で、1,000行ずつ(0.54秒・0.38秒)と大きな差はありませんでした。数百行を超えたあたりから、まとめる効果はゆるやかになります。

バルクロードの細かいオプションやエラー対処は、別記事「MySQL CSVインポート|LOAD DATA INFILE」「PostgreSQL CSVインポート|COPYと\copyの違い」で扱っています。

max_allowed_packet:MySQLの1文の大きさの上限

MySQL には、1つの文(パケット)として受け取れる大きさの上限 max_allowed_packet があります。複数行VALUESで行数を増やしていくと、この上限に当たることがあります。

SHOW VARIABLES LIKE 'max_allowed_packet';
+--------------------+----------+
| Variable_name      | Value    |
+--------------------+----------+
| max_allowed_packet | 67108864 |
+--------------------+----------+

MySQL 8.0 の初期値は 67108864(64MB)です。上限を 1MB に下げて、約1.9MB の INSERT 文(10万行を1文)を実行した結果です。

SET GLOBAL max_allowed_packet = 1048576;   -- 1MB(新しい接続から有効)
ERROR 1153 (08S01) at line 1: Got a packet bigger than 'max_allowed_packet' bytes

このとき、1行も登録されませんでした(件数は 0)。対処は次の2つです。

対処 方法
1文を小さくする(おすすめ) 1,000行ずつなど、決まった行数で区切って複数の文にする
上限を上げる SET GLOBAL max_allowed_packet = 67108864;、または設定ファイルの [mysqld] に max_allowed_packet=64M

上限を上げるよりも、文を区切るほうが環境に左右されません。大きな文字列や画像データを1行に入れる場合も同じ上限が関係します。

PostgreSQL には同じ名前の設定はありません。10万行・約1.9MB の1文もそのまま実行できました。ただし、アプリからプレースホルダ($1、$2…)で値を渡す場合は、1文あたりのパラメータ数に上限(65,535個)があるため、やはり適度な行数で区切ります。

エラーを再現して確認する

INSERT で出やすい4種類のエラーを、実際のメッセージで確認します。

列数の不一致

INSERT INTO items (name, price) VALUES ('のり', 100, 5);   -- 値が多い
INSERT INTO items (name, price) VALUES ('のり');           -- 値が少ない
-- MySQL 8.0(どちらも同じメッセージ)
ERROR 1136 (21S01): Column count doesn't match value count at row 1
-- PostgreSQL 16
ERROR:  INSERT has more expressions than target columns
ERROR:  INSERT has more target columns than expressions

PostgreSQL は「値が多い」「列が多い」を別のメッセージで教えてくれます。列リストの個数と、VALUES の括弧の中の個数を数え直すのが直し方です。

型の不一致

INSERT INTO items (name, price) VALUES ('のり', 'abc');                  -- 数値の列に文字
INSERT INTO items (name, price) VALUES ('のり', 99999999999);            -- INT の範囲を超える
INSERT INTO items (name, price) VALUES ('あいうえおかきくけこさ', 100);  -- 10文字の列に11文字
-- MySQL 8.0
ERROR 1366 (HY000): Incorrect integer value: 'abc' for column 'price' at row 1
ERROR 1264 (22003): Out of range value for column 'price' at row 1
ERROR 1406 (22001): Data too long for column 'name' at row 1
-- PostgreSQL 16
ERROR:  invalid input syntax for type integer: "abc"
ERROR:  integer out of range
ERROR:  value too long for type character varying(10)

なお、'100' のように数値として読める文字列は、どちらのDBでも数値の列にそのまま入りました。

MySQL でこれらがエラーになるのは、sql_mode に STRICT_TRANS_TABLES が含まれているためです(8.0 の初期値に含まれています)。厳格モードを外すと、エラーではなく警告になり、値が丸められて登録されます。

SET SESSION sql_mode = '';
INSERT INTO items (name, price) VALUES ('のり', 'abc');
INSERT INTO items (name, price) VALUES ('あいうえおかきくけこさ', 100);
SELECT name, price FROM items ORDER BY id DESC LIMIT 2;
Warning 1366: Incorrect integer value: 'abc' for column 'price' at row 1
Warning 1265: Data truncated for column 'name' at row 1

+----------------------+-------+
| name                 | price |
+----------------------+-------+
| あいうえおかきくけこ |   100 |
| のり                 |     0 |
+----------------------+-------+

'abc' は 0 に、11文字は10文字に切り詰められて入りました。意図しないデータが残るので、sql_mode は初期値の厳格モードのまま使うのがおすすめです。

NOT NULL 違反

INSERT INTO items (name, price) VALUES (NULL, 100);   -- NULL を明示
INSERT INTO items (price) VALUES (100);               -- 列を省略
-- MySQL 8.0
ERROR 1048 (23000): Column 'name' cannot be null
ERROR 1364 (HY000): Field 'name' doesn't have a default value
-- PostgreSQL 16(どちらも同じ)
ERROR:  null value in column "name" of relation "items" violates not-null constraint
DETAIL:  Failing row contains (7, null, 100, 0).

MySQL の 1364 は「列を省略したが、初期値がない」という意味です。NOT NULL で DEFAULT のない列は、列リストに必ず含めます。

重複キー

INSERT INTO items_archive (id, name, price) VALUES (1, 'ノート', 300);   -- id=1 は登録済み
-- MySQL 8.0
ERROR 1062 (23000): Duplicate entry '1' for key 'items_archive.PRIMARY'
-- PostgreSQL 16
ERROR:  duplicate key value violates unique constraint "items_archive_pkey"
DETAIL:  Key (id)=(1) already exists.

重複したときに「何もしない」「更新する」を選びたい場合は、UPSERT の構文を使います(別記事「SQL UPSERTの書き方」)。

エラーの早見表

原因 MySQL 8.0 PostgreSQL 16 直し方
列数の不一致 1136 Column count doesn't match value count INSERT has more expressions than target columns など 列リストと値の個数を揃える
数値の列に文字 1366 Incorrect integer value invalid input syntax for type integer 値と列の対応(順番)を見直す
範囲を超える 1264 Out of range value integer out of range 値を確認、または列を BIGINT に
文字数を超える 1406 Data too long value too long for type character varying(n) 値を確認、または列の長さを広げる
NOT NULL 1048/1364 violates not-null constraint 値を指定する、DEFAULT を付ける
重複キー 1062 Duplicate entry duplicate key value violates unique constraint 値を変える、UPSERT を使う

列名を省略するとどうなるか

INSERT は列リストを省略して書くこともできます。

INSERT INTO items_archive VALUES (10, 'クリップ', 90);

この場合、テーブル定義の列の順番どおりに、すべての列の値を書くことになります。短く書けますが、次の2つの場面で意図しない結果につながります。

場面1:あとから列が追加された

ALTER TABLE items_archive ADD COLUMN note VARCHAR(20);
INSERT INTO items_archive VALUES (11, 'ホチキス', 500);   -- 以前と同じ書き方
-- MySQL 8.0
ERROR 1136 (21S01): Column count doesn't match value count at row 1
-- PostgreSQL 16
INSERT 0 1

MySQL では、列が1つ増えただけで、それまで動いていた INSERT がエラーになりました。 PostgreSQL は値が足りない分を初期値で埋めるため通りますが、列の途中に意味の違う列が入った場合は気付きにくくなります。

場面2:列の順番を取り違えた

CREATE TABLE pairs (price INT, quantity INT);
INSERT INTO pairs VALUES (100, 5);   -- 100 は価格? 数量?

どちらも INT なので、順番を逆に書いてもエラーにはならず、そのまま登録されます。SQL を読んだだけでは、どちらが価格かを判断できません。

列リストを書いておけば、テーブルに列が増えても動き続け、読む人にも対応がひと目で伝わります。

INSERT INTO pairs (price, quantity) VALUES (100, 5);
書き方 列の追加 読みやすさ おすすめ度
INSERT INTO t VALUES (...) MySQL はエラー、PostgreSQL は気付きにくい 定義を見ないと対応がわからない 手元での一時的な確認向き
INSERT INTO t (a, b) VALUES (...) 影響を受けにくい 対応がひと目でわかる アプリやスクリプトではこちら

INSERT SELECT でも同じです。INSERT INTO b SELECT * FROM a は短く書けますが、どちらかのテーブルに列が増えると対応がずれるので、列リストと SELECT の列を両方とも書き出すと安定します。

5つのDBMSの構文早見表

MySQL と PostgreSQL は上で実行した結果、SQLite は 3.45.1(Python の sqlite3 モジュール)で実行して確かめた結果です。SQL Server と Oracle は手元で実行しておらず、公式ドキュメントで確認した内容です。

やりたいこと MySQL 8.0 PostgreSQL 16 SQLite 3.45 SQL Server Oracle
複数行VALUES ○ ○ ○ ○(1文で1,000行まで) 23ai 以降で○。それより前は INSERT ALL
INSERT SELECT ○ ○ ○ ○ ○
全列を初期値で () VALUES () DEFAULT VALUES DEFAULT VALUES DEFAULT VALUES VALUES (DEFAULT, ...)
登録した行を返す LAST_INSERT_ID() RETURNING RETURNING OUTPUT inserted.列 RETURNING ... INTO(PL/SQL の変数へ)
バルクロード LOAD DATA COPY .import(sqlite3 コマンド) BULK INSERT、bcp SQL*Loader
重複時の動き ON DUPLICATE KEY UPDATE ON CONFLICT ON CONFLICT MERGE MERGE

Oracle の INSERT ALL(公式ドキュメントで確認・未実行)

Oracle Database では、23ai より前の版は VALUES に複数の括弧を並べる書き方に対応していません。代わりに INSERT ALL を使います。

-- Oracle(23ai より前でも使える書き方)
INSERT ALL
  INTO items (name, price) VALUES ('ペン', 120)
  INTO items (name, price) VALUES ('消しゴム', 80)
  INTO items (name, price) VALUES ('定規', 150)
SELECT * FROM dual;

INSERT ALL は本来、1つの SELECT の結果を複数のテーブルへ振り分けるための構文です。最後の SELECT * FROM dual は「1行を返す SELECT」で、これに対して各 INTO が1回ずつ実行されます。Oracle Database 23ai 以降では、ほかのDBと同じ複数行VALUESの書き方も使えます。

SQL Server の OUTPUT(公式ドキュメントで確認・未実行)

-- SQL Server
INSERT INTO items (name, price)
OUTPUT inserted.id, inserted.name
VALUES (N'ペン', 120), (N'定規', 150);

SQL Server の複数行VALUESは、1文で指定できる行数が1,000行までです。それを超えるときは文を分けるか、INSERT SELECT やバルクロードを使います。

どの方法を選ぶかの判断表

状況 選ぶ方法
1件だけ登録する 1行の INSERT(列リストつき)
数行〜数万行をアプリから登録する 複数行VALUES を500〜1,000行ずつ
既存の「1行INSERTが並んだSQLファイル」を速くしたい 先頭に BEGIN;、末尾に COMMIT; を足す
DB内の別テーブルからコピー・加工する INSERT SELECT
CSV など手元のファイルから大量に登録する バルクロード(LOAD DATA/COPY)
登録した行の連番がすぐ必要 PostgreSQL は RETURNING、MySQL は LAST_INSERT_ID()
重複していたら更新したい UPSERT(ON CONFLICT/ON DUPLICATE KEY UPDATE)

よくある質問

Q. 複数行VALUESは、1文で何行まで書けますか? A. MySQL と PostgreSQL には行数そのものの上限はなく、MySQL は max_allowed_packet(8.0 の初期値は64MB)という文の大きさの上限、PostgreSQL はプレースホルダを使う場合のパラメータ数の上限が関係します。SQL Server は1文で1,000行までです。どのDBでも、500〜1,000行ずつに区切っておくと扱いやすくなります。

Q. INSERT SELECT で、同じテーブルから同じテーブルへ登録できますか? A. できます。INSERT INTO items (name, price) SELECT name, price FROM items; のように書くと、SELECT した時点の行が複製されます。主キーが連番なら、列リストから主キーを外して新しい番号を振らせます。

Q. 大量に登録するとき、インデックスや外部キーはそのままでよいですか? A. 数万行程度ならそのままで十分な速さが出ます。数百万行を超えるような場合は、登録後にインデックスを作成する、という手順のほうが全体として短くなることがあります。その場合も、最後に制約を付け直して整合性を確かめます。

Q. MySQL 8.4 や 9.7 では INSERT の書き方が変わりますか? A. この記事で扱った INSERT の構文(複数行VALUES、INSERT SELECT、LAST_INSERT_ID()、max_allowed_packet)は、そのまま使えます。実行時間は版や設定で変わるので、速度が大事な処理は使う版で測ってみてください。

まとめ

  • 複数行の登録は VALUES (...), (...), (...) と括弧をカンマで並べます。途中の1行がエラーなら、その文の行はすべて登録されません。
  • INSERT SELECT は VALUES の代わりに SELECT を書くだけです。列の対応は名前ではなく順番で決まります。
  • 速度は「1行ずつ < トランザクション < 複数行VALUES < INSERT SELECT < バルクロード」の順。10万行で、1行ずつと複数行VALUESの差は MySQL で約89倍、PostgreSQL で約44倍でした。
  • 複数行VALUESは500〜1,000行ずつに区切ると、MySQL の max_allowed_packet に当たりにくく、速さも十分です。
  • 登録した連番は、PostgreSQL は RETURNING、MySQL は LAST_INSERT_ID()(複数行なら最初の番号)で受け取ります。
  • 列リストは省略せずに書くと、列が追加されても動き続け、読みやすさも保てます。
  • エラーは「列数」「型」「NOT NULL」「重複キー」の4つに分けて、メッセージから原因を見分けます。

本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。

(記載例)出典:株式会社RJC「SQL INSERT複数行の書き方|INSERT SELECTと速度比較つき」

株式会社RJCでは、INSERT文の書き方やデータ登録の速度の比べ方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。

株式会社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で一緒に開発しながら、 成長を楽しみませんか?