結論

バルク(bulk)とは「大量」「ひとまとめ」という意味で、IT では大量のデータを1件ずつではなく、まとめて処理することを指します。代表がバルクインサート(複数の行を1回でまとめて登録すること)で、手元の計測では10万行の登録が、1行ずつ毎回コミットする書き方より PostgreSQL で約130倍、MariaDB で約180倍速くなりました。

この記事でわかること

  • バルクのふだんの意味と IT の意味(バルク処理・バルクインサート・バルク品・バルクメール)の違い
  • PostgreSQL・MariaDB・SQLite で10万行を5つの方法で入れた実測と、速くなる理由
  • コミットの単位・1文の行数の上限・途中で失敗したときの挙動を、エラー原文つきで

2026年10月29日更新|PostgreSQL 16.15・MariaDB 10.11.14・SQLite 3.53.1で実行確認

この記事で実際に動かしたもの:作業用のフォルダーに PostgreSQL 16 と MariaDB 10.11 を 127.0.0.1 で起動し、Python 3.13 から SQLite を含む3つのデータベースへ、同じ注文データ(1万行・2万行・10万行)を「1行ずつ」「まとめて」の5つの方法で登録して、時間・コミットの回数・ログの量・失敗時に残る行数を測りました。

「バルク」は、物流・化粧品・パソコンの部品・メール配信・データベースと、分野ごとに少しずつ違う意味で使われます。共通する芯は「小分けにせず、大きな単位でまとめて扱う」です。IT での使われ方を先に図にします。

flowchart TD
    B[バルク<br>大量・ひとまとめ] --> P[バルク処理]
    B --> O[バルク品<br>バルクメール]
    P --> I[バルク<br>インサート]
    P --> A[バルクAPI]
    I --> L[COPY<br>LOAD DATA]

よく調べられることの答えを先にまとめます。

知りたいこと 答え 詳しい節
バルクとは(意味) 英語の bulk。「大量」「かさ」「ひとまとめ」。IT では大量データをまとめて扱うこと バルクとは
バルク処理とは 多数の件数を1件ずつではなく、まとめた単位で一度に処理すること バルク処理とは
バルクインサートとは 複数の行を1回の SQL 文(や1回の命令)でまとめてテーブルに登録すること バルクインサートとは
バルクインサートはどれだけ速い? 10万行で、1行ずつ毎回コミットの33〜107秒に対し、まとめると0.07〜0.3秒 3つのDBで実測
何行ずつまとめればよい? 手元では1,000行前後で頭打ち。SQLite は8,192行×4列で上限エラー 1文に入れる行数と上限
途中で1行失敗したら? 1つのトランザクションなら全部戻る(0行)。区切ってコミットすると一部だけ残る 途中で失敗したとき
バルク品とは パソコンの部品などで、箱・説明書を省いてまとめて流通する商品 場面別のバルク
バルクメールとは 同じ内容を多数の宛先へ一斉に送るメール。Gmail は1日5,000件超の送信者に追加の要件 場面別のバルク

確認環境:Ubuntu 24.04.5 LTS(2 CPU・メモリ 8GB・ext4 の仮想ディスク。fsync 1回あたり約0.25ミリ秒を pg_test_fsync で計測)の上で、PostgreSQL 16.15(Ubuntu のパッケージ。作業用フォルダーに initdb して 127.0.0.1 のポートで起動、WAL は実ディスクに配置)、MariaDB 10.11.14(Ubuntu の .deb を作業用フォルダーに展開して起動、REDO ログは実ディスクに配置、innodb_flush_log_at_trx_commit=1)、SQLite 3.53.1(Python 3.13.18 の sqlite3 モジュール、実ディスク上のファイル)を使いました。ドライバーは psycopg 3.3.6 と PyMySQL 2.2.8 です。時間は各方法3回の中央値で、1行ずつ毎回コミットする10万行の計測だけは1回です。同じマシンで他の作業も動いているため、数値は2〜5割ほどぶれます。比べるときは桁の違いを見てください。SQL Server の BULK INSERT・Oracle の FORALL・Elasticsearch の Bulk API・Gmail の送信者ガイドラインは、各社の公式ドキュメントで確かめた範囲だけを書いています(確認日 2026年10月29日)。

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

(記載例)出典:株式会社RJC「バルクとは?意味とバルクインサートの速さを3つのDBで実測」

バルクとは(ふだんの意味とITでの意味)

この節では、「バルク」という言葉の元の意味と、ふだんの会話・業界・IT での意味の違いが分かります。

バルクは英語の bulk で、「大きさ・かさ」「大量」「(包装しない)ばら積み」という意味の言葉です。動詞としては「まとめる・ひとまとめにする」の意味もあり、in bulk で「大量に・まとめて」になります。日本語の「バルク」は、この「大量に・まとめて」の意味で、業界ごとに少し違うものを指します。

場面 「バルク」が指すもの 例
ふだんの意味 大量・まとめ買い・量り売り バルク買い、バルク食品(量り売りのナッツなど)
物流 包装せずにそのまま積む貨物 穀物・石炭・鉱石を運ぶバルクキャリア(ばら積み船)
化粧品・食品の製造 容器に詰める前の中身 化粧水のバルク(充填前の液)
ガス 据え置き型の大きなタンク LP ガスのバルク貯槽
パソコンの部品 箱・説明書を省いて流通する商品 メモリ・SSD・CPU クーラーのバルク品
IT(データ) 大量のデータをまとめて処理すること バルク処理・バルクインサート・バルクロード
IT(通信・API) 複数の操作を1回の通信でまとめて送ること Bulk API、USB のバルク転送
メール 同じ内容を多数の宛先へ一斉に送るメール メールマガジン・お知らせの一斉配信

