PostgreSQL:使用复合键对大型表进行查询时性能不佳

Rob*_*ves 10 sql postgresql postgresql-performance amazon-aurora postgresql-12

我们有一个包含 180m 行、大小为 20 GB 的表。\n表 DDL 为:

\n
create table app.table\n(\n    a_id    integer   not null,\n    b_id    integer   not null,\n    c_id    integer   not null,\n    d_id    integer   not null,\n    e_id    integer   not null,\n    f_id    integer   not null,\n    a_date  timestamp not null,\n    date_added          timestamp,\n    last_date_modified  timestamp default now()\n);\n
Run Code Online (Sandbox Code Playgroud)\n

价值分布:

\n
    \n
  • a_id 的范围是 0-160,000,000
  • \n
  • b_id 有一个值(该表是分区表的单个分区的副本,并且该 ID 恰好是分区键)
  • \n
  • c_id的范围是0-4
  • \n
  • d_id 有一个值(当前)
  • \n
  • e_id 有一个值(当前)
  • \n
\n

主键是复合键:

\n
create table app.table\n(\n    a_id    integer   not null,\n    b_id    integer   not null,\n    c_id    integer   not null,\n    d_id    integer   not null,\n    e_id    integer   not null,\n    f_id    integer   not null,\n    a_date  timestamp not null,\n    date_added          timestamp,\n    last_date_modified  timestamp default now()\n);\n
Run Code Online (Sandbox Code Playgroud)\n

我们正在r6g.xlargeAurora PostgreSQL v12.8 中运行集群。这是一个没有其他流量访问它的实例。我们已经跑到ANALYZE桌子VACUUM ANALYZE上了:

\n
INFO:  "table": scanned 30000 of 1711284 pages, containing 3210000 live\n rows and 0 dead rows; 30000 rows in sample, 183107388 estimated total rows\n
Run Code Online (Sandbox Code Playgroud)\n

问题

\n

shared_buffers当天气寒冷时(或者我们能得到的最冷的温度),这个查询需要 9 秒来运行:

\n
alter table app.table add constraint table_pk primary key (a_id, b_id, c_id, d_id, e_id);\n
Run Code Online (Sandbox Code Playgroud)\n

EXPLAIN输出:

\n
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)\n"  Index Cond: ((a_id = ANY (\'{66986803,90478329,...,121697593}\'::integer[])) AND (b_id = 34))"\n"  Filter: (c_id = ANY (\'{2,3}\'::integer[])))"\n  Rows Removed by Filter: 3\n  Buffers: shared hit=12610 read=10593\n  I/O Timings: read=9706.055\nPlanning:\n  Buffers: shared hit=112 read=29\n  I/O Timings: read=29.227\nPlanning Time: 33.437 ms\nExecution Time: 9806.271 ms\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n

无论如何,我们希望此类查询有更好的性能,如果可能的话,在 1-2 秒左右。关于如何提高性能有什么想法吗?

\n
\n

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

\n

尝试添加覆盖索引以包含“a_date”:

\n
create unique index covering_idx on app.table (a_id, b_id, c_id, d_id, e_id) include (a_date)\n
Run Code Online (Sandbox Code Playgroud)\n

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

\n
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)\n  Index Cond: ((a_id = ANY (\'{134952505,150112033,\xe2\x80\xa6,42959574}\'::integer[])) AND (b_id = 34))\n  Filter: ((e_id = ANY (\'{0,0}\'::integer[])) AND (c_id = ANY (\'{2,3}\'::integer[])))\n  Rows Removed by Filter: 2\n  Heap Fetches: 0\n  Buffers: shared hit=12353 read=7733\n  I/O Timings: read=6955.935\nPlanning:\n  Buffers: shared hit=80 read=8\n  I/O Timings: read=8.458\nPlanning Time: 11.930 ms\nExecution Time: 7031.054 ms\n
Run Code Online (Sandbox Code Playgroud)\n
\n

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

\n

我们发现,当使用位图堆扫描(而不是索引扫描)执行查询时,速度会提高。我们通过使用以下命令强制计划发现了这一点pg_hint_plan

\n

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

\n
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)\n  Recheck Cond: ((a_id = ANY (\'{24933126,19612702,27100661,73628268,...,150482461}\'::integer[])) AND (b_id = 34))\n  Filter: ((d_id = ANY (\'{0,0}\'::integer[])) AND (c_id = ANY (\'{2,3}\'::integer[])))\n Rows Removed by Filter: 4\n  Heap Blocks: exact=5644\n  Buffers: shared hit=14526 read=11136\n  I/O Timings: read=22507.527\n  ->  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)\n       Index Cond: ((a_id = ANY (\'{24933126,19612702,27100661,,150482461}\'::integer[])) AND (b_id = 34))\n       Buffers: shared hit=14505 read=5513\n       I/O Timings: read=3923.878\nPlanning:\n  Buffers: shared hit=6718\nPlanning Time: 21.493 ms\n{Execution Time: 4066.582 ms\n
Run Code Online (Sandbox Code Playgroud)\n

目前,我们正在考虑在生产中强制使用这个计划pg_hint_plan- 但我们更想知道为什么计划者选择了一个不太理想的计划!我们已经运行VACUUM ANALYZEdefault_statistics_target1000 个。

\n

jja*_*nes 2

这个问题可能是针对 Aurora 的,我对此没有太多经验。

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

每次读取大约 1 毫秒的读取时间对于使用良好 SSD 层的东西来说似乎相当慢(我自己的蹩脚 SSD 层就可以做到这一点),但我找不到任何关于 Aurora 存储层的良好数据。

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

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