暂无图片
暂无图片
1
暂无图片
暂无图片
暂无图片

PostgreSQL 会重写表的几种情况

原创 墨香溪 2025-07-26
490

前几天看到《PostgreSQL DDL变更的坑和巧妙方案》原文链接:https://www.modb.pro/db/1946417163296845824?utm_source=index_ori,里面提到几种情况下会重写表,因为图里面主要是关于DDL 表变更的,里面涉及的DDL 场景比较多,单独总结一下看哪些DDL情况下会重写表并验证一番,增强记忆。 测试基于PG 16

  •  列的数据类型变化
    更改现有列的类型通常会导致整个表及其索引被重写,除非在更改列的类型时,更改后的数据类型没有更改列的内容,旧类型对新类型是二进制强制(binary coercible)或在新类型上不受约束的域。
    但是,索引仍然会被重建,除非系统可以验证新索引在逻辑上等价于现有的。例如列的排序规则改变,那么需要重建索引,因为新的排序顺序可能会有不同。但是,比如我们在将text 类型改成varchar,这种没有排序规则更改的情况下,可以不用重建索引。

    这里比较绕的是二进制强制转换,什么是二进制强制转换?参考 https://developer.aliyun.com/article/228271

    二进制强制转换是指两种类型的转换是“免费的“,转换过程中不需要调用任何函数,比如text 和varchr. 但是二进制转换不是对称关系,比如xml to text,是“免费的”,但是text to xml 则需要额外的检查。

    那么哪些数据类型转换是二进制强制的呢?

    可以使用\dC+ 查看

    

下面测试一下:


ivanyu=# drop table t1;
DROP TABLE
ivanyu=# create table t1 as select * from pg_class;
SELECT 413
ivanyu=# create index on t1(relowner);
CREATE INDEX
ivanyu=# select pg_relation_filepath('t1');
 pg_relation_filepath 
----------------------
 base/16388/24607
(1 row)

ivanyu=# \d t1
                          Table "public.t1"
       Column        |     Type     | Collation | Nullable | Default 
---------------------+--------------+-----------+----------+---------
 oid                 | oid          |           |          | 
 relname             | name         |           |          | 
 relnamespace        | oid          |           |          | 
 reltype             | oid          |           |          | 
 reloftype           | oid          |           |          | 
 relowner            | oid          |           |          | 
 relam               | oid          |           |          | 
 relfilenode         | oid          |           |          | 
 reltablespace       | oid          |           |          | 
 relpages            | integer      |           |          | 
 reltuples           | real         |           |          | 
 relallvisible       | integer      |           |          | 
 reltoastrelid       | oid          |           |          | 
 relhasindex         | boolean      |           |          | 
 relisshared         | boolean      |           |          | 
 relpersistence      | "char"       |           |          | 
 relkind             | "char"       |           |          | 
 relnatts            | smallint     |           |          | 
 relchecks           | smallint     |           |          | 
 relhasrules         | boolean      |           |          | 
 relhastriggers      | boolean      |           |          | 
 relhassubclass      | boolean      |           |          | 
 relrowsecurity      | boolean      |           |          | 
 relforcerowsecurity | boolean      |           |          | 
 relispopulated      | boolean      |           |          | 
 relreplident        | "char"       |           |          | 
 relispartition      | boolean      |           |          | 
 relrewrite          | oid          |           |          | 
 relfrozenxid        | xid          |           |          | 
 relminmxid          | xid          |           |          | 
 relacl              | aclitem[]    |           |          | 
 reloptions          | text[]       | C         |          | 
 relpartbound        | pg_node_tree | C         |          | 
Indexes:
    "t1_relowner_idx" btree (relowner)

ivanyu=# select pg_relation_filepath('t1_relowner_idx');
 pg_relation_filepath 
----------------------
 base/16388/24612
(1 row)

ivanyu=# alter table t1 alter column relowner type integer;
ALTER TABLEivanyu=# \d t1
                          Table "public.t1"
       Column        |     Type     | Collation | Nullable | Default 
