《PostgreSQL面试题集锦》学习与回答

      新计划每天做一两道查漏补缺~ 以下题目来自: 

PostgreSQL面试题集锦

1. MVCC 实现机制以及和Oracle的差异

MVCC

多版本并发控制,核心作用:使得读写操作不相互阻塞,提升并发性能。

实现原理:通常有2种实现方法:

  • 写新数据时,把旧数据存入其他位置(如oracle的回滚段、sqlserver的tempdb)。当读数据时,读的是快照的旧数据。
  • 写新数据时,旧数据不删除,直接插入新数据。以pg为代表,在元组头中引入xmin,xmax,cid,ctid,t_infomask几个字段,并结合commitlog,snapshot来进行可见性判断。

以pg为例:

  • 插入数据:xmin为执行插入的事务号,xmax为0
  • 删除数据:xmin不变,xmax为执行删除的事务号
  • 更新数据:相当于删除+插入
  • 同一事务执行多个DML语句,cid会递增,表示执行了几条命令

与Oracle的差异:由于实现原理不同,两者优缺点也基本相反

pg优点

  • 由于不用另外写回滚数据,DML效率高
  • 回滚可以立即完成 为什么PostgreSQL的回滚是瞬间完成的?_51CTO博客_postgresql 回滚
  • 不会有ora-1555快照过旧、undo表空间不够等问题

pg缺点

  • 旧数据存储在数据文件中,会有表膨胀的问题,因此需要引入vacuum机制
  • 事务ID递增,需要处理事务回卷问题,因此又要引入freeze机制。

pg事务篇(一)—— 事务与多版本并发控制MVCC_Hehuyi_In的博客-CSDN博客_pg 事务

2. 为什么会有表膨胀及表膨胀的危害

为什么会有表膨胀

从原理来说:

       pg旧数据存储在数据文件中,并不立刻清理,只是标记为无效。这些旧数据如果不能及时清理,业务表和数据文件会越来越大,引发表膨胀。