どの場面でも「1つずつ包む・1件ずつ扱う」の反対で、小分けの手間を省いて、大きな単位で効率よく扱うという考え方は同じです。物流のバルク貨物が「梱包の手間と費用を省く」のと、データベースのバルクインサートが「1件ごとの往復と確定(コミット)の手間を省く」のは、同じ発想です。

同じ1万件でも、扱い方で手間が変わる

1件ずつ1件ごとに送る・解析する・確定する。手間が件数の分だけ繰り返される
↔
バルクまとめて送り、まとめて確定する。繰り返しの手間が1回分(または数回分)で済む

見分ける問い:「1件ごとに同じ準備と後片付けをしていないか」

場面別の「バルク」:バルク品・バルクメール・バルクAPI

この節では、IT の周りで出会う「バルク品」「バルクメール」「バルクAPI」「バルク転送」が、それぞれ何を指すのかが分かります。

バルク品(パソコンの部品)

バルク品は、メーカーの化粧箱・説明書・付属品を省き、まとめた単位で流通する商品です。もともとはパソコンメーカーなどが大量に仕入れる組み込み用の部品で、店頭では箱入りの「リテール品」と並んで売られます。中身の部品は同じでも、次の点が違うことがあります。

  • 保証:メーカー保証が無く、販売店の保証だけのことがある(期間も短いことが多い)。
  • 付属品:ネジ・ケーブル・ドライバーのディスク・説明書が付かないことがある。
  • サポート:メーカーの問い合わせ窓口の対象外のことがある。

保証の条件は販売店ごとに違うため、販売ページで確かめるのが確実です。

バルクメール(一斉配信のメール)

バルクメールは、同じ内容のメールを多数の宛先へ一斉に送ることや、そのメールを指す言葉です。メールマガジン、会員へのお知らせ、通知メールの一斉配信などが当たります。受け取る側の迷惑メールの判定でも「一斉配信らしさ」は判断材料の1つで、迷惑メールの意味で bulk mail と呼ぶこともあります。

大量に送る側には、受け取る側のサービスが決まりを設けています。Google の「メール送信者のガイドライン」では、Gmail の個人アカウントへ1日5,000件を超えるメールを送る送信者に、SPF・DKIM・DMARC の設定、迷惑メール率を0.3%未満に保つこと、マーケティングのメールでワンクリックの登録解除を用意すること、などを求めています(Google 公式ヘルプ、2026年10月29日時点)。この記事はデータベースが主題なので、配信の手順や設定には立ち入りません。

バルクAPI・バルク転送

Web の API でも「複数の操作を1回のリクエストにまとめる」機能を Bulk API と呼ぶことがあります。たとえば Elasticsearch の Bulk API は、公式ドキュメントで「複数の index・create・delete・update の操作を1回のリクエストで実行する」と説明され、本文は1行に1つの JSON を並べる NDJSON という形です。最適な大きさについては「設定を変えて試し、自分の負荷に合う大きさを見つける」よう案内されています。USB の通信方式にも、大量のデータを送るための「バルク転送」という名前の方式があります。

バルク処理とは(バッチ処理との違い)

この節では、バルク処理の意味と、よく混同されるバッチ処理・ストリーム処理との違いが分かります。

バルク処理とは、多数の件数を1件ずつではなく、まとめた単位で一度に処理することです。データベースへの大量登録・大量更新・大量削除、ファイルからの一括取り込み、API への一括送信などが当たります。

似た言葉の「バッチ処理」は、いつ処理するか(夜間などに、ためておいたデータを決まった時刻にまとめて流す)の言葉です。バルク処理はどう処理するか(1件ずつではなく塊で)の言葉です。夜間バッチの中で、データベースにはバルクインサートで書き込む、というように両方が一緒に使われることがよくあります。

言葉 何についての言葉か 反対の言葉 例
バルク処理 処理の単位(まとめて扱う) 1件ずつの処理(row by row) 10万行を COPY で一度に登録
バッチ処理 処理のタイミング(ためて決まった時刻に) オンライン処理・リアルタイム処理 毎晩2時に売上を集計
ストリーム処理 データが届くたびに流れるように処理 バッチ処理 アクセスログを数秒ごとに集計
ミニバッチ 小さな塊に分けて順に処理 全件を一度に 1,000行ずつ区切ってコミット

バルクインサートとは(書き方の4つの型)

この節では、バルクインサートの意味と、データベースごとの代表的な書き方が分かります。

バルクインサートとは、複数の行を1回の SQL 文、または1回の命令でまとめてテーブルに登録することです。1行ずつ INSERT 文を送るのに比べ、送受信・文の解析・確定(コミット)の回数が減るため速くなります。書き方は大きく4つの型に分かれます。

1

複数行 VALUES

INSERT … VALUES (…),(…),(…)。1つの文に何行も並べる。多くの DB で使える。

SQL だけで書ける

2

executemany・バッチ

1行用の文に、値の組をまとめて渡す。ドライバーが中でまとめ方を工夫する。

プログラムから使う

3

専用の一括読み込み

PostgreSQL の COPY、MySQL・MariaDB の LOAD DATA、SQL Server の BULK INSERT。

いちばん速い

4

INSERT … SELECT

別のテーブルの検索結果をそのまま登録する。データが DB の中にあるときに使う。

通信が発生しない

計測で使った注文テーブルと、それぞれの型の書き方です。まず、複数行 VALUES の SQL です。

CREATE TABLE orders(
  id         bigserial PRIMARY KEY,
  user_id    int       NOT NULL,
  item       text      NOT NULL,
  price      int       NOT NULL,
  created_at timestamp NOT NULL
);

