PostgreSQL学徒

日常答疑系列第一期

1前言

周末因为朋友来了成都嗨了两天,群聊记录基本没怎么看,所以赶着晚上看了一下群聊里面的问题。简单做个记录吧,之前群里做的一些答疑发现已经有热心群友做了记录了,👉🏻 https://www.modb.pro/db/614297,后续公众号上也会定期收集解答一下几个群里各位群友的问题。

2问题 I

Image

首先是 setof record,record 对应记录类型或者记录变量,记录变量类似于行类型变量,但是它们没有预定义的结构,只能通过 select 或 for 命令来获取实际的行结构,因此记录变量在被初始化之前无法访问。至于 setof 则是返回结果集,一个集合,看个例子

postgres=# create table test1(id int,info text);
CREATE TABLE
postgres=# insert into test1 values(1,'hello');
INSERT 0 1
postgres=# insert into test1 values(2,'world');
INSERT 0 1
postgres=# CREATE OR REPLACE FUNCTION myfunc ()
    RETURNS SETOF test1
    AS $$
DECLARE
    var record;
    t_sql text := 'select * from test1';
BEGIN
    FOR var IN EXECUTE t_sql LOOP
        RETURN NEXT var;
    END LOOP;
END;
$$
LANGUAGE plpgsql;
CREATE FUNCTION
postgres=# select * from myfunc();
 id | info  
----+-------
  1 | hello
  2 | world
(2 rows)

上面是常规的用法,再看下 setof record

postgres=# CREATE OR REPLACE FUNCTION myfunc2 ()
    RETURNS SETOF record
    AS $$
DECLARE
    var record;
    t_sql text := 'select * from test1';
BEGIN
    FOR var IN EXECUTE t_sql LOOP
        RETURN NEXT var;
    END LOOP;
END;
$$
LANGUAGE plpgsql;
CREATE FUNCTION
postgres=# select * from myfunc2();
ERROR:  a column definition list is required for functions returning "record"
LINE 1: select * from myfunc2();
                      ^
postgres=# select * from myfunc2() as (myid int,myinfo text);  ---指定结构
 myid | myinfo 
------+--------
    1 | hello
    2 | world
(2 rows)

可以看到,第二种用法报错如前面所说,没有预先定义的结构,所以需要手动指定。

3问题 II

Image

这个是关于底层元组的,简单模拟一下

postgres=# create table t2(id int);
CREATE TABLE
postgres=# insert into t2 values(1);
INSERT 0 1
postgres=# insert into t2 values(2);
INSERT 0 1
postgres=# insert into t2 values(3);
INSERT 0 1
postgres=# select lp,lp_len,t_data from heap_page_items(get_raw_page('t2', 0));
 lp | lp_len |   t_data   
----+--------+------------
  1 |     28 | \x01000000
  2 |     28 | \x02000000
  3 |     28 | \x03000000
(3 rows)

postgres=# create table test(s varchar);
CREATE TABLE
postgres=# insert into test values('abcd');
INSERT 0 1
postgres=# insert into test values('abc');
INSERT 0 1
postgres=# select lp,lp_len,t_data from heap_page_items(get_raw_page('test', 0));
 lp | lp_len |    t_data    
----+--------+--------------
  1 |     29 | \x0b61626364
  2 |     28 | \x09616263
(2 rows)

这个 lp_len 是元组的长度,关于 pageinspect 的返回可以参照源码src/include/storage/itemid.handsrc/include/access/htup_details.h,那么这个 28 字节是怎么来的呢?首先是元组头,每条元组都有 TupleHeader,结构如下👇🏻

Image

There is a fixed-size header (occupying 23 bytes on most machines), followed by an optional null bitmap, an optional object ID field, and the user data. The header is detailed in Table 70.4. The actual user data (columns of the row) begins at the offset indicated by t_hoff, which must always be a multiple of the MAXALIGN distance for the platform. The null bitmap is only present if the HEAP_HASNULL bit is set in t_infomask. If it is present it begins just after the fixed header and occupies enough bytes to have one bit per data column (that is, the number of bits that equals the attribute count in t_infomask2). In this list of bits, a 1 bit indicates not-null, a 0 bit is a null. When the bitmap is not present, all columns are assumed not-null. The object ID is only present if the HEAP_HASOID_OLD bit is set in t_infomask. If present, it appears just before the t_hoff boundary. Any padding needed to make t_hoff a MAXALIGN multiple will appear between the null bitmap and the object ID. (This in turn ensures that the object ID is suitably aligned.)

