pg中每一行除了自定义的字段外,还包含一个行头部,头部包含如下字段
- tableoid 表的OID,可以与pg_class的oid列进行连接来获得表的名称
- xmin 插入该行版本的事务身份(事务ID)
- xmax 删除事务的身份(事务ID)
- cmin 插入事务中的命令标识符(从0开始)
- cmax 删除事务中的命令标识符,或者为0
- infomask 提供一组定义版本属性的信息位。
- ctid 行版本在其表中的物理位置,由块号和相对位置的元组组成(block_id,row_num)。注意尽管ctid可以被用来非常快速地定位行版本,但是一个行的ctid会在被更新或者被VACUUM FULL移动时改变。因此,ctid不能作为一个长期行标识符。 应使用主键来标识逻辑行
- null bitmap 空值位图,标记行的空字段
可以通过sql语句查询隐藏的字段
mydb=# select tableoid,xmin,xmax,cmin,xmax,ctid,* from t;
tableoid | xmin | xmax | cmin | xmax | ctid | id | s
----------+------+------+------+------+-------+----+-------
24603 | 784 | 0 | 0 | 0 | (0,1) | 1 | alice
tuple的DML操作内部实现
创建一个示例表和pageinspect扩展,pageinspect可以分析页内部结构。
#创建表
CREATE TABLE t(
id integer GENERATED ALWAYS AS IDENTITY,
s text
);
#创建索引
CREATE INDEX ON t(s);
#创建扩展
mydb=# create extension pageinspect;
#创建获取行元数据的函数
CREATE FUNCTION heap_page(relname text, pageno integer)
RETURNS TABLE(ctid tid, state text, xmin text, xmax text)
AS $$
SELECT (pageno,lp)::text::tid AS ctid,
CASE lp_flags
WHEN 0 THEN 'unused'
WHEN 1 THEN 'normal'
WHEN 2 THEN 'redirect to '||lp_off
WHEN 3 THEN 'dead'
END AS state,
t_xmin || CASE
WHEN (t_infomask & 256) > 0 THEN ' c'
WHEN (t_infomask & 512) > 0 THEN ' a'
ELSE ''
END AS xmin,
t_xmax || CASE
WHEN (t_infomask & 1024) > 0 THEN ' c'
WHEN (t_infomask & 2048) > 0 THEN ' a'
ELSE ''
END AS xmax
FROM heap_page_items(get_raw_page(relname,pageno))
ORDER BY lp;
$$ LANGUAGE sql;
#创建获取索引元数据的函数
CREATE FUNCTION index_page(relname text, pageno integer)
RETURNS TABLE(itemoffset smallint, htid tid)
AS $$
SELECT itemoffset,
htid -- ctid before v.13
FROM bt_page_items(relname,pageno);
$$ LANGUAGE sql;
- Insert操作
mydb=# begin;
BEGIN
mydb=*# INSERT INTO t(s) VALUES ('FOO');
INSERT 0 1
mydb=*# SELECT pg_current_xact_id();
pg_current_xact_id
--------------------
774
(1 row)
mydb=*# SELECT '(0,'||lp||')' AS ctid,
CASE lp_flags
WHEN 0 THEN 'unused'
WHEN 1 THEN 'normal'
WHEN 2 THEN 'redirect to '||lp_off
WHEN 3 THEN 'dead'
END AS state,
t_xmin as xmin,
t_xmax as xmax,
(t_infomask & 256) > 0 AS xmin_committed,
(t_infomask & 512) > 0 AS xmin_aborted,
(t_infomask & 1024) > 0 AS xmax_committed,
(t_infomask & 2048) > 0 AS xmax_aborted
FROM heap_page_items(get_raw_page('t',0)) ;
ctid | state | xmin | xmax | xmin_committed | xmin_aborted | xmax_committed | xmax_aborted
-------+--------+------+------+----------------+--------------+----------------+--------------
(0,1) | normal | 774 | 0 | f | f | f | t
(1 row)
#提交事务,查询一下行数据,再此查看行上的状态
# 此时xmin_committed变成true,代表此插入事务已提交
mydb=# SELECT '(0,'||lp||')' AS ctid,
CASE lp_flags
WHEN 0 THEN 'unused'
WHEN 1 THEN 'normal'
WHEN 2 THEN 'redirect to '||lp_off
WHEN 3 THEN 'dead'
END AS state,
t_xmin as xmin,
t_xmax as xmax,
(t_infomask & 256) > 0 AS xmin_committed,
(t_infomask & 512) > 0 AS xmin_aborted,
(t_infomask & 1024) > 0 AS xmax_committed,
(t_infomask & 2048) > 0 AS xmax_aborted
FROM heap_page_items(get_raw_page('t',0)) ;
ctid | state | xmin | xmax | xmin_committed | xmin_aborted | xmax_committed | xmax_aborted
-------+--------+------+------+----------------+--------------+----------------+--------------
(0,1) | normal | 788 | 0 | t | f | f | t
(1 row)
# 1. 插入操作时添加一个指针号1到页中,1号指针指向页中的第一个记录。xmin字段设置为当前的事务ID,由于事务还没有提交,xmin_aborted和xmax_committed字段还没有设置。xmax字段为0,因为行还未删除。
# 2. 一旦事务成功完成,必须要记录事务的提交信息。pg使用提交日志clog保持事务提交信息。clog位于PGDATA/pg_xact目录。clog用2个位来标记每个事务:提交和回滚。事务提交后,clog的标志位被设置为commited。
# 3. 当其他事务要访问此数据时,必须要判断xmin事务是否完成。通过查询clog中的事务标志位判断事务是否提交,如果未提交,此行数据将不可见,如果提交,会将提交信息写到tuple头中。
# 4. 行上的事务提交信息是由其他读取的事务更新的,并非插入事务更新。
- Delete操作:
mydb=# BEGIN;
BEGIN
mydb=*# DELETE FROM t;
DELETE 1
mydb=*# SELECT * FROM heap_page('t',0);
ctid | state | xmin | xmax
-------+--------+-------+------
(0,1) | normal | 788 c | 791
(1 row)
mydb=*# commit;
COMMIT
mydb=# select * from t;
id | s
----+---
(0 rows)
mydb=# SELECT * FROM heap_page('t',0);
ctid | state | xmin | xmax
-------+--------+-------+-------
(0,1) | normal | 788 c | 791 c
(1 row)
# 1. 删除操作是在原行的xmax字段写入当前的事务id,由于事务还未提交,因此不知道xmax_commited的状态
# 2. 提交或回滚后在clog中将相应的事务标记为提交或回滚
# 3. 当有事务查询此行时,会更具clog中的事务状态将xmax_commited标记为提交或回滚
- Update操作:
update可以看成时delete和insert的组合,先删除原行,再插入一个新行。
mydb=# SELECT * FROM heap_page('t',0);
ctid | state | xmin | xmax
-------+--------+------+------
(0,1) | normal | 793 | 0 a
(1 row)
mydb=# begin;
BEGIN
mydb=*# UPDATE t SET s = 'Bobo';
UPDATE 1
mydb=*# SELECT pg_current_xact_id();
pg_current_xact_id
--------------------
794
(1 row)
mydb=*# SELECT * FROM heap_page('t',0);
ctid | state | xmin | xmax
-------+--------+-------+------
(0,1) | normal | 793 c | 794
(0,2) | normal | 794 | 0 a
(2 rows)
mydb=*# commit;
COMMIT
mydb=# select * from t;
id | s
----+------
2 | Bobo
(1 row)
mydb=# SELECT * FROM heap_page('t',0);
ctid | state | xmin | xmax
-------+--------+-------+-------
(0,1) | normal | 793 c | 794 c
(0,2) | normal | 794 c | 0 a
(2 rows)
# 1. 先将原行的xmax标记为当前事务id
# 2. 插入新行,将新行的xmin标记为当前事务id
# 3. 提交事务,在clog中标记事务的状态
# 4. 其他事务查询此行时,更新旧行上的xmax提交状态,更新新行上的xmin提交状态
总结:
- 执行插入时,往数据块中插入一行数据,将行上的xmin置为当前事务id,xmax置为0,提交事务时仅更新clog相应事务的状态。
- 删除操作时,将原行的xmax置为当前事务id,提交事务时仅更新clog相应事务的状态。
- 更新操作时,先对原行执行delete操作,再做一个insert操作,提交事务时仅更新clog相应事务的状态。
- 行上的xmin、xmax提交回滚状态是由其他事务查询行时触发更新的。
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。




