简单的WHERE EXISTS ... ORDER BY ...在PostrgeSQL中查询速度非常慢

时间:2017-07-23 15:08:30

标签: postgresql entity-framework-core sql-execution-plan postgres-9.6 partial-index

我有这个非常简单的查询,由我的ORM(实体框架核心)生成:

SELECT *
 FROM "table1" AS "t1"
 WHERE EXISTS (
     SELECT 1
     FROM "table2" AS "t2"
     WHERE ("t2"."is_active" = TRUE) AND ("t1"."table2_id" = "t2"."id"))
 ORDER BY "t1"."table2_id"
  1. 有2个“is_active”记录。其他涉及的列(“id”)是主键。查询正好返回4行。
  2. 表1是9600万条记录。
  3. 表2是3000万条记录。
  4. 此查询中涉及的3列已编入索引(is_active,id,table2_id)。
  5. 生成这个简单查询的C#/ LINQ代码是:Table2.Where(t => t.IsActive).Include(t => t.Table1).ToList();`
  6. SET STATISTICS 10000已设置为所有3列。
  7. VACUUM FULL ANALYZE在两个表上运行。
  8. 没有ORDER BY子句,查询会在几毫秒内返回,我希望4条记录不会返回任何其他内容。 EXPLAIN输出:

    Nested Loop  (cost=1.13..13.42 rows=103961024 width=121)
      ->  Index Scan using table2_is_active_idx on table2  (cost=0.56..4.58 rows=1 width=8)
            Index Cond: (is_active = true)
            Filter: is_active
      ->  Index Scan using table1_table2_id_fkey on table1 t1 (cost=0.57..8.74 rows=10 width=121)
            Index Cond: (table2_id = table1.id)
    

    使用ORDER BY子句,查询需要5分钟才能完成! EXPLAIN输出:

    Merge Semi Join  (cost=10.95..4822984.67 rows=103961040 width=121)
      Merge Cond: (t1.table2_id = t2.id)
      ->  Index Scan using table1_table2_id_fkey on table1 t1  (cost=0.57..4563070.61 rows=103961040 width=121)
      ->  Sort  (cost=4.59..4.59 rows=2 width=8)
            Sort Key: t2.id
            ->  Index Scan using table2_is_active_idx on table2 a  (cost=0.56..4.58 rows=2 width=8)
                  Index Cond: (is_active = true)
                  Filter: is_active
    

    内部第一个索引扫描应返回不超过2行。然后外部的第二个索引扫描没有任何意义,其成本为4563070和103961040行。它只需匹配table2中的2行和table1中的4行!

    这是一个非常简单的查询,只返回很少的记录。为什么Postgres未能正确执行它?

3 个答案:

答案 0 :(得分:3)

添加索引:

CREATE INDEX _index 
ON table2 
USING btree (id) 
WHERE is_active IS TRUE;

并像这样重写查询

SELECT table1.*
FROM table2
INNER JOIN table1 ON (table1.table2_id = table2.id)
WHERE table2.is_active IS TRUE 
ORDER BY table2.id

有必要考虑PostgreSQL以不同方式处理“is_active IS TRUE”和“is_active = TRUE”进程。因此索引谓词和查询中的表达式必须匹配。

如果您无法重写查询,请尝试添加索引:

CREATE INDEX _index 
ON table2 
USING btree (id) 
WHERE is_active = TRUE;

答案 1 :(得分:3)

好的,我以最意想不到的方式解决了我的问题。我将Postgresql从9.6.1升级到9.6.3。就是这样。重新启动服务后,解释计划现在看起来很好,这次查询运行得很好。我没有改变任何东西,没有新的索引,没有。我能想到的唯一解释是9.6.1中存在查询规划器错误并在9.6.3中解决。谢谢大家的答案!

答案 2 :(得分:2)

您的猜测是正确的, 是Postgres 9.6.1 中的错误,完全适合您的用例。升级是正确的做法。 Upgrading to the latest point-release is always the right thing to do.

Quoting the release notes for Postgres 9.6.2:

  
      
  • 修复半连接和基于外键的连接选择性估计   反连接,以及继承案例(Tom Lane)

         

    用于存在外键关系的新代码   考虑到这些情况下的错误,做出估计   更糟糕的是不比9.6之前的代码更好。

  •   

您仍应创建像Dima advised这样的部分索引。但请保持简单:

is_active = TRUEis_active IS TRUE subtly differ,其中第二个返回FALSE而非NULL输入NULL。但是,在 WHERE 条款中,只有TRUE才符合条件,这一点都不重要。这两种表达只是噪音。在Postgres中,您可以直接使用 boolean 值:

CREATE INDEX t2_id_idx ON table2 (id) WHERE is_active;  -- that's all

使用LEFT JOIN重写您的查询。这将在table2中为table1中的“活动”行的结果添加由NULL值组成的行,而[INNER] JOIN中没有任何兄弟。要匹配您当前的逻辑,它必须是SELECT t1.* FROM table2 t2 JOIN table1 t1 ON t1.table2_id = t2.id -- and no parentheses needed WHERE t2.is_active -- that's all ORDER BY t1.table2_id;

EXISTS

但是根本没有必要以这种方式重写您的查询。您拥有的SELECT * FROM table1 t1 WHERE EXISTS ( SELECT 1 FROM table2 WHERE is_active -- that's all WHERE id = t1.table2_id ) ORDER BY table2_id; 半联盟也同样出色。获得部分索引后,在同一查询计划中生成结果。

ANALYZE
顺便说一下,既然您通过升级修复了错误,并且一旦创建了部分索引(并且至少在桌面上运行VACUUM ANALYZE<key>NSAppTransportSecurity</key> <dict> <key>NSAllowsArbitraryLoads</key> <true/> </dict> - 或者autovacuum为您做了这一点),那么你将 从不 再次为此获得错误的查询计划,因为Postgres维护部分索引的单独估计值,这对您的数字是明确的。详细说明: