Skip to content

PG17:批量加载性能 ​

批量加载数据——CSV 导入、异构库迁移、日志灌入、报表回填——是数据工程里最常见的高吞吐场景,也是最考验数据库 I/O 栈的场景。PostgreSQL 17 在这一版里对「把大量数据搬进库、搬给客户端」的整条链路做了系统性增强:

  • COPY ... ON_ERROR ignore(新增于 PG17):遇到坏行不再整批失败,跳过并继续,配合 LOG_VERBOSITY(新增于 PG17)报告被跳过的行,让「脏数据加载」从「改数据重跑」变成「加载后核对跳过清单」;
  • pg_stat_progress_copy.tuples_skipped(新增于 PG17):跳过行数进入进度视图,长任务可观测;
  • 流式 I/O(streaming I/O / read_stream API):PG17 重构了顺序读取路径,顺序扫描、ANALYZE 等操作的读取吞吐明显提升,配套的新 GUC io_combine_limit 让文件系统读可以组合成更大的批次;
  • 大数据块传输改进:服务器向客户端传输大数据块的路径优化,COPY TO 类工作负载受益。

本页先讲清这些增强的用法与适用边界,再用一个「千万行 CSV 实战」把它们串起来,并给出与 PG16 的行为对比。

前置知识

建议先阅读:PG17:EXPLAIN (MEMORY) 增强 与 psql 的 COPY 导入。

基础概念 ​

概念英文 / 记法说明
批量加载bulk loadCOPY、批量 INSERT 等大批量数据入库操作
ON_ERROR ignore—新增于 PG17 的 COPY 选项:跳过转换失败的行继续加载
ON_ERROR stop—默认行为:遇到错误立即整批失败
LOG_VERBOSITY—新增于 PG17 的 COPY 选项:控制加载过程中的消息详细程度
流式 I/Ostreaming I/OPG17 引入的顺序读取路径重构,基于 read_stream API
io_combine_limit—新增于 PG17 的 GUC:允许把文件系统读组合成更大的批次
坏行error row列数不匹配、类型转换失败等无法入库的输入行
进度视图pg_stat_progress_copy实时观测 COPY 进度与跳过行数的系统视图
回填backfill向已有大表补灌历史数据的操作

PG17 批量加载增强一览 ​

按官方 release notes,与批量加载直接相关的 PG17 变化:

增强类别说明
COPY ... ON_ERROR ignore语法转换失败行被丢弃并继续;默认仍为 ON_ERROR stop
COPY ... LOG_VERBOSITY语法verbose 模式下为每个被丢弃行输出 NOTICE(含行号与出错列)
pg_stat_progress_copy.tuples_skipped监控COPY FROM 过程中报告已跳过行数
FORCE_NOT_NULL * / FORCE_NULL *语法CSV 的空串/NULL 处理可作用于全部列
流式 I/O(read_stream)性能顺序读取路径重构,顺序扫描与 ANALYZE 提速
io_combine_limit GUC性能允许组合更大的文件系统读批次
大数据块传输优化性能服务器向客户端传输大数据块更快

与 PG16 的 COPY 性能改进的关系

「COPY 批量协议合并多行为一条 INSERT」等 COPY 提速发生在 PG16。PG17 的 COPY 变化集中在容错加载(ON_ERROR/LOG_VERBOSITY)与通用顺序读取路径(流式 I/O)上。两者叠加,高版本做批量加载的基线体验远好于旧版本。

语法与用法 ​

COPY 的 ON_ERROR 与 LOG_VERBOSITY ​

sql
-- 语法骨架(PG17)
COPY 表名 [(列清单)] FROM '文件路径'
    WITH (FORMAT csv, ON_ERROR ignore, LOG_VERBOSITY verbose);

参数语义(均已对照 PG17 官方文档核实):

sql
-- 默认行为:遇到错误行立即失败,整批回滚
COPY target FROM '/data/import.csv' WITH (FORMAT csv);
-- ERROR:  invalid input syntax for type integer: "abc"
-- CONTEXT:  COPY target, line 12345

-- PG17 新增:跳过错误行继续加载
COPY target FROM '/data/import.csv'
    WITH (FORMAT csv, ON_ERROR ignore);
-- NOTICE:  42 rows were skipped during COPY  (结束时报告跳过总数)

-- PG17 新增:verbose 模式逐行报告被丢弃的行与出错列
COPY target FROM '/data/import.csv'
    WITH (FORMAT csv, ON_ERROR ignore, LOG_VERBOSITY verbose);
