INSERT 0 1 到底告诉你什么?
摘要:本文解释了 PostgreSQL 中神秘的 "INSERT 0 1" 命令标签的含义,这个标签会在执行 INSERT 语句后出现。本文将其格式分解为两个部分——OID 字段(历史上是对象标识符,现在为了向后兼容始终为 0)和行数——然后通过实际的 SQL 示例演示其工作原理。
如果你在终端或 IDE 中运行过 insert 语句,你一定见过它:insert 0 1 这个神秘的消息。虽然看起来像是某种古老的二进制,但它实际上是数据库引擎给出的精确状态报告。
命令标签的剖析
在 PostgreSQL 中,每个成功的命令都会返回一个"命令标签"。对于插入操作,格式如下:
INSERT [oid] [rows]
- "0"(oid):从历史上看,Postgres 可以为每行分配一个内部对象标识符。自版本 12 以来,这个功能已完全从用户表中移除,这就是为什么你今天总是看到 0。
- "1"(rows):这是你的查询实际处理的行数。
底层实现
如果你对源代码感到好奇,这个消息是在 PostgreSQL 后端的几个文件中构建的。它的核心位于 src/backend/tcop/cmdtag.c 中的一个名为 BuildQueryCompletionString 的函数。
首先,每个命令标签在 src/include/tcop/cmdtaglist.h 中声明:
PG_CMDTAG(CMDTAG_INSERT, "INSERT", false, false, true)
最后一个 true 表示"在完成字符串中显示行数"。
然后,BuildQueryCompletionString 组装最终的字符串。以下是 src/backend/tcop/cmdtag.c 中的相关摘录:
/*
* In PostgreSQL versions 11 and earlier, it was possible to create a
* table WITH OIDS. When inserting into such a table, INSERT used to
* include the Oid of the inserted record in the completion tag. To
* maintain compatibility in the wire protocol, we now write a "0" (for
* InvalidOid) in the location where we once wrote the new record's Oid.
*/
if (command_tag_display_rowcount(tag) && !nameonly)
{
if (tag == CMDTAG_INSERT)
{
*bufp++ = ' ';
*bufp++ = '0'; /* legacy OID field, always 0 now */
}
*bufp++ = ' ';
bufp += pg_ulltoa_n(qc->nprocessed, bufp); /* the row count */
}
你是否曾注意到 PostgreSQL 项目如何记录 Postgres 12 和更早版本之间这种行为的变化,从而解释了我们为什么保留值 0?
那么,它是如何工作的呢?它以标签名 INSERT 开头,追加 0(一个硬编码的零,用于遗留的 OID,为了与 PostgreSQL 11 及更早版本的 wire 协议保持兼容而保留),然后追加一个空格和已处理的行数。这就构成了我们的 INSERT 0 1。
字符串构建完成后,src/backend/tcop/dest.c 中的 EndCommand 会将其作为 CommandComplete 协议消息发送给客户端:
len = BuildQueryCompletionString(completionTag, qc, force_undecorated_output);
pq_putmessage(PqMsg_CommandComplete, completionTag, len + 1);
在客户端,psql 通过 PQcmdStatus() 获取它,并在 PrintQueryStatus(src/bin/psql/common.c)中将其打印到终端。
实际示例
为了测试这一点,让我们创建一个 users 表。
create table users (
id int generated always as identity primary key,
name text not null,
email text unique,
created_at timestamptz default now()
);
简单的插入
insert into users (name, email)
values ('alice', 'alice@example.com');
-- output: INSERT 0 1
结果:创建了 1 行新数据。
插入多行
insert into users (name, email)
values
('bob', 'bob@example.com'),
('charlie', 'charlie@example.com');
-- output: INSERT 0 2
结果:创建了 2 行新数据。
"Upsert"(冲突时更新)
这是一个常见的困惑来源。当使用 on conflict do update 时,Postgres 仍然返回 insert 标签,即使发生了更新。
insert into users (name, email)
values ('alice', 'alice@example.com')
on conflict (email)
do update set name = excluded.name;
-- output: INSERT 0 1
结果:虽然未创建新行(它是更新操作),但已处理的行总数为 1。
结论
insert 0 1 只是 Postgres 的说法,意思是"任务完成:已处理 1 行。"
关于 OID 的说明:从历史上看,PostgreSQL 允许使用 with oids 创建表,这会在每行添加一个隐藏的 4 字节系统列。这通常用作系统范围内的唯一标识符。然而,此功能在版本 11 中被弃用,并在 PostgreSQL 12 中正式从用户表中移除。虽然 insert 命令标签仍然为 OID 保留一个位置以保持协议兼容性,但由于用户表的 OID 在 PostgreSQL 12 中被完全移除,现在它始终为 0。