从具体场景来说:哪些情况会导致旧数据不能及时清理

  • 未开启autovacuum或者禁用了track_counts参数
  • autovacuum过慢(例如IO问题、触发阈值不合理、执行周期不合理、配置了延迟触发,worker过于忙碌等)
  • 大事务或者DML量过大,产生死元组速度快于清理
  • 长事务,包括pg_dump和pg_dumpall,默认会以可重复读级别开启事务
  • 慢查询(包括增删改查和DDL
  • 游标未关闭
  • 复制槽 + hot_standby_feedback + 备库大查询,会导致主库可以vacuum的xmin很小

表膨胀的危害

  • 表和数据文件占用空间持续增长
  • 查询表时要扫描的数据块可能增多,查询速度变慢
  • 需要用vacuum full处理(如果不用其他插件)vacuum full会获取表的8级锁,阻塞对表的所有操作,影响业务。并且最大会占有原来磁盘空间的两倍,可能打爆磁盘空间。

对应前面具体场景,如何避免表膨胀

  • 开启autovacuum,启用track_counts参数
  • 保证autovacuum性能:
    • 将数据库迁移至高性能存储
    • 合理的触发阈值(autovacuum_vacuum_threshold和autovacuum_vacuum_scale_factor
    • 合理的执行周期 autovacuum_naptime
    • 性能足够时,关闭延迟设置 autovacuum_vacuum_cost_delay
    • 合理的工作进程数和内存(autovacuum_max_workersautovacuum_work_mem
  • 应用程序设计时,尽量避免如下:
    • 过于频繁的DML操作
    • SQL(包括增删改查和DDL在内的所有的SQL)
    • 大事务、长事务
    • 打开游标后不关闭
    • 在不必要的场景使用已提交读以上隔离级别
    • 对大库执行pg_dump进行逻辑备份(隐式repeatable read隔离级别的全库备份)
  • 设置idle_in_transaction_session_timeout,控制长事务的存活时间
  • 设置old_snapshot_threshold参数,强制删除为过老的事务快照保留的dead元组(这会导致长事务读取已被删除的tuple时出错)。
  • 对于大表,建议使用分区,可以加快vacuum的速度

如何处理表膨胀

  • vacuum full,cluster
  • 重建表(手动,或者pg_reorg,pg_repack

参考:揭开表膨胀的神秘面纱

3. 长事务的危害以及如何溯源长事务

长事务的危害

小事务但长期不提交

  • 如果前面执行过DML语句,会锁定相关数据,阻塞后面语句
  • 阻塞create index(也包括 concurrently)
  • 大量死元组无法vacuum导致表膨胀
  • 大量事务id无法冻结
  • WAL无法及时清理,占用空间大
  • 占用连接数
  • 开启old_snapshot_threshold后,长事务可能导致索引失效
  • 搭配子事务容易使性能急剧下降
  • 逻辑复制下会阻塞复制槽的创建

大事务:除上面外可能还有

  • 出现较大范围锁表
  • WAL大量增加
  • 主从出现延迟

溯源

什么样的事务才会是有危害的长事务?

        pg_stat_activity视图中 backend_xid或backend_xmin字段非空的事务。单纯begin tran; 不提交并不会有问题,因为它并没有真正申请事务id和获取快照。

  • backend_xid:已申请的事务号(virtualxid不算),从申请事务号开始持续到事务结束。

  • backend_xmin:进程快照xmin,表示在快照创建时最旧的未提交事务id(实际上就是Transaction Horizon)

        因此监控语句需要加上这两个条件

select count(*) from pg_stat_activity where state <> 'idle' 
and (backend_xid is not null or backend_xmin is not null) 
and now()-xact_start > interval '3600 sec'::interval;

参考:

生产案例 | 费解的索引失效

https://foucus.blog.csdn.net/article/details/123230865

4. 子事务的危害和注意事项

如何产生子事务

  • savepoint
  • pl/pgsql 中的BEGIN / EXCEPTION WHEN .. / END代码块
  • PL/Python代码中的plpy.subtransaction()

子事务的危害

  • 加速事务id消耗,增加事务id回卷风险:每个savepoint都会消耗一个事务id
  • 增加内存占用:每个savepoint消耗8K的会话本地内存(CurTransactionContext)
  • 子事务SLRU溢出(Subtrans SLRU overflow):本质上这是由于子事务嵌套过深、或者子事务日志过大,SLRU缓存中不再能放下,这会导致大量缓存miss,进而导致大量磁盘IO,因为pg需要从磁盘去读取子事务信息。典型特征是出现SubtransControlLock等待事件(13版本开始重命名为SubtransSLRU)。

子事务溢出分为以下两种情况:

       ① 每个会话最多可容纳 64个(源码为PGPROC_MAX_CACHED_SUBXIDS参数)未中止的子事务。如果超过,则快照被标记为suboverflowed,这种快照无法包括可见性判断需要的所有数据,因此pg有时不得不读取pg_subtrans目录文件,会造成性能急剧下降。
       ② 主库上的子事务(不要求超过64,高并发时即使1个也可能)和长事务的组合可能会造成备库性能急剧下降甚至不可用,这个问题只发生在备库上。从根本上说,问题的发生是因为副本在创建快照和检查元组可见性时的行为与主副本不同。

       问题2的主要特征:

  • A. 在主库:使用到了子事务(不要求超过64),对一些记录进行更新;有一个正在运行的长事务;这里的"长"取决于系统中XID的增长:比如说,如果你有 1k TPS 递增的 XID,那么问题可能会在几十秒后出现,所以"长" = "几十秒长",如果 你有 10k TPS,"长"可能意味着"几秒钟长"。所以在某些系统中,一个常规的慢查询也可能变成这样一个令人头疼的事务
  • B. 在备库:查询的元组被主库的子事务修改了
  • Multixact IDs的不当使用:将SELECT .. FOR UPDATE和子事务结合,情况会变得十分糟糕
SELECT [some row] FOR UPDATE;
SAVEPOINT save;
UPDATE [the same row];

wait_event/wait_event_type组合上:

  • LWLock:MultiXactMemberControlLock
  • LWLock:MultiXactOffsetControlLock
  • LWLock:multixact_member
  • LwLock:multixact_offset
  • 子事务+逻辑复制,可能产生大量小文件,导致walsender hang

注意事项

  • 尽量避免使用子事务,尤其在高并发场景下
  • 单会话打开的子事务数不要超过64,并发数越多,这个数字会越小
  • 如果有备库,要避免长事务+子事务,txid增长越快,能容忍的长事务时间越短
  • 避免SELECT .. FOR UPDATE和子事务结合
  • 做好事务id增长、db年龄、长事务、等待事件的监控

参考

子事务滥用的危害

长事务与子事务

子事务的危害

子事务及其性能

5. 表结构变更哪些操作是非online       

  • pg 11前,新增带default值的列
  • 所有版本,新增volatile default值的列(例如random(),timeofday()
  • 改短列长度
  • 改列类型(二进制兼容类型不需要rewrite table,但需要rewrite index,例如将 VARCHAR 转换为 TEXT

另外补充非表结构变更,但是非online的操作:

  • 修改表空间(alter table set tablespace)
  • SET { LOGGED | UNLOGGED }
  • cluster
  • vacuum full
  • SET/RESET storage_parameter:可能会rewrite,与设置的参数有关

       简单的验证方法是看操作后pg_class中的relfilenode值是否改变。

       alter table的部分online操作是指不用rewrite table,而不是不需要获取8级锁。如果表正在执行一个大查询,对它执行新增字段也会被阻塞,同时阻塞后面该表所有语句。

       alter table 这部分源码在 tablecmds.c,简单看了下:

ATController -> ATRewriteTables -> ATRewriteTable
AlterTableGetLockLevel 中有各种操作使用的锁级别

		switch (cmd->subtype)
		{
				/*
				 * These subcommands rewrite the heap, so require full locks.
				 */
			case AT_AddColumn:	/* may rewrite heap, in some cases and visible
								 * to SELECT */
			case AT_SetTableSpace:	/* must rewrite heap */
			case AT_AlterColumnType:	/* must rewrite heap */
				cmd_lockmode = AccessExclusiveLock;
case AT_SetLogged:		/* SET LOGGED */
...
			/* force rewrite if necessary; see comment in ATRewriteTables */
			if (tab->chgPersistence)
			{
				tab->rewrite |= AT_REWRITE_ALTER_PERSISTENCE;
				tab->newrelpersistence = RELPERSISTENCE_PERMANENT;
			}
			pass = AT_PASS_MISC;
			break;
		case AT_SetUnLogged:	/* SET UNLOGGED */
...
			/* force rewrite if necessary; see comment in ATRewriteTables */
			if (tab->chgPersistence)
			{
				tab->rewrite |= AT_REWRITE_ALTER_PERSISTENCE;
				tab->newrelpersistence = RELPERSISTENCE_UNLOGGED;
			}
			pass = AT_PASS_MISC;
			break;

参考:

PostgreSQL: Documentation: 14: ALTER TABLE

That Guy From Delhi: What Postgres SQL causes a Table Rewrite?

6. 物理备份需要注意什么(pg_start_backup

      备份完成后务必记得执行pg_stop_backup关闭备份状态

       pg_start_backup函数会启动force full page write,备份期间对页的修改会将整页写入WAL日志,导致WAL量暴增。如果忘记关闭会导致磁盘空间快速增加、dml性能下降、从库要应用的日志过多可能出现主从延迟,另外可能影响服务器IO性能。

       推荐使用pg_basebackup或者pg_rman等集成工具,自动执行startstop

7. 逻辑备份是如何确保一致性的

       备份前会启动一个事务,9.1版本开始默认隔离级别为REPEATABLE READ(之前为SERIALIZABLE),这样可以在整个备份期间使用事务开启时的快照,导出的所有表读取的都是该时间点的数据。如果加 --serializable-deferrable 参数,则使用的是可串行化隔离级别。

       另外逻辑备份会对表加1级锁,避免备份过程中表结构被改变或者表被drop、truncate等。

       以下代码在pg_dump.c 的 setup_connection 函数中

/*
	 * Start transaction-snapshot mode transaction to dump consistent data.       
	 */
	ExecuteSqlStatement(AH, "BEGIN");
    // pg 9.1版本及以上,默认为REPEATABLE READ隔离级别;9.1以下默认为SERIALIZABLE隔离级别
	if (AH->remoteVersion >= 90100)
	{
		if (dopt->serializable_deferrable && AH->sync_snapshot_id == NULL)
			ExecuteSqlStatement(AH,
								"SET TRANSACTION ISOLATION LEVEL "
								"SERIALIZABLE, READ ONLY, DEFERRABLE");
		else
			ExecuteSqlStatement(AH,
								"SET TRANSACTION ISOLATION LEVEL "
								"REPEATABLE READ, READ ONLY");
	}
	else
	{
		ExecuteSqlStatement(AH,
							"SET TRANSACTION ISOLATION LEVEL "
							"SERIALIZABLE, READ ONLY");
	}

         以下代码在pg_dump.c 的 getTables 函数中

        /*
		 * Read-lock target tables to make sure they aren't DROPPED or altered
		 * in schema before we get around to dumping them.		 
		 *
		 * We only need to lock the table for certain components; see
		 * pg_dump.h
		 */
		if (tblinfo[i].dobj.dump &&
			(tblinfo[i].relkind == RELKIND_RELATION ||
			 tblinfo->relkind == RELKIND_PARTITIONED_TABLE) &&
			(tblinfo[i].dobj.dump & DUMP_COMPONENTS_REQUIRING_LOCK))
		{
			resetPQExpBuffer(query);
			appendPQExpBuffer(query,
							  "LOCK TABLE %s IN ACCESS SHARE MODE",
							  fmtQualifiedDumpable(&tblinfo[i]));
			ExecuteSqlStatement(fout, query->data);
		}

参考:

pg_dump一致性备份以及cache lookup failed错误的原因分析_weixin_33835690的博客-CSDN博客

PostgreSQL backup and recovery - online logical backup & recovery-阿里云开发者社区

8. WAL 堆积的原因有哪些

不能清理

  • 主库大事务、长事务、包括pg_dump,pg_dumpall导出
  • 未开启归档,或归档命令执行失败(命令报错、目录不存在等)
select * from pg_stat_archiver;
  • 复制槽失效
  • max_wal_size,wal_keep_size(pg 13前为wal_keep_segment)设置过大

清理慢

  • 归档效率低,默认单进程归档pg_BackRest可以实现多进程归档;归档目录IO性能过差
  • 设置了复制槽且备库接收/应用WAL慢

产生速度过快

  • 主库DML量过大,产生WAL日志过多
  • 过于频繁的检查点,配合全页写机制可能会雪上加霜。当启用全页写时,pg会在每个检查点之后、每个页面第一次发生变更时,将整页写入WAL日志。
  • 物理备份期间,强制启用全页写。只要页发生变化,就会将整页写入WAL日志(不管是不是第一次,也不管有没有检查点)。因此,它写入的量是更大的。
  • 忘记执行pg_stop_backup关闭备份状态
  • archive_timeout 参数设置过小导致频繁产生新WAL文件

9. 长连接的危害是什么

  • 连接数堆积,可能超过 max_connections
  • 连接数越多,进程调度和管理越复杂,pg性能会线性下降(pg 14对此进行了优化,但依然不建议过多)
  • 无法及时释放内存,甚至遇到过导致oom的情况
  • 空闲过长可能会被防火墙或者DB超时中断,导致应用报错

10. infomask 标志位的作用是什么

核心作用:提升元组可见性判断效率

pg事务篇(三)—— 事务状态与Hint Bits(t_infomask)_Hehuyi_In的博客-CSDN博客_pg事务篇(三)

11. 空值是如何存储的以及索引是否存储空值

索引是否存储空值

BTree 索引存储空值(SQL Server也存,Oracle不存),在官方文档也有提到

Also,an IS NULL or IS NOT NULL condition on an index column can be used with a B-Tree index。

PostgreSQL: Documentation: 14: 11.2. Index Types

空值是如何存储的

       在pg元组头数据中,有一个t_bits 的数组,用于存储空值位图。当元组中没有null值的时候,t_bits可以被认为是空的,当元组有null值的列时,t_bits使用一个bit来表示列是否为null。

     详情参考:postgresql源码学习(55)—— 列中的NULL值是如何存储和判断的?Hehuyi_In的博客-CSDN博客

12. 为什么需要有全页写(full_page_write

避免两种场景下的“部分写”(数据块不一致)问题:

  • 由于DB page与 OS page默认大小不一致,在pg异常宕机(或出现磁盘错误)时,数据文件中的页有可能只写入了一部分。
  • 使用操作系统命令备份正在写入的数据库时,备份文件中的数据块可能不一致。

无论是崩溃恢复还是备份还原的恢复,都无法基于不一致的数据块进行。

postgresql源码学习(34)—— 事务日志⑩ - 全页写机制_pgsql 全页写_Hehuyi_In的博客-CSDN博客

13. 索引失效的各种原因

不能用

  • invalid索引

       可能是创建中途被取消,也可以手动置为invalid。注意这种索引虽然不能用,但还是会被更新,增加开销。
update pg_index set indisvalid=false where indexrelid='i_ii'::regclass;

  • 不支持的条件类型,例如hash index不支持范围查询
  • 列与索引字符集/排序规则不一致(例如表关联字段)
  • 隐式转换
  • 软解析使用缓存的全表扫描执行计划,后续默认不会再解析生成新执行计划
  • hint固定执行计划
  • 非SARG条件:
  • 字段上用函数,immutable类型函数可以建函数索引
  • 字段上做运算
  • 非操作符条件,not in 、<> 、!=  、not like
  • like左边带%(使用pg_trgm插件创建gin索引除外)
  • 数据库认为索引unsafe:开启old_snapshot_threshold参数,存在HOT Broken chain

不想用

优化器认为走索引cost比全表扫描更高

  • 查询/返回数据量占比过大,可以再细分几种场景:
  • 很小的表,例如10行的表返回9行
  • 大表,但符合条件的过多(例如字段in大量值)
  • 唯一值过少(例如性别)
  • 数据倾斜(例如deleted=0 10行,deleted=1 99999行)
  • 统计信息过旧(包括没有统计信息),执行计划估算错误
  • 关联度:列物理顺序与逻辑顺序的相关性(统计信息中的correlation字段)
  • 不符合最左原则
  • 重复的索引
  • 优化器刺客 limit,pg会倾向于不走索引,但在符合条件的数据非常少时,可能会有严重的性能问题

参考

67-oracle数据库,有索引,但是没有被使用的N种情况,以及应对方法(上篇)

68-oracle数据库,有索引,但是没有被使用的N种情况,以及应对方法(下篇)

聊一聊索引失效

14. commit log 的作用

      保存事务最终状态,用于在可见性判断中确定事务的运行状态(在t_infomask未设置时,会根据clog来判断事务是否提交)。

postgresql源码学习(51)—— 提交日志CLOG 原理 用途 管理函数_Hehuyi_In的博客-CSDN博客

15. 数据库的连接方式以及各自适用的场景

《PostgreSQL面试题集锦》学习与回答_第1张图片

图片来自PGCA课程《物理连接》

16. 各种索引的适用场景(HASH/GIN/BTREE/GIST/BLOOM/BRIN

《PostgreSQL面试题集锦》学习与回答_第2张图片

图片来自《PG DBA的一天》

17. 行锁是如何实现的,行锁是否会存储在共享内存中

       pg采用元组级常规锁+xmax结合的方式实现行锁。不单纯用元组级常规锁,是为了避免事务修改行过多时,锁表急剧增大导致性能劣化,并且锁表在共享内存中的大小是有限的。因此,行锁也是不存储在内存中的。

行锁等级与兼容性

       pg中通常有两种方式会用到行锁:

  • 对行执行update,delete操作
  • 显式指定行锁(for update,for no key update,for share,for key share)

       由于pg行锁是由常规锁+xmax结合实现的,其实行锁的等级也是借助常规锁等级实现了映射。上面的四种行锁分别对应常规锁的8、7、2、1,锁之间的兼容性也一致。

xmax保存什么

       通常如果只有一个事务增加行锁,那么直接将行的xmax设为事务id,并在infomask中设置对应锁类型即可。如果有多个事务对一个元组加共享锁,pg则会将多个事务组合,并为其指定唯一的MultiXactId,此时在xmax处保存的就是MultiXactId。

参考:

postgresql源码学习(十三)—— 行锁①-行锁模式与xmax_Hehuyi_In的博客-CSDN博客_postgresql锁

第28题

18. 流复制和逻辑复制的区别以及各自适用的场景

流复制和逻辑复制的区别

对比项 流复制 逻辑复制
引入版本 pg 9.0 pg 10
实现原理 将WAL文件传送到备库,由备库进行物理级replay 将WAL文件传送到备库,按照配置规则解析为SQL语句并执行
数据一致性 高,主备库物理完全一致 一般,主备库物理可能一致,数据可能不一致
安装要求

1. 同构平台、大版本一致

2. wal_level 至少为 replica

3. 复制槽非必须

1. 平台和大版本可以不一致

2. wal_level = logical

3. 需要逻辑复制槽

同步范围 实例级,可同步所有对象的dml,ddl操作 表级,可同步表的dml及部分ddl操作(14版本支持truncate)
同步级别 整个实例只能设置为同步或异步 可以对不同订阅单元设置不同同步级别
同步架构 一主多从、级联从库 一对多、多对一、多对多、级联

适用场景

流复制:

  • 可靠的数据库高可用
  • 可靠的数据库容灾
  • 提供低延迟的只读备库

逻辑复制:

  • 大版本升级
  • 跨平台迁移(例如windows -> linux)
  • 仅需同步数据库中部分表
  • 仅部分表需要设置为同步模式,其余可为异步模式
  • 备库需要执行写操作
  • 多对一、多对多的数据同步

19. 流复制冲突是什么以及为什么会产生复制冲突

postgresql源码学习(48)—— 流复制冲突(备库锁阻塞与Vacuum冲突)_Hehuyi_In的博客-CSDN博客

20. 简述 PostgreSQL 中的权限体系

最常用的如下:

  • instance级:pg_hba.conf,哪些服务器可以连接到数据库、认证方式
  • DB级:连接、创建等
  • schema级:usage、创建
  • table级:增删改查、reference、truncate、trigger
  • 列级:增改查、reference
  • 行级:创建行策略,只允许用户看某些行

PostgreSQL: Documentation: 14: 5.7. Privileges

21. 常见的高可用方案以及高可用选型及优缺点

22. synchronous_commit 五种级别的区别,为什么备库的查询不能立马看到主库插入的数据

synchronous_commit 五种级别的区别

首先要看节点是单实例还是主从架构,两者的可用级别和含义是不一样的。

单实例

  • 可用级别为off、on、local,并且此时on和local含义是一样的
  • off:表示提交事务时,不用等相应WAL数据写入WAL文件,即可向客户端返回成功(异步提交)
  • on和local:表示提交事务时,需要等相应WAL数据写入WAL文件,才向客户端返回成功(同步提交)

主从架构

  • off:同上
  • local:同上
  • remote_write:主库提交事务时需要等相应WAL数据写入从库操作系统缓存中,才向客户端返回成功(同步流复制)。

  • on:主库提交事务时需要等相应WAL数据写入从库WAL文件中,才向客户端返回成功(同步流复制)。

  • remote_apply:主库提交事务时需要等相应WAL数据需在从库中replay完,才向客户端返回成功(同步流复制)。

为什么备库的查询不能立马看到主库插入的数据

  • WAL日志的发送、接收、write、flush、replay阶段都可能有延迟,除非同步级别设为remote_apply,否则主库提交不意味从库已经应用完日志,可以查到对应数据。

pg 之 synchronous_commit参数_Hehuyi_In的博客-CSDN博客_synchronous_commit

23. 事务ID回卷的原因以及如何维护优化

事务ID回卷的原因

       pg将总共可用的事务id(约42亿)视为一个环,并一分为二,对于某个特定的事务id,其后约21亿个id属于未来,均不可见;其前约21亿个id属于过去,均可见。

       由于目前事务id只有32位,在大业务量下很可能用完,触发事务id回卷(循环使用)。一旦新事务使用了旧id,旧事务将可以看到新事务数据,新事务又看不到旧事务数据,打破数据一致性。

       为此,pg引入了冻结机制,将不再需要使用的事务id进行冻结,冻结后的事务id被认为比所有事务id都旧。这样既保证了数据一致性,又使得有限的事务id可以循环复用。

如何维护优化

《PostgreSQL面试题集锦》学习与回答_第3张图片

 postgresql_internals-14 学习笔记(三)冻结、rebuild-CSDN博客

pg事务篇(二)—— 事务ID回卷与事务冻结(freeze)_Hehuyi_In的博客-CSDN博客_autovacuum_freeze_max_age

24. vacuum / autovacuum 的作用以及如何调优

vacuum / autovacuum 的作用

  • 死元组清理
  • 统计信息收集
  • 冻结事务ID,删除不必要的clog文件
  • 更新vm与fsm文件
  • 重写表、释放空闲磁盘空间(vacuum full)
  • autovacuum自8.3版本引入,根据一定规则自动定期触发vacuum操作,减少手动运维。

如何调优

《PostgreSQL面试题集锦》学习与回答_第4张图片

pg事务篇(四)—— vacuum官方文档_Hehuyi_In的博客-CSDN博客_pg 手动vacuum语法

postgresql_internals-14 学习笔记(二)常规vacuum_Hehuyi_In的博客-CSDN博客

25. 函数三态以及函数为什么需要有execute

函数三态

volatile函数(不稳定,默认)

  • 可以做任何事,包括修改数据库。
  • 在同一个事务中,即使是相同的参数,返回的结果也会不同。在函数内的每个query开始时获取snapshot,因此在函数执行过程中,外部已提交的数据可见。
  • 由于每次要重新计算,优化器无法提前预估,其性能可能较差
  • 不支持创建函数索引
  • 典型函数:timeofday()、random()、所有修改类函数


stable函数(稳定)

  • 不可以修改数据库
  • 在同一个事务中,对于相同的参数,返回的结果相同。在函数开始执行时获取snapshot,内部的每个query不再重复获取,因此在函数执行过程中,外部已提交的数据不可见。
  • 不支持创建函数索引
  • 典型函数:current_timestamp()


immutable函数(非常稳定)

  • 不可以修改数据库
  • 只要给定相同参数,永远返回相同的结果。快照获取原理与stable函数一致。
  • 优化器可以预估函数结果,在多次调用时仅将其当作一个值。
  • 支持创建函数索引
  • 典型函数:计算a+b的和

函数稳定性通过查看pg_proc.provolatile得到

函数为什么需要有execute

  • 执行动态SQL:使用一个函数处理不同的表、列等,灵活性高
  • 强制SQL进行硬解析:避免SQL因为数据倾斜使用错误的执行计划

       与普通SQL不同,plpgsql中默认使用Plan Caching,会自动将SQL以prepare方式执行,尝试生成和缓存generic plan 进行软解析。但是,如果有数据倾斜问题,缓存的执行计划可能是低效的,对部分核心业务来说是不可接受的。此时可以考虑使用execute语句,强制根据每个变量值生成对应执行计划,提高准确度。

参考:

PostgreSQL函数

PostgreSQL: Documentation: 14: 38.7. Function Volatility Categories

PostgreSQL: Documentation: 14: 43.11. PL/pgSQL under the Hood

第33题

26. 为什么要使用 create index concurrently 以及 CIC 的危害

为什么要用CIC

       降低锁级别,提升业务并发度。create index需要持有5级锁,会阻塞对表的DML操作;而CIC只需要持有4级锁,与DML操作兼容,基本可以做到不影响业务。

CIC的危害

有一些算不上危害这么严重,但需要注意:

  • pg 14中,14.4版本前CIC有重大bug,有概率导致索引损坏、数据丢失。
  • CIC需要扫描两遍表,耗时更长,资源消耗更多
  • 当有长事务(参考第3题)时,创建语句会持续被阻塞
  • 如果CIC语句异常结束(被取消、被kill等),会在DB中留下一个invalid索引。该索引无法被使用,但每次DML操作还需要更新它,降低效率
  • CIC是自阻塞的,不能在一个表同时执行
  • 分区表不支持在主表CIC创建索引(单独在各子表可以)

参考

PostgreSQL在线创建索引你不得不注意的"坑" - 墨天轮

PostgreSQL CREATE INDEX CONCURRENTLY 的原理以及哪些操作可能堵塞索引的创建-阿里云开发者社区

PostgreSQL: Documentation: devel: CREATE INDEX

27. HOT原理

没有HOT时的场景(Heap Only Tuple)

       当pg更新一条数据时,实际是将旧数据标记为dead再新插入一条数据。当列上有索引时,即使更新的不是索引键字段,也要再新增一个索引项,指向新元组(因为整条元组的物理位置改变了)。当update的数据量大时,不仅性能低,也会引发索引的膨胀,同时还会增加额外的索引清理成本

什么是HOT(堆内元组)

pg 8.3版本引入,满足以下条件的元组被称为堆内元组:

  • 更新前后的元组能在同一个数据块内放下fillfactor参数越小,可用于更新的空间越大)
  • 所有索引键字段未被更新(更新前后为同一个值的,认为没有更新)

HOT原理

       当进行HOT更新时,不需要再新增索引项和指针索引仍指向旧元组,查询数据时通过ctid访问新版本元组即可。这大大提升了带索引字段的更新效率,也减少了索引的膨胀。

       另外,结合pg的page pruning技术,可以在平时的操作(例如select)时就对页内死元组和无用的索引指针进行清理,而不需要等到vacuum执行,减少了vacuum的工作量。

       更新后还会设置元组标记:

  • 新元组t_informask2字段设置为HEAP_ONLY_TUPLE
  • 老元组t_informask2字段设置为HEAP_HOT_UPDATED

参考

postgresql_internals-14 学习笔记(一)_Hehuyi_In的博客-CSDN博客

https://www.cnblogs.com/duanleiblog/p/14378565.html

https://www.cnblogs.com/abclife/p/13620700.html

PostgreSQL: Documentation: 14: 70.7. Heap-Only Tuples (HOT)

28. PostgreSQL中是否有锁升级

       普通行锁没有。pg的行锁实现机制并使它不需要在内存中记录修改行的信息,因此可以有无限个行锁,不需要使用锁升级。像SqlServer就有锁升级,在持有大量行锁时可能升级为页锁甚至表锁,避免锁占用大量内存。       

These locks are acquired when internal fields of a row are being updated (or deleted or marked for update). Postgres doesn't remember any information about modified rows in memory and so has no limit to the number of rows locked without lock escalation.

       补充:pg中的predicate lock(谓词锁)存在锁升级,但它仅用在可串行化的隔离级别,普通业务不会用到。

参考:

PostgreSQL: Documentation: 6.5: Locking and Tables

[译文] PostgreSQL 中的锁:其他锁 - 墨天轮

第17题

29. 复制槽的作用以及复制槽的危害

复制槽的作用

启用hot_feedback_on后,备库会将WAL接收的位置告知主库,创建复制槽后这个信息会保存在复制槽。对于物理复制,可以保证主库不提前删除备库尚未使用的日志,避免主从同步中断,不过物理复制并不是必须的。对逻辑复制而言,逻辑复制槽是必须的。

复制槽的危害

如果备库接收WAL过慢,主库会堆积大量WAL导致磁盘空间暴增。另外可能造成主库vacuum可以清理的元组非常少,加剧表膨胀问题。

30. 为什么会有死锁以及死锁检测机制

为什么会有死锁

       事务间出现了相互等待,使得其中的每个事务都无法进行下一步动作。此时需要有死锁检测机制发现死锁,并终止其中一个事务,打破循环等待。

死锁检测机制

  • 首先对于自旋锁和轻量锁,pg没有死锁检测机制。
  • 如果事务只通过本地锁表和fast path就能获得锁,则它不受死锁检测的影响。
  • 对于常规锁(也包括行锁),deadlock_timeout参数默认为1s,及锁等待出现一秒后进行死锁检测。

        事务T1在等待事务T2,可以用一个有向图表示。如果每个等待是一条“边”,那么死锁检测其实就是一个找“环”的过程。

       为了尽量减少死锁,pg将持锁事务与等待队列中事务之间的等待称为一条实边,而将等待队列中事务的等待称为虚边。如果环中包含虚边,由于尚未真正持有锁,还可以尽量调整。而实边中如果出现了环,检测就会停止,报错出现死锁并将其记录到日志中。

postgresql源码学习(十五)—— 行锁③-死锁检测_postgresql 行锁_Hehuyi_In的博客-CSDN博客

31. SQL 慢能从哪些方面入手排查

整体慢

  • 系统负载:CPU、内存、IO资源使用率,是否为数据库造成的
  • IO延迟:未达到IO瓶颈时,10ms以上通常有问题,联系硬件组排查
  • 业务并发量是否过高
  • 并行度设置是否合理
  • 等待事件如何

单个慢

① 一直慢

  • 慢在解析、执行、还是数据返回阶段。解析阶段考虑使用绑定变量、简化SQL写法和长度,数据返回阶段考虑减少与客户端的交互、或减少返回的数据量
  • 执行计划如何,是否有明显cost高或返回大量数据的部分
  • 条件过滤度如何,能否减少访问的数据量
  • 过滤度高的条件是否有索引
  • 索引有否失效(参考第13题)
  • SQL写法是否有问题,能否改写
  • 等待事件如何
  • 能否使用并行加速

② 突然/偶尔变慢

  • 是否有数据倾斜,某些参数返回的数据量极大
  • 执行计划是否有改变
  • 统计信息是否过期,例如谓词越界等

32. 为什么需要使用分区表以及分区表的优势和劣势

为什么需要使用分区表

① 管理优势

  • 快速删除数据
  • 快速归档及加载数据
  • 冷热数据分层

② 性能优势

  • 分区裁剪,加速数据访问
  • 分区子表可以并发执行vacuum
  • 分区可以放至不同目录,打散IO(通过条带化或底层存储raid等技术会更好)

分区表劣势

  • 低版本分区类型较少,实现复杂,性能优化也较少
  • 为充分发挥优势,需要合理设计分区键及查询,增加了应用设计复杂度
  • hash分区数过多时,性能有可能反而下降,需要合理设置数量

9.x - 13.0 postgresql 分区表新特性及简单用法_Hehuyi_In的博客-CSDN博客_pg13 实时 分区表

33. 软硬解析的概念

SQL只负责告诉数据库要查什么,但一般不会告诉它要怎么查。

硬解析

       对于一个SQL语句,优化器首先需要进行词法分析、语法分析,将其转换为pg能识别的查询树,再对其解析重写和优化,生成执行计划树,执行器才能知道如何执行该语句,这种完整的解析叫做硬解析。

软解析
       显然,如果每个语句每次都执行如此复杂的步骤,效率会很低,因此pg会将SQL解析出来的执行计划缓存在进程内存中,符合一定条件时可以直接使用,提高效率,这种解析叫做软解析。

PG绑定变量SQL解析的 五次机制

五次机制是为了防止数据倾斜,导致使用低效的执行计划。

  • 前5次执行的SQL:都根据实际传入变量生成执行计划(叫做custom plan),属于硬解析。
  • 第6次执行的SQL:生成一个通用的执行计划(generic plan),并与前5次执行计划比较。
  •        如果不差于前5次:固定第6次的执行计划,后续即使参数再发生变化,该SQL的执行计划也不会再变,属于软解析。
  •        如果差于前5次中任何一个执行计划,以后每次都重新生成执行计划,即都是硬解析。

强制使用软/硬解析

       PG 12 中引入了 force_custom_plan 参数,有以下可选值:

  • auto:默认,即按照五次机制处理
  • force_custom_plan:永远进行硬解析,适用于有数据倾斜且性能和稳定性要求高的SQL
  • force_generic_plan:永远使用generic plan,适用于没有数据倾斜或者性能和稳定性要求不高的SQL

两种计划的使用次数

       PG 14 在pg_prepared_statements视图中新增了generic_plans和custom_plans两列,可以看到两种计划的次数。由于pg的执行计划只是缓存在进程中,pg_prepared_statements视图只能看到本会话的SQL情况,看不到其他会话和全局信息。

PREPARE plane(integer) AS SELECT * FROM test WHERE id = $1;

-- 可以带不同值多次执行
EXECUTE plane(1);

-- 必须在同会话执行
select * from pg_prepared_statements;

《PostgreSQL面试题集锦》学习与回答_第5张图片

可以看到第6次的执行计划中,Index Cond 由 id = 1 变成了 id = $1,说明第6次已经是软解析了。

postgres=# PREPARE plane(integer) AS SELECT * FROM test WHERE id = $1;
PREPARE

postgres=# explain analyze EXECUTE plane(1);
                                                   QUERY PLAN                                    
                
-------------------------------------------------------------------------------------------------
----------------
 Index Scan using test_pkey on test  (cost=0.15..8.17 rows=1 width=44) (actual time=0.009..0.010 
rows=1 loops=1)
   Index Cond: (id = 1)
 Planning Time: 0.444 ms
 Execution Time: 0.026 ms
(4 rows)

postgres=# explain analyze EXECUTE plane(1);
                                                   QUERY PLAN                                    
                
-------------------------------------------------------------------------------------------------
----------------
 Index Scan using test_pkey on test  (cost=0.15..8.17 rows=1 width=44) (actual time=0.010..0.011 
rows=1 loops=1)
   Index Cond: (id = 1)
 Planning Time: 0.123 ms
 Execution Time: 0.023 ms
(4 rows)

postgres=# explain analyze EXECUTE plane(1);
                                                   QUERY PLAN                                    
                
-------------------------------------------------------------------------------------------------
----------------
 Index Scan using test_pkey on test  (cost=0.15..8.17 rows=1 width=44) (actual time=0.010..0.012 
rows=1 loops=1)
   Index Cond: (id = 1)
 Planning Time: 0.075 ms
 Execution Time: 0.023 ms
(4 rows)

postgres=# explain analyze EXECUTE plane(1);
                                                   QUERY PLAN                                    
                
-------------------------------------------------------------------------------------------------
----------------
 Index Scan using test_pkey on test  (cost=0.15..8.17 rows=1 width=44) (actual time=0.012..0.014 
rows=1 loops=1)
   Index Cond: (id = 1)
 Planning Time: 0.129 ms
 Execution Time: 0.030 ms
(4 rows)

postgres=# explain analyze EXECUTE plane(1);
                                                   QUERY PLAN                                    
                
-------------------------------------------------------------------------------------------------
----------------
 Index Scan using test_pkey on test  (cost=0.15..8.17 rows=1 width=44) (actual time=0.010..0.011 
rows=1 loops=1)
   Index Cond: (id = 1)
 Planning Time: 0.090 ms
 Execution Time: 0.024 ms
(4 rows)

postgres=# explain analyze EXECUTE plane(1);
                                                   QUERY PLAN                                    
                
-------------------------------------------------------------------------------------------------
----------------
 Index Scan using test_pkey on test  (cost=0.15..8.17 rows=1 width=44) (actual time=0.023..0.024 
rows=1 loops=1)
   Index Cond: (id = $1)
 Planning Time: 0.118 ms
 Execution Time: 0.051 ms
(4 rows)

      这部分源码在plancache.c文件GetCachedPlan函数中,网上查到的基本是旧版本:

《PostgreSQL面试题集锦》学习与回答_第6张图片

      PG 14 中,将选择执行计划的部分单独拆到了choose_custom_plan函数,由上层的GetCachedPlan函数调用。

/*
 * GetCachedPlan: get a cached plan from a CachedPlanSource.
 *
 * This function hides the logic that decides whether to use a generic
 * plan or a custom plan for the given parameters: the caller does not know
 * which it will get.
 *
 */
...
	/* Decide whether to use a custom plan */
	customplan = choose_custom_plan(plansource, boundParams);
...
	if (customplan)
	{
		/* Build a custom plan */
		plan = BuildCachedPlan(plansource, qlist, boundParams, queryEnv);
		/* Accumulate total costs of custom plans */
		plansource->total_custom_cost += cached_plan_cost(plan, true);

		plansource->num_custom_plans++;
	}
	else
	{
		plansource->num_generic_plans++;
	}
...

choose_custom_plan函数,5次机制的5就是从这来的

/*
 * choose_custom_plan: choose whether to use custom or generic plan
 *
 * This defines the policy followed by GetCachedPlan.
 */
static bool
choose_custom_plan(CachedPlanSource *plansource, ParamListInfo boundParams)
{
...
	/* Generate custom plans until we have done at least 5 (arbitrary)
       5次机制的5就是从这来的 */
	if (plansource->num_custom_plans < 5)
		return true;
}

参考:PostgreSQL 14 pg_prepared_statements新增统计软/硬解析次数_mob604756fa6ad7的技术博客_51CTO博客

34. vm / fsm / init 文件是什么

  • vm文件,可见性映射文件:如果一个页中的所有元组都是可见的(或者均已冻结),vm文件中会将两个对应标志位设为1。后续可以跳过对这些页的vacuum,freeze操作,提升性能,另外在执行计划中也可以使用  index-only scans,更加高效。只用于表不用于索引。
  • fsm文件,空闲空间映射文件:保存页中可用空间的映射,在新数据插入时快速定位可用位置。既用于表也用于索引。由于索引需要按顺序插入、不能像普通数据可以插入任意页,因此索引的fsm文件记录所有pagefree space意义不大,它只记录完全为空以及可以重用的页
  • init文件,初始文件:仅对unlogged table可用

postgresql_internals-14 学习笔记(一)_Hehuyi_In的博客-CSDN博客

35. 内存回收机制:kswapd/direct memory reclaim/pdflush

由于内容比较多,整理了一个思维导图

Linux 内存回收,思维导图记录_Hehuyi_In的博客-CSDN博客

① 如何判断内存不足

  • watermark min:vm.min_free_kbytes / 4KB,仅做单位转换,此阈值以下说明内存严重不足,会触发直接内存回收,还可能触发OOM。
  • watermark low:watermark min *5/4,此阈值以下说明内存不足,会触发kswapd进程回收内存
  • watermark high:此阈值以上说明内存充足,kswapd进程休眠

《PostgreSQL面试题集锦》学习与回答_第7张图片

②  内存回收方式

  • kswapd

        kswapd是一个内核进程,定期运行,在watermark low阈值以下时会被唤醒,在后台进行内存回收,回收到high阈值以上时会休眠。由于是异步回收,不会阻塞其他进程,但由于涉及磁盘读写,对于时间敏感的SQL,可能会感受到性能下降。

  • direct memory reclaim

       由于kswapd是定时运行,如果有进程一次申请过多内存,可用内存可能会降到min阈值以下,此时kswapd会触发直接内存回收。这种内存回收方式是同步的,会阻塞其他进程,造成长时间的延迟,系统IO、CPU利用率会升高,SQL执行明显变慢,因此要尽量避免直接内存回收。

  • OOM

       如果直接内存回收后,系统的可用内存还不足以进行内存分配,则会进一步触发OOM机制。

       OOM机制会根据算法选择并kill掉一个占用物理内存较高的进程,以释放内存资源,目标是回收到high阈值以上。如果达不到,会继续杀死占用物理内存较高的进程,直到达到high阈值。由于DB通常是占用内存最多的,通常都是最先被OOM杀掉的,造成严重事故。

③ pdflush线程(感觉类似bg writer进程)

       pdflush的作用是同步内存和磁盘数据,数据写入磁盘前可能会缓存在内存,pdflush试图保证内存和磁盘的数据是一致的,不会因为缓存功能而造成数据丢失或损坏。它和kswapd进程一样,都是定期被唤醒,都是以守护进程的形式存在。

       缓存写入磁盘一般有三个原因:

  • 用户要求缓存马上写入磁盘
  • 缓存过多,超过一定阀值,需要写入磁盘
  • 内存不足,需要将缓存写入磁盘以腾出地方

④ 回收对象

       kswapd和直接内存回收 主要回收的对象是内存中的文件页和匿名页。

  • 文件页(File-backed Page):内核缓存的磁盘数据(Buffer)和文件数据(Cache)都叫作文件页。如果文件页是干净的(clean),则直接释放内存,不影响系统性能。如果是脏页(dirty),则需要先写入磁盘,再释放内存,这由pdflush线程完成,会发生IO操作,因此会影响系统性能。
  • 匿名页(Anonymous Page):这部分内存没有实际载体,如堆、栈数据等,因为匿名页可能还会用到,因此不能直接释放。如果开启了swap机制,则会先把内存存入磁盘中,等到需要的时候再从磁盘中读出。这个过程会发生IO操作,因此会影响系统性能。
  • 文件页和匿名页的回收都是基于LRU算法(最近最少使用算法)。

参考

【Linux内核】内存管理——内存回收机制_linux内存回收机制_Ethan-Code的博客-CSDN博客

https://www.cnblogs.com/centos-python/articles/8522364.html

备库cpu定时冲高的有趣案例

36. 进程调度,D进程的危害和形成原因

D进程的危害

       DDisk SleepD进程为不可中断睡眠进程,通常是在进行IO操作。大量D进程出现时说明服务器遇到IO问题,可能导致服务器性能急剧下降甚至卡死。

可能的原因

  • 存储、虚拟化层等异常导致IO性能急剧下降,通常可以看到磁盘延迟飙高
  • 大量的IO操作,例如备份、大量慢SQL或者统计分析类语句执行
  • 内存使用过多,触发了swap、直接内存回收、碎片整理等操作,出现大量IO

37. 抓包解包,分析 PostgreSQL 协议

38. 存储,SAN/NAS/DAS

《PostgreSQL面试题集锦》学习与回答_第8张图片

Storage Basics and Fundamentals | Mycloudwiki - Page 7

39. 一条 IO 请求的生命周期

参考:从物理磁盘到数据库 —— 存储IO链路访问图-CSDN博客

你可能感兴趣的:(PostgreSQL,杂七杂八,postgresql,面试,答案,回答,解答)