0Pricing
PostgreSQL Performance & Query Optimization · บทเรียน

อัตราการส่งผ่านของ COPY เทียบกับ INSERT หลายแถว

ทดสอบประสิทธิภาพและเลือกวิธีนำเข้าข้อมูลที่เพิ่มจำนวนแถวต่อวินาทีภายใต้ข้อจำกัดจริงให้สูงสุด

อัตราการส่งผ่านของ COPY เทียบกับ INSERT หลายแถว เป็นบทเรียน PostgreSQL Performance & Query Optimization ฟรีบน CoddyKit นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน คุณสามารถอ่านบทเรียนทั้งหมดด้านล่างฟรี — จากนั้นลองปฏิบัติด้วยตัวคุณเองในเบราว์เซอร์พร้อมตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7 บทเรียนนี้เป็นส่วนหนึ่งของเส้นทางการเรียน PostgreSQL Performance & Query Optimization และความก้าวหน้าของคุณจะซิงค์ข้ามเว็บและแอป CoddyKit คอร์ส PostgreSQL Performance & Query Optimization มีบทเรียนทั้งหมด 4 บทเรียน

บางส่วนของบทเรียนนี้ยังไม่ได้รับการแปล และแสดงเป็นภาษาอังกฤษ

Why Ingestion Speed Matters

When you load millions of rows into PostgreSQL, the method you choose determines whether the job takes seconds or hours. This lesson benchmarks the two main ingestion paths: COPY and multi-row INSERT.

  • COPY streams rows through a single, optimized bulk path.
  • Multi-row INSERT packs many tuples into one statement to amortize round-trips.

The right choice depends on data source, round-trip cost, and how the rows arrive.

The Naive Baseline: Single-Row INSERT

The slowest pattern is one row per statement. Each statement pays for parsing, planning, a network round-trip, and (without batching) a separate commit.

At thousands of rows, the per-statement overhead dominates and throughput collapses. This is the baseline every other method beats.

-- Slow: one round-trip and (by default) one commit per row
INSERT INTO events (user_id, kind, payload) VALUES (1, 'click', '{}');
INSERT INTO events (user_id, kind, payload) VALUES (2, 'view',  '{}');
INSERT INTO events (user_id, kind, payload) VALUES (3, 'click', '{}');
-- ... repeated 1,000,000 times

Multi-Row INSERT: Amortizing Round-Trips

A multi-row INSERT lists many tuples in one statement. You pay parse and plan cost once, send one network round-trip, and commit the whole batch together.

  • A good batch size is typically 500 to 5,000 rows per statement.
  • Going much higher gives diminishing returns and bloats the parsed statement.
-- One statement, one round-trip, many rows
INSERT INTO events (user_id, kind, payload) VALUES
  (1, 'click', '{}'),
  (2, 'view',  '{}'),
  (3, 'click', '{}'),
  (4, 'view',  '{}');
-- typically 500-5000 tuples per statement

COPY: The Bulk Highway

COPY is PostgreSQL's purpose-built bulk loader. It bypasses per-row statement parsing entirely and streams rows through a tight loop, making it usually the fastest way to ingest large volumes.

  • COPY ... FROM loads data into a table.
  • It reads text, CSV, or binary formats.

Server-side COPY FROM 'file' requires superuser or the pg_read_server_files role; clients usually use \copy instead.

-- Server-side COPY from a CSV file (needs file access privileges)
COPY events (user_id, kind, payload)
FROM '/data/events.csv'
WITH (FORMAT csv, HEADER true);

Client-Side \copy and COPY FROM STDIN

When the file lives on the client (not the server), use psql's \copy meta-command or COPY ... FROM STDIN. These stream data over the existing client connection, so no special server file privileges are needed.

Most ETL drivers (psycopg, JDBC, libpq) expose a streaming COPY FROM STDIN API that is the fastest programmatic load path.

-- psql meta-command: file is read on the CLIENT machine
\copy events (user_id, kind, payload) FROM 'events.csv' WITH (FORMAT csv, HEADER true)

