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

时间:2015-12-30 20:54:37

标签: sql postgresql postgresql-performance database-indexes

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

我使用子查询重写了查询,试图强制使用索引,然后注意到一些有趣的东西。如果您查看下面的两个查询计划,您将看到当我将子查询的结果集限制为43k时,查询计划确实使用了email_activities上的索引,而将子查询中的限制设置为甚至44k将导致查询计划使用seq扫描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:~10m行
  • email_recipients:~11m行

2 个答案:

答案 0 :(得分:19)

索引扫描 - >位图索引扫描 - >顺序扫描

对于少数行,运行索引扫描是值得的。随着要返回的行数越多(表的百分比越高,并且取决于数据分布,值频率和行宽),就越有可能在一个数据页上找到多行。然后切换到位图索引扫描是值得的。一旦必须访问大部分数据页,运行顺序扫描会更便宜,过滤剩余行并完全跳过索引的开销。

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

更多相关答案:

小心强制查询计划

您不能直接在Postgres中强制使用某个计划程序方法,但是为了进行调试,您可以使其他方法看起来非常昂贵。请参阅手册中的Planner Method Configuration

SET enable_seqscan = off(与其他答案中的建议一样)对顺序扫描执行此操作。但是,这仅用于在会话中进行调试。除非您确切知道自己在做什么,否则 将其用作生产中的常规设置。它可以强制荒谬的查询计划。 Quoting the manual

  

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

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

在这种特殊情况下,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 1
   FROM   email_activities
   WHERE  email_recipient_id = r.id);

答案 1 :(得分:2)

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