我怎样才能“更好地思考”?当阅读 PostgreSQL 查询计划时?
今天我花了一个多小时对一个我无法理解的查询计划感到困惑。该查询是一个UPDATE
,它根本无法运行。完全陷入僵局:pg_locks
表明它也没有等待任何东西。现在,我不认为自己是最好或最差的查询计划读者,但我发现这个特别困难。我想知道如何阅读这些内容?是否有 Pg ace 遵循的方法来查明错误?
我计划提出另一个关于如何解决此问题的问题,但现在我具体讨论如何阅读这些类型的计划。
QUERY PLAN
--------------------------------------------------------------------------------------------
Nested Loop Anti Join (cost=47680.88..169413.12 rows=1 width=77)
Join Filter: ((co.fkey_style = v.chrome_styleid) AND (co.name = o.name))
-> Nested Loop (cost=5301.58..31738.10 rows=1 width=81)
-> Hash Join (cost=5301.58..29722.32 rows=229 width=40)
Hash Cond: ((io.lot_id = iv.lot_id) AND ((io.vin)::text = (iv.vin)::text))
-> Seq Scan on options io (cost=0.00..20223.32 rows=23004 width=36)
Filter: (name IS NULL)
-> Hash (cost=4547.33..4547.33 rows=36150 width=24)
-> Seq Scan on vehicles iv (cost=0.00..4547.33 rows=36150 width=24)
Filter: (date_sold IS NULL)
-> Index Scan using options_pkey on options co (cost=0.00..8.79 rows=1 width=49)
Index Cond: ((co.fkey_style = iv.chrome_styleid) AND (co.code = io.code))
-> Hash Join (cost=42379.30..137424.09 rows=16729 width=26)
Hash Cond: ((v.lot_id = o.lot_id) AND ((v.vin)::text = (o.vin)::text))
-> Seq Scan on vehicles v (cost=0.00..4547.33 rows=65233 width=24)
-> Hash (cost=20223.32..20223.32 rows=931332 width=44)
-> Seq Scan on options o (cost=0.00..20223.32 rows=931332 width=44)
(17 rows)
这个查询计划的问题 - 我相信我理解 - 可能最好由 irc 的
:RhodiumToad
说出来(他在这方面肯定更擅长,所以我敢打赌他的解释会更好): //irc.freenode.net/#postgresql
哦,这个计划可能是灾难性的 该计划的问题在于它为每行运行一个非常昂贵的哈希连接 问题是来自其他连接的 rows=1 估计和 规划器认为可以将非常昂贵的查询放在嵌套循环的内部路径中,其中外部路径估计只返回一行。 因为,显然,根据规划者的估计,昂贵的部分只会运行一次 但这在实践中很明显会搞砸 问题在于规划者相信自己的估计 理想情况下,规划者需要知道“预计返回 1 行”和“不可能返回超过 1 行”之间的区别 但根本不清楚如何将其合并到现有代码中
他接着说:
它可以影响任何连接,但通常针对子查询的连接是最有可能的
现在,当我阅读此计划时,我注意到的第一件事是嵌套循环反连接
,其成本为169,413< /code> (我会坚持上限)。此反连接分解为
Nested Loop
的结果,成本为 31,738
,以及 Hash Join
的结果,成本为137,424
。现在,137,424
比 31,738
大得多,所以我知道问题出在哈希连接上。
然后我继续在查询之外EXPLAIN ANALYZE
哈希连接段。它在 7 秒内执行。我确保(lot_id、vin)和(co.code 和 v.code)上有索引——有。我分别禁用了 seq_scan
和 hashjoin
,发现速度提高了不到 2 秒。还不够接近,无法解释为什么一小时后没有进展。
但是,毕竟我完全错了!是的,这是查询中较慢的部分,但是因为 rows="1"
位(我认为它位于嵌套循环反连接
上)。这是规划器错误估计行数的错误(缺乏能力)?我该如何解读这一点才能得出与 RhodiumToad 相同的结论?
难道只是 rows="1"
应该触发我弄清楚这一点吗?
我确实对所有涉及的表运行了 VACUUM FULL ANALYZE,这是 Postgresql 8.4。
I spent over an hour today puzzling myself over a query plan that I couldn't understand. The query was an UPDATE
and it just wouldn't run at all. Totally deadlocked: pg_locks
showed it wasn't waiting for anything either. Now, I don't consider myself the best or worst query plan reader, but I find this one exceptionally difficult. I'm wondering how does one read these? Is there a methodology that the Pg aces follow in order to pinpoint the error?
I plan on asking another question as to how to work around this issue, but right now I'm speaking specifically about how to read these types of plans.
QUERY PLAN
--------------------------------------------------------------------------------------------
Nested Loop Anti Join (cost=47680.88..169413.12 rows=1 width=77)
Join Filter: ((co.fkey_style = v.chrome_styleid) AND (co.name = o.name))
-> Nested Loop (cost=5301.58..31738.10 rows=1 width=81)
-> Hash Join (cost=5301.58..29722.32 rows=229 width=40)
Hash Cond: ((io.lot_id = iv.lot_id) AND ((io.vin)::text = (iv.vin)::text))
-> Seq Scan on options io (cost=0.00..20223.32 rows=23004 width=36)
Filter: (name IS NULL)
-> Hash (cost=4547.33..4547.33 rows=36150 width=24)
-> Seq Scan on vehicles iv (cost=0.00..4547.33 rows=36150 width=24)
Filter: (date_sold IS NULL)
-> Index Scan using options_pkey on options co (cost=0.00..8.79 rows=1 width=49)
Index Cond: ((co.fkey_style = iv.chrome_styleid) AND (co.code = io.code))
-> Hash Join (cost=42379.30..137424.09 rows=16729 width=26)
Hash Cond: ((v.lot_id = o.lot_id) AND ((v.vin)::text = (o.vin)::text))
-> Seq Scan on vehicles v (cost=0.00..4547.33 rows=65233 width=24)
-> Hash (cost=20223.32..20223.32 rows=931332 width=44)
-> Seq Scan on options o (cost=0.00..20223.32 rows=931332 width=44)
(17 rows)
The issue with this query plan - I believe I understand - is probably best said by RhodiumToad
(he is definitely better at this, so I'll bet on his explanation being better) of irc://irc.freenode.net/#postgresql
:
oh, that plan is potentially disastrous
the problem with that plan is that it's running a hugely expensive hashjoin for each row
the problem is the rows=1 estimate from the other join and
the planner thinks it's ok to put a hugely expensive query in the inner path of a nestloop where the outer path is estimated to return only one row.
since, obviously, by the planner's estimate the expensive part will only be run once
but this has an obvious tendency to really mess up in practice
the problem is that the planner believes its own estimates
ideally, the planner needs to know the difference between "estimated to return 1 row" and "not possible to return more than 1 row"
but it's not at all clear how to incorporate that into the existing code
He goes on to say:
it can affect any join, but usually joins against subqueries are the most likely
Now when I read this plan the first thing I noticed was the Nested Loop Anti Join
, this had a cost of 169,413
(I'll stick to upper bounds). This Anti-Join breaks down to the result of a Nested Loop
at cost of 31,738
, and the result of a Hash Join
at a cost of 137,424
. Now, the 137,424
, is much greater than 31,738
so I knew the problem was the Hash Join.
Then I proceed to EXPLAIN ANALYZE
the Hash Join segment outside of the query. It executed in 7 secs. I made sure there was indexes on (lot_id, vin), and (co.code, and v.code) -- there was. I disabled seq_scan
and hashjoin
individually and notice a speed increase of less than 2 seconds. Not near enough to account for why it wasn't progressing after an hour.
But, after all this I'm totally wrong! Yes, it was the slower part of the query, but because the rows="1"
bit (I presume it was on the Nested Loop Anti Join
). Here it is a bug (lack of ability) in the planner mis-estimating the amount of rows? How am I supposed to read into this to come to the same conclusion RhodiumToad
did?
Is it simply rows="1"
that is supposed to trigger me figuring this out?
I did run VACUUM FULL ANALYZE
on all of the tables involved, and this is Postgresql 8.4.
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论
评论(2)
解决此类问题需要一些关于哪里可能出错的经验。但要发现查询计划中的问题,请尝试从内到外验证生成的计划,检查行数估计是否合理以及成本估计是否与花费的时间匹配。顺便提一句。两个成本估计不是下限和上限,第一个是生成第一行输出的估计成本,第二个数字是估计的总成本,请参阅 解释文档了解详细信息,还有一些 规划器文档可用。它还有助于了解不同的访问方法如何工作。作为起点,维基百科有关于嵌套循环、哈希 和合并联接。
在您的示例中,您首先要执行以下操作:
运行
EXPLAIN ANALYZE SELECT * FROM options WHERE name IS NULL;
并查看返回的行是否与估计值匹配。 2 倍通常不是问题,您正在尝试找出数量级的差异。然后请参阅
EXPLAIN ANALYZE SELECT * FROM cars WHERE date_sold IS NULL;
返回预期的行数。然后上一级进行哈希连接:
查看 EXPLAIN ANALYZE SELECT * FROM cars AS iv INNER JOIN options io ON (io.lot_id = iv.lot_id) AND ((io.vin)::text = (iv) .vin)::text) WHERE iv.date_sold IS NULL AND io.name IS NULL; 结果为 229 行。
再上一层添加
INNER JOIN options co ON (co.fkey_style = iv.chrome_styleid) AND (co.code = io.code)
并且预计仅返回一行。这可能就是问题所在,因为如果实际行数从 1 到 100,则遍历包含的嵌套循环的内部循环的总成本估计会减少 100 倍。规划者所犯的根本错误可能是它期望加入
co
的两个谓词彼此独立并成倍增加它们的选择性。但实际上它们可能高度相关,并且选择性更接近 MIN(s1, s2) 而不是 s1*s2。Seeing through issues like this requires some experience on where things can go wrong. But to find issues in the query plans, try to validate the produced plan from inside out, check if the number of rows estimates are sane and cost estimates match spent time. Btw. the two cost estimates aren't lower and upper bounds, first is the estimated cost to produce the first row of output, the second number is the estimated total cost, see explain documentation for details, there is also some planner documentation available. It also helps to know how the different access methods work. As a starting point Wikipedia has information on nested loop, hash and merge joins.
In your example, you'd start with:
Run
EXPLAIN ANALYZE SELECT * FROM options WHERE name IS NULL;
and see if the returned rows matches the estimate. A factor of 2 off isn't usually a problem, you're trying to spot order of magnitude differences.Then see
EXPLAIN ANALYZE SELECT * FROM vehicles WHERE date_sold IS NULL;
returns expected amount of rows.Then go up one level to the hash join:
See if
EXPLAIN ANALYZE SELECT * FROM vehicles AS iv INNER JOIN options io ON (io.lot_id = iv.lot_id) AND ((io.vin)::text = (iv.vin)::text) WHERE iv.date_sold IS NULL AND io.name IS NULL;
results in 229 rows.Up one more level adds
INNER JOIN options co ON (co.fkey_style = iv.chrome_styleid) AND (co.code = io.code)
and is expected to return only one row. This is probably where the issue is because if the actual numebr of rows goes from 1 to 100, the total cost estimate of traversing the inner loop of the containing nested loop is off by a factor of 100.The underlying mistake that the planner is making is probably that it expects that the two predicates for joining in
co
are independent of each other and multiplies their selectivities. While in reality they may be heavily correlated and the selectivity is closer to MIN(s1, s2) not s1*s2.您分析过表格吗? pg_stats 对这些表有什么说明?查询计划基于统计数据,这些必须没问题。你用什么版本? 8.4?
可以使用统计信息、relpages 数量、行数以及 postgresql.conf 中规划器成本常量的设置来计算成本。
work_mem 也参与其中,它可能太低并迫使规划器进行 seqscan,从而降低性能......
Did you ANALYZE the tables? And what does pg_stats has to say about these tables? The queryplan is based on the stats, these have to be ok. And what version do you use? 8.4?
The costs can be calculated by using the stats, the amount of relpages, amount of rows and the settings in postgresql.conf for the Planner Cost Constants.
work_mem is also involved, it might be too low and force the planner to do a seqscan, to kill performance...