问题概述
PostgreSQL使用TOAST 机制来存储较大的行值,当表中行值较长时可能会把值切片并存储到pg_toast表中。
如果很不巧,我们在存储过程中使用到了TOAST 存储的行值,并且是在commit之后获取了TOAST 存储的行值,那么就会遇到一个报错“no known snapshots”
该问题在12.8,13.4,14,15及以后版本中已经修复,测试不存在该问题
12.8的发布日志中可以看到修复记录
Restore the Portal-level snapshot after COMMIT or ROLLBACK within a procedure (Tom Lane) This change fixes cases where an attempt to fetch a toasted value immediately after COMMIT/ROLLBACK would fail with errors like “no known snapshots” or “missing chunk number 0 for toast value”.
问题验证
pg12.3中存在该问题
[postgres@mydb1a pg14_0]$ psql
psql (12.3)
Type "help" for help.
postgres=# CREATE TABLE toasted(id serial primary key, data text);
INSERT INTO toasted(data) VALUES((SELECT string_agg(random()::text, ':') FROM generate_series(1, 1000)));
INSERT INTO toasted(data) VALUES((SELECT string_agg(random()::text, ':') FROM generate_series(1, 1000)));
DO $$ DECLARE v_r record; BEGIN FOR v_r in SELECT data FROM toasted LOOP INSERT INTO toasted(data) VALUES(v_r.data);COMMIT;END LOOP;END;$$;
CREATE TABLE
postgres=# INSERT INTO toasted(data) VALUES((SELECT string_agg(random()::text, ':') FROM generate_series(1, 1000)));
INSERT 0 1
postgres=# INSERT INTO toasted(data) VALUES((SELECT string_agg(random()::text, ':') FROM generate_series(1, 1000)));
INSERT 0 1
postgres=# DO $$ DECLARE v_r record; BEGIN FOR v_r in SELECT data FROM toasted LOOP INSERT INTO toasted(data) VALUES(v_r.data);COMMIT;END LOOP;END;$$;
ERROR: no known snapshots
CONTEXT: PL/pgSQL function inline_code_block line 1 at FOR over SELECT rows
postgres=#
pg12.8中可以正常执行,无报错
[postgres@mydb1a pg14_0]$ psql -p 5438
psql (12.3, server 12.8)
Type "help" for help.
postgres=# CREATE TABLE toasted(id serial primary key, data text);
ERROR: relation "toasted" already exists
postgres=# INSERT INTO toasted(data) VALUES((SELECT string_agg(random()::text, ':') FROM generate_series(1, 1000)));
INSERT 0 1
postgres=# INSERT INTO toasted(data) VALUES((SELECT string_agg(random()::text, ':') FROM generate_series(1, 1000)));
INSERT 0 1
postgres=# DO $$ DECLARE v_r record; BEGIN FOR v_r in SELECT data FROM toasted LOOP INSERT INTO toasted(data) VALUES(v_r.data);COMMIT;END LOOP;END;$$;
DO
postgres=#
解决方案
1)升级到12.8,13.4,14,15及之后版本即可,测试示例用12.8版本加原data目录启动数据库,执行无报错
[postgres@mydb1a pg12_8]$ bin/pg_ctl -D /app/pg12/data/ start
waiting for server to start....2023-05-12 05:08:55.167 CST [633156] LOG: 00000: starting PostgreSQL 12.8 on x86_64-pc-linux-gnu, compiled by gcc (GCC) 7.3.0, 64-bit
2023-05-12 05:08:55.167 CST [633156] LOCATION: PostmasterMain, postmaster.c:1019
2023-05-12 05:08:55.167 CST [633156] LOG: 00000: listening on IPv4 address "0.0.0.0", port 5432
2023-05-12 05:08:55.167 CST [633156] LOCATION: StreamServerPort, pqcomm.c:590
2023-05-12 05:08:55.167 CST [633156] LOG: 00000: listening on IPv6 address "::", port 5432
2023-05-12 05:08:55.167 CST [633156] LOCATION: StreamServerPort, pqcomm.c:590
2023-05-12 05:08:55.168 CST [633156] LOG: 00000: listening on Unix socket "/tmp/.s.PGSQL.5432"
2023-05-12 05:08:55.168 CST [633156] LOCATION: StreamServerPort, pqcomm.c:584
2023-05-12 05:08:55.568 CST [633156] LOG: 00000: redirecting log output to logging collector process
2023-05-12 05:08:55.568 CST [633156] HINT: Future log output will appear in directory "pg_log".
2023-05-12 05:08:55.568 CST [633156] LOCATION: SysLogger_Start, syslogger.c:675
............... done
server started
[postgres@mydb1a pg12_8]$ psql
psql (12.3, server 12.8)
Type "help" for help.
postgres=# DO $$ DECLARE v_r record; BEGIN FOR v_r in SELECT data FROM toasted LOOP INSERT INTO toasted(data) VALUES(v_r.data);COMMIT;END LOOP;END;$$;
DO
postgres=# show data_directory;
data_directory
----------------
/app/pg12/data
(1 row)
postgres=#
2)如果行值小于8k,可以设置字段的存储策略为plain,并重写数据,因为修改存储策略不会改变已有数据的存储情况
测试示例中data字段原存储策略为external,插入长度6000的值后,触发了toast(向表中存储超过TOAST_TUPLE_THRESHOLD字节(通常是2kB)的行值的时候就会触发toast),toast表中有记录,执行代码块报错;
修改存储策略为plain(需注意:此策略不允许存储大小超过8k的行值),已有数据存储结构不会改变,执行代码块报错,重写数据后,toast表无记录,执行代码块无报错
postgres=# INSERT INTO toast2(data) VALUES( lpad('x', 6000));
INSERT 0 1
postgres=# INSERT INTO toast2(data) VALUES( lpad('x', 6000));
INSERT 0 1
postgres=# DO $$ DECLARE v_r record; BEGIN FOR v_r in SELECT data FROM toast2 LOOP INSERT INTO toast2(data) VALUES(v_r.data);COMMIT;END LOOP;END;$$;
ERROR: no known snapshots
CONTEXT: PL/pgSQL function inline_code_block line 1 at FOR over SELECT rows
postgres=# select oid from pg_class where relname='toast2';
oid
--------
187318
(1 row)
postgres=# select count(1) from pg_toast.pg_toast_187318;
count
-------
12
(1 row)
postgres=# alter table toast2 alter COLUMN data set storage plain;
ALTER TABLE
postgres=# DO $$ DECLARE v_r record; BEGIN FOR v_r in SELECT data FROM toast2 LOOP INSERT INTO toast2(data) VALUES(v_r.data);COMMIT;END LOOP;END;$$;
ERROR: no known snapshots
CONTEXT: PL/pgSQL function inline_code_block line 1 at FOR over SELECT rows
postgres=# truncate toast2;
TRUNCATE TABLE
postgres=# INSERT INTO toast2(data) VALUES( lpad('x', 6000));
INSERT 0 1
postgres=# INSERT INTO toast2(data) VALUES( lpad('x', 6000));
INSERT 0 1
postgres=# DO $$ DECLARE v_r record; BEGIN FOR v_r in SELECT data FROM toast2 LOOP INSERT INTO toast2(data) VALUES(v_r.data);COMMIT;END LOOP;END;$$;
DO
postgres=# select count(1) from pg_toast.pg_toast_187318;
count
-------
0
(1 row)
postgres=#
另附:4种TOAST存储策略的区别
-
PLAIN避免压缩或者线外存储;而且它禁用变长类型的单字节头部。这是不可TOAST数据类型列的唯一可能的策略。只是对那些不能TOAST的数据类型才有可能。 -
EXTENDED允许压缩和线外存储。这是大多数可TOAST数据类型的默认策略。 首先将尝试进行压缩,如果行仍然太大,那么则进行线外存储。 -
EXTERNAL允许线外存储,但是不许压缩。使用EXTERNAL将令那些在宽text和bytea列上的子串操作更快(代价是增加了存储空间), 因此这些操作被优化为只抓取未压缩线外数据中需要的部分。 -
MAIN允许压缩,但不允许线外存储(实际上,在这样的列上仍然会进行线外存储,但只是作为没有办法把行变得足以放入一页的情况下的最后手段)。
参考文档
http://postgres.cn/docs/12/storage-toast.html
https://www.postgresql.org/docs/release/12.8/
「喜欢这篇文章,您的关注和赞赏是给作者最好的鼓励」
关注作者
【版权声明】本文为墨天轮用户原创内容,转载时必须标注文章的来源(墨天轮),文章链接,文章作者等基本信息,否则作者和墨天轮有权追究责任。如果您发现墨天轮中有涉嫌抄袭或者侵权的内容,欢迎发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。