-- Equivalent SQL that streams from the client connection
COPY events (user_id, kind, payload) FROM STDIN WITH (FORMAT csv);

Why COPY Wins: Less Per-Row Work

The throughput gap comes from what each row costs:

  • Single INSERT: parse + plan + execute + round-trip + commit per row.
  • Multi-row INSERT: parse + plan once per batch; still builds a full parse tree for every tuple.
  • COPY: no SQL parsing per row at all; values are decoded directly into tuples.

COPY also generates fewer WAL records per row of work, which is a large part of its speed advantage.

Benchmarking Fairly

To compare methods honestly, hold everything else constant and measure wall-clock time plus rows per second. Use \timing in psql, or wrap loads in a timed harness.

  • Load the same dataset each run.
  • TRUNCATE between runs so you start from an empty, comparable state.
  • Run each method a few times and take the median to dampen noise.
\timing on

TRUNCATE events;
-- run method A (multi-row INSERT batches), note the time

TRUNCATE events;
-- run method B (COPY FROM), note the time

-- rows_per_second = row_count / elapsed_seconds

Transactions and Commit Costs

A common reason single-row INSERTs are slow is one commit per row. Each commit forces a WAL flush (an fsync) to disk. Wrapping many inserts in a single transaction collapses thousands of fsyncs into one.

Both COPY and multi-row INSERT already commit per-statement, but if you script many statements, wrap them in one explicit transaction.

BEGIN;
INSERT INTO events (user_id, kind) VALUES (1, 'click');
INSERT INTO events (user_id, kind) VALUES (2, 'view');
-- ... many statements, ONE fsync at the end
COMMIT;

Indexes, Triggers, and Constraints Slow Loads

The fastest ingestion method still crawls if every inserted row must update five indexes and fire triggers. A classic bulk-load tactic is to load first, then build indexes.

  • Drop or disable non-essential indexes, then recreate them after the load.
  • Building an index once over the full table is far cheaper than maintaining it row by row.
  • Disable expensive triggers during the load when the data is trusted.
-- Bulk-load pattern: strip overhead, load, then rebuild
DROP INDEX IF EXISTS idx_events_user_id;

COPY events (user_id, kind, payload)
FROM '/data/events.csv' WITH (FORMAT csv, HEADER true);

CREATE INDEX idx_events_user_id ON events (user_id);

UNLOGGED Tables and Staging

For transient staging data, an UNLOGGED table skips WAL writes entirely, which can dramatically speed up loads. The trade-off: unlogged tables are not crash-safe and are truncated after a crash.

A robust ETL pattern loads into an unlogged or temporary staging table with COPY, transforms it, then inserts the clean result into the durable target.

-- Fast, non-durable staging area for ETL
CREATE UNLOGGED TABLE events_staging (
  user_id integer,
  kind    text,
  payload jsonb
);

COPY events_staging FROM '/data/events.csv' WITH (FORMAT csv, HEADER true);

INSERT INTO events SELECT * FROM events_staging WHERE kind IS NOT NULL;

Choosing Under Real Constraints

Pick the method that fits how the data arrives:

  • Bulk file or stream available? Use COPY / \copy — it is the throughput winner.
  • Rows arrive programmatically in code? Prefer your driver's COPY FROM STDIN; if unavailable, fall back to multi-row INSERT in batches of 500-5,000.
  • Need per-row conflict handling (ON CONFLICT)? COPY can't do that — use multi-row INSERT, or COPY to a staging table then UPSERT.
-- COPY has no ON CONFLICT; stage then upsert when you need it
COPY events_staging FROM STDIN WITH (FORMAT csv);

INSERT INTO events AS e (user_id, kind, payload)
SELECT user_id, kind, payload FROM events_staging
ON CONFLICT (user_id, kind) DO UPDATE
  SET payload = EXCLUDED.payload;

Quick Check: Maximize Throughput

Test your understanding of the core ingestion decision.

Recap

