木星的卫星 IO。来源:ALMA (ESO/NAOJ/NRAO)、I. de Pater 等人;NRAO/AUI NSF,S. Dagnello;NASA/JPL/空间科学研究所
SQL 查询优化对于刚开始使用 PostgreSQL 的人来说是一项挑战。造成这种情况的客观原因有很多,例如:
1、系统性能领域的难度一般。
2、缺乏良好的“实验”环境,让人们可以体验数据库在更大规模上是如何工作的。
3、在仍在开发的 Postgres 可观测性工具中缺乏某些功能(尽管速度很快)。
4、优质学习材料的匮乏。
所有这些障碍都是合理的。它们限制了拥有完善的 Postgres 查询优化技能的工程师数量。然而,有一个特定的人工障碍相当有影响力,而且相对容易消除。
所有这些障碍都是合理的。它们限制了拥有 Postgres 查询优化技能的工程师的数量。然而,有一个特定的人为障碍相当有影响力,也相对容易消除。
就是这样:EXPLAIN
命令默认禁用 BUFFERS
选项。我确信每个需要做一些 SQL 优化工作的人都必须启用和使用它。
EXPLAIN ANALYZE 还是 EXPLAIN (ANALYZE, BUFFERS)?
BUFFERS
选项可帮助我们查看 Postgres 在执行查询执行计划中的每个节点时执行了多少 IO 工作。对于主要执行 IO 密集型作的数据库系统,处理过多的数据页(或“缓冲区”、“块”——取决于上下文)是性能不佳的最常见原因。
此外,我们将考虑 EXPLAIN (ANALYZE)
计划的几个示例,并讨论为什么每个人都需要在对特定查询的性能进行故障排除时使用 BUFFERS
选项。
1) 看 IO 任务完成详情
让我们考虑一个简单的例子 – 两个表,每个表有 2 个 bigint
列,id
(顺序)和 num
(随机),内容完全相同;第二个表在 num
上有一个索引,而第一个表没有索引:
create table t1 asselect id::int8, round(random() * 100000)::int8 as num
from generate_series(1, 10000000) as id;
create table t2 as select * from t1;
alter table t1 add primary key (id);
alter table t2 add primary key (id);
create index i_t2_num on t2 using btree (num);
vacuum analyze t1;
vacuum analyze t2;
结果:
test=# \d t1
Table "nik.t1"
Column | Type | Collation | Nullable | Default
--------+--------+-----------+----------+---------
id | bigint | | not null |
num | bigint | | |
Indexes:
"t1_pkey" PRIMARY KEY, btree (id)
test=# \d t2
Table "nik.t2"
Column | Type | Collation | Nullable | Default
--------+--------+-----------+----------+---------
id | bigint | | not null |
num | bigint | | |
Indexes:
"t2_pkey" PRIMARY KEY, btree (id)
"i_t2_num" btree (num)
现在让我们想象一下,我们需要获取 1000 行,num > 10000
并按 num
排序。让我们比较一下两个表的计划:
test=# explain analyze select * from t1 where num > 10000 order by num limit 1000;
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------
Limit (cost=312472.59..312589.27 rows=1000 width=16) (actual time=294.466..296.138 rows=1000 loops=1)
-> Gather Merge (cost=312472.59..1186362.74 rows=7489964 width=16) (actual time=294.464..296.100 rows=1000 loops=1)
Workers Planned: 2
Workers Launched: 2
-> Sort (cost=311472.57..320835.02 rows=3744982 width=16) (actual time=289.589..289.604 rows=782 loops=3)
Sort Key: num
Sort Method: top-N heapsort Memory: 128kB
Worker 0: Sort Method: top-N heapsort Memory: 128kB
Worker 1: Sort Method: top-N heapsort Memory: 127kB
-> Parallel Seq Scan on t1 (cost=0.00..106139.24 rows=3744982 width=16) (actual time=0.018..188.799 rows=3000173 loops=3)
Filter: (num > 10000)
Rows Removed by Filter: 333161
Planning Time: 0.242 ms
Execution Time: 296.194 ms
(14 rows)
test=# explain analyze select * from t2 where num > 10000 order by num limit 1000;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------
Limit (cost=0.43..24.79 rows=1000 width=16) (actual time=0.033..1.867 rows=1000 loops=1)
-> Index Scan using i_t2_num on t2 (cost=0.43..219996.68 rows=9034644 width=16) (actual time=0.031..1.787 rows=1000 loops=1)
Index Cond: (num > 10000)
Planning Time: 0.114 ms
Execution Time: 1.935 ms
(5 rows)
(此处和下面的示例显示了具有预热缓存的执行计划 – 换句话说,第 2 次或后续执行;我们将在本文末尾讨论缓存状态的主题)。
在 t1
中查找目标行的速度要慢 ~150 倍 – 296.194 ms
对 1.935 ms
– 因为它没有合适的索引。BUFFERS
选项会告诉我们什么?让我们使用 EXPLAIN (ANALYZE, BUFFERS)
来检查它:
test=# explain (analyze, buffers) select * from t1 where num > 10000 order by num limit 1000;
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------------
Limit (cost=312472.59..312589.27 rows=1000 width=16) (actual time=314.798..316.400 rows=1000 loops=1)
Buffers: shared hit=54173
-> Gather Merge (cost=312472.59..1186362.74 rows=7489964 width=16) (actual time=314.794..316.358 rows=1000 loops=1)
Workers Planned: 2
Workers Launched: 2
Buffers: shared hit=54173
-> Sort (cost=311472.57..320835.02 rows=3744982 width=16) (actual time=309.456..309.472 rows=784 loops=3)
Sort Key: num
Sort Method: top-N heapsort Memory: 128kB
Buffers: shared hit=54173
Worker 0: Sort Method: top-N heapsort Memory: 127kB
Worker 1: Sort Method: top-N heapsort Memory: 128kB
-> Parallel Seq Scan on t1 (cost=0.00..106139.24 rows=3744982 width=16) (actual time=0.019..193.371 rows=3000173 loops=3)
Filter: (num > 10000)
Rows Removed by Filter: 333161
Buffers: shared hit=54055
Planning Time: 0.212 ms
Execution Time: 316.461 ms
(18 rows)
test=# explain (analyze, buffers) select * from t2 where num > 10000 order by num limit 1000;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------
Limit (cost=0.43..24.79 rows=1000 width=16) (actual time=0.044..3.089 rows=1000 loops=1)
Buffers: shared hit=1003
-> Index Scan using i_t2_num on t2 (cost=0.43..219996.68 rows=9034644 width=16) (actual time=0.042..2.990 rows=1000 loops=1)
Index Cond: (num > 10000)
Buffers: shared hit=1003
Planning Time: 0.167 ms
Execution Time: 3.172 ms
(7 rows)
请注意,当我们使用BUFFERS – 获取 buffer 和 timing 数字不是免费的,但对开销的讨论超出了本文的目标。
对于 t2
,我们有 1003 个缓冲区中命中,即 ~7.8 MiB。
有人可能会说,所使用的计划和访问方法的结构(Parallel Seq Scan
vs. Index Scan
),以及第一个计划中的 Rows Removed by Filter: 333161
(效率低下的强烈信号!)足以理解差异并做出正确的决定。好吧,对于这种微不足道的情况,是的,我同意。此外,我们将探索 BUFFERS
选项显示其优势的更复杂的示例。在这里,我只注意到知道缓冲区编号也很有帮助,因为我们可以开始了解顺序扫描和索引扫描之间的区别。
2) “感受”物理数据量,并在某种程度上布局
我们来做一些数学运算。我们的表格有两个 8 字节的列,每个元组加上一个 23 字节的标头(填充到 24 字节)——它为每个元组提供 36 字节(换句话说,对于每个行版本)。如果我们忽略额外的数据,如页眉和可见性图,一千万行应该需要 36 * 10000000 / 1024/1024 = ~343。实际上,该表为 ~422 MiB:
test=# \dt+ t2
List of relations
Schema | Name | Type | Owner | Persistence | Access method | Size | Description
--------+------+-------+-------+-------------+---------------+--------+-------------
nik | t2 | table | nik | permanent | heap | 422 MB |
(1 row)
正如我们之前看到的,要从 t2
获取目标 1000 行,我们需要 1003 个缓冲区命中 (~7.8 MiB)。我们能做得更好吗?答案是肯定的。例如,我们可以在索引i_t2_num
中包含 id
,调整 autovacuum 使其处理表的频率要比使用默认设置时高得多,以保持可见性地图的良好维护,并从 Index Only Scans 中受益:
create index i_t2_num_id on t2 using btree(num, id);
vacuum t2;
这将是一个很大的加速:
test=# explain (analyze, buffers) select * from t2 where num > 10000 order by num limit 1000;
QUERY PLAN
------------------------------------------------------------------------------------------------------------------------------------------
Limit (cost=0.43..21.79 rows=1000 width=16) (actual time=0.046..0.377 rows=1000 loops=1)
Buffers: shared hit=29
-> Index Only Scan using i_t2_num_id on t2 (cost=0.43..192896.16 rows=9034613 width=16) (actual time=0.044..0.254 rows=1000 loops=1)
Index Cond: (num > 10000)
Heap Fetches: 0
Buffers: shared hit=29
Planning Time: 0.211 ms
Execution Time: 0.479 ms
(8 rows)
– 低至 29 个缓冲区命中,或仅 232 KiB 数据!
同样,如果没有缓冲区编号,我们无论如何都可以看到差异:Index Only Scan
将解释为什么执行时间低于 1ms,而 Heap Fetches: 0
将是一个很好的信号,表明我们拥有最新的可见性映射,因此 Postgres 根本不需要处理堆。
使用单列索引我们能做得更好吗?可以,但需要对表进行一些物理重组。num
值是使用random()
生成的,因此当执行器在执行Index Scan
时找到索引项,它随后需要处理堆中的许多不同页面。换句话说,我们需要的具有num
值的元组在表中存储得很稀疏。我们可以使用CLUSTER
来重组表(在生产环境中,我们会使用一些非阻塞方法 —— 例如,pg_repack可能会有帮助)
drop index i_t2_num_id; -- not needed, we learn Index Scan behavior now
cluster t2 using i_t2_num;
再次检查计划:
test=# explain (analyze, buffers) select * from t2 where num > 10000 order by num limit 1000;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------
Limit (cost=0.43..24.79 rows=1000 width=16) (actual time=0.071..0.395 rows=1000 loops=1)
Buffers: shared hit=11
-> Index Scan using i_t2_num on t2 (cost=0.43..219998.90 rows=9034771 width=16) (actual time=0.068..0.273 rows=1000 loops=1)
Index Cond: (num > 10000)
Buffers: shared hit=11
Planning Time: 0.183 ms
Execution Time: 0.491 ms
(7 rows)
只需 11 次缓冲区命中,即 88 KiB,即可读取 1000 行!再次是亚毫秒计时。用于Index Scan
。如果没有 BUFFERS
我们能理解其中的区别吗?让我向您展示 CLUSTER
之前和之后的两个执行计划,以便您可以自己比较这些计划,看看我们是否可以在不使用 BUFFERS
的情况下理解差异的原因:
在应用 CLUSTER
之前:
test=# explain analyze select * from t2 where num > 10000 order by num limit 1000;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------
Limit (cost=0.43..24.79 rows=1000 width=16) (actual time=0.033..1.867 rows=1000 loops=1)
-> Index Scan using i_t2_num on t2 (cost=0.43..219996.68 rows=9034644 width=16) (actual time=0.031..1.787 rows=1000 loops=1)
Index Cond: (num > 10000)
Planning Time: 0.114 ms
Execution Time: 1.935 ms
(5 rows)
应用 CLUSTER
后:
test=# explain analyze select * from t2 where num > 10000 order by num limit 1000;
QUERY PLAN
----------------------------------------------------------------------------------------------------------------------------------
Limit (cost=0.43..24.79 rows=1000 width=16) (actual time=0.074..0.394 rows=1000 loops=1)
-> Index Scan using i_t2_num on t2 (cost=0.43..219998.90 rows=9034771 width=16) (actual time=0.072..0.287 rows=1000 loops=1)
Index Cond: (num > 10000)
Planning Time: 0.198 ms
Execution Time: 0.471 ms
(5 rows)
时间上约 4 倍的差异(1.935 ms
与 0.471 ms
),如果不使用 BUFFERS
或仔细检查数据库元数据(\d+ t2
将显示该表是使用 i_t2_num
进行聚类的),在此很难解释清楚。
使用 BUFFERS
的更多情况将极大地帮助我们了解内部发生的事情,以便我们可以在查询优化方面做出正确的决定:
1、高水平的 table/index 膨胀。
2、非常宽的表格(显著的 TOAST 大小)。
3、HOT 更新与索引扩增。
我鼓励我的读者尝试各种计划,看看每种情况的不同之处(如果你有问题,请随时在X上联系我)
在这里,我想展示另一个重要示例 – 在数据更改率高的生产 OLTP 系统上,这种情况并不少见。
在一个 psql 会话中,启动一个事务(分配了真正的 XID)并保持打开状态:
test=# select txid_current();
txid_current
--------------
142719647
(1 row)
test=#
在另一个 psql 会话中,让我们删除 t2
中的一些行并执行 VACUUM 来清理死元组:
test=# delete from t2 where num > 10000 and num < 90000;
DELETE 7998779
test=# vacuum t2;
VACUUM
现在让我们看看我们刚刚使用 CLUSTER 非常快速地制定的执行计划发生了什么(请记住,我们只有 11 次缓冲区命中,即 88 KiB,才能获得我们的 1000 个目标行,并且执行时间不到 1 毫秒):
test=# explain (analyze, buffers) select * from t2 where num > 10000 order by num limit 1000;
QUERY PLAN
-------------------------------------------------------------------------------------------------------------------------------------
Limit (cost=0.43..52.28 rows=1000 width=16) (actual time=345.347..345.431 rows=1000 loops=1)
Buffers: shared hit=50155
-> Index Scan using i_t2_num on t2 (cost=0.43..93372.27 rows=1800808 width=16) (actual time=345.345..345.393 rows=1000 loops=1)
Index Cond: (num > 10000)
Buffers: shared hit=50155
Planning Time: 0.222 ms
Execution Time: 345.481 ms
(7 rows)
现在慢了 ~700 倍(345.481 ms
对 0.491 ms
),我们有 50155 次缓冲区命中,即 50155 * 8 / 1024 = ~392 MiB。而 VACUUM
也无济于事!为了理解原因,让我们重新运行 VACUUM
,这次使用 VERBOSE
:
test=# vacuum verbose t2;
INFO: vacuuming "nik.t2"
INFO: launched 1 parallel vacuum worker for index cleanup (planned: 1)
INFO: table "t2": found 0 removable, 7999040 nonremovable row versions in 43239 out of 54055 pages
DETAIL: 7998779 dead row versions cannot be removed yet, oldest xmin: 142719647
Skipped 0 pages due to buffer pins, 10816 frozen pages.
CPU: user: 0.25 s, system: 0.01 s, elapsed: 0.27 s.
VACUUM
这里7998779 dead row versions cannot be removed yet
意味着 VACUUM
无法清理死 Tuples。这是因为我们有一个长时间运行的事务(请记住,我们在另一个 psql 会话中将其保持打开状态)。这就解释了为什么 SELECT
需要从缓冲池中读取 ~392 MiB,因此查询执行时间超过 300 毫秒。
同样,BUFFERS
选项帮助我们看到问题——没有它,我们只会知道使用 Index Scan 读取 1000 行的执行时间非常短,但只有高缓冲区命中数告诉我们我们处理的是高数据量,而不是一些硬件问题、Postgres 错误或锁定问题。
精简克隆 – 扩展 SQL 优化过程的最佳方式
如前所述,Postgres 查询优化并不是一个微不足道的工程领域。一个好的优化工作流程最重要的组成部分之一是用于实验、执行计划分析和验证优化想法的环境。
首先,良好的实验环境必须具有真实的数据集。不仅 Postgres 设置和表行数很重要(两者都定义了 Planner 将为查询选择的计划),而且数据必须是最新的,并且应该易于迭代。例如,如果您定期从生产中刷新数据,这是一个不错的举措,但您需要等待多长时间,如果您大幅更改了数据,需要多长时间才能进行另一个实验(换句话说,从头开始)?如果您进行了大量实验,特别是如果您的团队中不孤单地从事 SQL 优化任务,那么能够:
1、一方面,为了快速重置状态。
2、另一方面,不干涉同事。
我们如何实现这一目标?这里的关键是优化查询的方法。许多人认为计时是执行计划中的主要指标(那些有执行的 – 通过运行 EXPLAIN ANALYZE 获得)。这是很自然的 – 总体 timing 值是我们在优化查询时要减少的,这也是 EXPLAIN ANALYZE 默认提供的。
1、硬件性能
2、文件系统和作系统及其设置。
3、缓存的状态(文件缓存和 Postgres 缓冲池)。
4、并发活动和锁定。
以前在进行 SQL 优化时,我也最关注时序数字。直到我意识到:
时间是不稳定的。数据量稳定。
执行计划以planned rows
和actual rows
的形式显示数据量——但是,这些数字“太高了”,它们隐藏了有关 Postgres 需要做多少实际 IO 工作来读取或写入这些行的信息。虽然 BUFFERS
选项准确显示了完成了多少 IO!
在生产环境中选择 1000 行可能需要 1 毫秒的执行时间,在实验环境中可能需要 1 秒(反之亦然),我们可能会花费数小时来试图理解为什么会有这种差异。
但是,使用 BUFFERS
,如果我们在两个环境中处理相同或相似大小的数据库,我们总是处理相同或相似的数据卷,从而为我们提供相同(相似)的缓冲区编号。如果我们足够幸运,可以在故障排除/优化 SQL 查询时使用生产克隆(保持相同的数据物理布局、相同的膨胀等),我们将拥有
1、如果没有读取,如果 Postgres 缓冲池已预热,则缓冲区buffer hits
完全相同。
2、如果缓冲池在实验环境中较冷或不够大,那么我们将看到buffer reads
,这将转化为生产环境/具有预热缓冲池状态的系统上的buffer hits
数。
这些观察结果使我们能够制定以下方法:
以 BUFFERS 为中心的 SQL 查询优化
在优化查询时,暂时忘记 TIMING
使用 BUFFERS
。仅在开始和完成 optimization 过程时返回 timing numbers。在流程中,重点关注计划结构和缓冲区编号。是的,SQL 优化的最终目标是尽可能低的 TIMING
值。但在大多数情况下,它是通过减少所涉及的数据量来实现的 - 减少 BUFFERS
数量。
遵循这条规则,我们可以从中受益:
1、在优化查询时,我们可以不再担心生产环境和非生产环境之间的资源差异。我们可以使用较慢的磁盘、较少的 RAM、较弱的处理器 - 如果我们执行面向 BUFFERS 的优化并避免将 timing 值与 production 直接比较,那么这些方面都无关紧要。
2、此外,以 BUFFERS 为中心的方法可以从使用瘦克隆中受益匪浅。在一台计算机上,我们可以运行多个 Postgres 实例,共享一个初始数据目录,并使用 ZFS提供的 Copy-on-Write,以允许同时独立执行多个实验过程。许多人和自动化作业(例如在 CI 中触发)可以工作而不会相互干扰。
正是使用精简克隆进行 SQL 优化并专注于 BUFFERS
的想法促使我们 Postgres.ai 在几年前开始研究
Database Lab Engine (DLE)是一个开源工具,可用于处理任何大小的 Postgres 数据库的精简克隆。使用 DLE,快速增长的项目扩展了他们的 SQL 优化和测试工作流程(在此处阅读更多内容:SQL 优化、案例研究、Joe Bot)。
三点总结
始终使用 EXPLAIN (ANALYZE, BUFFERS)
,而不仅仅是 EXPLAIN ANALYZE
,这样您就可以在执行查询时看到 Postgres 完成的实际 IO 工作。
这样可以更好地了解所涉及的数据量。如果您开始将缓冲区编号转换为字节,那就更好了——只需将它们乘以区块大小(大多数情况下为 8 KiB)。
当您在优化过程中时,不要考虑时间数字 - 这可能感觉有悖常理,但这就是让您忘记环境差异的原因。这就是允许使用精简克隆的原因 – 看看 Database Lab Engine 和其他公司是怎么用它的。
最后,在优化查询时,如果您能够减少BUFFERS
数量,这意味着要执行此查询,Postgres 在相关的缓冲池中所需的缓冲区将更少,从而减少输入输出(IO),将争用风险降至最低,并在缓冲池中为其他内容留出更多空间。遵循这种方法最终可能会对数据库的整体性能产生积极的全局影响。
奖励:规划师的 IO 工作 (Postgres 13+)
PostgreSQL 13 为查询计划中的“planning”部分引入了 BUFFERS
选项:
test=# explain (buffers) select from a;
QUERY PLAN
-----------------------------------------------------
Seq Scan on a (cost=0.00..39.10 rows=2910 width=0)
Planning:
Buffers: shared hit=1
(3 rows)
这在某些情况下可能很有用。例如,最近,我参与了对一个计划非常慢(几分钟)的事件进行故障排除,结果发现规划和考虑 MergeJoin 路径期间,计划者需要查阅索引,该索引有很多“死”条目尚未被 autovacuum 清理——正是这个新功能可以揭示为什么计划如此缓慢的情况。
建议和可能的未来
如何使用 EXPLAIN 命令
只需在每次运行 EXPLAIN ANALYZE
时开始添加 BUFFERS
。请注意,您需要使用括号来组合关键字 ANALYZE
和 BUFFERS
,因此它变成了 EXPLAIN (ANALYZE, BUFFERS)
- 是的,不是很方便。但它会得到回报。
如果你是 psql 用户(和我一样),那么你可以定义一个快捷方式:
test=# \set eab EXPLAIN (ANALYZE, BUFFERS)
test=# :eab select 1;
QUERY PLAN
------------------------------------------------------------------------------------
Result (cost=0.00..0.01 rows=1 width=4) (actual time=0.001..0.001 rows=1 loops=1)
Planning:
Buffers: shared hit=3
Planning Time: 1.216 ms
Execution Time: 0.506 ms
(5 rows)
它可以放在 ~/.psqlrc
中
将缓冲区编号转换为字节可能非常有用 – 我注意到一些工程师更好地理解以字节表示的指标,并且更容易解释为什么他们的查询需要优化。下面是一个示例 SQL 函数,可用于将缓冲区编号转换为人类可读形式的字节:
create function buf2bytes (
in buffers numeric,
in s integer default 2,
out bytes text
) as $func$
with settings as (
select current_setting('block_size')::numeric as bs
), data as (
select
buffers::numeric * bs / 1024 as kib,
floor(log(1024, buffers::numeric * bs / 1024)) + 1 as log,
bs
from settings
), prep as (
select
case
when log <= 8 then round((kib / 2 ^ (10 * (log - 1)))::numeric, s)
else buffers * bs
end as value,
case log -- see https://en.wikipedia.org/wiki/Byte#Multiple-byte_units
when 1 then 'KiB'
when 2 then 'MiB'
when 3 then 'GiB'
when 4 then 'TiB'
when 5 then 'PiB'
when 6 then 'EiB'
when 7 then 'ZiB'
when 8 then 'YiB'
else 'B'
end as unit
from data
)
select format('%s %s', value, unit)
from prep;
$func$ immutable language sql;
test=# select buf2bytes(12345);
buf2bytes
-----------
96.45 MiB
(1 row)
test=# select buf2bytes(1234567, 3);
buf2bytes
-----------
9.419 GiB
(1 row)
对内容创作者的建议
我看到很多文章、讲座、书籍讨论 SQL 优化,但不涉及 BUFFERS
,不讨论真正的 IO。如果您是此类材料的作者之一,请考虑在将来的内容中使用 BUFFERS
。
比较好的文档,例如:
https://www.cybertec-postgresql.com/en/how-to-interpret-postgresql-explain-analyze-output/
https://docs.gitlab.com/ee/development/understanding_explain_plans.html
https://www.youtube.com/watch?v=31EmOKBP1PY
以下是一些缺少 BUFFERS
的内容示例:
https://www.crunchydata.com/blog/get-started-with-explain-analyze
https://www.enterprisedb.com/postgres-tutorials/postgresql-query-introduction-explanation-and-50-examples
https://www.timescale.com/blog/how-we-made-distinct-queries-up-to-8000x-faster-on-postgresql
上面的例子只是众多例子中的一小部分——无意让这些作者感到不安。我选择这些材料是为了强调一些拥有有影响力的博客的知名公司还没有遵循我在这里倡导的想法。我希望将来会有更多的材料开始将 BUFFERS
指标纳入演示计划,讨论它们,并根据它们得出结论。
翻译自:
https://postgres.ai/blog/20220106-explain-analyze-needs-buffers-to-improve-the-postgres-query-optimization-process