---------------------+--------------+-----------+----------+---------
 oid                 | oid          |           |          | 
 relname             | name         |           |          | 
 relnamespace        | oid          |           |          | 
 reltype             | oid          |           |          | 
 reloftype           | oid          |           |          | 
 relowner            | integer      |           |          |  << 字段类型被更改
 relam               | oid          |           |          | 
 relfilenode         | oid          |           |          | 
 reltablespace       | oid          |           |          | 
 relpages            | integer      |           |          | 
 reltuples           | real         |           |          | 
 relallvisible       | integer      |           |          | 
 reltoastrelid       | oid          |           |          | 
 relhasindex         | boolean      |           |          | 
 relisshared         | boolean      |           |          | 
 relpersistence      | "char"       |           |          | 
 relkind             | "char"       |           |          | 
 relnatts            | smallint     |           |          | 
 relchecks           | smallint     |           |          | 
 relhasrules         | boolean      |           |          | 
 relhastriggers      | boolean      |           |          | 
 relhassubclass      | boolean      |           |          | 
 relrowsecurity      | boolean      |           |          | 
 relforcerowsecurity | boolean      |           |          | 
 relispopulated      | boolean      |           |          | 
 relreplident        | "char"       |           |          | 
 relispartition      | boolean      |           |          | 
 relrewrite          | oid          |           |          | 
 relfrozenxid        | xid          |           |          | 
 relminmxid          | xid          |           |          | 
 relacl              | aclitem[]    |           |          | 
 reloptions          | text[]       | C         |          | 
 relpartbound        | pg_node_tree | C         |          | 
Indexes:
    "t1_relowner_idx" btree (relowner)
ivanyu=# select pg_relation_filepath('t1');
 pg_relation_filepath 
----------------------
 base/16388/24607      <<file id没有改变,不会导致重写
(1 row)

ivanyu=# select pg_relation_filepath('t1_relowner_idx');
 pg_relation_filepath 
----------------------
 base/16388/24613     << 索引被重写了
(1 row)
--oid 转换到bigint 需要int8 函数转换,会导致表重写ivanyu=# alter table t1 alter column reltablespace type bigint;
ALTER TABLE
ivanyu=# \d t1
                          Table "public.t1"
       Column        |     Type     | Collation | Nullable | Default 
---------------------+--------------+-----------+----------+---------
 oid                 | oid          |           |          | 
 relname             | name         |           |          | 
 relnamespace        | oid          |           |          | 
 reltype             | oid          |           |          | 
 reloftype           | oid          |           |          | 
 relowner            | integer      |           |          | 
 relam               | oid          |           |          | 
 relfilenode         | oid          |           |          | 
 reltablespace       | bigint       |           |          | 
 relpages            | integer      |           |          | 
 reltuples           | real         |           |          | 
 relallvisible       | integer      |           |          | 
 reltoastrelid       | oid          |           |          | 
 relhasindex         | boolean      |           |          | 
 relisshared         | boolean      |           |          | 
 relpersistence      | "char"       |           |          | 
 relkind             | "char"       |           |          | 
 relnatts            | smallint     |           |          | 
 relchecks           | smallint     |           |          | 
 relhasrules         | boolean      |           |          | 
 relhastriggers      | boolean      |           |          | 
 relhassubclass      | boolean      |           |          | 
 relrowsecurity      | boolean      |           |          | 
 relforcerowsecurity | boolean      |           |          | 
 relispopulated      | boolean      |           |          | 
 relreplident        | "char"       |           |          | 
 relispartition      | boolean      |           |          | 
 relrewrite          | oid          |           |          | 
 relfrozenxid        | xid          |           |          | 
 relminmxid          | xid          |           |          | 
 relacl              | aclitem[]    |           |          | 
 reloptions          | text[]       | C         |          | 
 relpartbound        | pg_node_tree | C         |          | 
Indexes:
    "t1_relowner_idx" btree (relowner)


ivanyu=# select pg_relation_filepath('t1');
 pg_relation_filepath 
----------------------
 base/16388/24614  《〈
