- データベース
PostgreSQL CSVインポート|COPYと\copyの違いとエラー対処
PostgreSQLでCSVを取り込む・書き出すには、SQLの COPY か、psqlのメタコマンド \copy を使います。2つの違いは「CSVファイルをどこで読むか」で、COPY はデータベースサーバーのファイルを、\copy は手元(psqlを動かしているPC)のファイルを扱います。 迷ったら、まず \copy を選ぶと権限エラーに出会いにくくなります。
この記事では、PostgreSQL 16.15で実際に実行したコマンドと結果をそのまま載せています。COPYと\copyの判断表、オプション一覧(PostgreSQL 16の DEFAULT、17の ON_ERROR を含む)、よく出るエラー8種の再現と直し方、さらにPythonとNode.jsから10万行を取り込んだときの INSERTとCOPYの速度差の実測値 までまとめました。
PostgreSQL CSVインポート・エクスポートの早見表
| やりたいこと | コピペ用コマンド(psqlで実行) |
|---|---|
| 手元のCSVを取り込む(ヘッダーあり) | \copy items FROM 'items.csv' WITH (FORMAT csv, HEADER true) |
| 列を指定して取り込む | \copy items(id,name,price) FROM 'items.csv' WITH (FORMAT csv, HEADER true) |
| サーバー上のCSVを取り込む | COPY items FROM '/var/tmp/items.csv' WITH (FORMAT csv, HEADER true); |
| テーブルをCSVに書き出す | \copy items TO 'items_out.csv' WITH (FORMAT csv, HEADER true) |
| クエリ結果をCSVに書き出す | \copy (SELECT id,name FROM items WHERE price >= 150) TO 'out.csv' WITH (FORMAT csv, HEADER true) |
| Shift_JISのCSVを取り込む | \copy items FROM 'sjis.csv' WITH (FORMAT csv, HEADER true, ENCODING 'SJIS') |
| Shift_JISで書き出す | \copy items TO 'out_sjis.csv' WITH (FORMAT csv, HEADER true, ENCODING 'SJIS') |
| タブ区切り(TSV)を取り込む | \copy items FROM 'items.tsv' WITH (FORMAT csv, HEADER true, DELIMITER E'\t') |
| 条件に合う行だけ取り込む | \copy items FROM 'items.csv' WITH (FORMAT csv, HEADER true) WHERE price >= 150 |
| 全項目を引用符で囲んで書き出す | \copy items TO 'out.csv' WITH (FORMAT csv, HEADER true, FORCE_QUOTE *) |
| シェルから1行で実行 | psql -h HOST -U USER -d DB -c "\copy items FROM 'items.csv' WITH (FORMAT csv, HEADER true)" |
| 標準入力から流し込む | cat items.csv | psql -d DB -c "COPY items FROM STDIN WITH (FORMAT csv, HEADER true)" |
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「PostgreSQL CSVインポート|COPYと\copyの違いとエラー対処」
COPYと\copyの違いと選び方
COPY ... FROM 'ファイル名' は、PostgreSQLのサーバープロセスがサーバー側のディスクからファイルを読みます。\copy は、psqlが手元のファイルを読み、中身を COPY ... FROM STDIN としてサーバーへ送ります。 書き出し(TO)も同じ関係です。
判断表:どちらを使うか
| 比べる点 | COPY ... FROM/TO 'file' |
\copy ... FROM/TO 'file' |
|---|---|---|
| ファイルの場所 | DBサーバー側 | psqlを動かしている側 |
| パスの基準 | サーバーから見た絶対パス(TOは絶対パス必須) | psqlのカレントディレクトリからの相対パスも可 |
| ファイルを読む/書くOSユーザー | PostgreSQLのサービスユーザー(例:postgres) | psqlを実行したユーザー |
| 必要なDB権限 | スーパーユーザー、または pg_read_server_files(FROM)/pg_write_server_files(TO) |
テーブルの INSERT(FROM)/SELECT(TO)だけ |
| 書ける場所 | SQLとしてどこからでも(アプリ、関数内など) | psqlのみ(1行で書く必要あり) |
| クラウドのマネージドDB | ファイル指定は基本的に使えない | 使える |
| 速さ | ネットワークを通らない分わずかに速い | 実用上ほぼ同じ(下の実測を参照) |
選び方の目安はシンプルです。 手元のCSVを扱うなら \copy、DBサーバーにログインしてサーバー上のファイルを扱うなら COPY、アプリから扱うなら COPY ... FROM STDIN をドライバ経由で使います(後半のPython・Node.jsの例)。
権限の違いを実行して確認する
一般ユーザー t107_app(テーブルへの SELECT, INSERT, UPDATE, DELETE のみ付与)で試した結果です。
-- 準備(スーパーユーザーで実行)
CREATE TABLE items(
id integer PRIMARY KEY,
name text NOT NULL,
price integer NOT NULL,
stock integer,
note text,
created_at timestamptz DEFAULT now()
);
CREATE ROLE t107_app LOGIN PASSWORD 't107pw';
GRANT SELECT, INSERT, UPDATE, DELETE ON items TO t107_app;
取り込むCSV(items.csv):
id,name,price,stock,note
1,ボールペン,120,50,黒
2,ノート,180,30,"A5,方眼"
3,クリップ,90,,
4,"付箋 ""強粘着""",250,12,
一般ユーザーで COPY を実行すると、次のエラーになります。
t107=> COPY items(id,name,price,stock,note) FROM '/tmp/items.csv' WITH (FORMAT csv, HEADER true);
ERROR: permission denied to COPY from a file
DETAIL: Only roles with privileges of the "pg_read_server_files" role may COPY from a file.
HINT: Anyone can COPY to stdout or from stdin. psql's \copy command also works for anyone.
同じユーザーでも \copy なら成功します。
t107=> \copy items(id,name,price,stock,note) FROM 'items.csv' WITH (FORMAT csv, HEADER true)
COPY 4
t107=> SELECT id, name, price, stock, note, note IS NULL AS note_null FROM items;
id | name | price | stock | note | note_null
----+---------------+-------+-------+---------+-----------
1 | ボールペン | 120 | 50 | 黒 | f
2 | ノート | 180 | 30 | A5,方眼 | f
3 | クリップ | 90 | | | t
4 | 付箋 "強粘着" | 250 | 12 | | t
(4 rows)
カンマを含む値("A5,方眼")や、"" でエスケープした引用符も正しく取り込めています。CSV形式では、引用符で囲まない空欄はNULLになります。
サーバー側の COPY を一般ユーザーに許可するときは、目的のロールだけを付与します。
GRANT pg_read_server_files TO t107_app; -- COPY FROM 'file' を許可
GRANT pg_write_server_files TO t107_app; -- COPY TO 'file' を許可
-- COPY ... PROGRAM(外部コマンド実行)は pg_execute_server_program
これらのロールはサーバー上の任意のファイルを読み書きできる強い権限なので、運用では \copy(または FROM STDIN)で足りるかを先に検討すると安心です。 ロール設計の詳細はロールと権限の記事に譲ります。
CSVを取り込む(インポート)の書き方
ヘッダー付きCSVを取り込む
\copy items(id,name,price,stock,note) FROM 'items.csv' WITH (FORMAT csv, HEADER true)
FORMAT csvを付けないと、既定の text 形式(タブ区切り)として読まれます。 CSVなら必ず付けます。- 列リストを書くと、CSVの列順とテーブルの列順が違っても対応づけられます。 書かない列(上の例では
created_at)には列の既定値が入ります。 \copyは1行で書く必要があります(途中で改行できません)。セミコロンも付けません。
ヘッダーの列名までチェックする(HEADER MATCH)
PostgreSQL 15以降は HEADER match で、ヘッダーの列名と数が列リストと一致するかを検査できます。列のずれによる取り違えを防げます。
t107=> \copy items(id,name,price,stock,note) FROM 'fewcols.csv' WITH (FORMAT csv, HEADER match)
ERROR: wrong number of fields in header line: got 3, expected 5
CONTEXT: COPY items, line 1: "id,name,price"
条件に合う行だけ取り込む(WHERE)
PostgreSQL 12以降、COPY FROM に WHERE を付けられます。WITH (...) の中ではなく、後ろに書きます。
t107=> \copy items(id,name,price,stock,note) FROM 'items.csv' WITH (FORMAT csv, HEADER true) WHERE price >= 150
COPY 2
WITH (..., WHERE price >= 150) と中に書くと ERROR: syntax error at or near ">=" になりました。
既定値を使いたい欄を指定する(DEFAULT、PostgreSQL 16以降)
PostgreSQL 16で追加された DEFAULT オプションは、指定した文字列が来た欄に、列の既定値を入れます。 NULLとは別に「既定値を使う」を表現できます。
-- def.csv
id,name,price,stock,note,created_at
21,クリアファイル,40,200,,\D
t107=> \copy items(id,name,price,stock,note,created_at) FROM 'def.csv' WITH (FORMAT csv, HEADER true, DEFAULT '\D')
COPY 1
t107=> SELECT id, name, created_at IS NOT NULL AS has_created FROM items WHERE id = 21;
id | name | has_created
----+----------------+-------------
21 | クリアファイル | t
(1 row)
created_at に now() の値が入りました。
NULLと空文字を区別して取り込む
CSV形式では「引用符なしの空欄=NULL」「""=空文字」が既定です。NULL オプションや FORCE_NULL / FORCE_NOT_NULL で変えられます。
fn.csv・fnn.csv は、どちらも note 列が1行目 ""、2行目 空欄 のCSVです。
t107=> \copy items(id,name,price,stock,note) FROM 'fn.csv' WITH (FORMAT csv, HEADER true, FORCE_NULL (note))
COPY 2
t107=> \copy items(id,name,price,stock,note) FROM 'fnn.csv' WITH (FORMAT csv, HEADER true, FORCE_NOT_NULL (note))
COPY 2
t107=> SELECT id, note, note IS NULL AS note_null FROM items WHERE id BETWEEN 24 AND 27 ORDER BY id;
id | note | note_null
----+------+-----------
24 | | t
25 | | t
26 | | f
27 | | f
(4 rows)
| CSVの値 | 既定 | FORCE_NULL (列) |
FORCE_NOT_NULL (列) |
|---|---|---|---|
空欄(,,) |
NULL | NULL | 空文字 |
"" |
空文字 | NULL | 空文字 |
NULL 'NULL' を指定すると文字列 NULL をNULLとして読みます。このとき引用符なしの空欄はNULLではなく空文字になる点に注意します(実行して確認済み)。
CSVを書き出す(エクスポート)の書き方
テーブル全体・クエリ結果を書き出す
t107=> \copy (SELECT id,name,price FROM items WHERE price >= 150 ORDER BY id) TO 'expensive.csv' WITH (FORMAT csv, HEADER true)
COPY 2
-- expensive.csv
id,name,price
2,ノート,180
4,"付箋 ""強粘着""",250
クエリ結果を書き出すときは、SELECT文を丸かっこで囲みます。 JOINや集計の結果もそのままCSVにできます。
全項目を引用符で囲む・区切り文字を変える
t107=> \copy items(id,name,price,stock,note) TO 'all.csv' WITH (FORMAT csv, HEADER true, FORCE_QUOTE *)
COPY 4
id,name,price,stock,note
"1","ボールペン","120","50","黒"
"2","ノート","180","30","A5,方眼"
"3","クリップ","90",,
"4","付箋 ""強粘着""","250","12",
NULLは FORCE_QUOTE でも引用符で囲まれず、空欄のまま出力されます。 NULLを目に見える文字にしたいときは NULL '(null)'、区切り文字を変えるときは DELIMITER ';' を加えます。
Shift_JISで書き出す
\copy (SELECT id,name FROM items) TO 'out_sjis.csv' WITH (FORMAT csv, HEADER true, ENCODING 'SJIS') のように ENCODING 'SJIS' を付けると、Shift_JISで出力されます(実行し、iconvで読み戻して確認済み)。Excelで開くときの文字化けの考え方は、CSVの文字化けの記事で詳しく扱っています。
COPYオプション一覧
WITH ( ... ) の中に、カンマ区切りで書きます。
| オプション | 書き方の例 | 方向 | 説明 | 版 |
|---|---|---|---|---|
| FORMAT | FORMAT csv |
両方 | text(既定・タブ区切り)/csv/binary |
- |
| HEADER | HEADER true / HEADER match |
両方(matchはFROM) | 1行目を列名として扱う。matchは列名の一致を検査 | matchは15〜 |
| DELIMITER | DELIMITER ';' / DELIMITER E'\t' |
両方 | 区切り文字(1バイト文字)。csvの既定はカンマ | - |
| NULL | NULL 'NULL' |
両方 | NULLを表す文字列。csvの既定は引用符なしの空欄 | - |
| DEFAULT | DEFAULT '\D' |
FROM | この文字列の欄に列の既定値を入れる | 16〜 |
| QUOTE | QUOTE '''' |
両方(csv) | 引用符の文字。既定は " |
- |
| ESCAPE | ESCAPE '\' |
両方(csv) | 引用符内のエスケープ文字。既定はQUOTEと同じ | - |
| FORCE_QUOTE | FORCE_QUOTE * / FORCE_QUOTE (name) |
TO(csv) | NULL以外を必ず引用符で囲む | - |
| FORCE_NOT_NULL | FORCE_NOT_NULL (note) |
FROM(csv) | 空欄をNULLにせず空文字にする | * 指定は17〜 |
| FORCE_NULL | FORCE_NULL (note) |
FROM(csv) | "" もNULLにする |
9.4〜、* 指定は17〜 |
| ENCODING | ENCODING 'SJIS' |
両方 | ファイルの文字コード。省略時はクライアントの文字コード | - |
| FREEZE | FREEZE true |
FROM | 同じトランザクションで作った/TRUNCATEしたテーブルに凍結済みで書く | - |
| ON_ERROR | ON_ERROR ignore |
FROM(text/csv) | 型変換エラーの行を飛ばして続ける(既定は stop) |
17〜 |
| LOG_VERBOSITY | LOG_VERBOSITY verbose |
FROM | ON_ERROR ignore で飛ばした行の行番号・列名を出す |
17〜 |
| WHERE | ... WITH (...) WHERE price > 0 |
FROM | 条件に合う行だけ取り込む(WITHの外に書く) | 12〜 |
ON_ERROR と LOG_VERBOSITY はPostgreSQL 17の公式ドキュメントで確認した内容です。PostgreSQL 16で ON_ERROR を書くと、次のエラーになります(実行結果)。
ERROR: option "on_error" not recognized
LINE 1: ...,note ) FROM STDIN WITH (FORMAT csv, HEADER true, ON_ERROR i...
PostgreSQL 17以降では、ON_ERROR ignore を付けると型変換できない行を飛ばし、最後に飛ばした件数がNOTICEで表示されます。対象は データ型への変換エラー で、列数の過不足や制約違反(主キー重複など)は対象外です(公式ドキュメントの記述に基づく。PostgreSQL 17では未実行)。
よくあるエラーと直し方(PostgreSQL 16で再現)
| エラー文(先頭) | 主な原因 | 直し方 |
|---|---|---|
permission denied to COPY from a file |
一般ユーザーでサーバー側COPY | \copy に変える/pg_read_server_files を付与 |
could not open file "..." for reading: No such file or directory |
サーバー側にファイルがない | \copy に変える/サーバー上の絶対パスを指定 |
could not open file "..." for reading: Permission denied |
postgresユーザーがファイルを読めない | 読める場所に置き、権限を644に/\copy に変える |
extra data after last expected column |
CSVの列がテーブル(列リスト)より多い | 値のカンマを引用符で囲む/列リストを合わせる |
missing data for column "..." |
CSVの列が足りない | 列リストをCSVの列に合わせる(残りは既定値) |
invalid input syntax for type integer |
型に合わない値(全角数字、BOMなど) | 値を直す/いったんtext列のテーブルに取り込む |
unterminated CSV quoted field |
閉じていない引用符 | 開き引用符を探して閉じる(行番号は後ろにずれることがある) |
invalid byte sequence for encoding "UTF8" |
Shift_JISのCSVをUTF-8として読んだ | ENCODING 'SJIS' を付ける |
duplicate key value violates unique constraint |
主キーが既にある | 先に削除する/一時テーブル経由でUPSERT |
No such file or directory と Permission denied
t107=> COPY items FROM '/tmp/t107_nofile.csv' WITH (FORMAT csv, HEADER true);
ERROR: could not open file "/tmp/t107_nofile.csv" for reading: No such file or directory
HINT: COPY FROM instructs the PostgreSQL server process to read a file. You may want a client-side facility such as psql's \copy.
t107=> COPY items(id,name,price,stock,note) FROM '/home/me/work/items.csv' WITH (FORMAT csv, HEADER true);
ERROR: could not open file "/home/me/work/items.csv" for reading: Permission denied
HINT: (上と同じ)
(2つ目は実際には作業ディレクトリのパスで再現し、表示ではパスを短くしています。)
どちらもHINTのとおり、ファイルを読んでいるのは「サーバープロセス(postgresユーザー)」です。 手元のファイルなら \copy に変えれば解決します。サーバー上のファイルを使うなら、postgresユーザーが親ディレクトリを辿れて、ファイルを読める(例:chmod 644)場所に置きます。
\copy でファイルがないときは、エラーの形が変わります(psqlが出すため)。
t107=> \copy items FROM 'nofile.csv' WITH (FORMAT csv)
nofile.csv: No such file or directory
extra data after last expected column
値の中にカンマがあるのに引用符で囲まれていないと、列が1つ増えたと解釈されます。
-- extra.csv(直す前)
id,name,price,stock,note
10,定規,150,5,30cm,透明
ERROR: extra data after last expected column
CONTEXT: COPY items, line 2: "10,定規,150,5,30cm,透明"
直し方:値を引用符で囲みます(10,定規,150,5,"30cm,透明")。CSVを作る側で、カンマ・改行・引用符を含む値を囲む設定にするのが確実です。CONTEXT に行番号が出るので、大きなファイルでも場所をすぐ特定できます。
invalid input syntax for type integer
ERROR: invalid input syntax for type integer: "三百"
CONTEXT: COPY items, line 2, column price: "三百"
行番号と列名まで表示されます。 値の揺れが多いCSVは、いったん全列 text の作業用テーブルに取り込み、WHERE price !~ '^[0-9]+$' のようにSQLで不正な行を確認してから、price::int で変換して本テーブルへ入れると扱いやすくなります(一時テーブルの作り方は下の主キー重複の例と同じです)。
Shift_JISのCSV:invalid byte sequence for encoding "UTF8"
t107=> \copy items(id,name,price,stock,note) FROM 'sjis.csv' WITH (FORMAT csv, HEADER true)
ERROR: invalid byte sequence for encoding "UTF8": 0x95
CONTEXT: COPY items, line 2
t107=> \copy items(id,name,price,stock,note) FROM 'sjis.csv' WITH (FORMAT csv, HEADER true, ENCODING 'SJIS')
COPY 1
ENCODING 'SJIS' を付けるだけで取り込めます。 PostgreSQLの SJIS はWindowsのCP932相当(WIN932 は別名)で、①や髙を含むCP932のCSVも ENCODING 'SJIS' で取り込めました(実行して確認)。SET client_encoding TO 'SJIS'; でも同じ効果ですが、セッション全体に影響するので、ENCODING オプションのほうが範囲を限定できます。
BOM付きUTF-8のCSV
ExcelでUTF-8 CSVとして保存すると、先頭にBOM(3バイトの目印 EF BB BF)が付きます。
| 状況 | 結果(実行) |
|---|---|
HEADER true で取り込む |
成功(1行目ごと読み飛ばすので影響なし) |
HEADER match で取り込む |
column name mismatch in header line field 1: got "id", expected "id" |
| ヘッダーなしのCSV | invalid input syntax for type integer: "20" |
ERROR: invalid input syntax for type integer: "20"
CONTEXT: COPY items, line 1, column id: "20"
表示上は "20" に見えますが、先頭に見えないBOMが付いています。 取り込む前にBOMを取り除きます。
# 1行目先頭のBOMだけを削除(GNU sed)
sed -i '1s/^\xEF\xBB\xBF//' bom_nohead.csv
t107=> \copy items(id,name,price,stock,note) FROM 'bom_nohead.csv' WITH (FORMAT csv)
COPY 1
duplicate key value violates unique constraint
ERROR: duplicate key value violates unique constraint "items_pkey"
DETAIL: Key (id)=(1) already exists.
CONTEXT: COPY items, line 2
COPYは1件でもエラーになると全体が取り消され、1行も入りません。 再取り込みなら先に対象を削除するか、一時テーブルに取り込んでから INSERT ... ON CONFLICT で入れます(UPSERTの詳細は別記事で扱っています)。
CREATE TEMP TABLE items_tmp (LIKE items INCLUDING DEFAULTS);
\copy items_tmp(id,name,price,stock,note) FROM 'items.csv' WITH (FORMAT csv, HEADER true)
INSERT INTO items SELECT * FROM items_tmp
ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, price = EXCLUDED.price;
なお、serial や IDENTITY の列にCSVで番号を直接入れたあとは、シーケンスを setval で合わせておくと、次のINSERTで番号が重なりません。
PythonとNode.jsからCOPYで取り込む
アプリから大量のCSVを取り込むときも、ドライバのCOPY機能(COPY ... FROM STDIN)を使えます。ファイルはアプリ側で読むので、サーバー側の権限は不要です。
検証環境:PostgreSQL 16.15、Python 3.14.7 + psycopg 3.3.6、Node.js 22.22.2 + pg 8.23.1 + pg-copy-streams 7.0.0。同じマシン上で接続(127.0.0.1)。
テーブルとデータ(10万行、約3.9MB):
CREATE TABLE orders(id integer PRIMARY KEY, item_name text, qty integer, ordered_at timestamp);
id,item_name,qty,ordered_at
1,商品1,2,2026-01-01 00:01:00
2,商品2,3,2026-01-01 00:02:00
...
Python(psycopg 3)
pip install "psycopg[binary]"
import csv, time
import psycopg
DSN = "host=127.0.0.1 dbname=t107 user=t107_app password=t107pw"
CSV_PATH = "orders.csv"
def read_rows():
with open(CSV_PATH, encoding="utf-8", newline="") as f:
r = csv.reader(f)
next(r) # ヘッダーを飛ばす
return list(r)
def insert_loop(conn, rows):
with conn.cursor() as cur:
for row in rows:
cur.execute("INSERT INTO orders VALUES (%s, %s, %s, %s)", row)
def insert_many(conn, rows):
with conn.cursor() as cur:
cur.executemany("INSERT INTO orders VALUES (%s, %s, %s, %s)", rows)
def copy_file(conn, rows=None):
# CSVファイルをそのままサーバーへ流し込む(\copy と同じ仕組み)
with conn.cursor() as cur, open(CSV_PATH, "rb") as f:
with cur.copy("COPY orders FROM STDIN WITH (FORMAT csv, HEADER true)") as cp:
while data := f.read(1024 * 64):
cp.write(data)
def copy_rows(conn, rows):
# Pythonで加工した行を1行ずつ送る
with conn.cursor() as cur:
with cur.copy("COPY orders (id, item_name, qty, ordered_at) FROM STDIN") as cp:
for row in rows:
cp.write_row(row)
rows = read_rows()
for name, fn in [("INSERTを1行ずつ", insert_loop), ("executemany", insert_many),
("copy(ファイル)", copy_file), ("copy(write_row)", copy_rows)]:
with psycopg.connect(DSN) as conn:
conn.execute("TRUNCATE orders")
t = time.perf_counter()
fn(conn, rows)
conn.commit()
n = conn.execute("SELECT count(*) FROM orders").fetchone()[0]
print(f"{name:<16} {n}件 {time.perf_counter() - t:6.2f}秒")
実行結果(1回目。2回目は下の比較表に記載):
INSERTを1行ずつ 100000件 10.12秒
executemany 100000件 2.68秒
copy(ファイル) 100000件 0.40秒
copy(write_row) 100000件 0.44秒
write_row() は値をPythonのリストで渡すので、取り込み前に値を加工・検証したいときに便利です。 write_row() を使う場合はCSV形式ではなく既定の text 形式で送られ、エスケープはpsycopgが行います。
Node.js(pg + pg-copy-streams)
npm install pg pg-copy-streams
// load_node.mjs
import fs from "node:fs";
import { pipeline } from "node:stream/promises";
import pg from "pg";
import { from as copyFrom } from "pg-copy-streams";
const client = new pg.Client({
host: "127.0.0.1", database: "t107", user: "t107_app", password: "t107pw",
});
await client.connect();
// COPY FROM STDIN(pg-copy-streams)
async function copyStream() {
const ingest = client.query(copyFrom("COPY orders FROM STDIN WITH (FORMAT csv, HEADER true)"));
await pipeline(fs.createReadStream("orders.csv"), ingest);
console.log(" rowCount:", ingest.rowCount);
}
// 比較用の「1行ずつINSERT」関数(client.queryをループ)は省略
await client.query("TRUNCATE orders");
const t = performance.now();
await copyStream();
console.log(`pg-copy-streams ${((performance.now() - t) / 1000).toFixed(2)}秒`);
await client.end();
実行結果(1行ずつINSERTする関数を加えて同時に計測した1回目):
INSERTを1行ずつ 12.32秒
rowCount: 100000
pg-copy-streams 0.27秒
ストリームで流すので、ファイル全体をメモリに読み込まずに済みます。 大きなCSVでもメモリ使用量が増えにくい書き方です。
速度比較のまとめ(10万行)
| 方法 | 時間(2回の実測) | 1行ずつINSERTとの比 |
|---|---|---|
| Python:INSERTを1行ずつ(1トランザクション) | 10.12秒 / 12.52秒 | 1倍 |
| Python:executemany | 2.68秒 / 2.58秒 | 約4〜5倍速い |
| Python:copy(ファイルを流す) | 0.40秒 / 0.37秒 | 約25〜34倍速い |
| Python:copy(write_row) | 0.44秒 / 0.41秒 | 約23〜30倍速い |
| Node.js:INSERTを1行ずつ | 12.32秒 / 5.88秒 | 1倍 |
| Node.js:pg-copy-streams | 0.27秒 / 0.20秒 | 約29〜46倍速い |
psql:\copy |
0.33秒 | - |
psql:サーバー側 COPY |
0.31秒 | - |
同じマシン内で接続した結果で、共有環境のためINSERTの時間には揺れがあります。ネットワーク越しの接続では1行ごとの往復が増えるため、差はさらに開く傾向があります。 また、\copy とサーバー側 COPY の差は今回の条件ではごくわずかでした。
よくある質問
Q. Dockerやクラウドのマネージドデータベースでも使えますか?
A. 手元のファイルを \copy で送る方法なら使えます。サーバー側の COPY ... FROM 'ファイル' は、マネージドサービスではサーバーのファイルシステムに触れないため基本的に使えません。Dockerのコンテナ内で COPY を使う場合は、ファイルをコンテナ内(ボリューム)に置き、コンテナ内のパスを指定します。
Q. エラーの行だけ飛ばして取り込めますか?
A. PostgreSQL 17以降は ON_ERROR ignore で型変換エラーの行を飛ばせます。16以前では、全列 text の一時テーブルに取り込み、SQLで正しい行だけを本テーブルへ入れる方法が確実です。
Q. Windowsのpsqlで \copy のパスはどう書きますか?
A. \copy items FROM 'C:/work/items.csv' WITH (FORMAT csv, HEADER true) のようにスラッシュ区切りで書き、シングルクォートで囲みます。
まとめ
COPYはサーバー側のファイル、\copyは手元のファイルを扱う。 迷ったら\copy- 一般ユーザーの
COPY ... 'ファイル'にはpg_read_server_files/pg_write_server_filesが必要 - CSVでは
FORMAT csvを必ず付ける。 列リストで列順のずれを吸収できる - エラーの
CONTEXTに行番号・列名が出るので、そこから直す - Shift_JISは
ENCODING 'SJIS'、BOMはHEADER trueなら影響なし、ヘッダーなしなら削除 - PostgreSQL 16の
DEFAULT、17のON_ERRORで取り込みの幅が広がる - アプリからはドライバのCOPYを使うと、10万行で1行ずつINSERTの20倍以上速い
本記事の内容は、ご自由にお使いください。
ご利用の際は、出典として本ページへのリンクを記載いただけますようお願いします。
(記載例)出典:株式会社RJC「PostgreSQL CSVインポート|COPYと\copyの違いとエラー対処」
株式会社RJCでは、データベースの扱い方を仲間と学び合っています。こうした知識を共有しながら、日々技術を磨いています。
株式会社RJC ― SI事業・SES事業・AI駆動開発。RJCは一緒に成長を楽しめる会社です。
WE ARE HIRING
RJCで一緒に開発しながら、
成長を楽しみませんか?
RJCは、Web・モバイル・AIを活用した開発プロジェクトで、テックリードやPM・PMOも活躍するシステム開発会社です。会社を知る、待遇を確かめる、話を聞いてみる。気になるところ見てみてください!
- 127日年間休日
- 12時間平均残業時間
- 毎日ガチャ遊びココロも大切にする福利厚生。アマギフなどの賞品ラインナップ!
ほかにも、チケットレストラン、書籍読み放題、2年ごとの慰労報奨(休暇 or 金一封)、11期連続の黒字決算。