Skip to content

插入数据(INSERT) ​

INSERT 是 DML 的入口——所有数据最终都靠它写进表里。本节讲清 INSERT 的全部形态:单行 / 多行 VALUES、INSERT ... SELECT、DEFAULT VALUES 与显式 DEFAULT,RETURNING 的取回能力,以及"批量插入到底应该用多行 VALUES 还是 COPY"这个绕不开的性能问题。

前置知识

阅读本节前,建议先了解:创建表(CREATE TABLE)

基础概念 ​

INSERT 的作用是向目标表追加新行。它不会修改已有行,也不会删除任何数据;如果插入的数据违反约束(主键重复、非空、检查约束),整条语句会失败并回滚——一条 INSERT 是原子的,要么所有行都进去,要么一行都不进。

写法语法形态典型用途能否一次插多行
单行 VALUESINSERT INTO t (a) VALUES (1)应用层单条写入否
多行 VALUESINSERT INTO t (a) VALUES (1), (2), (3)中小批量写入是
INSERT ... SELECTINSERT INTO t (a) SELECT a FROM s表间搬运 / ETL是(任意行数)
INSERT ... DEFAULT VALUESINSERT INTO t DEFAULT VALUES全默认值插一行否
INSERT ... RETURNING上述任意形式 + RETURNING ...写回同时取回结果是

几个关键术语:

  • 目标列列表(Column List):INSERT INTO t (col1, col2) 中括号里的部分。省略列列表时,值的顺序必须与表的物理列顺序完全一致,这是最常见的事故来源之一。
  • DEFAULT:在 VALUES 中写 DEFAULT,表示"该列交给它的默认值决定"(可能是 DEFAULT 表达式,也可能是 GENERATED 生成列 / IDENTITY 序列)。它和"不写这一列"的效果相同,但允许在不省略列列表的前提下跳过某列。
  • DEFAULT VALUES:整行都用默认值,只能插一行。
  • RETURNING:PostgreSQL 扩展。让 INSERT 在写入的同时返回被插入行的列值,包括数据库生成的列(如 IDENTITY 生成的主键、DEFAULT now() 得到的时间戳)。
  • 生成列(GENERATED ALWAYS AS IDENTITY / GENERATED ALWAYS AS (...) STORED):前者由序列产生,禁止显式写值(除非 OVERRIDING SYSTEM VALUE);后者由表达式算出,压根不能出现在 INSERT 的列列表里。
  • ON CONFLICT:冲突处理子句(即 UPSERT)。本节只讲 DO NOTHING 的初步用法,完整用法见下一节。
  • OVERRIDING SYSTEM VALUE:当表使用 GENERATED ALWAYS AS IDENTITY 而又必须手工指定主键时(如数据迁移),用它显式覆盖。

语法与用法 ​

完整语法骨架如下(方括号为可选部分):

text
[ WITH [ RECURSIVE ] with_query [, ...] ]
INSERT INTO table_name [ AS alias ] [ ( column_name [, ...] ) ]
    [ OVERRIDING { SYSTEM | USER } VALUE ]
    { DEFAULT VALUES | VALUES ( { expression | DEFAULT } [, ...] ) [, ...] | query }
    [ ON CONFLICT [ conflict_target ] conflict_action ]
    [ RETURNING * | output_expression [ [ AS ] output_name ] [, ...] ]

五个最常用的形态:

sql
-- 1. 单行插入
INSERT INTO users (email, name) VALUES ('a@example.com', 'Alice');

-- 2. 多行插入(推荐,一次往返写多行)
INSERT INTO users (email, name) VALUES
  ('b@example.com', 'Bob'),
  ('c@example.com', 'Carol');

-- 3. 从查询结果插入
INSERT INTO users_archive (email, name)
SELECT email, name FROM users WHERE created_at < now() - interval '1 year';

-- 4. 全默认值插入一行
INSERT INTO users DEFAULT VALUES;

-- 5. 跳过某一列(用 DEFAULT 占位)
INSERT INTO users (email, name, status) VALUES ('d@example.com', 'Dave', DEFAULT);

取值来源的几种写法:

值的形式说明示例
字面量字符串、数字、布尔VALUES ('a', 1, true)
表达式任意可求值的 SQL 表达式VALUES (now(), upper('ab'), 1 + 2)
DEFAULT使用该列的默认值VALUES ('a', DEFAULT)
类型转换显式 CAST 或 ::VALUES ('2026-09-24'::date)
数组 / JSON用字面量语法VALUES ('{a,b}', '{"k":"v"}')
子查询标量子查询(只能返回一行一列)VALUES ((SELECT max(id) + 1 FROM t))

下面用一张贯穿本节的示例表:

sql
-- 示例:用户订单表
CREATE TABLE orders (
  id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  order_no    text        NOT NULL UNIQUE,
  user_email  text        NOT NULL,
  amount      numeric(12,2) NOT NULL CHECK (amount >= 0),
  status      text        NOT NULL DEFAULT 'pending',
  tags        text[]      NOT NULL DEFAULT '{}',
  created_at  timestamptz NOT NULL DEFAULT now()
);

详细说明 ​

单行与多行 VALUES ​

多行 VALUES 是 PostgreSQL 最实用的语法之一,它在一次网络往返里完成多行写入,且整体作为一个原子语句执行。

sql
-- 单行:一次往返写一行
INSERT INTO orders (order_no, user_email, amount)
VALUES ('ORD-1001', 'alice@example.com', 199.00);

-- 多行:一次往返写三行
INSERT INTO orders (order_no, user_email, amount, status) VALUES
  ('ORD-1002', 'bob@example.com',    88.50, 'paid'),
  ('ORD-1003', 'carol@example.com', 320.00, 'pending'),
  ('ORD-1004', 'dave@example.com',   15.90, 'paid');

多行 VALUES 的执行代价大致线性于行数,但固定开销(解析、计划、往返)只付一次。示意性实测对比(同样插入 10000 行,本地回环连接):

text
-- 以下为示意数据,用于说明数量级差异
方式                          耗时        相对倍数
单行 INSERT × 10000 次        4820 ms     1.00
多行 VALUES(每批 100 行)      186 ms     0.04
多行 VALUES(每批 1000 行)      92 ms     0.02
COPY(单次)                    41 ms     0.009

注意:多行 VALUES 也不是越大越好。单条语句过大(例如几十万行一次)会显著增加内存占用、延长事务持锁时间,并让失败时的重试代价变高。

INSERT ... SELECT ​

当数据来源是另一张表时,用 INSERT ... SELECT 而不是"先查出来、再拼 SQL 插回去"。数据全程在数据库内部流动,不经过应用层,既快又不会因类型往返转换丢精度。

sql
-- 把过期订单归档
INSERT INTO orders_archive (id, order_no, user_email, amount, status, created_at)
SELECT id, order_no, user_email, amount, status, created_at
FROM orders
WHERE created_at < now() - interval '1 year';

列顺序按位置对应,不按名字匹配。务必显式写出两侧列名,避免两边列顺序不一致导致数据错位:

sql
-- 危险!两边列顺序不同会静默写错数据
INSERT INTO orders_archive
SELECT * FROM orders;

-- 安全:显式列出两侧列名
INSERT INTO orders_archive (order_no, user_email, amount)
SELECT order_no, user_email, amount FROM orders WHERE amount > 1000;

INSERT ... SELECT 也支持 ORDER BY、LIMIT、CTE 与窗口函数,可以把复杂的取数逻辑直接内联:

sql
-- 每个用户取最近一笔订单,写入对账表
WITH ranked AS (
  SELECT *,
         row_number() OVER (PARTITION BY user_email ORDER BY created_at DESC) AS rn
  FROM orders
)
INSERT INTO reconciliation (order_no, user_email, amount)
SELECT order_no, user_email, amount
FROM ranked
WHERE rn = 1;

注意 INSERT ... SELECT ... ORDER BY 中的 ORDER BY 不保证写入物理顺序,它只影响 SELECT 的输出顺序(例如配合 LIMIT 时才有意义)。

DEFAULT VALUES 与显式 DEFAULT ​

如果表的所有列都有默认值或允许 NULL,可以用 DEFAULT VALUES 插入一行"全默认"记录。它常用于"先占一行拿主键"的场景:

sql
-- 插入一行全默认值,并直接拿到生成的主键
INSERT INTO orders (order_no, user_email, amount)
VALUES ('ORD-9999', 'temp@example.com', 0);

-- 若表允许,也可整行默认
INSERT INTO settings DEFAULT VALUES;

-- 显式 DEFAULT:只跳过部分列
INSERT INTO orders (order_no, user_email, amount, status, created_at)
VALUES ('ORD-1005', 'eve@example.com', 66.00, DEFAULT, DEFAULT);

DEFAULT 的语义要点:

  • DEFAULT 不等于 NULL。若列定义了 DEFAULT 0,写 DEFAULT 得到 0,写 NULL 得到 NULL(并可能违反 NOT NULL)。
  • GENERATED ALWAYS AS (...) STORED 的生成列不能出现在列列表中,写 DEFAULT 也不行,必须整个省略。
  • 若列没有 DEFAULT 且允许 NULL,写 DEFAULT 等价于写 NULL。
sql
-- 反例:生成列不能出现在 INSERT 的列列表中
-- 假设 orders 有列 total numeric GENERATED ALWAYS AS (amount * 1.13) STORED
INSERT INTO orders (order_no, user_email, amount, total)
VALUES ('ORD-1006', 'frank@example.com', 100.00, 113.00);
-- ERROR:  cannot insert a non-DEFAULT value into column "total"
-- DETAIL:  Column "total" is a generated column.

省略列列表的风险 ​

INSERT INTO t VALUES (...)(不写列列表)能跑,但极其脆弱:

sql
-- 建表
CREATE TABLE t_demo (a int, b text, c date);

-- 省略列列表:值按物理列顺序 a, b, c 对应
INSERT INTO t_demo VALUES (1, 'x', '2026-01-01');

-- 现在有人加了一列到中间(老版本 PostgreSQL 允许指定位置)
ALTER TABLE t_demo ADD COLUMN d boolean NOT NULL DEFAULT false;

-- 旧语句依然能执行,但语义已经变了:
-- 值仍按 a, b, c 匹配,d 走默认值。若新列是新增在最前,则会直接类型不匹配报错。

正确的做法是永远显式写出列列表:

sql
-- 推荐:列名与值一一对应,加列、改列顺序都不受影响
INSERT INTO t_demo (a, b, c) VALUES (1, 'x', '2026-01-01');

同理,INSERT INTO t SELECT * FROM s 也应该写成 INSERT INTO t (a, b, c) SELECT a, b, c FROM s。

显式主键与 IDENTITY / serial 的冲突 ​

GENERATED ALWAYS AS IDENTITY 与 serial 在"能不能手工指定值"上行为完全不同:

列定义方式手工指定主键需要什么开关说明
serial / bigserial允许无需开关本质是 DEFAULT nextval(...),显式值直接覆盖
GENERATED BY DEFAULT AS IDENTITY允许无需开关手工值优先,序列不推进
GENERATED ALWAYS AS IDENTITY默认禁止必须加 OVERRIDING SYSTEM VALUE防止误覆盖
sql
-- 表使用 GENERATED ALWAYS AS IDENTITY
INSERT INTO orders (id, order_no, user_email, amount)
VALUES (1001, 'ORD-2001', 'a@example.com', 10.00);
-- ERROR:  cannot insert a non-DEFAULT value into column "id"
-- DETAIL:  Column "id" is an identity column defined as GENERATED ALWAYS.
-- HINT:  Use OVERRIDING SYSTEM VALUE to override.

-- 数据迁移场景:显式覆盖
INSERT INTO orders (id, order_no, user_email, amount)
OVERRIDING SYSTEM VALUE
VALUES (1001, 'ORD-2001', 'a@example.com', 10.00);

最关键的一点:无论哪种方式,显式插入主键都不会推进序列。 迁移完数据后必须手动把序列推到最大值,否则后续自动生成的主键会和历史数据撞车:

sql
-- 迁移后修复序列水位(IDENTITY 列同样适用)
SELECT setval(
  pg_get_serial_sequence('orders', 'id'),
  (SELECT max(id) FROM orders)
);

-- 更稳的写法:加 1 并把 is_called 设为 false,让下一次恰好取到 max+1
SELECT setval(
  pg_get_serial_sequence('orders', 'id'),
  (SELECT max(id) FROM orders),
  true
);

RETURNING:写回同时取回 ​

RETURNING 解决了"插入后还要再查一次"的往返问题,尤其是需要拿数据库生成的值的场景(自增主键、默认时间戳、触发器改写后的值)。

sql
-- 拿到生成的主键与默认时间戳
INSERT INTO orders (order_no, user_email, amount)
VALUES ('ORD-3001', 'gina@example.com', 520.00)
RETURNING id, order_no, status, created_at;
text
  id | order_no  | status  |          created_at
-----+-----------+---------+-------------------------------
  12 | ORD-3001  | pending | 2026-09-24 15:03:21.774+08
(1 row)

INSERT 0 1

RETURNING 里可以写表达式、函数、甚至基于插入值的计算:

sql
-- 返回计算结果与常量标记
INSERT INTO orders (order_no, user_email, amount)
VALUES ('ORD-3002', 'hugo@example.com', 200.00)
RETURNING id, amount, amount * 1.13 AS amount_with_tax, now() AS returned_at;

-- 多行插入时,RETURNING 会为每一行各返回一行
INSERT INTO orders (order_no, user_email, amount) VALUES
  ('ORD-3003', 'i@example.com', 1.00),
  ('ORD-3004', 'j@example.com', 2.00)
RETURNING id, order_no;

INSERT ... SELECT ... RETURNING 还能作为 CTE 的一部分,把结果继续喂给后续语句:

sql
-- 插入并立即用返回的行做后续统计
WITH ins AS (
  INSERT INTO orders (order_no, user_email, amount)
  SELECT 'ORD-BATCH-' || g, 'batch@example.com', g * 10
  FROM generate_series(1, 5) AS g
  RETURNING id, amount
)
SELECT count(*) AS inserted, sum(amount) AS total FROM ins;

ON CONFLICT DO NOTHING 初步 ​

当插入的数据可能撞上唯一约束时,默认行为是整条语句报错回滚。如果想"撞了就跳过、不报错",用 ON CONFLICT DO NOTHING:

sql
-- 撞上 order_no 的唯一约束就跳过该行,其余行照常插入
INSERT INTO orders (order_no, user_email, amount) VALUES
  ('ORD-1002', 'dup@example.com', 1.00),   -- 已存在,跳过
  ('ORD-4001', 'new@example.com', 2.00)    -- 新行,插入
ON CONFLICT (order_no) DO NOTHING;
text
INSERT 0 1
-- 注意:影响行数是 1,而不是 2;被跳过的行不计入

ON CONFLICT 的括号里必须是已经存在唯一索引或唯一约束的列/表达式,否则报错:

sql
-- 错误!user_email 上没有唯一索引
INSERT INTO orders (order_no, user_email, amount)
VALUES ('ORD-4002', 'k@example.com', 3.00)
ON CONFLICT (user_email) DO NOTHING;
-- ERROR:  there is no unique or exclusion constraint matching the ON CONFLICT specification

还要注意:ON CONFLICT DO NOTHING 不返回冲突行,只返回真正插入的行。这在"需要知道哪些行已存在"的业务里会不够用——这时应改用 DO UPDATE(下一节详解),并用 xmax = 0 判断本次是新插还是更新。

批量写入:多行 VALUES vs COPY ​

三种批量写入方式的取舍:

方式适用规模相对速度是否走 WAL 常规路径是否支持 RETURNING
单行 INSERT 循环< 100 行最慢是是
多行 VALUES100 ~ 10 万行快是是
INSERT ... SELECT任意规模(源在库内)很快是是
COPY10 万行以上最快是(除非表为 UNLOGGED)否

COPY 快的核心原因是绕过了 SQL 解析、计划与逐行执行器,直接把文本/二进制流交给存储层。代价是不支持 RETURNING,也不支持 ON CONFLICT(除非先导入临时表再 INSERT ... SELECT)。

sql
-- 多行 VALUES:适合应用层批量提交,可分片控制大小
INSERT INTO orders (order_no, user_email, amount)
SELECT 'ORD-GEN-' || g, 'gen@example.com', (random() * 1000)::numeric(12,2)
FROM generate_series(1, 1000) AS g;
bash
# COPY:适合文件导入,psql 客户端
# 注意:服务端 COPY 需要超级用户或 pg_read_server_files 角色
psql -d demo -c "\copy orders (order_no, user_email, amount) FROM 'orders.csv' WITH (FORMAT csv, HEADER true)"

一个实用折中方案:先 COPY 到临时表,再用 INSERT ... SELECT ... ON CONFLICT 合并进目标表,既拿到 COPY 的速度,又有冲突处理与 RETURNING:

sql
-- 1) 建临时表并 COPY 导入原始数据(见 COPY 章节)
CREATE TEMP TABLE orders_stage (LIKE orders INCLUDING DEFAULTS);

-- 2) 合并进正式表,处理冲突并保留审计
INSERT INTO orders (order_no, user_email, amount)
SELECT order_no, user_email, amount FROM orders_stage
ON CONFLICT (order_no) DO UPDATE
  SET amount = EXCLUDED.amount
RETURNING order_no;

实战示例 ​

下面用一段完整流程演示"电商订单写入"的典型链路:建表 → 单行写入 → 批量写入 → 幂等重放 → 归档搬运 → 校验。

sql
-- 步骤 1:准备示例表
DROP TABLE IF EXISTS orders_demo;
CREATE TABLE orders_demo (
  id          bigint GENERATED ALWAYS AS IDENTITY PRIMARY KEY,
  order_no    text          NOT NULL UNIQUE,
  user_email  text          NOT NULL,
  amount      numeric(12,2) NOT NULL CHECK (amount >= 0),
  status      text          NOT NULL DEFAULT 'pending',
  tags        text[]        NOT NULL DEFAULT '{}',
  created_at  timestamptz   NOT NULL DEFAULT now()
);

CREATE TABLE orders_demo_archive (LIKE orders_demo INCLUDING ALL);
text
DROP TABLE
CREATE TABLE
CREATE TABLE
sql
-- 步骤 2:单行下单,并立刻拿到数据库生成的主键与时间戳
INSERT INTO orders_demo (order_no, user_email, amount)
VALUES ('ORD-5001', 'alice@example.com', 199.00)
RETURNING id, order_no, status, created_at;
text
 id | order_no | status  |          created_at
----+----------+---------+-------------------------------
  1 | ORD-5001 | pending | 2026-09-24 15:20:11.318+08
(1 row)

INSERT 0 1
sql
-- 步骤 3:批量写入 3 笔订单(一次往返)
INSERT INTO orders_demo (order_no, user_email, amount, status, tags) VALUES
  ('ORD-5002', 'bob@example.com',    88.50, 'paid',    '{promo}'),
  ('ORD-5003', 'carol@example.com', 320.00, 'pending', '{}'),
  ('ORD-5004', 'dave@example.com',   15.90, 'paid',    '{gift}')
RETURNING id, order_no, amount;
text
 id | order_no | amount
----+----------+--------
  2 | ORD-5002 |  88.50
  3 | ORD-5003 | 320.00
  4 | ORD-5004 |  15.90
(3 rows)

INSERT 0 3
sql
-- 步骤 4:消息重投场景,同一批订单重复写入,要求幂等不报错
INSERT INTO orders_demo (order_no, user_email, amount, status) VALUES
  ('ORD-5002', 'bob@example.com',    88.50, 'paid'),
  ('ORD-5005', 'erin@example.com',   45.00, 'pending')
ON CONFLICT (order_no) DO NOTHING
RETURNING id, order_no;
text
 id | order_no
----+----------
  5 | ORD-5005
(1 row)

INSERT 0 1
-- 注意:ORD-5002 已存在被跳过,因此 RETURNING 只返回 1 行,
-- 业务层若需要感知"被跳过",可改用 DO UPDATE ... RETURNING (xmax = 0)
sql
-- 步骤 5:从查询结果批量写入(模拟数据回流)
INSERT INTO orders_demo (order_no, user_email, amount, status)
SELECT 'ORD-RB-' || id, user_email, amount, 'refunded'
FROM orders_demo
WHERE status = 'paid' AND amount < 100
RETURNING order_no, status;
text
 order_no  |  status
-----------+----------
 ORD-RB-2  | refunded
 ORD-RB-4  | refunded
(2 rows)

INSERT 0 2
sql
-- 步骤 6:把已退款订单搬到归档表(INSERT ... SELECT)
INSERT INTO orders_demo_archive (order_no, user_email, amount, status, tags, created_at)
SELECT order_no, user_email, amount, status, tags, created_at
FROM orders_demo
WHERE status = 'refunded';
text
INSERT 0 2
sql
-- 步骤 7:核对两边数据量
SELECT
  (SELECT count(*) FROM orders_demo)         AS main_rows,
  (SELECT count(*) FROM orders_demo_archive) AS archive_rows,
  (SELECT sum(amount) FROM orders_demo)      AS main_amount;
text
 main_rows | archive_rows | main_amount
-----------+--------------+-------------
         7 |            2 |      713.40
(1 row)

上述流程中,只有步骤 4 体现了幂等:同样的 SQL 重跑一次,ON CONFLICT DO NOTHING 会跳过所有已存在行,最终数据条数不变。这正是把"重试"和"重复消费"变安全的标准手段。

注意事项 ​

  • INSERT 失败会回滚整条语句。 多行 VALUES 中只要有一行违反约束,整个语句都不生效。如果业务上希望"能插的插进去",需要拆成多批或改用冲突处理。
sql
-- 整条语句都会失败,前两行也不会插入
INSERT INTO orders_demo (order_no, user_email, amount) VALUES
  ('ORD-6001', 'ok1@example.com',  10.00),
  ('ORD-5001', 'dup@example.com',  20.00),   -- 主键/唯一冲突
  ('ORD-6002', 'ok2@example.com',  30.00);
-- ERROR:  duplicate key value violates unique constraint "orders_demo_order_no_key"
  • 同时指定主键和序列时会撞车。 手工插入了 id = 100 却不推进序列,之后自动插入很可能又取到 100,直接报唯一冲突。迁移后必须 setval 修复水位。
  • 省略列列表是定时炸弹。 表结构一变,老语句可能从"报错"变成"静默写错列"。永远显式列出列名。
  • ON CONFLICT DO NOTHING 的影响行数小于提交行数。 INSERT 0 1 可能表示你提交了 100 行、99 行被跳过。不要用它判断"业务是否执行成功"。
  • INSERT ... RETURNING 的顺序不保证与 VALUES 顺序一致。 尤其是并行 INSERT ... SELECT 时,返回值顺序是不确定的。需要顺序时,在返回结果上加 ORDER BY(外层查询)。
  • COPY 不走 SQL 解析,因此不会触发 INSERT 规则(RULE),但会触发 BEFORE/AFTER INSERT 行级触发器。 大批量导入时触发器会显著拖慢速度,必要时先禁用。
  • 大事务会放大锁与 WAL 压力。 一次插几百万行会让事务长时间持有锁、阻塞 autovacuum、造成主从复制延迟。生产上要分批提交。
  • 生成的列不能显式赋值。 GENERATED ALWAYS AS (...) STORED 的列出现在列列表中即报错,写 DEFAULT 也不行。

最佳实践 ​

  1. 永远显式写出列列表。 无论是 VALUES 还是 INSERT ... SELECT,把列名写全。这是抵抗表结构演进的最低成本手段。
sql
-- 推荐
INSERT INTO orders_demo (order_no, user_email, amount) VALUES ('ORD-7001', 'a@example.com', 10.00);

-- 不推荐:表加列后语义可能悄悄改变
INSERT INTO orders_demo VALUES (DEFAULT, 'ORD-7001', 'a@example.com', 10.00, DEFAULT, DEFAULT, DEFAULT);
  1. 需要数据库生成的值时,一律用 RETURNING。 省掉一次查询往返,也避免了"插入后按非唯一字段反查"带来的竞态。
sql
INSERT INTO orders_demo (order_no, user_email, amount)
VALUES ('ORD-7002', 'b@example.com', 20.00)
RETURNING id, created_at;
  1. 批量写入优先多行 VALUES,大批量导入优先 COPY。 应用层批量提交时把批大小控制在 500~5000 行之间:既摊薄了固定开销,又不会让单条语句过大。
sql
-- 批大小可控的多行写入(示例:每批 1000 行)
INSERT INTO orders_demo (order_no, user_email, amount)
SELECT 'ORD-B-' || g, 'batch@example.com', g % 100
FROM generate_series(1, 1000) AS g;
  1. 幂等写入统一用 ON CONFLICT。 定时同步、消息重投、断点续传都应走冲突处理,而不是"先 SELECT 判断存在与否再决定插或改"——后者在并发下有竞态窗口。
sql
INSERT INTO orders_demo (order_no, user_email, amount)
VALUES ('ORD-7003', 'c@example.com', 30.00)
ON CONFLICT (order_no) DO NOTHING;
  1. 分批提交,避免超长事务。 大批量写入放在 BEGIN/COMMIT 内分片执行,每片之间提交一次,控制 WAL 与锁的持有时间。
sql
-- 伪代码示意:每 5000 行提交一次
BEGIN;
  INSERT INTO orders_demo (order_no, user_email, amount)
  SELECT 'ORD-P-' || g, 'p@example.com', g % 500
  FROM generate_series(1, 5000) AS g;
COMMIT;
-- 重复上述块,推进 generate_series 的起点
  1. 数据迁移后修复序列水位。 显式插入主键后,务必 setval 到当前最大值,并检查 pg_sequences 对齐。
sql
SELECT setval(pg_get_serial_sequence('orders_demo', 'id'),
              (SELECT max(id) FROM orders_demo));

-- 核对当前水位与最大值
SELECT last_value, is_called FROM pg_sequences
WHERE schemaname = 'public' AND sequencename = 'orders_demo_id_seq';
  1. 导入前评估触发器与索引代价。 大批量导入若表上有多个索引和行级触发器,考虑"先导入、后建索引",或临时 ALTER TABLE ... DISABLE TRIGGER(需谨慎,仅限受控窗口)。

下一节

继续学习:UPSERT(INSERT ... ON CONFLICT)

参考链接 ​