(1 row)

ivanyu=# select pg_relation_filepath('t1_relowner_idx');
 pg_relation_filepath 
----------------------
 base/16388/24619 《〈
(1 row)
-- 表和索引被重建。


  • 加非空列
    加非空列会导致表重写,但是也有例外情况,比如使用虚拟列带virtual参数(PG18 才有),虚拟生成列基于原来的列,所以不会导致表重写, 以及默认值为非易失,比如常量,now(),current_timestamp,数学运算函数,时间虽然是变化,但是在一个事务中,是确定的。 常见的易失性函数为 random,currval(),timeofday(),setval, nextval(),clock_tiimestamp。
可以参见 

select proname,provolatile from pg_proc where provolatile='v';


ivanyu=# select pg_relation_filepath('t1');
 pg_relation_filepath 
----------------------
 base/16388/24614
(1 row)

ivanyu=# alter table t1 add column xml_text xml;
ALTER TABLE
ivanyu=# select pg_relation_filepath('t1');
 pg_relation_filepath 
----------------------
 base/16388/24614  《〈可 null 值不会
ivanyu=# alter table t1 add column test_notnull_date date default now(); ALTER TABLE ivanyu=# select pg_relation_filepath('t1'); pg_relation_filepath ---------------------- base/16388/24614 《now 不会 (1 row) ivanyu=# alter table t1 add column generated_col bigint generated always as (relowner+1) stored; ALTER TABLE ivanyu=# select pg_relation_filepath('t1'); pg_relation_filepath ---------------------- base/16388/24623 《stored 虚拟列会ivanyu=# select pg_relation_filepath('t1'); pg_relation_filepath ---------------------- base/16388/24623 (1 row) ivanyu=# alter table t1 add column col_current_timestamp timestamptz default current_timestamp; ALTER TABLE ivanyu=# select pg_relation_filepath('t1'); pg_relation_filepath ---------------------- base/16388/24623 《〈current_timestamp 不会重写 (1 row) ivanyu=# ivanyu=# alter table t1 add column col_clock_timestamp timestamptz default clock_timestamp(); ALTER TABLE ivanyu=# select pg_relation_filepath('t1'); pg_relation_filepath ---------------------- base/16388/24632 《〈 clock_timestamp 会重写 (1 row) ivanyu=# create sequence t1_seq; CREATE SEQUENCE ivanyu=# alter table t1 add column col_nextval bigint default nextval('t1_seq'); ALTER TABLE ivanyu=# select pg_relation_filepath('t1'); pg_relation_filepath ---------------------- base/16388/24640 《〈nextval 会重写 (1 row) ivanyu=# alter table t1 add column col_curval bigint default currval('t1_seq'); ALTER TABLE ivanyu=# select pg_relation_filepath('t1'); pg_relation_filepath ---------------------- base/16388/24647。《〈currval 会重写 (1 row)


  • 修改表为logged or unlogged (SET LOGGED / SET UNLOGGED)

   

ivanyu=# select pg_relation_filepath('t1'); pg_relation_filepath 
----------------------
 base/16388/24647
(1 row)
ivanyu=# alter table t1 set unlogged;
ALTER TABLE
ivanyu=# select pg_relation_filepath('t1');
 pg_relation_filepath 
----------------------
 base/16388/24653.  <<重写
(1 row)

ivanyu=# alter table t1 set  logged;
ALTER TABLE
ivanyu=# select pg_relation_filepath('t1');
 pg_relation_filepath 
----------------------
 base/16388/24659 《重写
(1 row)


  • 改变表的访问方法 set access method
由于表的底层存储格式和访问方式的变化,所以必须重写表。懒得创建新的access method,所有没有测试了。

  • 添加或者修改 stored 属性生成列的表达式也会导致表重写
在PG 18 上可以测试。


表重写是一个很值得小心的操作,因为他会锁表,需要多一倍表大小的空间,还会重建索引。 数据库中有table_rewrite 事件,可以用来创建触发器以监控,或者控制表重写。











「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论