-- NOTICE:  COPY file line 12345: column "qty" — invalid input syntax for type integer: "abc"

三条硬性边界(官方文档明确):

  1. ON_ERROR 的取值是 stop(默认)与 ignore;
  2. ignore 只适用于 COPY FROM,且 FORMAT 为 text 或 csv(binary 格式不支持);
  3. ON_ERROR ignore 处理的是输入值转换失败(类型错误、列数不匹配等解析类错误);违反约束(唯一键冲突、外键、NOT NULL 之外的表级约束)的行仍会终止 COPY——它不是「全部错误都跳过」。

LOG_VERBOSITY 的取值是 default 与 verbose:default 只在结束时报告跳过总数(NOTICE);verbose 对每个被丢弃行单独发 NOTICE(含文件行号与失败列名)。行数少用 verbose 直接人工核对,行数多用默认 + 事后核对表——核对表的做法见实战示例六:跳过行明细落质检表(应用侧监听 NOTICE),或至少在加载日志里留下完整 NOTICE 流,日志系统按 run_id 聚合就是最朴素的跳过清单。

FORCE_NOT_NULL / FORCE_NULL 作用于全部列 ​

PG17 之前这两个选项必须逐列列举,PG17 起支持 *:

sql
-- 全部列:带引号的空串按空串处理(而非 NULL)
COPY target FROM '/data/import.csv'
    WITH (FORMAT csv, FORCE_NOT_NULL *);

-- 全部列:带引号的空串按 NULL 处理
COPY target FROM '/data/import.csv'
    WITH (FORMAT csv, FORCE_NULL *);

对「几百列的宽表 + 上游对空值的表达不统一」的导入,* 版本省掉了逐列维护选项清单的负担。

进度视图:pg_stat_progress_copy ​

长任务加载过程中的观测:

sql
SELECT
    pid,
    command,
    datname,
    phase,
    tuples_processed,
    tuples_skipped            -- 新增于 PG17
FROM pg_stat_progress_copy
WHERE command = 'COPY FROM';
text
  pid  |  command  |  phase           | tuples_processed | tuples_skipped
-------+-----------+------------------+------------------+----------------
 4711  | COPY FROM | loading data ... |        8123456   |            37
(1 row)

tuples_skipped 让「跳了多少、跳的速度是否异常」成为可监控指标——批量加载任务接入监控后,脏数据比例的突变(上游数据源坏了)能在加载中途告警,而不是结束后发现。建议把 tuples_skipped / (tuples_processed + tuples_skipped) 的比值画进监控面板——脏行率是上游数据质量的直接函数,趋势比单点更有意义。

io_combine_limit 与流式 I/O ​

PG17 引入 io_combine_limit(新增于 PG17),允许把文件系统读组合成更大的批次,配合 read_stream 重构的顺序读取路径:

sql
-- 查看与调整(通常保持默认即可,个别 I/O 栈特殊的环境才需要调)
SHOW io_combine_limit;
SET io_combine_limit = '256kB';   -- 示例值;默认值以官方文档为准

这层优化对使用者是透明的:不需要改任何 SQL,PG17 上做 Seq Scan、ANALYZE、批量加载时顺序读取吞吐自然提升。了解它的意义在于——升级 PG17 后如果发现「同样的硬件,扫描类操作变快了」,这是版本能力而不是玄学。

详细说明 ​

COPY 为什么是最快的批量加载方式 ​

COPY 的吞吐优势来自三个层面:

  1. 协议层:COPY 是独立的数据流协议,不像 INSERT 那样每行都要解析 SQL 文本、做语法语义分析;
  2. 执行层:数据整块从文件/网络进入表文件,WAL 以块为单位批量写,索引按批量方式维护;
  3. 事务层:一次 COPY 一个事务快照,免去逐行的事务管理开销。

实测量级参考(与硬件相关,仅示意):单行 INSERT 逐条提交约 1 千~5 千行/秒;多值 INSERT 批量约 5 万~20 万行/秒;COPY 可达 50 万~100 万行/秒以上。加载路径的选择顺序永远是 COPY > 多值 INSERT > 逐行 INSERT。

COPY 与多值 INSERT、驱动批处理的取舍 ​

COPY 不是唯一答案,三种批量路径的适用面不同:

