PostgreSQL WHY modify numeric scale must rewrite table

4 minute read

背景

PostgreSQL在修改numeric类型精度的时候,为什么需要rewrite table, 以及如何避免rewrite table?

同时oracle在扩展精度时,耍了什么花招?

正文

前段时间写过一篇关于如何从typmod取出numeric长度和精度的文章, 本文也有牵涉, 所以有兴趣的朋友可以先预读此文 :

http://blog.163.com/digoal@126/blog/static/163877040201371763839672/

事情的起因是这样的,本文要讲的是今天群里一位朋友提到的, PostgreSQL在修改numeric类型精度的时候, 很慢的问题(当然,这个问题后面的PostgreSQL版本也许会使用本文的方法改进其内核代码,就不需要rewrite table了).

很慢的原因是修改精度的时候底层的数据变更了, 需要重建表. 所以如果数据量很大的表, 改精度是会很慢的.

首先来做个实验, 为什么要rewrite表的原因后面具体给出.

PostgreSQL

测试表

digoal=# create table t(id numeric(10,2));  
CREATE TABLE  
digoal=# insert into t select generate_series(1,10000);  
INSERT 0 10000  
digoal=# select pg_relation_filepath('t'::regclass);  
 pg_relation_filepath   
----------------------  
 base/16384/17542  
(1 row)  

当扩展长度时, 没有rewrite table.

digoal=# alter table t alter column id type numeric(11,2);  
ALTER TABLE  
digoal=# select pg_relation_filepath('t'::regclass);  
 pg_relation_filepath   
----------------------  
 base/16384/17542  
(1 row)  

当降低长度时,发生了rewrite.

digoal=# alter table t alter column id type numeric(9,2);  
ALTER TABLE  
digoal=# select pg_relation_filepath('t'::regclass);  
 pg_relation_filepath   
----------------------  
 base/16384/17545  
(1 row)  
  
digoal=# alter table t alter column id type numeric(8,2);  
ALTER TABLE  
digoal=# select pg_relation_filepath('t'::regclass);  
 pg_relation_filepath   
----------------------  
 base/16384/17548  
(1 row)  

当修改了精度时, 也需要rewrite table.

digoal=# alter table t alter column id type numeric(19,3);  
ALTER TABLE  
digoal=# select pg_relation_filepath('t'::regclass);  
 pg_relation_filepath   
----------------------  
 base/16384/17551  
(1 row)  

降低精度和扩展精度一样, 都会rewrite table.

digoal=# alter table t alter column id type numeric(19,2);  
ALTER TABLE  
digoal=# select pg_relation_filepath('t'::regclass);  
 pg_relation_filepath   
----------------------  
 base/16384/17554  
(1 row)  

oracle

SQL> create table t(id numeric(10,2));  
Table created.  
SQL> insert into t select rownum from dual connect by level <=10000;  
10000 rows created.  
SQL> set pagesize 1000 linesize 190  
SQL> select * from dba_extents where segment_name='T';  
  
OWNER                          SEGMENT_NAME                                                                      PARTITION_NAME SEGMENT_TYPE  
------------------------------ --------------------------------------------------------------------------------- ------------------------------ ------------------  
TABLESPACE_NAME                 EXTENT_ID    FILE_ID   BLOCK_ID      BYTES     BLOCKS RELATIVE_FNO  
------------------------------ ---------- ---------- ---------- ---------- ---------- ------------  
SYS                            TTABLE  
SYSTEM                                  0          1      61513      65536          8            1  
  
SYS                            TTABLE  
SYSTEM                                  1          1      61521      65536          8            1  
  
SYS                            TTABLE  
SYSTEM                                  2          1      61529      65536          8            1  

扩展精度不会带来rewrite table,但是oracle扩展精度时, 底层存储的数据并未修改, 在输出时输出也未包含末尾的0, 也就是说oracle没有严格按照字段定义的有效位数来输出. 这也要批评一下oracle耍流氓了.

SQL> alter table t modify id numeric(12,3);  
Table altered.  
SQL> select * from dba_extents where segment_name='T';  
  