-- 1行ずつ(これを10万回送ると遅い)
INSERT INTO orders(user_id, item, price, created_at) VALUES (1, 'item-00', 100, '2026-10-01 00:00:00');

-- 複数行 VALUES(バルクインサート)
INSERT INTO orders(user_id, item, price, created_at) VALUES
  (1, 'item-00', 100, '2026-10-01 00:00:00'),
  (2, 'item-01', 137, '2026-10-01 00:00:01'),
  (3, 'item-02', 174, '2026-10-01 00:00:02');

Python から3つのデータベースへ入れたときのコードです(計測に使ったものから要点だけを抜き出しました)。

import psycopg, pymysql, sqlite3, csv

rows = [(1, "item-00", 100, "2026-10-01 00:00:00"), ...]  # 10万行

# PostgreSQL:COPY(psycopg 3)
with psycopg.connect("host=127.0.0.1 port=55212 dbname=t212db user=postgres") as conn:
    with conn.cursor().copy("COPY orders(user_id,item,price,created_at) FROM STDIN") as cp:
        for r in rows:
            cp.write_row(r)
    # with を抜けるとコミットされる

# MariaDB / MySQL:executemany(PyMySQL は複数行 VALUES に書き換えて送る)
conn = pymysql.connect(host="127.0.0.1", port=33212, user="root", database="t212db")
with conn.cursor() as cur:
    cur.executemany(
        "INSERT INTO orders(user_id,item,price,created_at) VALUES (%s,%s,%s,%s)", rows)
conn.commit()

# SQLite:1つのトランザクションで executemany
conn = sqlite3.connect("bench.db")
with conn:  # 抜けるときにコミット
    conn.executemany(
        "INSERT INTO orders(user_id,item,price,created_at) VALUES (?,?,?,?)", rows)

MariaDB の LOAD DATA は、CSV ファイルを読み込ませる形です。手元の PC のファイルを送るときは LOCAL を付け、サーバーとクライアントの両方で許可(サーバーの local_infile、PyMySQL の local_infile=True)が要ります。

LOAD DATA LOCAL INFILE '/dev/shm/t212/tmp/orders.csv'
INTO TABLE orders
FIELDS TERMINATED BY ','
(user_id, item, price, created_at);

他のデータベース・サービスのバルク機能

名前と書き方はデータベースごとに違います。手元で動かしていないものは、公式ドキュメントで確かめた範囲です。

製品 複数行の登録 ファイルからの一括読み込み 確かめ方
PostgreSQL 複数行 VALUES COPY(サーバー側のファイル)・psql の \copy(手元のファイル) 実行
MySQL・MariaDB 複数行 VALUES LOAD DATA [LOCAL] INFILE 実行(MariaDB)
SQLite 複数行 VALUES・executemany sqlite3 コマンドの .import 実行(.import は公式)
SQL Server 複数行 VALUES BULK INSERT・bcp コマンド 公式ドキュメント
Oracle Database PL/SQL の FORALL・INSERT ALL SQL*Loader・外部表 公式ドキュメント
DynamoDB BatchWriteItem(1回25件まで) S3 からのインポート 別記事で実行

SQL Server の公式ドキュメントは、BULK INSERT を「ユーザー指定の形式でデータベース テーブルまたはビューにデータ ファイルをインポートします」と説明しています。Oracle の FORALL は「1つの DML 文を、VALUES 句と WHERE 句に異なる値を与えて複数回実行する」文で、1件ずつではなくまとめて SQL へ送る仕組みです。

複数行 VALUES の書き方の細部(INSERT SELECT・DEFAULT・RETURNING)は、関連記事「SQL INSERT複数行の書き方|INSERT SELECTと速度比較つき」で扱っています。この記事は「バルクとは何か」と、まとめ方の単位・失敗・ロックの判断に絞ります。

バルクインサートの速さを3つのDBで実測(10万行)

この節では、同じ10万行を5つの方法で入れたときに、どれだけ時間が違うのかが分かります。

1行ずつ毎回コミットする書き方は、PostgreSQL で33秒、MariaDB で55秒、SQLite で107秒かかりました。まとめて入れると、どれも0.3秒以下でした。方法は次の5つです。

  • A:1行ずつ INSERT し、1行ごとにコミット(自動コミット)
  • B:1行ずつ INSERT するが、全体を1つのトランザクションにまとめる
  • C:ドライバーの executemany に全行を渡す(1つのトランザクション)
  • D:複数行 VALUES で1,000行ずつ(1つのトランザクション)
  • E:専用の一括読み込み(PostgreSQL は COPY、MariaDB は LOAD DATA LOCAL INFILE)
方法(10万行) PostgreSQL 16 MariaDB 10.11 SQLite 3.53
A 1行ずつ・毎回コミット 32.917秒 55.127秒 106.721秒
B 1行ずつ・1トランザクション 6.680秒 6.228秒 0.120秒
C executemany 0.830秒 0.655秒 0.111秒
D 複数行 VALUES(1,000行ずつ) 1.193秒 0.647秒 0.073秒
E COPY / LOAD DATA 0.247秒 0.305秒 (なし)

1万行でも傾向は同じでした(PostgreSQL は A 3.285秒→E 0.024秒、MariaDB は A 3.079秒→E 0.037秒、SQLite は A 9.225秒→D 0.008秒)。計測に使ったスクリプトの出力の一部です。

