PostgreSQL学徒

一个由于命名引起的坑爹案例

前言

这两天被一个命名引起的问题折磨得够呛,闲话少叙,让我们看下这个有趣案例以及背后的原理,稍不注意你也可能掉坑里了!

复现

复现一下现场,造一个分区表

postgres=# create table test_partition(id int,info text,t_time timestamp) partition by range(t_time);
CREATE TABLE
postgres=# create table test_partition_01 partition of test_partition for values FROM ('2022-01-01') TO ('2023-01-01');
CREATE TABLE
postgres=# create table test_partition_02 partition of test_partition for values FROM ('2023-01-01') TO ('2024-01-01');
CREATE TABLE
postgres=# \d test_partition
               Partitioned table "public.test_partition"
 Column |            Type             | Collation | Nullable | Default 
--------+-----------------------------+-----------+----------+---------
 id     | integer                     |           |          | 
 info   | text                        |           |          | 
 t_time | timestamp without time zone |           |          | 
Partition key: RANGE (t_time)
Number of partitions: 2 (Use \d+ to list them.)

由于目前分区表不支持在父表上使用CIC的形式创建索引,这也是被诟病最多的地方。所以一般我们会选择在子表上使用CIC并行创建,最后ATTACH。那么如果资源富足,可以开启多个会话同时给多个子表创建索引,看个栗子 (此处使用普通形式创建,为了模拟和Greenplum的AOCO表一致行为,因为Greenplum AOCO不支持CIC给子表添加索引):

postgres=# begin;
BEGIN
postgres=*# create index on test_partition_01(id);
CREATE INDEX
postgres=*# select pg_backend_pid();
 pg_backend_pid 
----------------
          12212
(1 row)

给另一个子表添加索引

postgres=# begin;
BEGIN
postgres=*# create index on test_partition_02(id);
CREATE INDEX
postgres=*# select pg_backend_pid();
 pg_backend_pid 
----------------
          12322
(1 row)

可以看到,这种方式是可行的,锁不冲突。

那么这次困扰我许久的问题是什么原因呢?让我们稍作修改,整点活儿,让名字更长点!

postgres=# create table a_long_table_name_with_exactly_sixty_three_characters_testtbl01(id int,info text,t_time timestamp) partition by range(t_time);     ---父表
CREATE TABLE
postgres=# create table a_long_table_name_with_exactly_sixty_three_characters_test_par1 partition of a_long_table_name_with_exactly_sixty_three_characters_testtbl01 for values FROM ('2023-01-01') TO ('2024-01-01');  ---子表
CREATE TABLE                                                        
postgres=# create table a_long_table_name_with_exactly_sixty_three_characters_test_par2 partition of a_long_table_name_with_exactly_sixty_three_characters_testtbl01 for values FROM ('2022-01-01') TO ('2023-01-01');  ---子表
CREATE TABLE

postgres=# \d a_long_table_name_with_exactly_sixty_three_characters_testtbl01
Partitioned table "public.a_long_table_name_with_exactly_sixty_three_characters_testtbl01"
 Column |            Type             | Collation | Nullable | Default 
--------+-----------------------------+-----------+----------+---------
 id     | integer                     |           |          | 
 info   | text                        |           |          | 
 t_time | timestamp without time zone |           |          | 
Partition key: RANGE (t_time)
Number of partitions: 2 (Use \d+ to list them.)

这名字想必各位应该就可窥见一二了,这次再让我们老样子给子表加下索引

postgres=# begin;
BEGIN
postgres=*# create index on a_long_table_name_with_exactly_sixty_three_characters_test_par1(id,info);
CREATE INDEX
postgres=*# select pg_backend_pid();
 pg_backend_pid 
----------------
          12212
(1 row)

另一个会话添加索引

postgres=# begin;
BEGIN
postgres=*# select pg_backend_pid();
 pg_backend_pid 
----------------
          12322
(1 row)

postgres=*# create index on a_long_table_name_with_exactly_sixty_three_characters_test_par2(id,info);
---此处卡住

为什么这次就卡住了?等待事件显式在等待transactionid,这个我们很熟悉了,典型场景就是在等待行锁。CTRL+C的话,可以看到是卡在了pg_class_relname_nsp_index

^CCancel request sent
ERROR:  canceling statement due to user request
CONTEXT:  while inserting index tuple (11,37) in relation "pg_class_relname_nsp_index"

postgres=# select pid,pg_blocking_pids(pid),wait_event,wait_event_type,query from pg_stat_activity where query like '%create index%' and pid <> pg_backend_pid();
-[ RECORD 1 ]----+------------------------------------------------------------------------------------------
pid              | 12322
pg_blocking_pids | {12212}
wait_event       | transactionid
wait_event_type  | Lock
query            | create index on a_long_table_name_with_exactly_sixty_three_characters_test_par2(id,info);

这个索引位于pg_class上面,其定义不难理解:relname+relnamespace的唯一组合,意味着在某个模式下面,命名是独一无二的

Indexes:
    "pg_class_oid_index" PRIMARY KEY, btree (oid)
    "pg_class_relname_nsp_index" UNIQUE CONSTRAINT, btree (relname, relnamespace)
    "pg_class_tblspc_relfilenode_index" btree (reltablespace, relfilenode)

那么让我们回过头来看看,为什么会卡住:建索引的时候不指定索引名,那么会默认用表明+字段名+idx的后缀命名,看看第一个会话的索引