OWNER                          SEGMENT_NAME                                                                      PARTITION_NAME SEGMENT_TYPE  
------------------------------ --------------------------------------------------------------------------------- ------------------------------ ------------------  
TABLESPACE_NAME                 EXTENT_ID    FILE_ID   BLOCK_ID      BYTES     BLOCKS RELATIVE_FNO  
------------------------------ ---------- ---------- ---------- ---------- ---------- ------------  
SYS                            TTABLE  
SYSTEM                                  0          1      61513      65536          8            1  
  
SYS                            TTABLE  
SYSTEM                                  1          1      61521      65536          8            1  
  
SYS                            TTABLE  
SYSTEM                                  2          1      61529      65536          8            1  

在不增加长度的情况下, 不允许扩展精度.

SQL> alter table t modify id numeric(12,4);  
alter table t modify id numeric(12,4)  
                     *  
ERROR at line 1:  
ORA-01440: column to be modified must be empty to decrease precision or scale  

长度和精度同时扩展, 不会rewrite table.

SQL> alter table t modify id numeric(13,4);  
Table altered.  
SQL> select * from dba_extents where segment_name='T';  
  
OWNER                          SEGMENT_NAME                                                                      PARTITION_NAME SEGMENT_TYPE  
------------------------------ --------------------------------------------------------------------------------- ------------------------------ ------------------  
TABLESPACE_NAME                 EXTENT_ID    FILE_ID   BLOCK_ID      BYTES     BLOCKS RELATIVE_FNO  
------------------------------ ---------- ---------- ---------- ---------- ---------- ------------  
SYS                            TTABLE  
SYSTEM                                  0          1      61513      65536          8            1  
  
SYS                            TTABLE  
SYSTEM                                  1          1      61521      65536          8            1  
  
SYS                            TTABLE  
SYSTEM                                  2          1      61529      65536          8            1  

PostgreSQL中为什么修改scale, 也就是精度会导致rewrite table 呢?

来看看物理存储(cstring), 4个有效小数位, 末尾会存储00

digoal=# select numeric_out(100.12::numeric(10,4));  
 numeric_out   
-------------  
 100.1200  
(1 row)  

3个有效位, 末尾多一个0

digoal=# select numeric_out(100.12::numeric(10,3));  
 numeric_out   
-------------  
 100.120  
(1 row)  

5个有效位, 末尾多3个0

digoal=# select numeric_out(100.12::numeric(10,5));  
 numeric_out   
-------------  
 100.12000  
(1 row)  

当扩展长度时, 存储的数字和前面一样, 因为只存储了有效数字.

digoal=# select numeric_out(100.12::numeric(20,5));  
 numeric_out   
-------------  
 100.12000  
(1 row)  

下面通过物理存储来得到面向用户的输出, 首先我们如果要输出numeric(10,2)这样的数据. 先得到typmod.

digoal=# select numerictypmodin(_cstring '{10,2}');  
 numerictypmodin   
-----------------  
          655366  
(1 row)  

然后使用numeric_in得到面向用户的输出.

digoal=# select numeric_in('100.1200',1700,655366);  
 numeric_in   
------------  
     100.12  
(1 row)  
digoal=# select numeric_in('100.1550',1700,655366);  
 numeric_in   
------------  
     100.16  
(1 row)  

在物理存储和typmod不匹配的情况下, 输出的值和物理存储不一致.

因此PostgreSQL这套数字物理存储, 就决定了在修改精度时必须要rewrite table.

如果通过修改catalog来达到修改长度的目的, 可以这么做.

digoal=# \d t1  
         Table "public.t1"  
 Column |     Type      | Modifiers   
--------+---------------+-----------  
 id     | numeric(10,3) |   
  
digoal=# select * from t1;  
   id      
---------  
 123.556  
(1 row)  

假设我们要缩小精度到numeric(10,2).

digoal=# select numerictypmodin(_cstring '{10,2}');  
 numerictypmodin   
-----------------  
          655366  
(1 row)  
digoal=# select * from pg_attribute where attrelid ='t1'::regclass and attname='id';  
-[ RECORD 1 ]-+-------  
attrelid      | 16481  
attname       | id  
atttypid      | 1700  
attstattarget | -1  
attlen        | -1  
attnum        | 1  
attndims      | 0  
attcacheoff   | -1  
atttypmod     | 655367  
attbyval      | f  
attstorage    | m  
attalign      | i  
attnotnull    | f  
atthasdef     | f  
attisdropped  | f  
attislocal    | t  
attinhcount   | 0  
attcollation  | 0  
attacl        |   
attoptions    |   
attfdwoptions |   