pg E n=100000 median=0.247s runs=[0.239, 0.299, 0.247] rows/s=404,151
my E n=100000 median=0.305s runs=[0.317, 0.296, 0.305] rows/s=327,876
sq D n=100000 median=0.073s runs=[0.111, 0.068, 0.073] rows/s=1,375,498
pg A n=100000 median=32.917s runs=[32.917] rows/s=3,038
my A n=100000 median=55.127s runs=[55.127] rows/s=1,814
sq A n=100000 median=106.721s runs=[106.721] rows/s=937
133倍PostgreSQL:A→COPY32.917秒→0.247秒(10万行)
181倍MariaDB:A→LOAD DATA55.127秒→0.305秒(10万行)
889倍SQLite:A→Bトランザクションでくくるだけ

読み取れることは3つです。

  • いちばん大きく効くのは「毎回コミットをやめること」(A→B)。SQLite は B だけで107秒が0.12秒になりました。
  • サーバー型の DB(PostgreSQL・MariaDB)は、さらに「往復の回数を減らすこと」(B→C・D・E)が効きます。B のままでは6秒台でした。
  • 専用の一括読み込み(COPY・LOAD DATA)が最速。PostgreSQL の公式ドキュメントも「一連の INSERT の代わりに COPY で全行を1つの命令で読み込む」ことを勧めています。MySQL の公式マニュアルは、LOAD DATA は「通常、INSERT 文より20倍速い」と書いています。

なお、PostgreSQL では D(複数行 VALUES)が C(executemany)より遅い回がありました。psycopg 3.1 以降の executemany は、公式ドキュメントによると内部で「パイプラインモード」(応答を待たずに次の文を送る仕組み)を使います。PostgreSQL へ大量に入れるなら、複数行 VALUES の文を組み立てるより、COPY か executemany を使うほうが手軽で速いという結果でした。

なぜバルクにすると速くなるのか(往復・解析・コミット)

この節では、1行ずつの登録で時間がかかる3つの原因と、バルクインサートがそれぞれをどう減らすのかが分かります。

1行の INSERT には、行を書く本体の仕事の前後に「送る・解析する・確定する」の3つの手間が付きます。10万行を1行ずつ送ると、この手間が10万回繰り返されます。

1

送る(ネットワークの往復)

アプリが文を送り、DB が「終わった」と返すまで待つ。127.0.0.1 でも1往復に数十マイクロ秒かかり、別のサーバーなら1ミリ秒前後かかることもある。

B→C・D・E で減る

2

解析する(SQL の解釈と計画)

DB は文を読み、どう実行するかを決める。複数行 VALUES は1文の解析で多くの行を扱え、COPY は SQL の解析そのものがほぼ無い。

D・E で減る

3

確定する(コミットとディスクへの書き出し)

コミットのたびに、DB は変更の記録(PostgreSQL の WAL、InnoDB の REDO ログ、SQLite のジャーナル)をディスクへ確実に書き出す(fsync)。これがいちばん重い。

A→B で減る

SQLite で A が特に遅いのは、サーバーを持たない代わりに、コミットのたびにジャーナルファイルとデータベースファイルの両方を fsync するためです。1行あたり約1ミリ秒(106.721秒÷10万行)で、手元の fsync 1回(約0.25ミリ秒)の数回分にあたります。反対に、通信が無いので B 以降はサーバー型より1桁速くなりました。

行を書く本体の仕事(ページへの書き込み・インデックスの更新)は、どの方法でも同じだけ発生します。バルクインサートが減らすのは、本体の前後に付く「繰り返しの手間」で、本体の仕事そのものではありません。インデックスが多いテーブルでは、本体の仕事の割合が大きくなります(後の節で計測します)。

トランザクションの単位:何行ごとにコミットするか

この節では、コミットの間隔を変えると時間がどう変わるかと、どこで頭打ちになるかが分かります。

1行ずつの INSERT のまま、コミットの間隔だけを1行・10行・100行・1,000行・全部と変えて2万行を入れました。100〜1,000行ごとのコミットで、全部を1回にした場合とほぼ同じ速さになりました。

コミットの間隔(2万行) コミット回数 PostgreSQL MariaDB SQLite
1行ごと 20,000回 11.178秒 6.236秒 21.515秒
10行ごと 2,000回 2.120秒 2.188秒 1.877秒
100行ごと 200回 1.595秒 1.398秒 0.221秒
1,000行ごと 20回 1.143秒 1.151秒 0.046秒
全部で1回 1回 1.062秒 1.154秒 0.027秒

この表の PostgreSQL の「1行ごと」(2万行で11.178秒)は、前の節の A(自動コミット、2万行で5.364秒)より遅くなっています。psycopg で明示的に commit() を呼ぶと、1行ごとに「BEGIN・INSERT・COMMIT」の3往復になるためです。自動コミットなら1往復で済みます。「1行ずつコミット」をするなら、明示的なトランザクションより自動コミットのほうが速いという、少し意外な結果です。

では、いつも「全部で1回」がよいかというと、そうとは限りません。1つのトランザクションが大きいほど、次のことが起きます。

  • 失敗したときに全部が取り消され、最初からやり直しになる(次の節)。
  • ロックを持つ時間が長くなり、他の処理を待たせることがある(その次の節)。
  • 取り消し用の記録(PostgreSQL の以前の版の行、InnoDB の undo ログ)が溜まる。手元の MariaDB では、何度も大量登録を繰り返すうちに、取り消し用の記録が入る ibdata1 が 12MB から 76MB まで増えました。

トランザクションの境界を業務の単位でどう決めるか、10万件の更新を一括と1,000件ごとで比べた結果は、関連記事「トランザクション設計の考え方|境界の決め方と長い処理の分割手順」で扱っています。

1文に入れる行数(バッチサイズ)と上限エラー

