前言
最近,我每天晚上会抽出半个小时至一个小时的时间用来翻译 postgres-howto 系列,为此我也单独开了一个 repo:https://github.com/xiongcccc/postgres-howto/,一周下来,再次被这个仓库给深深惊艳到了,虽然笔者也算一名 PG 老油条了,但是在每篇翻译的过程中,基本都能学到不少学问,知识真的是学不完的,慢工出细活,久久方为功。
今天这篇文章的缘由便是在我翻译了 "Over-indexing" 这一篇之后,有感而发,必须立马再写点什么。Over-indexing —— 顾名思义,过度使用索引,那么我们真的搞懂了过度使用索引的危害了吗?
Over-indexing
过度使用索引的危害,想必各位都能讲出一二,什么影响写入速度,可能影响优化器判断诸如此类。在 Over-indexing 这篇 howto 里面,作者提到这样一个现象:

随着索引数量的增加,规划时间线性增长,并且在索引数量到 16 之后,呈现一个"陡坡",情况恶化地更糟糕了。
相较于执行时间👇🏻,可以看到大部分的时间都花在了 planning time 上。

首先,索引数量增多进而导致规划时间增长,其实不难理解,通过火焰图也可以看到,大部分时间都花在了 create_index_paths 上,也就是说,规划器需要分析更多的执行选项。
那么,为什么到了 16 之后情况急剧恶化呢?没错,锁的争用,如果你基于索引进行查找,那么 PostgreSQL 会在表上所有的索引加上 AccessShareLock (具体细节可以参考 plancat.c),也就是说,一个简单查询,要获取 16 个锁,而在 PostgreSQL 的锁的获取路径中,还有一项鲜为人知的优化,那便是 fastpath,根据表级锁的冲突矩阵我们可以看到,Access Share、Row Exclusive、Row Share 这几个锁是互不冲突的,而且是数据库内绝大多数操作都是 DML 的操作,因此这几个锁可以被称之为"弱锁",而高于Row Exclusive 的锁我们称之为"强锁",弱锁和弱锁互不冲突,如果一个事务对某个对象申请弱锁,同时能够查阅到其他事务在这个对象上没有申请过强锁,则可以在事务所在的会话上记录这个弱锁,而不需要把弱锁保存到主锁表中,这样我们就可以将弱锁保存到本地会话,避免频繁访问 pg_locks,提升性能。
但是关于 fastpath,在代码中是写死的
/*
* We allow a small number of "weak" relation locks (AccessShareLock,
* RowShareLock, RowExclusiveLock) to be recorded in the PGPROC structure
* rather than the main lock table. This eases contention on the lock
* manager LWLocks. See storage/lmgr/README for additional details.
*/
#define FP_LOCK_SLOTS_PER_BACKEND 16
总共 16 个,从第 17 个开始,将变为 false。如果并发一起来,你便会看到 LWLock 的征用,现象便是,很多活跃会话在 pg_stat_activity 中显示 wait_event='LockManager' (在 PG13 或更早版本中为 lock_manager)。
SO,怎么办?
首先,避免过度使用索引。特别是对于那些涉及 (或计划涉及) 高频查询 (高 QPS) 的表,尽量保持索引数量尽可能少。 如果你对表进行了分区,再次确保你没有过度使用索引,并且查询计划仅涉及特定分区 (分区修剪)。 如果必须拥有大量索引,尝试降低相应查询的 QPS。 另一个减少争用的选项是增强集群性能:使用更快的机器,并将只读查询卸载到其他副本上。 检查 pg_locks,将每个查询的关系数量减少到 16 个以下
除此之外,绑定变量的形式也可以

最后,再放张图吧,一图胜千言。关于 fastpath,也已经有相关 patch 了:https://www.postgresql.org/message-id/flat/116ef01e-942a-22c1-a2af-35bf69c1b07b%40enterprisedb.com#b19340c248755be70b805404becd43ad