postgres=# begin;
BEGIN
postgres=*# create index on a_long_table_name_with_exactly_sixty_three_characters_test_par1(id,info);
CREATE INDEX
postgres=*# \d a_long_table_name_with_exactly_sixty_three_characters_test_par1
Table "public.a_long_table_name_with_exactly_sixty_three_characters_test_par1"
 Column |            Type             | Collation | Nullable | Default 
--------+-----------------------------+-----------+----------+---------
 id     | integer                     |           |          | 
 info   | text                        |           |          | 
 t_time | timestamp without time zone |           |          | 
Partition of: a_long_table_name_with_exactly_sixty_three_characters_testtbl01 FOR VALUES FROM ('2023-01-01 00:00:00') TO ('2024-01-01 00:00:00')
Indexes:
    "a_long_table_name_with_exactly_sixty_three_characte_id_info_idx" btree (id, info)

其索引变成了a_long_table_name_with_exactly_sixty_three_characte_id_info_idx,被自动截断了!

postgres=*# select length('a_long_table_name_with_exactly_sixty_three_characte_id_info_idx');
 length 
--------
     63
(1 row)

因此,第二个会话为什么被阻塞也就说得通了,其创建的索引也是这个!当然需要等待了。如果第一个会话提交了,报错就更加明显了。

postgres=# begin;
BEGIN
postgres=*# create index on a_long_table_name_with_exactly_sixty_three_characters_test_par2(id,info);
CREATE INDEX
postgres=*# \d a_long_table_name_with_exactly_sixty_three_characters_test_par2
Table "public.a_long_table_name_with_exactly_sixty_three_characters_test_par2"
 Column |            Type             | Collation | Nullable | Default 
--------+-----------------------------+-----------+----------+---------
 id     | integer                     |           |          | 
 info   | text                        |           |          | 
 t_time | timestamp without time zone |           |          | 
Partition of: a_long_table_name_with_exactly_sixty_three_characters_testtbl01 FOR VALUES FROM ('2022-01-01 00:00:00') TO ('2023-01-01 00:00:00')
Indexes:
    "a_long_table_name_with_exactly_sixty_three_characte_id_info_idx" btree (id, info)

    postgres=# begin;   ---第一个会话提交
BEGIN
postgres=*# create index on a_long_table_name_with_exactly_sixty_three_characters_test_par2(id,info);
ERROR:  duplicate key value violates unique constraint "pg_class_relname_nsp_index"
DETAIL:  Key (relname, relnamespace)=(a_long_table_name_with_exactly_sixty_three_characte_id_info_idx, 2200) already exists.

原理

在官网上有这段说明,https://www.postgresql.org/docs/16/runtime-config-preset.html

The system uses no more than NAMEDATALEN-1 bytes of an identifier; longer names can be written in commands, but they will be truncated. By default, NAMEDATALEN is 64 so the maximum identifier length is 63 bytes. If this limit is problematic, it can be raised by changing the NAMEDATALEN constant in src/include/pg_config_manual.h.

系统使用的标识符长度不超过 NAMEDATALEN-1 字节;在命令中可以写入更长的名称,但它们会被截断。默认情况下,NAMEDATALEN 的值为 64,因此最大标识符长度为 63 字节。如果这个限制造成问题,可以通过更改 src/include/pg_config_manual.h 中的 NAMEDATALEN 常量来提高这个限制。

代码中写死了标识符的最大长度,即63个字节,最后一位为结束符'\0',再长的话会被自动截断以适合此限制

/*
 * Maximum length for identifiers (e.g. table names, column names,
 * function names).  Names actually are limited to one fewer byte than this,
 * because the length must include a trailing zero byte.
 *
 * Changing this requires an initdb.
 */

#define NAMEDATALEN 64

将日志设置为NOTICE便可以看到此日志:

postgres=# begin;
BEGIN
postgres=*# create table long_long_long_long_long_longlong_long_longlong_long_longlong_long_long(id int);
NOTICE:  identifier "long_long_long_long_long_longlong_long_longlong_long_longlong_long_long" will be truncated to "long_long_long_long_long_longlong_long_longlong_long_longlong_l"
CREATE TABLE

要修改此限制你需要修改代码,重编译,重新初始化,这无疑太过于繁琐,因此需要知晓此特性,取名的时候精简,不要虚头巴脑整一大堆,整出这些幺蛾子。

除此之外,还有许多类似限制,都在pg_config_manual.h中。比如:

  1. #define FUNC_MAX_ARGS       100:Maximum number of arguments to a function.
  2. #define INDEX_MAX_KEYS      32:Maximum number of columns in an index.
  3. #define PARTITION_MAX_KEYS  32:Maximum number of columns in a partition key
  4. #define NUM_SPINLOCK_SEMAPHORES     128:类似于Oracle latch
ItemUpper LimitComment
database sizeunlimited
number of databases4,294,950,911
relations per database1,431,650,303
relation size32 TBwith the default BLCKSZ of 8192 bytes
rows per tablelimited by the number of tuples that can fit onto 4,294,967,295 pages
columns per table1600further limited by tuple size fitting on a single page; see note below
field size1 GB
identifier length63 bytescan be increased by recompiling PostgreSQL
indexes per tableunlimitedconstrained by maximum relations per database
columns per index32can be increased by recompiling PostgreSQL
partition keys32can be increased by recompiling PostgreSQL

小结

这个案例最开始想复杂了,以为又踩到了什么BUG,其实Greenplum和PostgreSQL的机制类似,字符长度限制为63字节,所以为了避免整出各类幺蛾子,还是老老实实精简下标识符长度,言简意赅,主打一个"惜字如金"。

Image

参考

https://www.postgresql.org/docs/16/runtime-config-preset.html