PostgreSQL在过滤的多重排序查询中不使用索引

时间:2015-06-22 18:58:15

标签: sql postgresql sorting indexing postgresql-performance

我有一个非常简单的表

CREATE TABLE approved_posts (
  project_id INTEGER,
  feed_id INTEGER,
  post_id INTEGER,
  approved_time TIMESTAMP NOT NULL,
  post_time TIMESTAMP NOT NULL,
  PRIMARY KEY (project_id, feed_id, post_id)
)

我正在尝试优化此查询:

SELECT *
FROM approved_posts
WHERE feed_id IN (?, ?, ?)
AND project_id = ?
ORDER BY approved_time DESC, post_time DESC
LIMIT 1;

查询优化器正在获取与谓词匹配的每个approved_post,对所有100k结果进行排序,并返回找到的最顶层结果。

我在project_id, feed_id, approved_time, post_time上有一个索引,如果我要么使用它:
A。post_time
删除排序 B。IN (?, ?, ?)替换= ? 然后它只是进行反向索引扫描以获得第一个结果并且速度非常快。

选项 A:

 Limit  (cost=0.43..6.57 rows=1 width=24) (actual time=0.101..0.101 rows=1 loops=1)
   ->  Index Scan Backward using approved_posts_approved_time_idx on approved_posts p  (cost=0.43..840483.02 rows=136940 width=24) (actual time=0.100..0.100 rows=1 loops=1)
     Filter: (feed_id = ANY ('{73321,73771,73772,73773,73774}'::integer[]))
     Rows Removed by Filter: 37
 Total runtime: 0.129 ms

选项 B:

Limit  (cost=0.43..3.31 rows=1 width=24) (actual time=0.065..0.065 rows=1 loops=1)
   ->  Index Scan Backward using approved_posts_full_pagination_index on approved_posts p  (cost=0.43..126884.70 rows=44049 width=24) (actual time=0.063..0.063 rows=1 loops=1)
     Index Cond: ((project_id = 148772) AND (feed_id = 73321))
 Total runtime: 0.092 ms

但如果没有这些调整,它就不那么高效了......

Limit  (cost=169792.16..169792.17 rows=1 width=24) (actual time=510.225..510.225 rows=1 loops=1)
   ->  Sort  (cost=169792.16..170118.06 rows=130357 width=24) (actual time=510.224..510.224 rows=1 loops=1)
     Sort Key: approved_time, post_time
     Sort Method: top-N heapsort  Memory: 25kB
     ->  Bitmap Heap Scan on approved_posts p  (cost=12324.41..169140.38 rows=130357 width=24) (actual time=362.210..469.387 rows=126260 loops=1)
           Recheck Cond: (feed_id = ANY ('{73321,73771,73772,73773,73774}'::integer[]))
           ->  Bitmap Index Scan on approved_posts_feed_id_idx  (cost=0.00..12291.82 rows=130357 width=0) (actual time=354.496..354.496 rows=126260 loops=1)
                 Index Cond: (feed_id = ANY ('{73321,73771,73772,73773,73774}'::integer[]))
Total runtime: 510.265 ms

我甚至可以在这5个饲料ID上添加条件索引,它会再次做正确的事情。

我目前最好的解决方案是将每个feed_id放入自己的查询中,并在它们之间执行大量UNION。但是这不能很好地扩展,因为我可能想要从30个Feed中选择前500个,拉入15k行并且没有充分理由对它们进行排序。使用此策略管理偏移量也有些复杂。

是否有人知道如何在我的索引良好的数据上使用两种方法执行此IN子句并让Postgres做正确的事情?

我正在使用Postgres 9.3.3 。以下是我的索引

 "approved_posts_project_id_feed_id_post_id_key" UNIQUE CONSTRAINT, btree (project_id, feed_id, post_id)
 "approved_posts_approved_time_idx" btree (approved_time)
 "approved_posts_feed_id_idx" btree (feed_id)
 "approved_posts_full_pagination_index" btree (project_id, feed_id, approved_time, post_time)
 "approved_posts_post_id_idx" btree (post_id)
 "approved_posts_post_time_idx" btree (post_time)
 "approved_posts_project_id_idx" btree (project_id)

没有列可以为空。

此表有2m行,分为200个Feed ID和19个项目ID。

