首页 > 数据库 > 数据库开发技术 > 一个PG字段类型更新的实际案例

我们在处理PG中的表字段类型变更时,很多种情况下会由于锁表的缘故,进而影响业务系统的运行。在云环境下,由于同一套SCHEMA会涉及到全球多个数据中心,这个影响会进一步放大。
因此,在执行表变更的时候,需要倍加小心。本文只是列一个很简单的例子,来说明这个问题的重要性。
这里有一个全局的使用PostgreSQL 12.8的系统,在业务系统中,经过简化后,表DEVICE的定义如下:
mydb=# create table device(id int primary key, useragent varchar(128), col3 timestamp default now(), description varchar(512));
CREATE TABLE
mydb=# insert into device values(1, 'aaaaaaaaaaaaa', default , 'abcd');
INSERT 0 1
mydb=# select * from device;
id | useragent | col3 | description
----+---------------+----------------------------+-------------
1 | aaaaaaaaaaaaa | 2023-10-14 06:03:30.498963 | abcd
(1 row)
经过一段时间运行后,发现useragent字段长度不够用了。时不时的会出现字段值超长的错误。
mydb=# insert into device values(2, repeat('b', 512), default, 'useragent exceed length');
ERROR: value too long for type character varying(128)
但是业务系统庞杂,客户端做变更基本步太现实(涉及到应用程序变更),但是很多用户用的可能还是旧的应用程序。而如果改变服务器端的应用程序,从变更到部署将会花掉很长一段时间。
这个时候,就需要临时在服务器端直接调整字段长度或者去掉长度限制。
mydb=# alter table device alter useragent type varchar;
ALTER TABLE
mydb=# insert into device values(2, repeat('b', 512), default, 'useragent exceed length');
INSERT 0 1
这样处理,速度很快,也暂时不用修改应用服务器端代码。由于这个字段在实际应用中不是孤立存在的,有人担心前段会发送超长的值。实际上,由于它是HTTP header中的一个值,全局上会受HEADER length的限制。
这样救急以后,整个系统又都恢复正常了。但这还不算完。因为表的初始化定义是在部署过程中发生的。因此后边的新的部署以及新版本的更新部署都要考虑到这个变更的发生。
系统使用的是liquibase来进行schema的初始化,并且是多个应用进程的实例同时启动。对表进行初始化时需要考虑两种情况:
当前应用进程中的表初化,用的是原始定义:
mydb=# create table device(id int primary key, useragent varchar(128), col3 timestamp default now(), description varchar(512));
考虑到新系统,以及升级,使用下边的:
alter table device alter useragent type varchar;
由于后台数据库实际上已经变成了:
mydb=# \d device
Table "public.device"
Column | Type | Collation | Nullable | Default
-------------+-----------------------------+-----------+----------+---------
id | integer | | not null |
useragent | character varying | | |
col3 | timestamp without time zone | | | now()
description | character varying(512) | | |
Indexes:
"device_pkey" PRIMARY KEY, btree (id)
等于实际上要做一个:varchar -> varchar的变更。这基本上也不算问题。
理想的情况是做一个判断,如果字段 useragent类型信息是varchar(128),再做具体变更 。如果不是,则保持不变。
勇猛的开发人员会使用:
[06:26:04-postgres@centos1:/var/lib/pgsql]$ psql
psql (12.11)
Type "help" for help.
postgres=# SELECT character_maximum_length, character_octet_length FROM INFORMATION_SCHEMA.COLUMNS
postgres-# WHERE TABLE_NAME = 'device' AND COLUMN_NAME = 'useragent';
character_maximum_length | character_octet_length
--------------------------+------------------------
128 | 512
(1 row)
于是根据这个character_maximum_length值为128,来判断该字段是否需要进行变更。
不幸的是到了PG14以后:
[06:28:35-postgres@centos1:/var/lib/pgsql]$ psql mydb
psql (14.8)
Type "help" for help.
mydb=# SELECT character_maximum_length, character_octet_length FROM INFORMATION_SCHEMA.COLUMNS
mydb-# WHERE TABLE_NAME = 'device' AND COLUMN_NAME = 'useragent';
character_maximum_length | character_octet_length
--------------------------+------------------------
| 1073741824
(1 row)
这意味着,后续的PG升级至14甚至更新的版本以后,部署的时候,上述判断条件将会不成立,字段依然会是原来128的长度。那个时候将会是一场灾难。
上边的查询暴露出了不同版本PG元信息查询可能有不完全兼容的问题。
如何让这个问题变得更细腻和更兼容呢?
SELECT pg_catalog.format_type(a.atttypid, a.atttypmod) FROM pg_catalog.pg_attribute a WHERE a.attnum > 0 AND a.attname = 'useragent' AND NOT a.attisdropped
AND a.attrelid = (
SELECT c.oid
FROM pg_catalog.pg_class c
JOIN pg_catalog.pg_namespace n ON n.oid = c.relnamespace AND n.nspname = current_schema()
WHERE c.relname = 'device'
);
format_type
------------------------
character varying(128)
(1 row)
可以直接使用format_type将实际的字段类型定义输出来进行比对,而这个值的生成在PG12和PG14中是完全兼容的。至此问题得以彻底解决。

原文作者:数据库杂记;如有侵权,请联系管理员删除。