调整catalog后, 精度修改了.

digoal=# update pg_attribute set atttypmod=655366 where attrelid ='t1'::regclass and attname='id';  
UPDATE 1  
digoal=# select * from t1;  
-[ RECORD 1 ]  
id | 123.556  
digoal=# \d t1  
         Table "public.t1"  
 Column |     Type      | Modifiers   
--------+---------------+-----------  
 id     | numeric(10,2) |   

因为是通过修改catalog达到调整精度的目的, 所以原值不变, 从numeric的物理存储结构来看已经包含了typmod的存储, 见本文末尾, 所以原始值输出的时候numeric_in, 得到的值是123.556, 而不是123.56.

digoal=# insert into t1 values (123.5555);  
INSERT 0 1  
digoal=# select * from t1;  
-[ RECORD 1 ]  
id | 123.556  
-[ RECORD 2 ]  
id | 123.56  

对于扩长度是很有效的, 不会破坏逻辑规则(如上面缩小精度带来的输出原始值123.556的问题), 同时规避了rewrite table的问题.

digoal=# select numerictypmodin(_cstring '{10,4}');  
-[ RECORD 1 ]---+-------  
numerictypmodin | 655368  
  
digoal=# update pg_attribute set atttypmod=655368 where attrelid ='t1'::regclass and attname='id';  
UPDATE 1  
digoal=# \d t1  
         Table "public.t1"  
 Column |     Type      | Modifiers   
--------+---------------+-----------  
 id     | numeric(10,4) |   
  
digoal=# select * from t1;  
-[ RECORD 1 ]  
id | 123.556  
-[ RECORD 2 ]  
id | 123.56  
  
digoal=# insert into t1 values (123.55555);  
INSERT 0 1  
digoal=# select * from t1;  
-[ RECORD 1 ]  
id | 123.556  
-[ RECORD 2 ]  
id | 123.56  
-[ RECORD 3 ]  
id | 123.5556  

小结

1. 在精度调整方面, PostgreSQL目前还是需要改进的, 因为扩展精度不会造成数据变更. 所以可以不rewrite table.

当然如果对PG熟悉的话, 可以通过调整catalog定义来绕过rewrite table, 对普通用户不太实用.

2. oracle不允许表有数据的情况下降低精度, 原因是降低精度会带来数据的变更,

例如 :

digoal=# select numeric_in('100.1550',1700,655366);  
 numeric_in   
------------  
     100.16  
(1 row)  

显然数据已经不一样了.

但是PostgreSQL允许你降低精度,并且没有提示, 这个确实有待改进,

digoal=# create table t1 (id numeric(10,5));  
CREATE TABLE  
digoal=# insert  into t1 values (123.5555);  
INSERT 0 1  
digoal=# select * from t1;  
    id       
-----------  
 123.55550  
(1 row)  
digoal=# alter table t1 alter column id type numeric(10,3);  
ALTER TABLE  
digoal=# select * from t1;  
   id      
---------  
 123.556  
(1 row)  

修改精度后, 数据实际上变更了.

3. 另一方面, oracle扩展精度时, 底层存储的数据并未修改, 在输出时输出也未包含末尾的0, 也就是说oracle没有严格按照字段定义的有效位数来输出. 这也要批评一下oracle耍流氓了.

参考

1. http://blog.163.com/digoal@126/blog/static/163877040201371763839672/

2. src/backend/utils/adt/numeric.c

物理存储.

struct NumericShort  
{  
        uint16          n_header;               /* Sign + display scale + weight */  
        NumericDigit n_data[1];         /* Digits */  
};  
  
struct NumericLong  
{  
        uint16          n_sign_dscale;  /* Sign + display scale */  
        int16           n_weight;               /* Weight of 1st digit  */  
        NumericDigit n_data[1];         /* Digits */  
};  
  
union NumericChoice  
{  
        uint16          n_header;               /* Header word */  
        struct NumericLong n_long;      /* Long form (4-byte header) */  
        struct NumericShort n_short;    /* Short form (2-byte header) */  
};  
  
struct NumericData  
{  
        int32           vl_len_;                /* varlena header (do not touch directly!) */  
        union NumericChoice choice; /* choice of format */  
};  

Flag Counter

digoal’s 大量PostgreSQL文章入口