2

我有这个非常简单的查询,由我的 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在两张桌子上运行。

如果没有该ORDER BY子句,查询会在几毫秒内返回,我不希望返回 4 条记录。解释输出:

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)

WITHORDER BY子句,查询需要5分钟完成!解释输出:

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 行。它只需要匹配 2 行table2和 4 行table1

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

4

3 回答 3

3

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

于 2017-07-23T16:51:11.423 回答
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;
于 2017-07-23T16:28:20.960 回答
2

您的猜测是正确的,Postgres 9.6.1中有一个错误完全适合您的用例。升级是正确的做法。升级到最新的点发布始终是正确的做法。

引用 Postgres 9.6.2 的发行说明:

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

    考虑到外键关系存在的新代码在这些情况下做错了事,使得估计值比 9.6 之前的代码更差。

您仍然应该像Dima 建议的那样创建部分索引。但请保持简单:

is_active = TRUE并且is_active IS TRUE 细微的不同在于第二个返回FALSE而不是NULL输入NULL。但在仅限定条件的WHERE子句中,这些都不重要。TRUE这两种表达方式都只是噪音。在 Postgres 中,您可以boolean直接使用值:

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

并且不要LEFT JOIN. 这会将由 NULL 值组成的行添加到“活动”行的结果中,table2table1. 为了匹配您当前的逻辑,它必须是[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或运行一次 - 或者 autovacuum 为您完成了该操作),您将永远不会再为此获得错误的查询计划,因为 Postgres为部分索引维护单独的估计,这对您的数字是明确的。细节:VACUUM ANALYZE

于 2017-07-24T02:38:39.763 回答