每个表都有多个系统字段, 这些字段是由系统隐式定义的。
这些系统字段在psql中使用“\d”命令返回的结果中并不显示, 所以需要记住实际表中还存在这些隐含字段。 因为表中已隐含这些名字的字段, 所以用户定义的名称不能与这些字段的名称相同, 这一限制与名字是否为关键字没有关系, 即使字段名称用双引号括起来也不行。
这些系统字段如下:
-
oid: 行对象标识符(对象ID)。
该字段只有在创建表时使用了“with oids”或配置参数“default_with_oids”的值为真时出现。
该字段的类型是oid(类型名和字段名相同,对象标识符类型)。 -
tableoid: 包含本行的表的oid。( 后文就不详细介绍了)
对父表(该表存在有继承关系的子表) 进行查询时,使用此字段就可以知道某一行来自父表还是子表, 以及是来自哪个子表。
tableoid可以和pg_class的oid字段连接起来获取表名字。 -
xmin: 插入该行版本的事务ID。
-
xmax: 删除此行时的事务ID, 第一次插入时, 此字段为0。
如果查询出来此字段不为0, 则可能是删除这行的事务还未提交, 或者是删除此行的事务回滚了。 -
cmin: 事务内部的插入类操作的命令ID, 此标识是从0开始的。
-
cmax: 事务内部的删除类操作的命令ID, 如果不是删除命令, 此字段为0。
-
ctid: 一个行版本在它所处的表内的物理位置。
oid
PostgreSQL在内部使用对象标识符(OID) 作为各种系统表的主键。
系统不会给用户创建的表增加一个oid系统字段, 但用户可以在建表时使用“with oids”选项为表增加oid字段。 目前oid类型用一个4字节的无符号整数实现, 它不能提供大数据范围内的唯一性保证, 甚至在单个的大表中也不行。
因此, PostgreSQL官方不鼓励在用户创建的表中使用oid字段, 建议oid字段只是用于系统表。
另外, 不同表的oid字段生成的序列值是全局的, 并不是由某一张表单独分配,就好像所有的oid都使用了一个全局的序列。
oid类型代表一个对象标识符。
除此以外, oid还有几个表示具体对象类型的别名, 如下表:
| 类型名称 | 引用 | 描述 | 数值示例 |
|---|---|---|---|
| oid | 任意 | 数字形式的对象标识符 | 564182 |
| regclass | pg_class | 关系名字 | pg_type |
| regcollation | pg_collation | 排序规则名称 | “POSIX” |
| regconfig | pg_ts_config | 文本搜索配置 | english |
| regdictionary | pg_ts_dict | 文本搜索字典 | simple |
| regnamespace | pg_namespace | 命名空间名称 | pg_catalog |
| regoper | pg_operator | 操作符名字 | + |
| regoperator | pg_operator | 带参数类型的操作符 | *(integer,integer) or -(NONE,integer) |
| regproc | pg_proc | 函数名字 | sum |
| regprocedure | pg_proc | 函数与参数类型 | sum(int4) |
| regrole | pg_authid | 角色名 | smithee |
| regtype | pg_type | 数据类型名称 | integer |
表(包括toast表) 、 索引、 视图的对象标识符就是系统表“pg_class”的oid字段的值,示例如下:
osdba=# select oid,relname,relkind from pg_class where relname like 't_';
oid | relname | relkind
-------+---------+---------
16628 | t1 | r
16631 | t2 | r
16649 | t3 | r
16653 | t4 | r
(4 rows)
上例显示表“t1”的对象标识符为“16628”, 表“t2”的对象标识符为“16631”, 表“t3”的对象标识符为“16649”, 表“t4”的对象标识符为“16653”。
除oid这种通用的对象标识符类型外, 其他的类型都提供一种把字符串转换成oid类型的操作符, 这可以大大简化查询对象信息时的SQL语句, 示例如下。
想要知道对象标识符为“1259”的表是哪一张, 可以使用如下SQL命令查询:
osdba=# select 1259::regclass;
regclass
----------
pg_class
(1 row)
要查询系统表, 看表“t”有哪些字段, 一般的SQL命令是需要先查询pg_attribute表再关联pg_class表的, 示例如下:
osdba=# SELECT attrelid,attname,atttypid,attlen, attnum,attnotnull FROM pg_attribute
WHERE attrelid = (SELECT oid FROM pg_class WHERE relname = 't');
attrelid | attname | atttypid | attlen | attnum | attnotnull
----------+----------+----------+--------+--------+------------
16625 | tableoid | 26 | 4 | -7 | t
16625 | cmax | 29 | 4 | -6 | t
16625 | xmax | 28 | 4 | -5 | t
16625 | cmin | 29 | 4 | -4 | t
16625 | xmin | 28 | 4 | -3 | t
16625 | ctid | 27 | 6 | -1 | t
16625 | id | 23 | 4 | 1 | f
(7 rows)
使用regclass类型的自动转换运算符就可以不关联查询pg_class了, 示例如下:
osdba=# SELECT attrelid,attname,atttypid,attlen, attnum,attnotnull FROM pg_attribute
WHERE attrelid = 't'::regclass;
attrelid | attname | atttypid | attlen | attnum | attnotnull
----------+----------+----------+--------+--------+------------
16625 | tableoid | 26 | 4 | -7 | t
16625 | cmax | 29 | 4 | -6 | t
16625 | xmax | 28 | 4 | -5 | t
16625 | cmin | 29 | 4 | -4 | t
16625 | xmin | 28 | 4 | -3 | t
16625 | ctid | 27 | 6 | -1 | t
16625 | id | 23 | 4 | 1 | f
(7 rows)
下例中的SQL命令是查询操作符的左右操作数的类型, 使用“::regtype”后也不再需要关联查询pg_type系统表, 示例如下:
osdba=# select oprname,oprleft::regtype, oprright::regtype,oprresult::regtype,
oprcode from pg_operator limit 3;
oprname | oprleft | oprright | oprresult | oprcode
---------+---------+----------+-----------+---------
= | integer | bigint | boolean | int48eq
<> | integer | bigint | boolean | int48ne
< | integer | bigint | boolean | int48lt
(3 rows)
ctid
ctid表示数据行在它所处的表内的物理位置。
ctid字段的类型是tid。
尽管ctid可以非常快速地定位数据行, 但每次VACUUM FULL之后, 数据行在块内的物理位置会移动, 即ctid会发生变化, 所以ctid是不能作为长期的行标识符的, 应该使用主键来标识逻辑行。
查看ctid的示例如下:
osdba=# select ctid, id from t limit 10;
ctid | id
--------+----
(0,1) | 1
(0,2) | 1
(0,3) | 2
(0,4) | 3
(0,5) | 4
(0,6) | 5
(0,7) | 6
(0,8) | 7
(0,9) | 8
(0,10) | 9
(10 rows)
从上例中可以看出, ctid由两个数字组成, 第一个数字表示数据行所在的物理块的物理块号, 第二个数字表示数据行在物理块中的行号。
tid类型可以使用字符串输入, 如查询表“testtab01”的第10个物理块的第2行内容的命令如下:
osdba=# select ctid, id from testtab01 where ctid='(10,2)';
ctid | id
--------+-----
(10,2) | 652
(1 row)
利用ctid可以删除表中的重复记录, 如表“t”中有如下数据:
osdba=# select * from t;
id
----
123123
(6 rows)
删除此表中重复数据的SQL命令如下:
DELETE FROM t a
WHERE a.ctid <> (SELECT min(b.ctid)
FROM t b
WHERE a.id = b.id);
上例的SQL语句在表“t”中的记录比较多时, 效率比较低, 这时可以使用一个更高效的删除此表重复数据的SQL命令:
DELETE FROM t
WHERE ctid = ANY(ARRAY(SELECT ctid
FROM (SELECT row_number() OVER (PARTITION BY id), ctid
FROM t) x
WHERE x.row_number > 1));
xmin、 xmax、 cmin、 cmax
xmin、 xmax、 cmin、 cmax这4个字段在多版本实现中用于控制数据行是否对用户可见。
PostgreSQL将修改前后的数据存储在相同的结构中, 分为以下几种情况:
- 新插入一行时, 将新插入行的xmin填写为当前的事务ID, xmax填“0”。
- 修改这一行时, 实际上新插入一行, 原数据行上的xmin不变, xmax改为当前的事务ID, 新数据行上的xmin填为当前的事务ID, xmax填“0”。
- 删除一行时, 把被删除行上的xmax填写当前的事务ID。
从上面的叙述中就可以知道, xmin就是标记插入数据行的事务ID, 而xmax就是标记删除数据行的事务ID。
没有修改数据行的操作, 因为修改数据行, 实际上就是把原数据行上的xmax标记上自己的事务ID(相当于打上删除标记) , 然后再新插入一条记录。所以最新版本的数据即xmax为0的一条。
上面解释了xmin和xmax的含义, 另两个字段“cmin”和“cmax”有什么作用呢?
cmin和cmax用于判断同一个事务内的不同命令导致行版本的变化是否可见。
如果一个事务内的所有命令都是严格顺序执行的, 那么每个命令都能看到之前该事务内的所有变更, 这种情况下不需要使用命令标识。
一般编程中, 遍历一个数组或列表时, 是不允许在遍历过程中删除或增加元素的, 因为这样会导致逻辑错误。
而在数据库中, 对游标进行遍历时, 可以对游标引用的表进行插入或删除行的操作而不会出现逻辑错误, 这是因为游标是一个快照,遍历过程中的删除或增加操作不会影响游标的数据, 遍历游标时看到的是声明游标时的数据快照而不是执行时的数据, 所以它在扫描数据时会忽略声明游标后对数据的变更, 因为这些变更对该游标都应该是无效的。
游标后续看到的数据都是声明游标之前的快照, 相当于游标与后续的命令并发交错执行, 这与事务之间的交错执行类似, 存在数据可见性的问题。
PostgreSQL使用与解决事务内可见性问题类似的方法引入了命令ID的概念。
行上记录了操作这行的命令ID, 当其他命令读取这行数据时, 如果当前的命令ID大于等于数据行上的命令ID, 说明这行数据是可见的; 如果当前的命令ID小于数据行上的命令ID, 则这条数据不可见。
命令ID的分配规则如下:
- 每个命令使用事务内一个全局命令标识计数器的当前值作为当前命令标识。
- 事务开始时, 命令标识计数器被置为初值“0”。
- 执行更新性的命令时, 如INSERT、 UPDATE、 DELETE、 SELECT…FOR UPDATE, 在SQL命令执行后命令标识计数器加1。
- 当命令标识计数器经过不断累加又回到初值“0”时, 报错“cannot have more than 2^32-1 commands in a transaction”, 即一个事务中命令的个数最多为232-1个。