这些是最常见的Feed ID:

 feed_id | count  
---------+--------
   73607 | 558860
   73837 | 354018
   73832 | 220285
   73836 | 172664
   73321 | 118695
   73819 |  95999
   73821 |  75871
   73056 |  65779
   73070 |  54655
   73827 |  43710
   73079 |  36700
   73574 |  36111
   73055 |  25682
   73072 |  22596
   73589 |  19856
   73953 |  15286
   73159 |  13059
   73839 |   8925

feedid / projectid配对的最低/最高/平均基数而言,我们有:

 min |  max   |          avg          
-----+--------+-----------------------
   1 | 559021 | 9427.9140271493212670

2 个答案:

答案 0 :(得分:4)

使用feed_id的可能值列表,Postgres很难找到最佳查询计划。每个feed_id可以与1 - 559021行相关联(根据您的数字)。 Postgres目前还不够智能,无法看到LIMIT 1特殊情况的潜在优化。一个UNION ALL(不只是UNION)的几个查询,每个feed_idLIMIT 1加上另一个外LIMIT 1(就像您似乎尝试过的那样)演示了潜在的,但需要对可变数量的输入值进行复杂的查询连接。

还有另一种方法可以说服查询计划程序,它可以使用索引扫描从每个feed_id的索引中选择第一行:使用 {{重写您的查询1}} 加入:

LATERAL

或者,对SELECT a.* FROM (VALUES (?), (?), (?)) AS t(feed_id) , LATERAL ( SELECT * FROM approved_posts WHERE project_id = ? AND feed_id = t.feed_id ORDER BY approved_time DESC, post_time DESC LIMIT 1 ) a ORDER BY approved_time DESC, post_time DESC LIMIT 1; 的可变数量的值更方便:

feed_id

传递变量的整数数组,例如SELECT a.* FROM unnest(?) AS t(feed_id) -- provide int[] var , LATERAL ( ... 。使用 '{123, 234, 345}'::int[] 参数的功能也可以优雅地实现这一点。然后,您可以传递VARIADIC值列表:

integer上的索引适用于此,因为Postgres可以向前扫描索引的速度几乎与前向一样快,但(project_id, feed_id, approved_time, post_time)会更好。参见:

如果您不需要返回表的所有列,即使是仅索引扫描也可能是一个选项。

您的列(project_id, feed_id, approved_time DESC, post_time DESC)approved_time已定义为post_time。否则,你必须做更多:

详细介绍NOT NULL联接技术的相关答案:

为什么您的选项A有效?

仔细看看两件事

->  Index Scan Backward using approved_posts_approved_time_idx
    on approved_posts p (cost=0.43..840483.02 rows=136940 width=24)
                        (actual time=0.100..0.100 rows=1 loops=1)
     Filter: (feed_id = ANY ('{73321,73771,73772,73773,73774}'::integer[]))

大胆强调我的。

  1. 使用LATERAL上的另一个较小的索引。
  2. (approved_time)上没有索引条件(在这种情况下不可能),但过滤器
  3. Postgres选择一个完全不同的策略:它从这个索引自下而上(feed_id)读取行,直到找到与Index Scan Backward的给定值之一匹配的行。由于您只有很少的项目和提要(feed_id),因此在第一次匹配之前不必丢弃太多行 - 这就是结果。对于200 feed IDs and 19 project IDs 更快 的实际值为 ,因为“最新”行是先前找到的 - 与我的第一种方法不同更少的值更快。

    一种有前途的替代策略!根据数据分布和查询中的Feed,它可能比我的第一个解决方案更快 - 使用此索引启用它

    feed_id

    选择性地增加列"approved_posts_foo_idx" btree (project_id, approved_time DESC, post_time DESC) project_id的统计目标可能会付费,因此可以更准确地估算两种策略之间的转折点。

    由于您的项目只包含旧行(as per comment),因此您可以通过提示最大feed_id(和approved_time来改进此查询,但这可能不会增加太多) - 如果 知道每个项目的最大post_time(和/或每approved_time),或者至少上限。

    feed_id

答案 1 :(得分:0)

根据我的理解,如果第一个"其中"不是密钥的第一部分,密钥将不会被使用。尝试切换你的"在哪里""在您对project_id和feed_id的查询中。