PostgreSQL 未对过滤的多重排序查询使用索引

Posted

技术标签:

【中文标题】PostgreSQL 未对过滤的多重排序查询使用索引【英文标题】:PostgreSQL not using index on a filtered multiple sort query 【发布时间】:2015-09-08 08:42:39 【问题描述】:

我有一张很简单的桌子

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 个提要中选择前 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。

这些是最常见的提要 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

【问题讨论】:

9.3.3 提出了一个问题:为什么不至少 9.3.9(如果 9.4 不是一个选项)? We always recommend that all users run the latest available minor release for whatever major version is in use. 我们会根据您的建议进行升级 您提供了所有必要的详细信息,这使我能够找到您有趣问题的答案。许多问题未能提供基础知识,这在这里一直很麻烦 - 现在让您的问题在这方面大放异彩。 【参考方案1】:

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

还有另一种方法可以让查询规划器使用索引扫描从每个feed_id的索引中挑选第一行:用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 值的列表:

Pass multiple values in single parameter

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

Optimizing queries on a range of timestamps (two columns)

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

您的列approved_timepost_time 定义为NOT NULL。否则,你必须做更多:

Unused index in range of dates query

详细说明LATERAL 连接技术的相关答案:

Optimize GROUP BY query to retrieve latest record per user

为什么您的选项 A 有效?

仔细观察会发现两件事

-> 使用 approved_posts_approved_time_idx 向后索引扫描
    在approved_posts p 上(成本=0.43..840483.02 行=136940 宽度=24)
                        (实际时间=0.100..0.100 行=1 循环=1)
     过滤器: (feed_id = ANY ('73321,73771,73772,73773,73774'::integer[]))

我的大胆强调。

    仅在 (approved_time) 上使用了不同的较小索引。 feed_id 上没有 索引条件(在这种情况下不可能),而是一个 过滤器

Postgres 选择了一种完全不同的策略:它自下而上 (Index Scan Backward) 从此索引中读取行,直到找到与您给定的 feed_id 值之一匹配的行。由于您只有很少的项目和提要 (200 feed IDs and 19 project IDs),因此它可能不必在第一次匹配之前丢弃太多行 - 这就是结果。这实际上得到 fastermorefeed_id,因为“最新”行更早找到 - 不像我的第一种方法更快较少个值。

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

"approved_posts_foo_idx" btree (project_id, approved_time DESC, post_time DESC)

有选择地增加project_idfeed_id 列的统计目标可能是值得的,这样可以更准确地估计两种策略之间的临界点。

Postgresql - Query running a lot faster with enable_nestloop=false. Why is the planner not doing the right thing?

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

SELECT ...
WHERE  ...
AND   approved_time <= $upper_bound

【讨论】:

这是迄今为止教 postgres 使用哪个索引的最优雅的方法,并且在我们的查询构建器中更容易适应!很高兴知道,postgres 出错的地方在于值的巨大范围。 今天早上我想到了一个问题:如果根本问题是每个提要 id 仅有 1 个条目,那么当我们放弃辅助排序时怎么会(并且仅按批准时间 DESC 排序)它选择反向索引扫描而不需要任何其他更改?编辑:事实上,想想看,因为按approved_time 排序只对(approved_time, post_time) 进行反向索引扫描,它实际上已经以approved_time DESC,post_time DESC 顺序返回数据。一旦我们按照它已经给我们的顺序简单地请求它,为什么 postgres 会改变它的计划? @MikeFairhurst:很好的问题,我自己也对这个转折点感到困惑——直到我仔细查看了您的EXPLAIN 输出。考虑一下我的答案的附录。 太棒了!您提出的索引有效,无需重写查询!再次感谢您! 我的数据是非常临时的...我首先使用三个小的 feed_id 运行,但在没有横向的情况下获得了更快的结果。然后我注意到 project_id 比我上次显示的数据新;所以我找到了最古老的项目并比较了它的数据。在最古老的情况下,它需要 297 毫秒,而横向则需要 0.12 毫秒【参考方案2】:

据我了解,如果第一个“where”不是密钥的第一部分,则不会使用密钥。尝试将查询中“位置”的顺序切换为 project_id 和 feed_id。

【讨论】:

还是没有运气!不过谢谢。我一直在将 DESC 交换为 ASC 和 posttime/approvedtime,但没有考虑交换 WHERE 条件。绝对值得一试!

以上是关于PostgreSQL 未对过滤的多重排序查询使用索引的主要内容,如果未能解决你的问题,请参考以下文章

应该针对不同的排序和过滤条件创建哪些MongoDB索引来提高性能?

PostgreSQL 未对 JSONB 上的 GIN 索引使用索引扫描

在 MySQL 查询中使用 OR 时,有没有办法使用索引来避免文件排序?

laravel如何使用switch在数据表中使用查询进行多重过滤

如何从 postgresql 10.3 中的这个多重连接查询中删除嵌套循环

PostgreSQL - 来自许多表和OR条件的字段的gin索引