【问题标题】:PostgreSQL: Underperforming query on large table with composite keyPostgreSQL:对具有复合键的大表的查询性能不佳
【发布时间】:2022-08-20 00:35:20
【问题描述】:

我们有一个 180m 行的表,大小为 20 GB。 表 DDL 为:

create table app.table
(
    a_id    integer   not null,
    b_id    integer   not null,
    c_id    integer   not null,
    d_id    integer   not null,
    e_id    integer   not null,
    f_id    integer   not null,
    a_date  timestamp not null,
    date_added          timestamp,
    last_date_modified  timestamp default now()
);

价值分布:

  • a_id 的范围为 0-160,000,000
  • b_id只有一个值(这个表是一个分区表的单个分区的副本,这个ID正好是分区键)
  • c_id 的范围为 0-4
  • d_id 有一个值(当前)
  • e_id 有一个值(当前)

主键是复合键:

alter table app.table add constraint table_pk primary key (a_id, b_id, c_id, d_ie, e_ie);

我们在 Aurora PostgreSQL v12.8 中运行 r6g.xlarge 集群。这是一个没有其他流量的实例。我们已经在桌子上运行了 ANALYZEVACUUM ANALYZE

INFO:  \"table\": scanned 30000 of 1711284 pages, containing 3210000 live
 rows and 0 dead rows; 30000 rows in sample, 183107388 estimated total rows

问题

shared_buffers 很冷(或我们可以得到的最冷)时,此查询需要 9 秒才能运行:

select a_id, b_id, c_id, d_id, a_date
from app.table ts
where a_id in ( <5000 values> )
and b_id = 34
and c_id in (2,3)
and d_id = 0

EXPLAIN 输出:

Index Scan using table_pk on table ts  (cost=0.57..419134.91 rows=237802 width=24) (actual time=8.335..9803.424 rows=5726 loops=1)
\"  Index Cond: ((a_id = ANY (\'{66986803,90478329,...,121697593}\'::integer[])) AND (b_id = 34))\"
\"  Filter: (c_id = ANY (\'{2,3}\'::integer[])))\"
  Rows Removed by Filter: 3
  Buffers: shared hit=12610 read=10593
  I/O Timings: read=9706.055
Planning:
  Buffers: shared hit=112 read=29
  I/O Timings: read=29.227
Planning Time: 33.437 ms
Execution Time: 9806.271 ms

我们认为这是不合理的缓慢。当查询再次运行时,因此来自缓存,所需时间为 25 毫秒。如果可能,我们宁愿不预热。

无论如何,我们宁愿对这种查询有更好的性能,如果可能的话,大约在 1-2 秒左右。关于我们如何提高性能的任何想法?


编辑 - 添加覆盖索引的效果:

尝试添加覆盖索引以包含 \"a_date\":

create unique index covering_idx on app.table (a_id, b_id, c_id, d_id, e_id) include (a_date)

EXPLAIN 重新运行查询后的结果(使用冷的shared_buffers 缓存):

Index Only Scan using covering_idx on table ts (cost=0.57..28438.58 rows=169286 width=24) (actual time=8.020..7028.442 rows=5658 loops=1)
  Index Cond: ((a_id = ANY (\'{134952505,150112033,…,42959574}\'::integer[])) AND (b_id = 34))
  Filter: ((e_id = ANY (\'{0,0}\'::integer[])) AND (c_id = ANY (\'{2,3}\'::integer[])))
  Rows Removed by Filter: 2
  Heap Fetches: 0
  Buffers: shared hit=12353 read=7733
  I/O Timings: read=6955.935
Planning:
  Buffers: shared hit=80 read=8
  I/O Timings: read=8.458
Planning Time: 11.930 ms
Execution Time: 7031.054 ms

使用位图堆扫描与索引扫描时的效果:

我们发现,当使用位图堆扫描而不是索引扫描执行查询时,我们可以加快速度。我们通过使用pg_hint_plan 强制执行计划发现了这一点:

添加/*+ BitmapScan(table) */时:

Bitmap Heap Scan on table ts (cost=22912.96..60160.79 rows=9842 width=24) (actual time=3972.237..4063.417 rows=5657 loops=1)
  Recheck Cond: ((a_id = ANY (\'{24933126,19612702,27100661,73628268,...,150482461}\'::integer[])) AND (b_id = 34))
  Filter: ((d_id = ANY (\'{0,0}\'::integer[])) AND (c_id = ANY (\'{2,3}\'::integer[])))
 Rows Removed by Filter: 4
  Heap Blocks: exact=5644
  Buffers: shared hit=14526 read=11136
  I/O Timings: read=22507.527
  ->  Bitmap Index Scan on table_pk (cost=0.00..22898.00 rows=9842 width=0) (actual time=3969.920..3969.920 rows=5661 loops=1)
       Index Cond: ((a_id = ANY (\'{24933126,19612702,27100661,,150482461}\'::integer[])) AND (b_id = 34))
       Buffers: shared hit=14505 read=5513
       I/O Timings: read=3923.878
Planning:
  Buffers: shared hit=6718
Planning Time: 21.493 ms
{Execution Time: 4066.582 ms

目前,我们正在考虑使用pg_hint_plan 将这个计划强制投入生产——但我们更想知道为什么规划者选择了一个不太理想的计划!我们已经运行 VACUUM ANALYZE 和 1000 的 default_statistics_target

  • 它似乎只是用于获取记录的 IO,因为它正在使用索引。您是否考虑过对这张表进行分区?
  • 我刚刚意识到这是来自另一个表的分区的副本:P 然而,一个 20GB 的表似乎是进一步分区的候选者。
  • 我们可以进一步对其进行分区,但这仅意味着我们最终会跨分区进行查询。据我了解,分区应该旨在让您尽可能少地访问分区,这将违反。
  • 这完全取决于分区键范围......在不了解完整用例的情况下很难说。
  • 我懂了。我会尝试创建一个covering index,也许这里的问题是堆页面的随机访问。

标签: postgresql amazon-aurora postgresql-12


【解决方案1】:

这个问题可能非常特定于 Aurora,我对此没有太多经验。

您的仅索引扫描结果有点令人惊讶。我认为不应该通过 7733 次缓冲区读取来获得 5658 行(加上 2 行过滤和 0 堆获取)。我不希望它需要超过 5700 次读取。但我知道 Aurora 的存储层与社区 PostgreSQL 有很大不同,所以也许这与它有关。无论如何,这只是减少了 25%,而不是您正在寻找的 10 倍。编辑:我意识到那些额外的读取是内部索引页面。起初我拒绝了这个想法,因为 2075 个内部页面与 5658 个叶子页面的比例是一个荒谬的比例。但后来我意识到,一个查询读取的叶子页面只是存在的所有叶子页面的一小部分,而读取的内部页面可能是存在的所有内部页面的大部分。这可能是您的测试方法中的一个缺陷。为了避免不公平地缓存数据,每次随机选择一个不同的 5000 a_id 就足够了。重新启动整个数据库(或您用来清除缓存的任何方法)都太过分了。如果它不是矫枉过正,因为您确实在每次查询之间重新启动生产数据库,那么,停止这样做。

每次读取大约 1 毫秒的读取时间对于使用好的 SSD 层的东西来说似乎相当慢(我自己的蹩脚的那个做得很好),但我找不到任何关于你应该从 Aurora 的存储层得到什么的好的数据。

我也很好奇行估计值下降了 30 到 50 倍。这是为什么?对此提出更准确的估计应该不难。但是,我认为不同的计划不会更快,所以估计真的不重要。但你永远不知道一个谜会把你引向何方。如果您只有 a_id IN-list 并删除其余列条件怎么办?编辑:我想我意识到了这个问题的答案,用于计算 pg_stats.n_distinct 的 PostgreSQL 采样方法存在微妙的偏差,在一个非常大的表聚集在被采样的列上的情况下,可能会大大低估 n_distinct(此处为 a_id) , n_distinct 对选择性估计非常重要。幸运的是,您可以使用 alter table app."table" alter a_id set (n_distinct = 9999999); 手动覆盖此估计值。但同样,这对你来说没什么用,因为没有更好的计划了。不过,这对于其他查询可能很重要。

但我认为你的赌注是退后一步。你为什么要运行这个查询?它的“商业案例”是什么? 5000 个 id 的列表来自哪里?他们有什么模式吗?

【讨论】:

  • “我也很好奇行估计值下降了 30 到 50 倍。这是为什么呢?” - 我不确定。这也让我很困惑。即使我将ANALYZE 设置为default_statistics_target 的表设置为1000,它仍然认为它会拉回相同数量的行。
  • 至于条件的删除 - 有趣的是,我们发现速度与删除这些条件非常相似(即仅存在 a_idb_id 时)。我们认为我们可以在 API 层中检索更多数据并尽可能多地缓存。如果 DB 层会变慢,那么我们可能不得不解决它。但是,我们仍然对为什么它很慢感到好奇,因为它看起来过于缓慢,而且我们仍然担心冷查询。
  • @RobertHargreaves 为什么这么慢似乎很简单。您正在跳转到索引中 >5000 个随机点,这会生成 >5000 个随机 IO;并且随机 IO 很慢。我看不出 API 缓存在这里有什么帮助,除非有一些你没有向我们展示的规律性。如果您没有足够的 RAM 来缓存所需的内容,为什么将相同的 RAM 分布在两个大部分冗余的缓存上会使事情变得更好? API缓存不会仍然遭受冷查询吗?
  • @RobertHargreaves 我编辑了我的答案,以添加我在编写第一个答案后得出的一些认识。他们没有解决你的问题,只是更全面地解释它。
  • 感谢您添加这些编辑 - 他们非常有帮助!我们只是重新启动了数据库来模拟冷缓存——我们实际上并没有在生产中这样做:)
【解决方案2】:

您正在尝试优化查询性能冷缓存.

这是一个没有其他流量的实例。我们在桌子上跑了ANALYZEVACUUM ANALYZE

(除此之外,ANALYZE 单独对VACUUM ANALYZE 没有添加任何内容,所以这是多余的。)

为了优化,最小化数据页数必须阅读。 所以 ...

  1. ...减少存储大小如果可能,每行。 (对于仅索引扫描,这对于所涉及的索引最重要。)

  2. ... 增加数据局部性:同一数据页中的更多元组意味着要读取的页面更少。

    只需重新排序 PK 列

    你应该得到一些从简单地重新排序您的 PK 中的列得到改进。你现在有:

    primary key (a_id, b_id, c_id, d_ie, e_id)
    

    带领先a_id。不同a_id 的索引元组尽可能分散。正是您的查询的作用不是需要。你披露:

    b_id 有一个值 [...]
    d_id 有一个值(当前)
    e_id 有一个值(当前)
    c_id 的范围为 0-4
    a_id 的范围为 0-160,000,000

    将这样的列重新排序为最大化局部性对于您的查询:

    ALTER TABLE app.table ADD CONSTRAINT table_pk PRIMARY KEY (b_id, d_id, e_id, c_id, a_id) INCLUDE (a_date);
    

    由于b_idd_id / e_id(当前)是常数,因此它们只是噪声/镇流器。重要的部分是将c_id 移动到d_id 之前,这样,我们就不会用c_id IN (0,1,4) 接触索引的分支,并且更多的元组最终会出现在更少的索引页上。这是一种温和的效果,因为无论如何我们似乎都使用了一半的光谱。

    更激进

    由于b_id 是一个常数,因此不应从一开始就淡化PK。 d_idd_id 也是如此如果它们实际上保持不变。

    我们的查询根本不需要e_id

    这个改编的查询:

    SELECT a_id, 34 AS b_id, c_id, 0 AS d_id, a_date
    FROM   app.table ts
    WHERE  c_id IN (2,3)
    AND    a_id IN ( < 5000 VALUES > )
    

    ..结合这个索引将是好多了

    CREATE INDEX foo ON app.table (c_id, d_id) INCLUDE (a_date)
    

    可能更好,但是:

    SELECT a_id, 34 AS b_id, 2 AS c_id, 0 AS d_id, a_date
    FROM   app.table ts
    WHERE  c_id = 2
    AND    a_id IN ( < 5000 VALUES > )
    UNION ALL
    SELECT a_id, 34 AS b_id, 3 AS c_id, 0 AS d_id, a_date
    FROM   app.table ts
    WHERE  c_id = 3
    AND    a_id IN ( < 5000 VALUES > )
    

    这应该只允许仅索引条件(查询计划中的Index Cond:)和查询计划中没有过滤器(Filter:)的仅索引扫描,以获得最大速度。

    甚至是最后一个查询的部分索引:

    CREATE INDEX foo_c2 ON app.table (d_id) INCLUDE (a_date) WHERE c_id = 2;
    CREATE INDEX foo_c3 ON app.table (d_id) INCLUDE (a_date) WHERE c_id = 3;
    

    允许更多的索引重复数据删除,因此涉及更少的索引页。
    为此考虑手册页的底部"Index-Only Scans and Covering Indexes"

【讨论】:

    猜你喜欢
    • 2013-09-22
    • 1970-01-01
    • 1970-01-01
    • 2017-08-13
    • 1970-01-01
    • 1970-01-01
    • 2020-04-12
    • 1970-01-01
    • 2017-11-04
    相关资源
    最近更新 更多