You can now benchmark and choose ingestion methods deliberately:

  • COPY / \copy is the throughput winner for bulk file and streaming loads; it skips per-row parsing and minimizes WAL.
  • Multi-row INSERT (500-5,000 rows per statement) beats single-row inserts and is the fallback when you need ON CONFLICT.
  • Wrap loads in one transaction to avoid per-row fsync costs.
  • Drop indexes, disable triggers, use UNLOGGED staging tables to remove per-row overhead, then rebuild.
  • Benchmark fairly: same data, TRUNCATE between runs, \timing, take the median rows/second.

คำถามที่พบบ่อย

บทเรียน “อัตราการส่งผ่านของ COPY เทียบกับ INSERT หลายแถว” ฟรีหรือไม่

ใช่ — ข้อความเต็มของ “อัตราการส่งผ่านของ COPY เทียบกับ INSERT หลายแถว” ฟรีให้อ่านที่นี่บนเว็บ เพื่อปฏิบัติแบบโต้ตอบ (ตัวแก้ไขโค้ดในตัวและติวเตอร์ AI ตลอด 24/7) และปลดล็อคส่วนที่เหลือของคอร์ส PostgreSQL Performance & Query Optimization ให้อัปเกรดเป็น CoddyKit PRO คอร์ส PostgreSQL Performance & Query Optimization มีบทเรียนทั้งหมด 4 บทเรียน

คุณจะเรียนรู้อะไรในบทเรียน “อัตราการส่งผ่านของ COPY เทียบกับ INSERT หลายแถว”

ทดสอบประสิทธิภาพและเลือกวิธีนำเข้าข้อมูลที่เพิ่มจำนวนแถวต่อวินาทีภายใต้ข้อจำกัดจริงให้สูงสุด คุณปฏิบัติ PostgreSQL Performance & Query Optimization ด้วยโค้ดที่ใช้งานได้จริงที่คุณเรียกใช้โดยตรงในเบราว์เซอร์ และติวเตอร์ AI ตลอด 24/7 ตอบคำถามของคุณขณะที่คุณไปผ่านบทเรียน

คุณต้องมีประสบการณ์ก่อนที่จะเริ่มเรียน PostgreSQL Performance & Query Optimization หรือไม่

ไม่จำเป็นต้องมีประสบการณ์มาก่อน PostgreSQL Performance & Query Optimization บน CoddyKit ออกแบบมาสำหรับผู้เริ่มต้นไปจนถึงผู้เรียนขั้นสูง คุณสามารถเริ่มต้นที่นี่หรือเริ่มจากตัวแรกและเรียนด้วยความเร็วของคุณเอง นี่คือบทเรียนที่ 1 จากทั้งหมด 4 บทเรียน

บทเรียน “อัตราการส่งผ่านของ COPY เทียบกับ INSERT หลายแถว” ใช้เวลานานแค่ไหน

บทเรียน CoddyKit ส่วนใหญ่ใช้เวลาประมาณ 5–10 นาที แต่ละบทเรียนจึงสั้นและเป็นแบบโต้ตอบ คุณสามารถก้าวหน้าอย่างต่อเนื่องและกลับมาเรียนต่อจากตรงที่เพิ่งหยุดบนเว็บและแอปได้เลย

ฉันเขียนและรันโค้ดในบทเรียน PostgreSQL Performance & Query Optimization นี้ได้ไหม

ได้ บทเรียน PostgreSQL Performance & Query Optimization ทุกบทมีตัวแก้ไขโค้ดในตัว คุณจึงเขียนและรันโค้ดจริงได้เลยในเบราว์เซอร์ และได้รับข้อเสนอแนะจาก AI ในทันที — ไม่ต้องติดตั้งในเครื่องของคุณ

บทเรียนทั้งหมดในหลักสูตรนี้

  1. อัตราการส่งผ่านของ COPY เทียบกับ INSERT หลายแถว
  2. การเลื่อนการสร้างดัชนีและข้อจำกัดระหว่างการโหลด
  3. การปรับ WAL และจุดตรวจสอบสำหรับการนำเข้าข้อมูล
  4. การทำ Upsert ขนาดใหญ่ด้วย ON CONFLICT
← กลับไปที่ PostgreSQL Performance & Query Optimization