- SQL
SQL INSERT複数行の書き方|INSERT SELECTと速度比較つき
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期連続の黒字決算。