有一个固定大小的头(在大多数机器上占用23个字节),然后是一个可选的空值位图,一个可选的对象ID字段,以及用户数据。实际的用户数据(行的列)从t_hoff指示的偏移量开始,它必须始终是平台的MAXALIGN距离的倍数。只有在t_infomask中设置了HEAP_HASNULL位,才会出现空值位图。如果它存在,它就在固定头之后开始,并占据足够的字节,以便每个数据列有一个比特(即等于t_infomask2中属性计数的比特数)。在这个位列表中,1位表示非空,0位为空。当位图不存在时,所有的列都被认为是不空的。只有当HEAP_HASOID_OLD位在t_infomask中被设置时,对象ID才会出现。如果存在,它就会出现在t_hoff边界之前。任何使t_hoff成为MAXALIGN倍数所需的填充将出现在空位图和对象ID之间。(这反过来又保证了对象ID的适当对齐。)

注释已经十分清晰了,由 23byte固定大小的前缀和可选的 NullBitMap 构成,假如还有 oid 的话,会在 t_hoff 前面,当分配的 oid 超过 4 字节整形最大值的时候会重新从 0 开始分配,但这并不会导致类似于事务 ID 回卷那样严重的影响,值得注意的是,从 v12 开始 default_with_oids 参数就没了,The parameter default_with_oids is gone, it had been disabled by default since after PostgreSQL 8.0,并且the default_with_oids parameter cannot be changed to 'on'。加上对齐,所以是 24 字节,加上 intger 的 4 字节,所以是 28,至于 t_data 则是 16 进制的表示,加上一定的对齐。

而第二行稍微复杂一点,t_data 的返回有个 0x0b,各位可能有点陌生,这个其实我在之前也写过详细的源码分析,关于变长的字段会有一些变长头 varlena,核心原理如下 👇🏻

/*
 * Bit layouts for varlena headers on big-endian machines:
 *
 * 00xxxxxx 4-byte length word, aligned, uncompressed data (up to 1G)
 * 01xxxxxx 4-byte length word, aligned, *compressed* data (up to 1G)
 * 10000000 1-byte length word, unaligned, TOAST pointer
 * 1xxxxxxx 1-byte length word, unaligned, uncompressed data (up to 126b)
 *
 * Bit layouts for varlena headers on little-endian machines:
 *
 * xxxxxx00 4-byte length word, aligned, uncompressed data (up to 1G)
 * xxxxxx10 4-byte length word, aligned, *compressed* data (up to 1G)
 * 00000001 1-byte length word, unaligned, TOAST pointer
 * xxxxxxx1 1-byte length word, unaligned, uncompressed data (up to 126b)

Image

0x0b 就是 00001011(后面7个bit不全为0,所以是varattrib_1b的数据类型),代表header,剩下的0000101,就是长度,总长度 5byte(包含header),所以纯元组的长度是 4byte,61、62、63、64则是十六进制的 abcd 的 ASCII 码。

不熟悉的可以回过头去温顾一下了。

4问题 III

Image

这个问题很常见,各种各样的 xmin/catalog_xmin,vacuum full 重建表无法回收空间的原理和 vacuum 无法清理死元组是一样的,假如有长事务,需要那些死元组版本,所以还需要保留那些死元祖。

同事之前也问过我类似 vacuum full 无法释放的问题,以及为什么 truncate 不会管这些 xmin,因为官网上写的很明白,truncate 不是 mvcc-safe 的。

Some DDL commands, currently only TRUNCATE and the table-rewriting forms of ALTER TABLE, are not MVCC-safe.

Image

至于找这些 xmin,参照之前的表膨胀分析以及这个 SQL 即可。

5问题 IV

Image

由于 PLPGSQL 里面默认使用 Plan Caching,优化器会把 SQL 以预备语句的方式执行,并在会话里保存这些预备语句。猜测可能是这个原因,可以使用 EXECUTE 的方式执行,或者配合 pgadmin + pldebugger 进行调试,手动打印 raise notice 的土办法也是可以的。

6小结

好了,That's all!

另外 PostgreSQL 大会这周五就要在杭州举办啦,目前议题暂定如下

Image

我会到杭州一趟,在第一天的线下主会场作为最后一个嘉宾,和各位分享 《PostgreSQL DBA Daily 2.0》,可能各位听了一天的技术干货脑子转不过弯了,于是社区安排我在最后一个来活跃活跃气氛,2.0 版本中大图加入了大量干货与实战经验,内容也进行了丰富完善,让我们拭目以待!先放个打码图

Image