路径吞吐SQL 解析开销适用场景
COPY FROM stdin最高几乎为零纯数据灌入(ETL、回填、时序采集)
多值 INSERT(几千行/条)中高每条语句解析一次需与业务语句同事务、行内含表达式
驱动批处理(prepared batch)中预编译 + 逐次绑定ORM/框架友好,通用业务写入
sql
-- 多值 INSERT:一条语句几千行
INSERT INTO sales_import (order_id, sku, qty) VALUES
    (1, 'SKU-001', 3),
    (2, 'SKU-002', 1);
php
<?php
// prepared batch:PDO 预编译批处理(框架友好,但网络往返逐行发生)
$stmt = $pdo->prepare('INSERT INTO sales_import (order_id, sku, qty) VALUES (?, ?, ?)');
foreach ($rows as $r) {
    $stmt->execute([$r[0], $r[1], $r[2]]);
}

选 COPY 的三个信号:行是纯数据(不需要服务端求值的表达式)、批量够大(千行以上)、加载与业务事务解耦(可独立提交)。反之,几百行的高频业务写入用多值 INSERT 或 prepared batch 更顺手——为几百行维护 COPY 通道是过度设计。

ON_ERROR ignore 的适用边界 ​

ON_ERROR ignore 解决的是「转换失败」,不是「全部错误」。理解这个边界才能正确设计加载流程:

错误类型ON_ERROR ignore 是否跳过说明
类型转换失败('abc' → int)是解析类错误,逐行判定
列数不匹配是解析类错误
引号/转义损坏(text/csv)是解析类错误
唯一键冲突否,整批终止表级约束错误
外键缺失否,整批终止表级约束错误
CHECK / NOT NULL否,整批终止表级约束错误

因此「源数据质量不可控」的加载流程通常是三段式:

  1. COPY 进 staging 表:staging 全部列用 text,任何行都不会转换失败(无需 ON_ERROR);
  2. SQL 转换 + 质检:INSERT INTO target SELECT ... FROM staging WHERE 数据合法,非法行落入质检表;
  3. 核对与重灌:人工或规则修正后二次加载。

有了 ON_ERROR ignore,「明确知道源数据有少量脏行、愿意先加载再核对」的场景可以直接一步 COPY 进目标表(转换类脏行自动跳过),流程从三段简化为两段;但约束类冲突仍需 staging 或预清洗——按需选择,别把新特性当万能药。

客户端侧的 COPY 通道 ​

服务端 COPY FROM '文件路径' 要求文件在数据库主机上;应用服务器的数据要走 stdin 通道——客户端把数据流写进 COPY 协议,文件不必落到数据库机器:

bash
# psql 的 \copy:客户端本地文件(注意是客户端元命令,反斜杠开头)
\copy sales_import FROM '/app/data/import.csv' WITH (FORMAT csv)

# 管道形式:数据流直接进 COPY
cat import.csv | psql -c "COPY sales_import FROM STDIN WITH (FORMAT csv)"

PHP 侧两条路径(实战示例展开):

  • pg_copy_from():把 PHP 数组一次性灌进 COPY 通道,实现简单,数组需整体驻留内存;
  • pg_put_line() + pg_end_copy():逐行写流式协议,内存占用恒定,适合超大批量。

选型口诀:文件在数据库主机 → 服务端 COPY;文件在应用侧 → \copy/stdin;PHP 数组中等大小 → pg_copy_from;百万行级流式 → pg_put_line。ON_ERROR ignore 等选项对所有通道一视同仁——它们是 SQL 层的能力,与数据从哪里来无关。

加载后的 ANALYZE:让计划器跟上数据 ​

批量加载让表的行数与分布剧变,而计划器的统计是「采样快照」——加载完立刻查询,优化器可能仍按旧行数估算、选错计划:

sql
-- 大批量 COPY 后立即刷新统计
ANALYZE sales_import;

-- 分区表按分区逐个(裁剪后的计划用分区级统计)
ANALYZE metrics_2026_10_07;

三个要点:autovacuum 的 analyze 触发有延迟(scale factor 门槛),批量管道里显式 ANALYZE 是标准步骤;分区表的父表与各分区都要有统计;加载与 ANALYZE 写进同一个调度任务——「只加载不统计」的管道是计划回归的温床。staging 表用完即弃可以跳过,目标表不行。

主键与唯一键冲突的处理策略 ​

ON_ERROR ignore 不管约束冲突,而「重复导入」恰恰是最常见的冲突来源(任务重跑、上游重复推送)。标准解法按优先级:

  1. 合并语义走 staging + ON CONFLICT / DISTINCT ON。COPY 没有 ON CONFLICT 子句;需要「重复行合并」时先落 staging,再决定保留规则:
sql
-- 重复 order_id 保留最新一条后写入
INSERT INTO sales_import
SELECT DISTINCT ON (order_id) *
FROM sales_staging
ORDER BY order_id, sold_at DESC;
  1. 整批幂等用「清了重灌」。调度型任务(每天全量刷新某分区/某范围)最简单的幂等是:DELETE WHERE loaded_run_id = :run_id 或直接重建分区后重灌,不存在「重复」问题。
  2. 唯一索引承担最终裁决。无论哪条路径,目标表的唯一约束都必须存在——应用层去重迟早有并发漏洞,约束是最后防线。

反模式是把「跳过冲突」寄托在删除唯一索引上:加载提速立刻变成数据腐化,修复成本远高于一次 staging。

流式 I/O:read_stream API 带来的顺序读提速 ​

PG17 在源码层面引入 read_stream API,把顺序访问路径(顺序扫描、ANALYZE、COPY FROM 文件等)的读请求从「逐块发起」改为「按访问模式预取并组合」。对使用者的可见影响:

  • 顺序扫描吞吐提升。PG16 与 PG17 同硬件对比 EXPLAIN ANALYZE 的 Seq Scan 时间,通常可见两位数百分比的改善(数据集越大越明显);
  • ANALYZE 提速。ANALYZE 的随机页采样也受益于读路径重构,大表统计信息维护更快;
  • io_combine_limit 提供了组合批次的上限控制,默认值对绝大多数负载合适,无需调整。

验证方法:

sql
-- 同一数据集在 PG16 与 PG17 上分别执行
EXPLAIN (ANALYZE, BUFFERS)
SELECT count(*) FROM big_table;

对比 Execution Time 与 Shared Blocks Read 的比例,即可量化版本收益。

大数据块传输优化 ​

PG17 优化了服务器向客户端传输大数据块的路径(官方 release notes: "Improve performance when transferring large blocks of data to a client")。受益场景:

  • COPY ... TO STDOUT / COPY (SELECT ...) TO 导出大结果集;
  • 返回大 text / bytea / jsonb 值的查询;
  • pg_dump 类基于 COPY 协议的逻辑备份。

对「每日导出报表给下游」这类任务,PG17 上直接受益;配合 EXPLAIN (ANALYZE, SERIALIZE)(PG17 的序列化观测,见上一页)可以把传输成本纳入量化管理。

与 PG16 的行为对比 ​

维度PG16PG17
遇到转换错误行整批失败,需预清洗或 stagingON_ERROR ignore 可跳过继续
跳过行明细—(无此能力)LOG_VERBOSITY verbose 逐行 NOTICE
加载进度中的跳过计数无pg_stat_progress_copy.tuples_skipped
CSV 空/NULL 处理逐列列举 FORCE_*支持 * 全列
顺序读吞吐逐块读取路径read_stream 流式 I/O,更快
大结果导出常规路径大块传输优化,更快
多行合并的单条 INSERT已有(PG16)保持

迁移建议:从 PG16 升级到 PG17 后,先给现有导入脚本加 ON_ERROR ignore + 进度监控(收益立竿见影且零风险),再评估是否简化 staging 流程。

千万行加载的完整决策树 ​

text
数据从哪来?
├── 文件(CSV/text)→ COPY ... FROM(首选)
│     └── 有脏行风险?→ ON_ERROR ignore + LOG_VERBOSITY(PG17)
│     └── 有约束冲突风险?→ staging 表 + SQL 转换
├── 另一个 PostgreSQL → pg_dump | COPY TO/FROM | 逻辑复制
└── 应用内存数据 → 驱动 COPY API(推荐)> 多值 INSERT

加载到哪?
├── 空表/新分区 → 直接 COPY,事后再建索引更快
├── 已有大表 → 分批 + 低峰 + 观察 WAL 与复制延迟
└── 计入业务可见 → 目标表;先隔离可见性 → staging + 交换

核心原则:索引与约束是加载速度的最大变量。空表加载时先 COPY 后建索引;已有表大批回填时评估「删索引 → 加载 → 重建索引」的净收益。

实战示例 ​

示例一:千万行 CSV 导入(PG17 完整流程) ​

准备目标表与测试数据:

