添加子查询时,PostgreSQL查询非常慢


10

我对具有150万行的表进行了相对简单的查询:

SELECT mtid FROM publication
WHERE mtid IN (9762715) OR last_modifier=21321
LIMIT 5000;

EXPLAIN ANALYZE 输出:

Limit  (cost=8.84..12.86 rows=1 width=8) (actual time=0.985..0.986 rows=1 loops=1)
  ->  Bitmap Heap Scan on publication  (cost=8.84..12.86 rows=1 width=8) (actual time=0.984..0.985 rows=1 loops=1)
        Recheck Cond: ((mtid = 9762715) OR (last_modifier = 21321))
        ->  BitmapOr  (cost=8.84..8.84 rows=1 width=0) (actual time=0.971..0.971 rows=0 loops=1)
              ->  Bitmap Index Scan on publication_pkey  (cost=0.00..4.42 rows=1 width=0) (actual time=0.295..0.295 rows=1 loops=1)
                    Index Cond: (mtid = 9762715)
              ->  Bitmap Index Scan on publication_last_modifier_btree  (cost=0.00..4.42 rows=1 width=0) (actual time=0.674..0.674 rows=0 loops=1)
                    Index Cond: (last_modifier = 21321)
Total runtime: 1.027 ms

到目前为止,一切都很好,速度很快,并使用了可用的索引。
现在,如果我稍微修改一下查询,结果将是:

SELECT mtid FROM publication
WHERE mtid IN (SELECT 9762715) OR last_modifier=21321
LIMIT 5000;

EXPLAIN ANALYZE输出是:

Limit  (cost=0.01..2347.74 rows=5000 width=8) (actual time=2735.891..2841.398 rows=1 loops=1)
  ->  Seq Scan on publication  (cost=0.01..349652.84 rows=744661 width=8) (actual time=2735.888..2841.393 rows=1 loops=1)
        Filter: ((hashed SubPlan 1) OR (last_modifier = 21321))
        SubPlan 1
          ->  Result  (cost=0.00..0.01 rows=1 width=0) (actual time=0.001..0.001 rows=1 loops=1)
Total runtime: 2841.442 ms

没那么快,并且使用seq scan ...

当然,由应用程序运行的原始查询会更复杂,甚至更慢,当然,休眠生成的原始查询不会(SELECT 9762715),但是即使如此,它也存在缓慢(SELECT 9762715)!该查询是由休眠生成的,因此更改它们是很大的挑战,并且某些功能不可用(例如UNION,不可用,那会很快)。

问题

  1. 为什么在第二种情况下不能使用索引?如何使用它们?
  2. 我可以通过其他方式提高查询性能吗?

其他想法

似乎我们可以通过手动执行SELECT来使用第一种情况,然后将结果列表放入查询中。即使IN()列表中有5000个数字,它也比第二种解决方案快四倍。但是,这似乎是错误的(而且,它可能快100倍:))。为什么查询计划者对这两个查询使用完全不同的方法是完全无法理解的,所以我想找到一个更好的解决方案。


您能以某种方式重写您的代码,以使hibernate生成JOIN而不是IN ()吗?另外,publication最近有分析吗?
dezso

是的,我同时进行了VACUUM ANALYZE和VACUUM FULL。性能没有变化。至于第二个,AFAIR我们尝试了这一点,它并没有显着影响查询性能。
–P.Péter2015年

1
如果Hibernate无法生成正确的查询,为什么不使用原始SQL?这就像坚持Google翻译,而您已经知道如何用英语表达它。关于您的问题:它实际上取决于隐藏在后面的实际查询(SELECT 9762715)
Erwin Brandstetter 2015年

正如我在下面提到的,即使内部查询 ,它也很慢(SELECT 9762715)。对于休眠问题:可以做到这一点,但需要认真重写代码,因为我们拥有用户定义的休眠标准查询,这些查询是即时翻译的。因此,从本质上讲,我们将修改休眠状态,这是一项艰巨的任务,并且会带来许多可能的副作用。
P.Péter

Answers:


6

问题的核心在这里变得很明显:

发布时进行序列扫描(成本= 0.01..349652.84 行= 744661宽度= 8)(实际时间= 2735.888..2841.393 行= 1循环= 1)

Postgres 估计将返回744661行,而实际上却是一行。如果Postgres不知道从查询中得到什么,它将无法更好地计划。我们将需要看到隐藏在后面的实际查询(SELECT 9762715)-并且可能还知道表定义,约束,基数和数据分布。显然,Postgres的是无法预测如何行会被它返回。有很多方法可以重写查询,取决于它是什么

如果您知道子查询返回的n行永远不会超过行,则可以使用以下命令告诉Postgres:

SELECT mtid
FROM   publication
WHERE  mtid IN (SELECT ... LIMIT n) --  OR last_modifier=21321
LIMIT  5000;

如果n足够小,则Postgres将切换到(位图)索引扫描。但是,这仅适用于简单情况。添加OR条件时停止工作:查询计划者当前无法解决该问题。