この節では、複数行 VALUES で1文に何行まとめると速いのかと、まとめすぎたときに出るエラーが分かります。

1つのトランザクションの中で、1文に入れる行数(バッチサイズ)を10行から20,000行まで変えて10万行を入れました。速さは100〜1,000行あたりで頭打ちになり、それより大きくしても速くならず、DB ごとの上限でエラーになりました。

1文の行数(10万行) PostgreSQL MariaDB SQLite
10行 1.402秒 1.336秒 0.083秒
100行 1.485秒 0.725秒 0.064秒
1,000行 1.379秒 0.550秒 0.062秒
5,000行 1.375秒 0.762秒 0.076秒
8,191行 1.520秒 0.606秒 0.063秒
8,192行 1.455秒 0.520秒 エラー
16,383行 1.793秒 0.667秒 エラー
16,384行 エラー 0.617秒 エラー

エラーになった境目は、どちらも「1文に入れられるパラメーター(? や %s の数)の上限」でした。1行に4列なので、行数×4がパラメーターの数です。

sq batch=8192 ERROR sqlite3.OperationalError: too many SQL variables
pg batch=16384 ERROR psycopg.OperationalError: sending query and params failed: number of parameters must be between 0 and 65535
  • SQLite:8,191行×4列=32,764 は通り、8,192行×4列=32,768 で失敗しました。SQLite の公式の「Limits」のページでは、パラメーターの最大数 SQLITE_MAX_VARIABLE_NUMBER の既定値は、3.32.0(2020年5月)より前は999、それ以降は32,766です。
  • PostgreSQL:16,383行×4列=65,532 は通り、16,384行×4列=65,536 で失敗しました。PostgreSQL の通信の決まり(プロトコル)では、パラメーターの数を16ビットの数で送るため、65,535 個までです。
  • MariaDB:PyMySQL はパラメーターを手元で SQL 文に埋め込んでから送るため、数の上限は無く、代わりに「1回に送れる大きさ」の上限 max_allowed_packet に当たります(次の小見出し)。

MariaDB の max_allowed_packet を超えたとき

上限を 1MB に下げ、5万行(2,184,437バイト)を1文で送りました。アプリ側には「サーバーがいなくなった」というエラーだけが返り、本当の理由はサーバーのエラーログにだけ残りました。

OperationalError: (2006, "MySQL server has gone away (ConnectionResetError(104, 'Connection reset by peer'))")

(MariaDB のエラーログ)
[Warning] Aborted connection 153 to db: 't212db' user: 'root' host: '127.0.0.1' (Got a packet bigger than 'max_allowed_packet' bytes)

「MySQL server has gone away」はネットワークの切断やタイムアウトでも出る文言なので、バルクインサートの最中に出たら、まず1文の大きさを疑うと早く見つかります。MariaDB 10.11 の既定値は 16MB(16,777,216)でした。上限を上げるより、1文を数百〜数千行に区切るほうが、メモリの使い方も安定します。

  1. 1まず1,000行ずつで始める手元の3つの DB では、ここでほぼ頭打ちだった
  2. 2行数×列数がパラメーターの上限を超えないか見るSQLite 32,766・PostgreSQL 65,535
  3. 31文のバイト数が max_allowed_packet を超えないか見る長い文字列や JSON の列があると早く当たる
  4. 4自分のデータで数通り測って決める列の数・行の大きさ・インデックスで最適な値は変わる

バルクインサートが途中で失敗したらどうなるか

この節では、1,000行のうち1行だけが制約に違反したときに、何行が残るのかが分かります。

551行目の item を空(NULL)にして、NOT NULL の制約に違反させました。全部を1つのトランザクションにすると0行、100行ごとにコミットすると900行が残りました。3つの DB で結果は同じでした。

やり方(1,000行・100行ずつ) 残った行 エラーの原文
PostgreSQL・1トランザクション 0行 NotNullViolation: null value in column "item" of relation "orders" violates not-null constraint
PostgreSQL・100行ごとにコミット 900行 同上
MariaDB・1トランザクション 0行 IntegrityError: (1048, "Column 'item' cannot be null")
MariaDB・100行ごとにコミット 900行 同上
SQLite・1トランザクション 0行 IntegrityError: NOT NULL constraint failed: orders.item
SQLite・100行ごとにコミット 900行 同上

100行ごとにコミットした場合、失敗した文(501〜600行目)の100行がまるごと抜け、その前後は登録されました。「どの範囲が抜けたか」を記録しておかないと、後から足りない行を特定できません。1つの文の中では、1行でも違反があれば、その文の全行が登録されない点も確かめられました。

PostgreSQL では、トランザクションの中でエラーが起きると、ロールバックするまで次の文を受け付けません。エラーを無視して続けようとしたときの原文です。

psycopg.errors.InFailedSqlTransaction: current transaction is aborted, commands ignored until end of transaction block

1つのトランザクションにまとめる

1行でも失敗すると全部取り消し(0行)

中途半端な状態が残らない

直したら、最初から入れ直せばよい

区切ってコミットする

失敗した塊だけが抜ける(900行)

ロックが短く、やり直しの量が少ない

抜けた範囲の記録と、入れ直しの仕組みが要る

SQL Server の BULK INSERT にも同じ考え方の設定があり、公式ドキュメントでは BATCHSIZE について「それぞれのバッチは、1 回のトランザクションでサーバーにコピーされます」「既定では、指定のデータ ファイル内にあるすべてのデータが 1 つのバッチになります」と説明されています。

重複する行を飛ばす(ON CONFLICT・INSERT IGNORE)

同じ id を2回含む1,001行を1文で入れると、3つの DB とも0行で失敗しました。重複を飛ばす書き方にすると、1,000行が登録されました。

DB 重複を飛ばす書き方 結果
PostgreSQL INSERT … ON CONFLICT (id) DO NOTHING 1,000行。何も知らせずに飛ばす
MariaDB INSERT IGNORE … 1,000行。警告 1062 Duplicate entry '500' for key 'PRIMARY'
SQLite INSERT OR IGNORE … 1,000行。何も知らせずに飛ばす

MariaDB の INSERT IGNORE は、重複だけでなく型の変換などのエラーも警告に変えて登録を続けます。SHOW WARNINGS を見ないと、何が飛ばされたか分かりません。PostgreSQL で重複時に上書きする ON CONFLICT … DO UPDATE は、1つの文の中に同じキーが2回あると次のエラーになりました。

CardinalityViolation: ON CONFLICT DO UPDATE command cannot affect row a second time
HINT:  Ensure that no rows proposed for insertion within the same command have duplicate constrained values.

バルクで入れる前に、アプリ側で同じキーを1つにまとめておくと避けられます。

投入中のロックと他の処理への影響

この節では、大量登録の途中(コミット前)に、別の接続から読んだり書いたりすると何が起きるかが分かります。

PostgreSQL は、コミット前の10万行を別の接続から読むと0行に見え、待たされませんでした。SQLite は、未コミットの行が多いと、別の接続からは読むことも書くこともできませんでした。

状態(コミット前) 別接続から SELECT 別接続から INSERT など
PostgreSQL・COPY で10万行 すぐ返る(count は 0) INSERT はすぐ通る。TRUNCATE はロック待ち
SQLite(既定)・未コミット1,000行 すぐ返る(count は 0) database is locked
SQLite(既定)・未コミット10万行 database is locked database is locked
SQLite(WAL モード)・未コミット10万行 すぐ返る(count は 0) database is locked

PostgreSQL では、COPY 中のテーブルに付くロックは RowExclusiveLock で、普通の INSERT・UPDATE と同じ強さです。テーブルを丸ごと消す TRUNCATE だけがぶつかり、待ち時間の上限(lock_timeout)を1秒にすると次の原文で止まりました。

別接続の count(*): 0
ロック: [('RowExclusiveLock', True)]
別接続の TRUNCATE: LockNotAvailable: canceling statement due to lock timeout
コミット後の count(*): 100000

SQLite(既定のジャーナルモード)では、未コミットの変更がメモリのキャッシュ(既定で約2MB)に収まるうちは、他の接続も読めます。収まらなくなると、SQLite は変更をファイルへ書き出すために、ファイル全体の排他ロックを取ります。そのため10万行のときだけ、読み取りも次の原文で失敗しました(待ち時間を0秒にして確かめています)。

journal_mode=delete 未コミット100000行 別接続のSELECT: OperationalError: database is locked
journal_mode=wal 未コミット100000行 別接続のSELECT: OK (0,)

PRAGMA journal_mode=WAL にすると、書き込みの最中でも他の接続は読めました。ただし書き込みは、どのモードでも同時に1つだけです。SQLite で大きなバルクインサートをするときは、他の処理が止まる時間を見込んでおくか、区切ってコミットします。

インデックス・WAL・ログ:大量投入で増えるもの

この節では、バルクインサートで速さを左右するインデックスと、ディスクに書かれる記録の量が分かります。

インデックスは後から作るほうが速い

インデックスを3つ付けたテーブルに COPY で10万行を入れると0.523秒、インデックス無しで入れてから3つ作ると、作成まで含めて0.324秒でした。

やり方(PostgreSQL・COPY 10万行) 投入 インデックス作成まで含む合計
インデックス3つを先に作っておく 0.523秒 0.523秒
投入してからインデックス3つを作る 0.186秒 0.324秒
インデックス無し(主キーだけ) 0.230秒 0.230秒

PostgreSQL の公式ドキュメント「データベースへのデータ投入」でも、新しく作ったテーブルなら「テーブルを作り、COPY でデータを入れ、それから必要なインデックスを作る」のが最速と書かれています。一方で、運用中のテーブルでインデックスを消すと、その間の検索が遅くなり、一意のインデックスなら重複を防げなくなります。消してよいのは、空のテーブルへの初回の投入や、他から使われていない作業用のテーブルのときです。

WAL(変更の記録)の量

PostgreSQL は、変更をデータファイルに書く前に WAL という記録に書きます。2万行を入れる前後で WAL の位置の差を測りました。

方法(PostgreSQL・2万行) 時間 WAL の量
A 1行ずつ・毎回コミット 5.364秒 3.8MB
B 1行ずつ・1トランザクション 1.469秒 3.0MB
C executemany 0.240秒 3.0MB
D 複数行 VALUES 0.266秒 3.0MB
E COPY 0.045秒 2.1MB

WAL の量は、方法による差より「行の量」でほぼ決まりました(毎回コミットはコミットの記録が増え、COPY は記録がまとまって少なくなります)。速さの差は WAL の量ではなく、WAL をディスクへ確定させる回数の差だと分かります。大量投入では WAL が溜まってチェックポイント(データファイルへの書き出し)が頻繁に起きるため、公式ドキュメントは投入中だけ max_wal_size を一時的に大きくすることも挙げています。

  • 1新しいテーブルなら、インデックスと外部キーは投入の後に作る手元では投入+作成で 0.523秒→0.324秒
  • 2投入の後に ANALYZE を流す統計情報が投入前のままだと、検索の計画が外れる。公式ドキュメントも強く勧めている
  • 3ディスクの空きを見ておくWAL・REDO ログ・取り消し用の記録が、データ本体とは別に増える
  • 4レプリカやバックアップへの影響を確かめる大量の WAL がレプリカへ送られ、遅れが出ることがある

バルクインサートでよくあるエラー・つまずき

この節では、この記事の計測で実際に出たエラーの原文と、原因・直し方が一覧で分かります。

エラーの原文 DB 原因 直し方
too many SQL variables SQLite 1文のパラメーターが32,766を超えた 行数×列数が上限内に収まるよう区切る
number of parameters must be between 0 and 65535 PostgreSQL 1文のパラメーターが65,535を超えた 区切る、または COPY を使う
(2006, "MySQL server has gone away …") MariaDB 1文が max_allowed_packet より大きい(サーバーのログに Got a packet bigger than) 1文を小さく区切る。サーバーのエラーログを見る
current transaction is aborted, commands ignored until end of transaction block PostgreSQL トランザクション中のエラーの後に、ロールバックせず次の文を送った エラーを受けたら rollback() してからやり直す
ON CONFLICT DO UPDATE command cannot affect row a second time PostgreSQL 1文の中に同じキーが2回ある 送る前にキーを1つにまとめる
database is locked SQLite 別の接続が書き込み中(大きい変更だと読み取りも) 区切ってコミットする、WAL モード、timeout を設定する
canceling statement due to lock timeout PostgreSQL 投入中のテーブルに TRUNCATE などの強いロックを求めた 投入が終わってから実行する
(1062, "Duplicate entry '500' for key 'PRIMARY'") MariaDB 主キーの重複。1文の全行が登録されない 重複を除く、または INSERT IGNORE・ON DUPLICATE KEY UPDATE

エラーにならないつまずきもあります。

やってはいけない → 代わりに

やってはいけない値を文字列の連結で SQL に埋め込み、複数行 VALUES を組み立てる
→
代わりにプレースホルダー(? や %s)とドライバーの executemany・COPY を使う。引用符を含む値も正しく扱われる

「速くするために自分で SQL を組み立てる」必要は、ほとんどの場合ない

やってはいけない → 代わりに

やってはいけない区切ってコミットし、失敗した塊を握りつぶして先へ進む
→
代わりに失敗した塊の範囲(何行目から何行目か)とエラーをログに残し、件数を最後に照らし合わせる

手元の例では、黙っていると1,000行のうち100行が抜けたまま終わった

CSV ファイルを PostgreSQL・MySQL に取り込むときのエラー(文字コード・区切り・権限)は、関連記事「PostgreSQL CSVインポート|COPYと\copyの違いとエラー対処」と関連記事「MySQL CSVインポート|LOAD DATA INFILEとエラー解決」で扱っています。この記事の COPY・LOAD DATA は速さの比較のために使い、オプションやエラー対処はそちらに任せます。

判断表:どのバルクインサートを選ぶか

この節では、データのある場所・量・失敗したときの扱いから、どの方法を選べばよいかが分かります。

状況 選ぶ方法 理由(手元の計測)
数十〜数百行を画面の操作で登録 executemany か複数行 VALUES を1トランザクションで この量なら差はミリ秒単位。書きやすさを優先
数万〜数百万行を CSV から(PostgreSQL) COPY・\copy 10万行 0.247秒で最速
数万〜数百万行を CSV から(MySQL・MariaDB) LOAD DATA [LOCAL] INFILE 10万行 0.305秒で最速
プログラムの中で作ったデータ(MySQL・MariaDB) PyMySQL などの executemany 複数行 VALUES に書き換えて送り、0.655秒
プログラムの中で作ったデータ(SQLite) BEGIN〜COMMIT でくくって executemany くくるだけで107秒→0.1秒
全部入るか、全部入らないかにしたい 1つのトランザクション 1行の違反で0行に戻った
他の処理を長く止めたくない・途中から再開したい 1,000行前後ずつ区切ってコミット+範囲の記録 1,000行ごとで全部1回とほぼ同じ速さ
データがすでに同じ DB の別のテーブルにある INSERT … SELECT アプリとの往復が発生しない
DynamoDB などの NoSQL・クラウドのサービス 各サービスのバッチ API(件数の上限と再送の仕組みに従う) 1回の件数と失敗時の扱いがサービスごとに違う

DynamoDB の一括登録(BatchWriteItem の25件の上限、未処理の項目の再送、トランザクションとの違い)は、関連記事「DynamoDB一括登録とトランザクション|BatchWriteItemとの違い」で扱っています。

区切ってコミットするときの骨組みです。失敗した範囲を記録し、最後に件数を照らし合わせます。

import logging

def bulk_insert(conn, rows, batch=1000):
    sql = "INSERT INTO orders(user_id,item,price,created_at) VALUES (%s,%s,%s,%s)"
    ok, failed = 0, []
    for start in range(0, len(rows), batch):
        chunk = rows[start:start + batch]
        try:
            with conn.cursor() as cur:
                cur.executemany(sql, chunk)
            conn.commit()
            ok += len(chunk)
        except Exception as e:
            conn.rollback()
            failed.append((start + 1, start + len(chunk)))
            logging.error("rows %d-%d failed: %s", start + 1, start + len(chunk), e)
    logging.info("inserted=%d failed_ranges=%s total=%d", ok, failed, len(rows))
    return ok, failed

バルクに関するよくある質問

この節では、バルクとバルクインサートについて、よく聞かれる質問に短く答えます。

Q. 「バルク」と「一括」は同じ意味ですか?
ほぼ同じです。IT では「一括登録」「一括更新」を英語風に「バルクインサート」「バルクアップデート」と呼びます。物流や化粧品の「バルク」は「包装しない・容器に詰める前」の意味が強く、「一括」とは少しずれます。

