在参加会议时,讨论转向了系统专栏,尤其是ctid。这是在备份和数据在磁盘上的物理位置的环境中进行的。问题是,这是否可以用来确定是否需要增量备份,以及是否足以复制ctid所指向的字段。不完全是这样,但我想用这个问题更详细地描述系统列。
在PostgreSQL表中,可能会发现(取决于服务器版本)以下其他系统列:
-
tableoid
-
xmin
-
xmax
-
cmin
-
cmax
-
ctid
-
oid
大多数列存在于每个表中,但在执行SELECT*时被排除在外。但是,可以在查询中包含列,然后PostgreSQL也将返回列的值。
让我们详细介绍一下:
tableoid表格
tableoid是行最初来自的表中的OID。在一个简单的表中,这只是表本身的OID。
oid=# CREATE TABLE show_oid (id INT); CREATE TABLE oid=# SELECT 'public.show_oid'::regclass::oid; oid ------- 79824 (1 row) oid=# INSERT INTO show_oid VALUES (1), (2); INSERT 0 2 oid=# SELECT tableoid, id FROM show_oid; tableoid | id ----------+---- 79824 | 1 79824 | 2 (2 rows)
复制
所有行都来自oid=79824的表。
一旦使用分区,这种情况就会改变。在PostgreSQL中,分区是一个继承的表。简单地说,数据位于子表(分区)中,当查询父表时,PostgreSQL还会搜索所有子表中的数据。下面是一个分区示例。首先,我们创建一个父表,并按范围对其进行分区:
oid=# DROP TABLE IF EXISTS show_oid; DROP TABLE oid=# CREATE TABLE show_oid (id INT) PARTITION BY RANGE (id); CREATE TABLE oid=# CREATE TABLE show_oid_p1 PARTITION OF show_oid oid-# FOR VALUES FROM (1) TO (10); CREATE TABLE oid=# CREATE TABLE show_oid_p2 PARTITION OF show_oid oid-# FOR VALUES FROM (10) TO (20); CREATE TABLE oid=# CREATE TABLE show_oid_p3 PARTITION OF show_oid oid-# FOR VALUES FROM (20) TO (30); CREATE TABLE oid=# SELECT 'public.show_oid'::regclass::oid AS show_oid, oid-# 'public.show_oid_p1'::regclass::oid AS show_oid_p1, oid-# 'public.show_oid_p2'::regclass::oid AS show_oid_p2, oid-# 'public.show_oid_p3'::regclass::oid AS show_oid_p3; show_oid | show_oid_p1 | show_oid_p2 | show_oid_p3 ----------+-------------+-------------+------------- 79827 | 79830 | 79833 | 79836 (1 row)
复制
我们可以看到每个分区都有一个不同的oid。现在,让我们插入一些数据:
oid=# INSERT INTO show_oid VALUES (1), (2), (9), (10), (11), (19), (20), (21), (29); INSERT 0 9 oid=# SELECT tableoid, id FROM show_oid ORDER BY tableoid, id; tableoid | id ----------+---- 79830 | 1 79830 | 2 79830 | 9 79833 | 10 79833 | 11 79833 | 19 79836 | 20 79836 | 21 79836 | 29 (9 rows)
复制
即使数据是从父表中选择的(show_oid,oid=79827),也没有一行从该表返回数据。所有数据都来自其中一个分区。在插入数据期间,PostgreSQL根据分区键将数据移动到分区中。父表保持为空:
oid=# SELECT tableoid, id FROM ONLY show_oid ORDER BY tableoid, id; tableoid | id ----------+---- (0 rows)
复制
这里唯一的关键字告诉PostgreSQL不要扫描任何继承的表。由于父表为空,因此不返回任何行。
xmin+xmax
xmin和xmax列包含事务ID。xmin显示行何时可见(所需的最小事务ID),xmax显示行何时不再可见(最大有效事务ID)。在像PostgreSQL这样的关系数据库中,可以同时有许多正在进行的事务。每个交易都有自己的交易ID,可以查询:
oid=# SELECT txid_current(); txid_current -------------- 7034 (1 row) oid=# SELECT txid_current(); txid_current -------------- 7035 (1 row)
复制
上面的例子浪费了两个事务ID:PostgreSQL中的每个命令都包装在一个事务中。如果不启动事务,PostgreSQL将自动将每个命令包装在单个事务中。这就是为什么示例中的事务ID随着txid\u current()函数的每次调用而增加的原因。
回到我们的问题上来:每个事务都可以更改数据。事务隔离级别指定哪些事务可以看到哪些更改。一旦提交了数据,以后的每个事务都可以看到更改-这一部分很容易。对于正在进行的事务,事务ID指定哪些更改在哪一点可见。让我们来看一个例子。首先,我们需要一张桌子:
oid=# CREATE TABLE show_xmin_xmax (id INT PRIMARY KEY); CREATE TABLE
复制
然后我们启动一个事务并插入数据:
oid=# BEGIN; BEGIN oid=# SELECT txid_current(); txid_current -------------- 7058 (1 row) oid=# INSERT INTO show_xmin_xmax VALUES (1), (2); INSERT 0 2 oid=# SELECT xmin, xmax, id FROM show_xmin_xmax ORDER BY id; xmin | xmax | id ------+------+---- 7058 | 0 | 1 7058 | 0 | 2 (2 rows) oid=# COMMIT; COMMIT
复制
事务ID为7058,该值出现在xmin列中。xmax列为0,因为此数据未被删除(或更新)。
oid=# BEGIN; BEGIN oid=# SELECT txid_current(); txid_current -------------- 7059 (1 row) oid=# SELECT xmin, xmax, id FROM show_xmin_xmax ORDER BY id; xmin | xmax | id ------+------+---- 7058 | 0 | 1 7058 | 0 | 2 (2 rows) oid=# UPDATE show_xmin_xmax SET id = 3 WHERE id = 1; UPDATE 1 oid=# SELECT xmin, xmax, id FROM show_xmin_xmax ORDER BY id; xmin | xmax | id ------+------+---- 7058 | 0 | 2 7059 | 0 | 3 (2 rows) oid=# COMMIT; COMMIT
复制
数据更新后,会出现一个新的行版本,xmin=7059。我们在这里看不到的是,旧行(id=1)也仍然在表中,但是xmax值现在也设置为7059,并且对当前和任何更新的事务都不可见。现在我们看不到这个删除的行,但是它仍然存在。在这篇博文的后面,我们将学习如何使这些数据可见。
然而,有一个技巧可以在删除数据时使更改可见:
oid=# BEGIN; BEGIN oid=# SELECT txid_current(); txid_current -------------- 7060 (1 row) oid=# SELECT xmin, xmax, id FROM show_xmin_xmax ORDER BY id; xmin | xmax | id ------+------+---- 7058 | 0 | 2 7059 | 0 | 3 (2 rows) oid=# DELETE FROM show_xmin_xmax WHERE id = 2 RETURNING xmin, xmax, *; xmin | xmax | id ------+------+---- 7058 | 7060 | 2 (1 row) DELETE 1 oid=# SELECT xmin, xmax, id FROM show_xmin_xmax ORDER BY id; xmin | xmax | id ------+------+---- 7059 | 0 | 3 (1 row) oid=# COMMIT; COMMIT
复制
DELETE的RETURNING子句还可以返回xmin和xmax列。正如我们在本例中所看到的,returnes数据的xmax设置为7060——当前事务ID。之后,对于常规查询,删除的数据不再可见。
cmin+cmax
与xmin和xmax一样,cmin和cmax存储事务信息。但是,这两列将命令序列号存储在事务中。假设您启动一个事务并执行多个命令:每次命令更改数据时,这些更改都会以增加的cmin和cmax信息存储。这有助于跟踪在事务期间的某个时间点发生的更改,并有助于以正确的顺序重播更改。示例表:
oid=# CREATE TABLE show_cmin_cmax (id INT PRIMARY KEY); CREATE TABLE
复制
让我们插入一些数据,但这次在事务中使用多个命令:
oid=# BEGIN; BEGIN oid=# SELECT txid_current(); txid_current -------------- 7042 (1 row) oid=# INSERT INTO show_cmin_cmax VALUES (1), (2); INSERT 0 2 oid=# INSERT INTO show_cmin_cmax VALUES (3), (4); INSERT 0 2 oid=# INSERT INTO show_cmin_cmax VALUES (5), (6); INSERT 0 2 oid=# SELECT xmin, xmax, cmin, cmax, id FROM show_cmin_cmax order by id; xmin | xmax | cmin | cmax | id ------+------+------+------+---- 7042 | 0 | 0 | 0 | 1 7042 | 0 | 0 | 0 | 2 7042 | 0 | 1 | 1 | 3 7042 | 0 | 1 | 1 | 4 7042 | 0 | 2 | 2 | 5 7042 | 0 | 2 | 2 | 6 (6 rows) oid=# COMMIT; COMMIT
复制
我们可以看到,所有行的xmin是7042,但是cmin和cmax随着每次插入而增加。使用相同INSERT命令插入的数据具有相同的cmin和cmax编号。让我们更新一些数据:
oid=# BEGIN; BEGIN oid=# SELECT txid_current(); txid_current -------------- 7043 (1 row) oid=# SELECT xmin, xmax, cmin, cmax, id FROM show_cmin_cmax order by id; xmin | xmax | cmin | cmax | id ------+------+------+------+---- 7042 | 0 | 0 | 0 | 1 7042 | 0 | 0 | 0 | 2 7042 | 0 | 1 | 1 | 3 7042 | 0 | 1 | 1 | 4 7042 | 0 | 2 | 2 | 5 7042 | 0 | 2 | 2 | 6 (6 rows) oid=# UPDATE show_cmin_cmax SET id = 10 WHERE id = 1; UPDATE 1 oid=# UPDATE show_cmin_cmax SET id = 20 WHERE id = 2; UPDATE 1 oid=# SELECT xmin, xmax, cmin, cmax, id FROM show_cmin_cmax order by id; xmin | xmax | cmin | cmax | id ------+------+------+------+---- 7042 | 0 | 1 | 1 | 3 7042 | 0 | 1 | 1 | 4 7042 | 0 | 2 | 2 | 5 7042 | 0 | 2 | 2 | 6 7043 | 0 | 0 | 0 | 10 7043 | 0 | 1 | 1 | 20 (6 rows) oid=# COMMIT; COMMIT
复制
我们可以看到xmin中的事务ID是如何变化的,因为更新发生在一个新的事务中(7043)。cmin和cmax再次从零开始。让我们试试删除并返回的技巧:
oid=# BEGIN; BEGIN oid=# SELECT txid_current(); txid_current -------------- 7044 (1 row) oid=# SELECT xmin, xmax, cmin, cmax, id FROM show_cmin_cmax order by id; xmin | xmax | cmin | cmax | id ------+------+------+------+---- 7042 | 0 | 1 | 1 | 3 7042 | 0 | 1 | 1 | 4 7042 | 0 | 2 | 2 | 5 7042 | 0 | 2 | 2 | 6 7043 | 0 | 0 | 0 | 10 7043 | 0 | 1 | 1 | 20 (6 rows) oid=# DELETE FROM show_cmin_cmax WHERE id = 3 RETURNING xmin, xmax, cmin, cmax, *; xmin | xmax | cmin | cmax | id ------+------+------+------+---- 7042 | 7044 | 0 | 0 | 3 (1 row) DELETE 1 oid=# DELETE FROM show_cmin_cmax WHERE id = 4 RETURNING xmin, xmax, cmin, cmax, *; xmin | xmax | cmin | cmax | id ------+------+------+------+---- 7042 | 7044 | 1 | 1 | 4 (1 row) DELETE 1 oid=# SELECT xmin, xmax, cmin, cmax, id FROM show_cmin_cmax order by id; xmin | xmax | cmin | cmax | id ------+------+------+------+---- 7042 | 0 | 2 | 2 | 5 7042 | 0 | 2 | 2 | 6 7043 | 0 | 0 | 0 | 10 7043 | 0 | 1 | 1 | 20 (4 rows) oid=# COMMIT; COMMIT
复制
因为两次删除都发生在同一个事务中(7044),所以返回的两行都将此事务ID显示为xmax。每个命令的cmin和cmax都在增加。
ctid
ctid列由两部分组成:表中的页面或块,以及在此页面中的位置。让我们来看一个稍微复杂一点的例子:
oid=# CREATE TABLE show_ctid (id INT PRIMARY KEY, data TEXT); CREATE TABLE oid=# ALTER TABLE show_ctid ALTER COLUMN data SET STORAGE plain; ALTER TABLE oid=# INSERT INTO show_ctid VALUES (1, 'Lorem'), (2, 'ipsum'), (3, 'dolor'), (4, 'sit'), (5, 'amet'); INSERT 0 5
复制
该表有一个整数作为主键,一个文本字段作为数据字段。Lorem ipsum的前5个单词用作文本。该列的存储类型更改为“普通”,以保持数据未压缩并在表本身中,并且不将其“外包”到TOAST表中。
oid=# \d+ show_ctid Table "public.show_ctid" Column | Type | Collation | Nullable | Default | Storage | Stats target | Description --------+---------+-----------+----------+---------+---------+--------------+------------- id | integer | | not null | | plain | | data | text | | | | plain | | Indexes: "show_ctid_pkey" PRIMARY KEY, btree (id) Access method: heap
复制
让我们看看ctid:
oid=# SELECT ctid, id, data FROM show_ctid order by id; ctid | id | data -------+----+------- (0,1) | 1 | Lorem (0,2) | 2 | ipsum (0,3) | 3 | dolor (0,4) | 4 | sit (0,5) | 5 | amet (5 rows)
复制
第0页,位置1到5。但这并没有真正的帮助,不是吗?幸运的是,有一个非常有用的扩展,允许我们深入了解细节:pageinspect。它由PostgreSQL contrib提供,应该可以在每个系统上使用。您需要安装它:
oid=# CREATE EXTENSION IF NOT EXISTS pageinspect; CREATE EXTENSION
复制
现在,我们可以使用get_raw_page()函数来选择页面,并使用heap_page_items()对其进行解码:
oid=# SELECT * oid-# FROM heap_page_items(get_raw_page('show_ctid', 0)); lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid | t_data ----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+------------------------ 1 | 8152 | 1 | 34 | 7049 | 0 | 0 | (0,1) | 2 | 2306 | 24 | | | \x010000000d4c6f72656d 2 | 8112 | 1 | 34 | 7049 | 0 | 0 | (0,2) | 2 | 2306 | 24 | | | \x020000000d697073756d 3 | 8072 | 1 | 34 | 7049 | 0 | 0 | (0,3) | 2 | 2306 | 24 | | | \x030000000d646f6c6f72 4 | 8040 | 1 | 32 | 7049 | 0 | 0 | (0,4) | 2 | 2306 | 24 | | | \x0400000009736974 5 | 8000 | 1 | 33 | 7049 | 0 | 0 | (0,5) | 2 | 2306 | 24 | | | \x050000000b616d6574 (5 rows)
复制
有一个t_ctid,它是页面中的位置。这里描述了更多细节。还有t_数据,它保存了行数据。为了解码数据,我们需要知道这些数据是什么。首先,数据是bytea,因此是十六进制编码的。这可以解码:
oid=# SELECT *, oid-# encode(t_data, 'escape') AS decoded oid-# FROM heap_page_items(get_raw_page('show_ctid', 0)); lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid | t_data | decoded ----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+------------------------+----------------------------- 1 | 8152 | 1 | 34 | 7049 | 0 | 0 | (0,1) | 2 | 2306 | 24 | | | \x010000000d4c6f72656d | \x01\000\000\000\rLorem 2 | 8112 | 1 | 34 | 7049 | 0 | 0 | (0,2) | 2 | 2306 | 24 | | | \x020000000d697073756d | \x02\000\000\000\ripsum 3 | 8072 | 1 | 34 | 7049 | 0 | 0 | (0,3) | 2 | 2306 | 24 | | | \x030000000d646f6c6f72 | \x03\000\000\000\rdolor 4 | 8040 | 1 | 32 | 7049 | 0 | 0 | (0,4) | 2 | 2306 | 24 | | | \x0400000009736974 | \x04\000\000\000 sit 5 | 8000 | 1 | 33 | 7049 | 0 | 0 | (0,5) | 2 | 2306 | 24 | | | \x050000000b616d6574 | \x05\000\000\000\x0Bamet (5 rows)
复制
这有点道理。对于PostgreSQL,整个t_数据字段是来自每个列的数据的组合。由于表结构已知,因此不需要分隔符。让我们再次看看表结构:
1.id INT
2.数据文本
整数有4个字节宽,这意味着我们需要查看前4个字节:
-
01\000\000\000 -> 01 00 00 00
-
02\000\000\000 -> 02 00 00 00
-
03\000\000\000 -> 03 00 00 00
-
04\000\000\000 -> 04 00 00 00
这个系统是小端的,这意味着最低有效字节存储在最小(第一个)地址。这4个数字是id字段中的主键,扩展为32位(4字节),因为它是一个整数。
但是这一部分呢:“\rLorem”?这有点复杂。
它是一个文本字段(可变长度)。PostgreSQL中的文本是C结构:长度字段vl\u len,通常为4字节长。和数据字段vl_dat,可容纳高达1 GB的数据。指定长度允许PostgreSQL存储任意数据,包括C中著名的000。然而,它比这更复杂,vl_len保存更多信息。我们需要在位表示中查看此字段:
oid=# SELECT *, oid-# encode(t_data, 'escape') AS decoded, oid-# ('x' || encode(SUBSTR(t_data, 5, 1), 'hex'))::BIT(8) AS bits oid-# FROM heap_page_items(get_raw_page('show_ctid', 0)); lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid | t_data | decoded | bits ----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+------------------------+-----------------------------+---------- 1 | 8152 | 1 | 34 | 7049 | 0 | 0 | (0,1) | 2 | 2306 | 24 | | | \x010000000d4c6f72656d | \x01\000\000\000\rLorem | 00001101 2 | 8112 | 1 | 34 | 7049 | 0 | 0 | (0,2) | 2 | 2306 | 24 | | | \x020000000d697073756d | \x02\000\000\000\ripsum | 00001101 3 | 8072 | 1 | 34 | 7049 | 0 | 0 | (0,3) | 2 | 2306 | 24 | | | \x030000000d646f6c6f72 | \x03\000\000\000\rdolor | 00001101 4 | 8040 | 1 | 32 | 7049 | 0 | 0 | (0,4) | 2 | 2306 | 24 | | | \x0400000009736974 | \x04\000\000\000 sit | 00001001 5 | 8000 | 1 | 33 | 7049 | 0 | 0 | (0,5) | 2 | 2306 | 24 | | | \x050000000b616d6574 | \x05\000\000\000\x0Bamet | 00001011 (5 rows)
复制
当设置最低阶位(在小端上)时,长度字段仅为剩余的7位,而不是整个4个字节。这也意味着剩余数据的长度最多为127字节(2^7-1=127),总长度包括长度的字节。这是一种更有效地存储短值(最多127字节)的快捷方式。
如果未设置最低位,则第二个最低位用于存储更多信息。这将为长度信息保留30位(4字节=32位,减去2位状态),或为未压缩数据保留2^30-1=1073741823字节。Toast数据可以压缩,因此可以存储大约1 GB以上的数据。
在我们的例子中,为所有5行设置了最低位,我们需要查看其余位:
0000110 -> 6
0000110 -> 6
0000110 -> 6
0000100 -> 4
0000101 -> 5
6字节(“Lorem”=5字节,加长度字节)、4字节(“sit”=3字节,加上长度字节)和5字节(“Bamet”=4字节,加长字节)。
为了节省空间,较短的值将始终进入单字节形式。此信息的解码通常由C宏完成。
返回xmin和xmax
之前我们查看了show\u xmin\u xmax表,更新并删除了一些数据,但无法验证删除的数据是否仍在表中。pageinspect也有帮助:
oid=# SELECT *, oid-# encode(t_data, 'escape') AS decoded oid-# FROM heap_page_items(get_raw_page('show_xmin_xmax', 0)); lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid | t_data | decoded ----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+------------+------------------ 1 | 8160 | 1 | 28 | 7058 | 7059 | 0 | (0,3) | 8193 | 1280 | 24 | | | \x01000000 | \x01\000\000\000 2 | 8128 | 1 | 28 | 7058 | 7060 | 0 | (0,2) | 8193 | 256 | 24 | | | \x02000000 | \x02\000\000\000 3 | 8096 | 1 | 28 | 7059 | 0 | 0 | (0,3) | 1 | 10496 | 24 | | | \x03000000 | \x03\000\000\000 (3 rows)
复制
我们在表中看到3个条目:
-
删除的条目(id=1)位于第一个位置,xmax为7059
-
第二个条目是id=2,也被事务7060删除
-
第三个条目是更新操作的结果,该操作将id=1行标记为已删除,同时插入id=3的新行,该操作的xmin=7059
让我们在表上运行VACUUM:
oid=# VACUUM show_xmin_xmax; VACUUM
复制
再看看表:
oid=# SELECT *, encode(t_data, 'escape') AS decoded FROM heap_page_items(get_raw_page('show_xmin_xmax', 0)); lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid | t_data | decoded ----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+------------+------------------ 1 | 0 | 0 | 0 | | | | | | | | | | | 2 | 0 | 0 | 0 | | | | | | | | | | | 3 | 8160 | 1 | 28 | 7059 | 0 | 0 | (0,3) | 1 | 10496 | 24 | | | \x03000000 | \x03\000\000\000 (3 rows)
复制
前两个条目消失了,空间是空的,可以被未来的DML操作重新使用。然而,PostgreSQL不会删除空间,也不会将其返回给操作系统。让我们插入一个新行:
oid=# BEGIN; BEGIN oid=# SELECT txid_current(); txid_current -------------- 7061 (1 row) oid=# INSERT INTO show_xmin_xmax VALUES (5); INSERT 0 1 oid=# SELECT xmin, xmax, id FROM show_xmin_xmax ORDER BY id; xmin | xmax | id ------+------+---- 7059 | 0 | 3 7061 | 0 | 5 (2 rows) oid=# COMMIT; COMMIT
复制
就一行, Vasili.
oid=# SELECT *, encode(t_data, 'escape') AS decoded FROM heap_page_items(get_raw_page('show_xmin_xmax', 0)); lp | lp_off | lp_flags | lp_len | t_xmin | t_xmax | t_field3 | t_ctid | t_infomask2 | t_infomask | t_hoff | t_bits | t_oid | t_data | decoded ----+--------+----------+--------+--------+--------+----------+--------+-------------+------------+--------+--------+-------+------------+------------------ 1 | 8128 | 1 | 28 | 7061 | 0 | 0 | (0,1) | 1 | 2048 | 24 | | | \x05000000 | \x05\000\000\000 2 | 0 | 0 | 0 | | | | | | | | | | | 3 | 8160 | 1 | 28 | 7059 | 0 | 0 | (0,3) | 1 | 10496 | 24 | | | \x03000000 | \x03\000\000\000 (3 rows)
复制
数据库没有在页面末尾追加这一新行,也没有将其写入另一个页面。相反,它重新使用了可用空间,并将其写入ctid=(0,1)。
oid
最后一列是oid。如今,不再支持为行设置oid。在较新版本的PostgreSQL中,尝试添加OID字段会引发错误:
oid=# CREATE TABLE with_oid () WITH (OIDS=TRUE); ERROR: tables declared WITH OIDS are not supported
复制
在11之前的版本中,仍然支持但不鼓励这样做。这意味着,每个oid=TRUE的表都有一个名为“oid”的附加列,该列由不同的事务id填充,这增加了运行真空以防止事务id被包围的需要。
总结
每个PostgreSQL表都有几个额外的列,默认情况下这些列是隐藏的,但当包含在查询中时可以显示。了解每列的详细信息很有帮助,因为它可以很好地了解表结构和PostgreSQL组织数据的方式。
原文标题:ctid and other PostgreSQL table internals
原文作者:Andreas ‘ads’ Scherbaum
原文链接:https://andreas.scherbaum.la/blog/archives/1117-ctid-and-other-PostgreSQL-table-internals.html