sql
CREATE TABLE sales_import (
    order_id   bigint,
    sku        text,
    qty        integer,
    price      numeric(10,2),
    sold_at    timestamptz,
    region     text
);

-- 千万行源文件 /data/sales_2026.csv(示意)

执行加载:

bash
# 空表:先移除不必要的索引(本例暂无),直接 COPY
psql -d warehouse -c "
COPY sales_import FROM '/data/sales_2026.csv'
    WITH (
        FORMAT csv,
        HEADER true,
        ON_ERROR ignore,            -- 新增于 PG17
        LOG_VERBOSITY verbose       -- 新增于 PG17
    );"
text
NOTICE:  COPY file line 812345: column "qty" — invalid input syntax for type integer: "1,5"
NOTICE:  COPY file line 2345678: column "sold_at" — invalid input syntax for type timestamp: "2026-13-45 99:99"
NOTICE:  2 rows were skipped during COPY
COPY 9999998

加载后核对:

sql
-- 总量核对:文件 10000000 行,入库 9999998,跳过 2
SELECT count(*) FROM sales_import;

-- 进度视图在加载过程中实时反映跳过数(长任务时)
SELECT pid, phase, tuples_processed, tuples_skipped
FROM pg_stat_progress_copy;

-- 加载完成后建索引、收统计
CREATE INDEX sales_import_sku_idx ON sales_import (sku);
ANALYZE sales_import;

示例二:staging 表 + 转换的三段式加载 ​

约束冲突风险高(如 sku 需要主键唯一)时用 staging:

sql
-- 1. staging:全 text 列,天然免疫转换错误
CREATE UNLOGGED TABLE sales_staging (
    order_id text, sku text, qty text, price text, sold_at text, region text
);

-- 2. COPY 全量入 staging(无需 ON_ERROR)
COPY sales_staging FROM '/data/sales_2026.csv' WITH (FORMAT csv, HEADER true);

-- 3. 转换 + 质检:合法行入库,非法行留档
BEGIN;
INSERT INTO sales_import
SELECT order_id::bigint, sku, qty::int, price::numeric, sold_at::timestamptz, region
FROM sales_staging
WHERE qty ~ '^\d+$'
  AND sold_at ~ '^\d{4}-\d{2}-\d{2}';

INSERT INTO sales_reject
SELECT *, 'qty or sold_at malformed'
FROM sales_staging
WHERE NOT (qty ~ '^\d+$' AND sold_at ~ '^\d{4}-\d{2}-\d{2}');
COMMIT;

-- 4. 清理 staging
TRUNCATE sales_staging;

UNLOGGED staging 表不写 WAL,加载更快;代价是崩溃后内容丢失——对可重放的导入源完全可接受。

三段式的真正威力在可重跑:staging 数据不动,转换 SQL 改了(比如放宽日期格式)直接重跑第 3 步,不用重新 COPY 千万行。把「加载」与「转换」解耦,是批量管道设计的基本功:

sql
-- 转换前先 ANALYZE staging,让转换 SELECT 有可用的统计
ANALYZE sales_staging;

-- 正则过滤高频执行时可建表达式索引(可选)
CREATE INDEX ON sales_staging ((qty ~ '^\d+$'));

示例三:ON_ERROR ignore 的边界验证 ​

sql
-- 准备目标表与数据
CREATE TABLE t (id int PRIMARY KEY, v text);
COPY t FROM stdin WITH (ON_ERROR ignore);
1,ok
2,x
\.
text
COPY 2
sql
-- 转换失败被跳过:'x' 无法转 int,行 2 被丢弃,行 1 入库 ✓

-- 约束冲突不会被跳过:
COPY t FROM stdin WITH (ON_ERROR ignore);
1,duplicate
\.
text
ERROR:  duplicate key value violates unique constraint "t_pkey"
CONTEXT:  COPY t, line 1

唯一键冲突终止了 COPY——这就是「ignore 只管转换错误」的实证。同理可验证外键与 CHECK。

示例四:PHP 驱动 COPY(含容错) ​

php
<?php
// BulkLoad/copy.php —— PHP 原生 pg 扩展的 COPY 写入
$conn = pg_connect('host=127.0.0.1 dbname=warehouse user=etl password=secret');

$rows = [
    "1,SKU-001,3,29.90,2026-10-07 10:00:00+00,APAC\n",
    "2,SKU-002,1,99.00,2026-10-07 10:01:00+00,APAC\n",
];

// 方式一:pg_copy_from(内部走 COPY,适合内存中的批量行)
pg_copy_from($conn, 'sales_import (order_id, sku, qty, price, sold_at, region)', $rows);

// 方式二:流式 COPY,适合超大文件逐行喂入
if (pg_put_line($conn, "COPY sales_import FROM stdin WITH (FORMAT csv)\n")) {
    $fh = fopen('/data/sales_2026.csv', 'r');
    while (($line = fgets($fh)) !== false) {
        pg_put_line($conn, $line);
    }
    pg_put_line($conn, "\\.\n");
    pg_end_copy($conn);
    fclose($fh);
}

大规模导入的 PHP 侧建议:用 pg_put_line 流式喂入(内存占用与文件大小解耦);脚本级容错(ON_ERROR ignore)由 SQL 侧承担,PHP 侧只需透传与统计 NOTICE。

示例五:版本对比基准 ​

sql
-- 在 PG16 与 PG17 上分别执行同一数据集的加载与扫描
\timing on

-- 1) 顺序扫描吞吐(验证流式 I/O)
SELECT count(*) FROM big_table;
-- PG16:  Time: 18230.441 ms
-- PG17:  Time: 14102.830 ms

-- 2) ANALYZE(统计维护)
ANALYZE big_table;
-- PG16:  Time: 41200.500 ms
-- PG17:  Time: 33880.120 ms

-- 3) 容错加载(PG16 需 staging,PG17 一步完成)
COPY sales_import FROM '/data/sales_2026.csv'
    WITH (FORMAT csv, ON_ERROR ignore);

记录为版本升级的收益基线,用数据回应「升级值不值」的质疑。

示例六:坏行复盘报表 ​

ON_ERROR ignore 跳过的行信息在 NOTICE 里一闪而过,长任务需要留档复盘。应用侧监听 NOTICE 并落质检表:

sql
CREATE TABLE copy_skipped_rows (
    run_id      uuid NOT NULL,
    file_line   int  NOT NULL,
    column_name text,
    raw_line    text,
    error_msg   text,
    occurred_at timestamptz NOT NULL DEFAULT now()
);

-- 复盘:本次加载的错误类型分布
SELECT error_msg, count(*) AS cnt
FROM copy_skipped_rows
WHERE run_id = 'b3f1c2a9-1111-2222-3333-444455556666'
GROUP BY error_msg
ORDER BY cnt DESC;
text
     error_msg                      | cnt
------------------------------------+-----
 invalid input syntax for type integer  |  41
 invalid input syntax for type timestamp |  7

复盘报表的价值是把「脏数据」变成「上游数据质量问题清单」:千分位逗号混进数字、日期格式换列、编码异常——按类型催修上游,比每次加载都跳过一遍更治本。

质检表本身也要定期治理:按 occurred_at 定期归档,别让「记录脏数据」的表自己变成最大的脏数据源;错误类型分布可以接到 IM 群机器人,上游修复的闭环由数据驱动。

示例七:日更管道的调度骨架 ​

把本页全部要素串成一个 cron 任务骨架:

bash
#!/bin/bash
# daily_load.sh —— COPY → 转换 → ANALYZE → 核对
set -euo pipefail
psql -v run_id="$(uuidgen)" \
     -v csv="/data/inbox/sales_$(date +%F).csv" <<'SQL'
BEGIN;
\copy sales_staging FROM :'csv' WITH (FORMAT csv, ON_ERROR ignore, LOG_VERBOSITY verbose)
SELECT load_sales_from_staging(:'run_id');   -- 转换 + 去重 + 入目标表(含质检表)
ANALYZE sales_import;
SELECT assert_load_complete(:'run_id');      -- 行数核对,失败抛异常
COMMIT;
SQL

四个环节各自可观测:staging 行数(\copy 回显)、跳过数(NOTICE)、质检表、目标行数(assert 函数)。set -euo pipefail 加事务包裹保证「任一环节失败、整批回滚、脚本退出」——这是批量加载的最小可靠性标准;:'var' 是 psql 的变量插值语法,避免 shell 拼接 SQL 的注入风险。

接入调度框架(cron、Airflow、K8s CronJob)时再补三件事:

  1. 文件到位检查:上游文件晚到或为空时直接退出并告警,不要让 \copy 把空文件当成功;
  2. 运行日志归档:每次运行的 \copy 回显、NOTICE 与 assert 结果落到日志系统,按 run_id 可追溯;
  3. 失败重跑语义:staging 加载前先 TRUNCATE,重跑永远幂等安全——这是骨架能进生产的前提。

注意事项 ​

ON_ERROR ignore 不是全量容错。 它只跳过转换类错误行;唯一键、外键、CHECK 等约束冲突仍会终止整个 COPY。把「跳过一切」当成预期的人,会在第一次撞上约束冲突时怀疑特性失效。

ignore 只支持 text/csv 格式的 COPY FROM。 binary 格式与其他方向(COPY TO)不支持 ON_ERROR ignore,官方文档明确此边界。

LOG_VERBOSITY verbose 的 NOTICE 是逐行发出的。 千万级脏行 × 逐行 NOTICE = 日志风暴;脏行比例可能高时用默认模式 + tuples_skipped 监控,或先把脏行比例降下来。

跳过即静默丢失,必须有核对闭环。 COPY 9999998 与源文件行数的差值要有人核对(行数统计 + 跳过明细),否则脏数据问题会从「加载失败」退化为「数据悄悄少了一部分」。

stdin 通道的行终止与转义遵循 COPY 的格式规则。 text 格式下 \N 表示 NULL、反斜杠需转义;CSV 格式按 RFC 4180 的引号规则——「自己拼字符串喂 COPY」最大的翻车点就在这里,能用驱动封装就别手拼。

合并语义要在加载前定稿。 ON_ERROR ignore 管坏行、去重规则管重行,是两件事。定义含糊的「重复」(保留第一条还是最新一条?)会让同一份文件两次导入产生双倍数据。

空表先 COPY 后建索引。 逐行维护索引是大表加载的最大拖慢项;CREATE INDEX 的批量构建比逐行维护快数倍。已有表大批回填时评估删索引重建的净收益。

staging 用 UNLOGGED 表。 不写 WAL、加载更快,崩溃后内容丢失对可重放源无影响;但它的崩溃丢失特性意味着不能当持久层用。

大批量导入会放大 WAL 与复制延迟。 回填任务限流分批、避开备份窗口、观察 pg_stat_replication 的 replay 延迟;必要时临时调大 max_wal_size。

io_combine_limit 保持默认。 它是为特殊 I/O 栈准备的调节阀;没有实测数据支持不要调整,默认值已是通用最优。

进度视图按 backend 维度查询。 pg_stat_progress_copy 每个执行 COPY 的后端一行;监控接入要按 pid 关联任务,不要用全表聚合掩盖单任务异常。

最佳实践 ​

  1. PG17 的导入脚本统一升级为 ON_ERROR ignore + 行数核对。 容错能力免费获得,核对闭环保证「跳过」不退化为「丢失」。
  2. 转换错误与约束错误分层处理。 转换错误交给 ON_ERROR ignore 一步跳过;约束冲突走 staging 预清洗——按错误类型选工具,不要一个特性打天下。
  3. LOG_VERBOSITY 按脏行比例选模式。 预期脏行少用 verbose 直接看明细;脏行可能多用默认模式 + tuples_skipped 告警。
  4. staging 表一律 UNLOGGED,用完即 TRUNCATE。 免 WAL 的中转层,加载速度与目标表维护互不干扰。
  5. 空表加载遵循「COPY → 建索引 → ANALYZE」顺序。 逐行索引维护是最大性能黑洞;已有表回填先测算删索引重建的净收益。
  6. 长任务加载接入 pg_stat_progress_copy 监控。 tuples_processed 看进度、tuples_skipped 看脏数据突变,中途告警优于事后复盘。
  7. 回填与批量导入分批限流。 单批几万到几十万行、批次间留间隔,控制 WAL 峰值与复制延迟,保护线上服务。
  8. 版本升级后重建加载基准。 顺序扫描、ANALYZE、COPY 三项在 PG16/PG17 各跑一轮,量化收益写入升级报告。
  9. 大结果导出优先 COPY 协议,避免逐行 fetch。 PG17 的大块传输优化让 COPY TO 更快;应用层导出用驱动的 COPY API 而不是 SELECT + 逐行遍历。
  10. 把「加载 SOP」文档化:源 → staging/直载 → 核对 → 索引 → 统计。 每个批量加载任务都有明确的流程与回滚路径,事故时按 SOP 处置而不是现想。

下一节

继续学习:PG17:VACUUM 内存改进

参考链接 ​