INSERT 0 1 到底告诉你什么?

0 阅读3分钟

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() 获取它,并在 PrintQueryStatussrc/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。