Q. バルクインサートとバッチインサートは違いますか?
多くの場合、同じものを指します。ドライバーやフレームワーク(JDBC の executeBatch など)では「バッチ」、データベースの機能名(SQL Server の BULK INSERT など)では「バルク」と呼ぶ傾向があります。

Q. 何行からバルクインサートにすべきですか?
決まった境目はありません。手元の計測では、1万行でも1行ずつ毎回コミットは3〜9秒、まとめると0.01〜0.04秒でした。数百行を超えてループで INSERT しているなら、まとめる価値があります。

Q. 1文に何行まとめるのがよいですか?
手元の3つの DB では、1,000行前後で速さが頭打ちになりました。それより大きくすると、SQLite は8,192行×4列で、PostgreSQL は16,384行×4列で上限エラーになりました。

Q. バルクインサートの途中で失敗すると、データはどうなりますか?
1つのトランザクションなら全部取り消されます(手元では0行)。区切ってコミットしていれば、失敗した塊だけが抜けます(手元では1,000行中900行が残りました)。

Q. ORM(Django・Rails・Spring Data など)でもバルクインサートできますか?
多くの ORM に、まとめて登録する機能があります(Django の bulk_create、Rails の insert_all など)。1件ずつ save を呼ぶ書き方とは、送られる SQL が違います。発行される SQL をログで確かめると、まとめて送られているかが分かります。

Q. バルクインサートでは自動採番の id やトリガーはどうなりますか?
自動採番は通常どおり振られます。PostgreSQL では RETURNING で登録した行の id を受け取れます。トリガーは行ごとに動くため、トリガーや外部キーが多いテーブルでは、まとめても速くなりにくくなります。

Q. バルクメールと迷惑メールは同じですか?
同じではありません。バルクメールは「一斉配信」という送り方の言葉で、受け取る人が登録したメールマガジンも含みます。ただし大量に送る送信者には、Gmail などが認証・登録解除・迷惑メール率の要件を設けています。

まとめ

  • バルク(bulk)とは「大量・ひとまとめ」の意味で、IT では大量のデータを1件ずつではなく、まとめて処理することを指す。物流・化粧品・パソコンの部品(バルク品)・メール(バルクメール)でも、芯は「小分けにせず大きな単位で扱う」。
  • バルクインサートは、複数行 VALUES・executemany・COPY / LOAD DATA などで複数行をまとめて登録すること。10万行で、1行ずつ毎回コミットの33〜107秒が、0.07〜0.3秒になった。
  • 最大の効果は「毎回コミットをやめる」こと、次が「往復の回数を減らす」こと。最速は COPY・LOAD DATA。PostgreSQL では複数行 VALUES より COPY か executemany が手軽で速かった。
  • 1文の行数は1,000行前後で頭打ち。行数×列数がパラメーターの上限(SQLite 32,766・PostgreSQL 65,535)や max_allowed_packet を超えると、エラーや「server has gone away」になる。
  • 1つのトランザクションは「全部か無か」、区切ったコミットは「失敗した塊だけ抜ける」。区切るなら範囲を記録し、件数を照らし合わせる。SQLite は大きな未コミットで読み取りまで止まる点に注意する。

参考・出典

確認日はいずれも 2026年10月29日です。

  • PostgreSQL 16「14.4. Populating a Database」:https://www.postgresql.org/docs/16/populate.html
  • PostgreSQL 16「COPY」:https://www.postgresql.org/docs/16/sql-copy.html
  • PostgreSQL 16「55.7. Message Formats」(Bind のパラメーター数):https://www.postgresql.org/docs/16/protocol-message-formats.html
  • MySQL 8.4 Reference Manual「Optimizing INSERT Statements」:https://dev.mysql.com/doc/refman/8.4/en/insert-optimization.html
  • MariaDB Documentation「LOAD DATA INFILE」:https://mariadb.com/kb/en/load-data-infile/
  • MariaDB Documentation「max_allowed_packet」(Server System Variables):https://mariadb.com/kb/en/server-system-variables/#max_allowed_packet
  • SQLite「Limits In SQLite」:https://www.sqlite.org/limits.html
  • SQLite「Write-Ahead Logging」:https://www.sqlite.org/wal.html
  • psycopg 3「Pipeline mode support」:https://www.psycopg.org/psycopg3/docs/advanced/pipeline.html
  • psycopg 3「Using COPY TO and COPY FROM」:https://www.psycopg.org/psycopg3/docs/basic/copy.html
  • PyMySQL「Cursor Objects」(executemany):https://pymysql.readthedocs.io/en/latest/modules/cursors.html
  • Microsoft Learn「BULK INSERT (Transact-SQL)」:https://learn.microsoft.com/ja-jp/sql/t-sql/statements/bulk-insert-transact-sql
  • Oracle「FORALL Statement」(PL/SQL Language Reference 19c):https://docs.oracle.com/en/database/oracle/oracle-database/19/lnpls/FORALL-statement.html
  • Elastic「Bulk index or delete documents」(Elasticsearch API):https://www.elastic.co/docs/api/doc/elasticsearch/operation/operation-bulk
  • Google Workspace 管理者ヘルプ「メール送信者のガイドライン」:https://support.google.com/a/answer/81126?hl=ja

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

(記載例)出典:株式会社RJC「バルクとは?意味とバルクインサートの速さを3つのDBで実測」

株式会社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分・履歴書不要です。まずは話を聞いてみたい、という方でも歓迎です! エントリーフォームへ

成長のヒミツ、 ぜんぶ丸わかり