我很少使用IN (SELECT ...)。通常,通常有一个EXISTS半联接,是实现它的更好方法。有时带有(LEFTJOINLATERAL)...

明显的解决方法是使用UNION,但是您排除了这一点。在不了解实际子查询和其他相关详细信息的情况下,我不能说更多。


2
没有查询的背后隐藏着 (SELECT 9762715)!如果我运行您在上面看到的确切查询。当然,原始的休眠查询要复杂一些,但是我(认为我)设法查明了查询计划者误入何处,因此我介绍了查询的那一部分。但是,以上说明和查询都是逐字ctrl-cv。
P.Péter

至于第二部分,内限制不起作用:EXPLAIN ANALYZE SELECT mtid FROM publication WHERE mtid IN (SELECT 9762715 LIMIT 1) OR last_modifier=21321 LIMIT 5000;又一个顺序扫描的,也运行约3秒钟...
P.Péter

@P.Péter:在我的本地测试中,它对Postgres 9.4上的实际子查询很有效。如果显示的是您的真实查询,那么您已经有了解决方案:在问题中使用常量而不是子查询的第一个查询。
Erwin Brandstetter

好吧,我还在新的测试表上尝试了一个子查询:CREATE TABLE test (mtid bigint NOT NULL, last_modifier bigint, CONSTRAINT test_property_pkey PRIMARY KEY (mtid)); CREATE INDEX test_last_modifier_btree ON test USING btree (last_modifier); INSERT INTO test (mtid, last_modifier) SELECT mtid, last_modifier FROM publication;。对于相同的查询,效果仍然存在test:任何子查询都导致了seq扫描...我尝试了9.1和9.4。效果是一样的。
P.Péter

1
@P.Péter:我再次进行了测试,意识到我没有OR条件就进行了测试。该技巧LIMIT仅适用于较简单的情况。
Erwin Brandstetter 2015年

6

我的同事找到了一种更改查询的方法,以便它需要简单的重写并完成它需要做的事情,即一步执行子选择,然后对结果进行进一步的操作:

SELECT mtid FROM publication 
WHERE 
  mtid = ANY( (SELECT ARRAY(SELECT 9762715))::bigint[] )
  OR last_modifier=21321
LIMIT 5000;

现在的解释分析是:

 Limit  (cost=92.58..9442.38 rows=2478 width=8) (actual time=0.071..0.074 rows=1 loops=1)
   InitPlan 2 (returns $1)
     ->  Result  (cost=0.01..0.02 rows=1 width=0) (actual time=0.010..0.011 rows=1 loops=1)
           InitPlan 1 (returns $0)
             ->  Result  (cost=0.00..0.01 rows=1 width=0) (actual time=0.001..0.002 rows=1 loops=1)
   ->  Bitmap Heap Scan on publication  (cost=92.56..9442.36 rows=2478 width=8) (actual time=0.069..0.070 rows=1 loops=1)
         Recheck Cond: ((mtid = ANY (($1)::bigint[])) OR (last_modifier = 21321))
         Heap Blocks: exact=1
         ->  BitmapOr  (cost=92.56..92.56 rows=2478 width=0) (actual time=0.060..0.060 rows=0 loops=1)
               ->  Bitmap Index Scan on publication_pkey  (cost=0.00..44.38 rows=10 width=0) (actual time=0.046..0.046 rows=1 loops=1)
                     Index Cond: (mtid = ANY (($1)::bigint[]))
               ->  Bitmap Index Scan on publication_last_modifier_btree  (cost=0.00..46.94 rows=2468 width=0) (actual time=0.011..0.011 rows=0 loops=1)
                     Index Cond: (last_modifier = 21321)
 Planning time: 0.704 ms
 Execution time: 0.153 ms

看来我们可以创建一个简单的解析器,以这种方式查找和重写所有子选择,并将其添加到休眠钩子中以操作本机查询。


这听起来很有趣。SELECT像问题中第一个查询一样,仅删除所有s 是否更容易?
dezso

当然,我可以采用两步方法:SELECT分别进行操作,然后在后面加上静态列表进行外部选择IN。但是,这要慢得多(如果子查询有多个结果,则要慢5到10倍),因为您有额外的网络往返路程,并且您有postgres格式化很多结果,然后用java解析这些结果(然后执行同样再次倒退)。上面的解决方案在语义上做同样的事情,而将进程留在postgres中。总而言之,在我们的案例中,这似乎是最快的方法,并且修改最少。
P.Péter

知道了 我不知道您一次可以获取多个ID。
dezso

1

回答第二个问题:是的,您可以将ORDER BY添加到子查询中,这将产生积极的影响。但是,它在性能上类似于“ EXISTS(子查询)”解决方案。即使子查询产生两行,也存在显着差异。

SELECT mtid FROM publication
WHERE mtid IN (SELECT #column# ORDER BY #column#) OR last_modifier=21321
LIMIT 5000;
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.