我有这个非常简单的查询,由我的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"
SET STATISTICS 10000
已设置为所有3列。VACUUM FULL ANALYZE
在两个表上运行。没有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未能正确执行它?
答案 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 = TRUE
和is_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维护部分索引的单独估计值,这对您的数字是明确的。详细说明: