INSERT INTO是PostgreSQL中最高频使用的语句之一,几乎所有业务系统的数据写入都离不开它。不过这条语句的细节比想象中多:列的顺序、默认值、主键冲突、批量插入的性能、返回自增主键等问题,稍不注意就会踩坑。本文把围绕INSERT INTO的常见问题和注意事项汇总起来,逐一讲解原理并给出可运行的示例。

INSERT INTO基础语法与常见报错
最基础的插入语句由表名、列名列表和VALUES子句组成,也可以不写列名直接插入整行数据。两种写法如下:
-- 显式指定列名(推荐写法)
INSERT INTO users (name, email, age) VALUES ('张三', 'zhangsan@ipipp.com', 28);
-- 省略列名,必须按表结构顺序提供所有列的值
INSERT INTO users VALUES (101, '李四', 'lisi@ipipp.com', 25);
推荐始终显式写出列名。原因在于表结构一旦调整(比如新增了字段),省略列名的写法会立刻报错,而且这种报错在代码上线后才暴露,排查成本高。显式写列名还有一个好处:只想插入部分列时,未提到的列会自动填充默认值,如果列没有默认值且不允许NULL,则会直接报错。
初学者最常见的报错有两个。第一个是“column xxx is of type integer but expression is of type character varying”,这是类型不匹配,比如往整型列里插字符串,解决办法是插入前做类型转换:age::int或者CAST(age AS integer)。第二个是“null value in column xxx violates not-null constraint”,说明该列不允许为空但又没提供值,需要检查列列表是否遗漏,或者给表加默认值。
还有一个容易被忽略的细节:字符串中的单引号必须转义。PostgreSQL标准写法是用两个连续单引号表示一个单引号,例如要插入it's ok,应写成'it''s ok',也可以用E开头的转义字符串E'it\'s ok'。如果是在程序代码里拼接SQL,强烈建议改用参数化查询而不是手动转义,既避免注入风险,也省去转义烦恼。
主键或唯一约束冲突怎么办:ON CONFLICT用法
插入数据时报“duplicate key value violates unique constraint”,说明撞上了主键或唯一索引。传统做法是先SELECT判断再INSERT,但这样在并发场景下依然会冲突。PostgreSQL 9.5以后提供了ON CONFLICT子句,一条语句优雅解决,也就是常说的UPSERT:
-- 冲突时什么都不做,直接跳过 INSERT INTO users (id, name) VALUES (1, '王五') ON CONFLICT (id) DO NOTHING; -- 冲突时更新指定列(UPSERT) INSERT INTO users (id, name, age) VALUES (1, '王五', 30) ON CONFLICT (id) DO UPDATE SET name = EXCLUDED.name, age = EXCLUDED.age;
这里的EXCLUDED是一个虚拟表,代表本次尝试插入但被冲突挡下来的那一行数据。写DO UPDATE时从EXCLUDED取值,就能实现“存在则更新,不存在则插入”的语义。需要注意的是,ON CONFLICT后面指定的列或约束必须真实存在唯一索引,否则会报“there is no unique or exclusion constraint matching the ON CONFLICT specification”。
另外要区分DO NOTHING和DO UPDATE的返回行为差异:DO NOTHING在发生冲突时影响的行数是0,而DO UPDATE无论插入还是更新,影响行数都是1。如果你的程序依赖影响行数做逻辑判断,这一点必须心里有数。
如何拿到自增主键:RETURNING子句
很多业务在插入一条记录后需要立刻拿到它的ID去做后续操作,比如创建订单后写订单明细。MySQL常用LAST_INSERT_ID,而PostgreSQL的做法是在INSERT语句末尾加上RETURNING:
INSERT INTO orders (user_id, amount) VALUES (1, 99.9) RETURNING id, created_at;
RETURNING后面可以跟任意列,甚至表达式,它会让INSERT像SELECT一样返回结果集,一条语句完成插入加取值,不用再发一次查询。RETURNING同样可以配合ON CONFLICT使用,也能用在UPDATE和DELETE语句上,这是PostgreSQL相当好用的特性。
顺带说一说自增列的两种实现。老项目常见SERIAL类型,新项目建议直接用GENERATED ALWAYS AS IDENTITY,它是SQL标准的写法,语义更严格,能防止手动向自增列插值。如果用SERIAL又手动插入了指定ID,序列并不会自动前进,后续插入可能再次冲突,此时需要用setval函数重置序列。
批量插入与性能优化
需要插入大量数据时,逐条执行INSERT的效率非常低,每条语句都有一次网络往返和一次事务开销。优化手段主要有三种。
第一种是多值INSERT,把多行合并在一条语句里,这是最简单有效的改法:
INSERT INTO users (name, age) VALUES
('甲', 20),
('乙', 21),
('丙', 22);
第二种是INSERT INTO ... SELECT,从其他表搬数据,常用于数据迁移、报表汇总、日志归档这类场景:
INSERT INTO user_backup (id, name, age) SELECT id, name, age FROM users WHERE created_at < '2023-01-01';
第三种是超大数据量时使用COPY命令,它的速度比INSERT快一个数量级,因为它跳过了SQL解析层,直接以流式协议传输数据。如果数据源是文件,优先考虑COPY;如果是程序内存中的数据,可以使用COPY FROM STDIN接口。需要注意COPY遇到错误会整批回滚,而多值INSERT可以通过ON_ERROR相关手段或分批提交来控制影响范围,生产环境导入脏数据时建议先分批验证。
容易踩雷的注意事项汇总
最后把零散但高频的坑整理成清单:
- 大小写与引号:未加引号的标识符会被转为小写,如果建表时用了双引号包住的大写表名如
"Users",查询时写users会报relation不存在。建议标识符统一小写。 - 默认值填充:可以用
DEFAULT关键字显式取默认值,例如VALUES (DEFAULT, 'xx'),在部分框架拼接SQL时很有用。 - 时间戳插入:推荐直接用
now()或CURRENT_TIMESTAMP,不要由应用端传时间字符串,避免时区问题。 - JSON类型:插入JSONB列时传入的必须是合法JSON字符串,配合
::jsonb转换可避免隐式转换报错。 - 事务控制:多条INSERT放在同一个事务里可以显著提速,但要注意失败时整体回滚,别把不相关的业务混在一个大事务中。
- 权限问题:报“permission denied for table”说明当前角色没有INSERT权限,需要用GRANT语句授权。
掌握这些要点后,日常开发中的绝大多数INSERT INTO场景都能从容应对。核心记住三件事:显式写列名、用ON CONFLICT处理冲突、用RETURNING拿回自增ID,这三招能覆盖八成以上的实际需求。
PostgreSQLINSERT INTOSQL语句修改时间:2026-09-08 10:05:13