索引扫描时 Postgres 不使用索引是更好的选择

Posted

技术标签:

【中文标题】索引扫描时 Postgres 不使用索引是更好的选择【英文标题】:Postgres not using index when index scan is much better option 【发布时间】:2016-04-04 20:59:57 【问题描述】:

我有一个简单的查询来连接两个非常慢的表。我发现查询计划对大表 email_activities(约 10m 行)进行 seq 扫描,而我认为使用索引执行嵌套循环实际上会更快。

我使用子查询重写了查询,试图强制使用索引,然后发现了一些有趣的东西。如果您查看下面的两个查询计划,您会看到当我将子查询的结果集限制为 43k 时,查询计划确实在 email_activities 上使用索引,而将子查询中的限制设置为 44k 将导致查询计划使用 seq scan on email_activities。一个显然比另一个更有效率,但 Postgres 似乎并不在意。

是什么原因造成的?如果其中一个集合大于某个大小,它是否有一个配置强制使用哈希连接?

explain analyze SELECT COUNT(DISTINCT "email_activities"."email_recipient_id") FROM "email_activities" where email_recipient_id in (select "email_recipients"."id" from email_recipients WHERE "email_recipients"."email_campaign_id" = 1607 limit 43000);
                                                                                            QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=118261.50..118261.50 rows=1 width=4) (actual time=224.556..224.556 rows=1 loops=1)
   ->  Nested Loop  (cost=3699.03..118147.99 rows=227007 width=4) (actual time=32.586..209.076 rows=40789 loops=1)
         ->  HashAggregate  (cost=3698.94..3827.94 rows=43000 width=4) (actual time=32.572..47.276 rows=43000 loops=1)
               ->  Limit  (cost=0.09..3548.44 rows=43000 width=4) (actual time=0.017..22.547 rows=43000 loops=1)
                     ->  Index Scan using index_email_recipients_on_email_campaign_id on email_recipients  (cost=0.09..5422.47 rows=65710 width=4) (actual time=0.017..19.168 rows=43000 loops=1)
                           Index Cond: (email_campaign_id = 1607)
         ->  Index Only Scan using index_email_activities_on_email_recipient_id on email_activities  (cost=0.09..2.64 rows=5 width=4) (actual time=0.003..0.003 rows=1 loops=43000)
               Index Cond: (email_recipient_id = email_recipients.id)
               Heap Fetches: 40789
 Total runtime: 224.675 ms

还有:

explain analyze SELECT COUNT(DISTINCT "email_activities"."email_recipient_id") FROM "email_activities" where email_recipient_id in (select "email_recipients"."id" from email_recipients WHERE "email_recipients"."email_campaign_id" = 1607 limit 50000);
                                                                                            QUERY PLAN
--------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Aggregate  (cost=119306.25..119306.25 rows=1 width=4) (actual time=3050.612..3050.613 rows=1 loops=1)
   ->  Hash Semi Join  (cost=4451.08..119174.27 rows=263962 width=4) (actual time=1831.673..3038.683 rows=47935 loops=1)
         Hash Cond: (email_activities.email_recipient_id = email_recipients.id)
         ->  Seq Scan on email_activities  (cost=0.00..107490.96 rows=9359988 width=4) (actual time=0.003..751.988 rows=9360039 loops=1)
         ->  Hash  (cost=4276.08..4276.08 rows=50000 width=4) (actual time=34.058..34.058 rows=50000 loops=1)
               Buckets: 8192  Batches: 1  Memory Usage: 1758kB
               ->  Limit  (cost=0.09..4126.08 rows=50000 width=4) (actual time=0.016..27.302 rows=50000 loops=1)
                     ->  Index Scan using index_email_recipients_on_email_campaign_id on email_recipients  (cost=0.09..5422.47 rows=65710 width=4) (actual time=0.016..22.244 rows=50000 loops=1)
                           Index Cond: (email_campaign_id = 1607)
 Total runtime: 3050.660 ms
版本:x86_64-unknown-linux-gnu 上的 PostgreSQL 9.3.10,由 gcc (Ubuntu/Linaro 4.6.3-1ubuntu5) 4.6.3 编译,64 位 email_activities:~1000 万行 email_recipients:~1100 万行

【问题讨论】:

HashAggregate 操作可能需要太多内存来存储 50k 行。尝试增加work_mem ? 缺少基本信息。请考虑tag info for [postgresql-perfiormance] 中的说明。此外,您的第二个查询是针对LIMIT 50000,而不是针对44k,如上所述。增加差异。 @ErwinBrandstetter,很抱歉造成混淆。我只是说将限制​​从 43k 提高到 44k 确实改变了对 seq 扫描的计划。 (从 50k 下降到 44k ......)。感谢您的标签信息。这是我第一次发布与 postgres 相关的帖子。 有人知道将 \d+ 粘贴到问题中的干净方法吗? 【参考方案1】:

索引(仅)扫描 --> 位图索引扫描 --> 顺序扫描

对于少数行,运行索引扫描是值得的。如果足够多的数据页对所有人可见(= 足够清理,并且没有太多的并发写入负载)并且索引可以提供所需的所有列值,则使用更快的仅索引扫描。随着预期返回的行数更多(表的百分比更高,并且取决于数据分布、值频率和行宽),它更有可能在一个数据页上找到多行。然后切换到位图索引扫描是值得的。 (或者组合多个不同的索引。)一旦必须访问大部分数据页,运行顺序扫描、过滤多余的行并完全跳过索引的开销会更便宜。

当以随机顺序访问数据页面并不比按顺序访问它们(多)昂贵时,索引使用变得(多)便宜且更有可能。当使用 SSD 而不是旋转磁盘时就是这种情况,甚至更多的缓存在 RAM 中 - 并且相应的配置参数 random_page_costeffective_cache_size 进行了相应设置。

在您的情况下,Postgres 切换到顺序扫描,期望找到rows=263962,这已经是整个表的 3%。 (虽然实际上只找到了rows=47935,但见下文。)

更多相关答案:

Efficient PostgreSQL query on timestamp using index or bitmap index scan?

谨防强制查询计划

您不能直接在 Postgres 中强制使用某个规划器方法,但您可以使 其他 方法看起来非常昂贵,以用于调试目的。请参阅手册中的Planner Method Configuration。

SET enable_seqscan = off(如另一个答案中所建议的)对顺序扫描执行此操作。但这仅用于在您的会话中进行调试。 不要将此作为生产中的一般设置,除非您确切知道自己在做什么。它可以强制执行荒谬的查询计划。 The manual:

这些配置参数提供了一种粗略的影响方法 查询优化器选择的查询计划。如果默认计划 优化器为特定查询选择的不是最优的,a 临时解决方案是使用这些配置参数之一来强制优化器选择不同的计划。更好的方法 提高优化器选择的计划的质量包括 调整计划者成本常数(参见Section 19.7.2), 手动运行ANALYZE,增加 default_statistics_target 配置参数,和 增加为特定列收集的统计信息量 使用ALTER TABLE SET STATISTICS

这已经是您需要的大部分建议了。

Keep PostgreSQL from sometimes choosing a bad query plan

在这种特殊情况下,Postgres 预计 email_activities.email_recipient_id 上的点击量是实际发现的 5-6 倍:

估计 rows=227007actual ... rows=40789 估计 rows=263962actual ... rows=47935

如果您经常运行此查询,那么让ANALYZE 查看更大的样本以获得有关特定列的更准确统计信息是值得的。你的表很大(~ 10M 行),所以这样做:

ALTER TABLE email_activities ALTER COLUMN email_recipient_id
SET STATISTICS 3000;  -- max 10000, default 100

然后ANALYZE email_activities;

最后的措施

非常罕见的情况下,您可能会在单独的事务或具有自己环境的函数中强制使用SET LOCAL enable_seqscan = off 索引。喜欢:

CREATE OR REPLACE FUNCTION f_count_dist_recipients(_email_campaign_id int, _limit int)
  RETURNS bigint AS
$func$
   SELECT COUNT(DISTINCT a.email_recipient_id)
   FROM   email_activities a
   WHERE  a.email_recipient_id IN (
      SELECT id
      FROM   email_recipients
      WHERE  email_campaign_id = $1
      LIMIT  $2)       -- or consider query below
$func$  LANGUAGE sql VOLATILE COST 100000 SET enable_seqscan = off;

该设置仅适用于函数的本地范围。

警告:这只是一个概念证明。从长远来看,即使是这种不那么激进的手动干预也可能会咬到你。基数、值频率、你的模式、全局 Postgres 设置,一切都随着时间而变化。您将升级到新的 Postgres 版本。您现在强制执行的查询计划,以后可能会成为一个非常糟糕的主意。

通常这只是您的设置问题的解决方法。最好找到并修复它。

替代查询

问题中缺少基本信息,但这个等效查询可能更快,并且更有可能在 (email_recipient_id) 上使用索引 - 对于更大的 LIMIT,这种情况越来越多。

SELECT COUNT(*) AS ct
FROM  (
   SELECT id
   FROM   email_recipients
   WHERE  email_campaign_id = 1607
   LIMIT  43000
   ) r
WHERE  EXISTS (
   SELECT FROM email_activities
   WHERE  email_recipient_id = r.id);

【讨论】:

感谢您的详细解答。 很好的答案,但它对我不起作用。显然我不得不终止一些空闲连接,请参阅this answer【参考方案2】:

即使存在索引,顺序扫描也会更有效。在这种情况下,postgres 似乎估计事情相当错误。 在这种情况下,所有相关表上的 ANALYZE <TABLE> 都会有所帮助。如果没有,您可以将变量 enable_seqscan 设置为 OFF,以强制 postgres 在技术上可能时使用索引,但代价是,有时在顺序扫描执行得更好时会使用索引扫描。

【讨论】:

同意分析,但我真的不建议将 enable_seqscan 设置为 OFF。可能会导致其他查询变慢 我在 Postgresql 中遇到过这个问题。对我来说,最好的结果似乎来自对表中的数据进行分区,这似乎真的迫使它使用更合理的集合,即使它最终进行扫描。 如果您想确保查询实际使用索引但不应该在生产环境中使用,这是一个非常有用的选项。

以上是关于索引扫描时 Postgres 不使用索引是更好的选择的主要内容,如果未能解决你的问题,请参考以下文章

Postgres 更新不使用主键索引

如何强制 Postgres 使用特定索引?

Postgres:强制分析器使用位图扫描而不是索引扫描

postgres中大型数据库的索引

索引扫描不适用于 postgres 中的 json 数据集

防止在 Postgres 中为特定查询使用索引