简体   繁体   English

防止 PostgreSQL 有时选择错误的查询计划

[英]Keep PostgreSQL from sometimes choosing a bad query plan

I have a strange problem with PostgreSQL performance for a query, using PostgreSQL 8.4.9.我在使用 PostgreSQL 8.4.9 进行查询时遇到了一个奇怪的 PostgreSQL 性能问题。 This query is selecting a set of points within a 3D volume, using a LEFT OUTER JOIN to add a related ID column where that related ID exists.此查询选择 3D 体积内的一组点,使用LEFT OUTER JOIN在存在相关 ID 的位置添加相关 ID 列。 Small changes in the x range can cause PostgreSQL to choose a different query plan, which takes the execution time from 0.01 seconds to 50 seconds. x范围的微小变化会导致 PostgreSQL 选择不同的查询计划,其执行时间从 0.01 秒到 50 秒。 This is the query in question:这是有问题的查询:

SELECT treenode.id AS id,
       treenode.parent_id AS parentid,
       (treenode.location).x AS x,
       (treenode.location).y AS y,
       (treenode.location).z AS z,
       treenode.confidence AS confidence,
       treenode.user_id AS user_id,
       treenode.radius AS radius,
       ((treenode.location).z - 50) AS z_diff,
       treenode_class_instance.class_instance_id AS skeleton_id
  FROM treenode LEFT OUTER JOIN
         (treenode_class_instance INNER JOIN
          class_instance ON treenode_class_instance.class_instance_id
                                                  = class_instance.id
                            AND class_instance.class_id = 7828307)
       ON (treenode_class_instance.treenode_id = treenode.id
           AND treenode_class_instance.relation_id = 7828321)
  WHERE treenode.project_id = 4
    AND (treenode.location).x >= 8000
    AND (treenode.location).x <= (8000 + 4736)
    AND (treenode.location).y >= 22244
    AND (treenode.location).y <= (22244 + 3248)
    AND (treenode.location).z >= 0
    AND (treenode.location).z <= 100
  ORDER BY parentid DESC, id, z_diff
  LIMIT 400;

That query takes nearly a minute, and, if I add EXPLAIN to the front of that query, seems to be using the following query plan:该查询需要将近一分钟,而且,如果我在该查询的前面添加EXPLAIN ,似乎正在使用以下查询计划:

 Limit  (cost=56185.16..56185.17 rows=1 width=89)
   ->  Sort  (cost=56185.16..56185.17 rows=1 width=89)
         Sort Key: treenode.parent_id, treenode.id, (((treenode.location).z - 50::double precision))
         ->  Nested Loop Left Join  (cost=6715.16..56185.15 rows=1 width=89)
               Join Filter: (treenode_class_instance.treenode_id = treenode.id)
               ->  Bitmap Heap Scan on treenode  (cost=148.55..184.16 rows=1 width=81)
                     Recheck Cond: (((location).x >= 8000::double precision) AND ((location).x <= 12736::double precision) AND ((location).z >= 0::double precision) AND ((location).z <= 100::double precision))
                     Filter: (((location).y >= 22244::double precision) AND ((location).y <= 25492::double precision) AND (project_id = 4))
                     ->  BitmapAnd  (cost=148.55..148.55 rows=9 width=0)
                           ->  Bitmap Index Scan on location_x_index  (cost=0.00..67.38 rows=2700 width=0)
                                 Index Cond: (((location).x >= 8000::double precision) AND ((location).x <= 12736::double precision))
                           ->  Bitmap Index Scan on location_z_index  (cost=0.00..80.91 rows=3253 width=0)
                                 Index Cond: (((location).z >= 0::double precision) AND ((location).z <= 100::double precision))
               ->  Hash Join  (cost=6566.61..53361.69 rows=211144 width=16)
                     Hash Cond: (treenode_class_instance.class_instance_id = class_instance.id)
                     ->  Seq Scan on treenode_class_instance  (cost=0.00..25323.79 rows=969285 width=16)
                           Filter: (relation_id = 7828321)
                     ->  Hash  (cost=5723.54..5723.54 rows=51366 width=8)
                           ->  Seq Scan on class_instance  (cost=0.00..5723.54 rows=51366 width=8)
                                 Filter: (class_id = 7828307)
(20 rows)

However, if I replace the 8000 in the x range condition with 10644 , the query is performed in a fraction of a second and uses this query plan:但是,如果我将x范围条件中的8000替换为10644 ,则查询将在几分之一秒内执行并使用以下查询计划:

 Limit  (cost=58378.94..58378.95 rows=2 width=89)
   ->  Sort  (cost=58378.94..58378.95 rows=2 width=89)
         Sort Key: treenode.parent_id, treenode.id, (((treenode.location).z - 50::double precision))
         ->  Hash Left Join  (cost=57263.11..58378.93 rows=2 width=89)
               Hash Cond: (treenode.id = treenode_class_instance.treenode_id)
               ->  Bitmap Heap Scan on treenode  (cost=231.12..313.44 rows=2 width=81)
                     Recheck Cond: (((location).z >= 0::double precision) AND ((location).z <= 100::double precision) AND ((location).x >= 10644::double precision) AND ((location).x <= 15380::double precision))
                     Filter: (((location).y >= 22244::double precision) AND ((location).y <= 25492::double precision) AND (project_id = 4))
                     ->  BitmapAnd  (cost=231.12..231.12 rows=21 width=0)
                           ->  Bitmap Index Scan on location_z_index  (cost=0.00..80.91 rows=3253 width=0)
                                 Index Cond: (((location).z >= 0::double precision) AND ((location).z <= 100::double precision))
                           ->  Bitmap Index Scan on location_x_index  (cost=0.00..149.95 rows=6157 width=0)
                                 Index Cond: (((location).x >= 10644::double precision) AND ((location).x <= 15380::double precision))
               ->  Hash  (cost=53361.69..53361.69 rows=211144 width=16)
                     ->  Hash Join  (cost=6566.61..53361.69 rows=211144 width=16)
                           Hash Cond: (treenode_class_instance.class_instance_id = class_instance.id)
                           ->  Seq Scan on treenode_class_instance  (cost=0.00..25323.79 rows=969285 width=16)
                                 Filter: (relation_id = 7828321)
                           ->  Hash  (cost=5723.54..5723.54 rows=51366 width=8)
                                 ->  Seq Scan on class_instance  (cost=0.00..5723.54 rows=51366 width=8)
                                       Filter: (class_id = 7828307)
(21 rows)

I'm far from an expert in parsing these query plans, but the clear difference seems to be that with one x range it uses a Hash Left Join for the LEFT OUTER JOIN (which is very fast), while with the other range it uses a Nested Loop Left Join (which seems to be very slow).我远不是解析这些查询计划的专家,但明显的区别似乎是,对于一个x范围,它使用Hash Left Join进行LEFT OUTER JOIN (非常快),而对于另一个范围,它使用Nested Loop Left Join (这似乎很慢)。 In both cases the queries return about 90 rows.在这两种情况下,查询都返回大约 90 行。 If I do SET ENABLE_NESTLOOP TO FALSE before the slow version of the query, it goes very fast, but I understand that using that setting in general is a bad idea .如果我在查询的慢速版本之前执行SET ENABLE_NESTLOOP TO FALSE ,它会运行得非常快,但我知道使用该设置通常是一个坏主意

Can I, for example, create a particular index in order to make it more likely that the query planner will choose the clearly more efficient strategy?例如,我可以创建一个特定的索引,以便查询规划器更有可能选择更有效的策略吗? Could anyone suggest why PostgreSQL's query planner should be choosing such a poor strategy for one of these queries?谁能建议为什么 PostgreSQL 的查询规划器应该为这些查询之一选择如此糟糕的策略? Below I have included details of the schema that may be helpful.下面我包含了可能有帮助的模式的详细信息。


The treenode table has 900,000 rows, and is defined as follows:树节点表有 900,000 行,定义如下:

                                     Table "public.treenode"
    Column     |           Type           |                      Modifiers                       
---------------+--------------------------+------------------------------------------------------
 id            | bigint                   | not null default nextval('concept_id_seq'::regclass)
 user_id       | bigint                   | not null
 creation_time | timestamp with time zone | not null default now()
 edition_time  | timestamp with time zone | not null default now()
 project_id    | bigint                   | not null
 location      | double3d                 | not null
 parent_id     | bigint                   | 
 radius        | double precision         | not null default 0
 confidence    | integer                  | not null default 5
Indexes:
    "treenode_pkey" PRIMARY KEY, btree (id)
    "treenode_id_key" UNIQUE, btree (id)
    "location_x_index" btree (((location).x))
    "location_y_index" btree (((location).y))
    "location_z_index" btree (((location).z))
Foreign-key constraints:
    "treenode_parent_id_fkey" FOREIGN KEY (parent_id) REFERENCES treenode(id)
Referenced by:
    TABLE "treenode_class_instance" CONSTRAINT "treenode_class_instance_treenode_id_fkey" FOREIGN KEY (treenode_id) REFERENCES treenode(id) ON DELETE CASCADE
    TABLE "treenode" CONSTRAINT "treenode_parent_id_fkey" FOREIGN KEY (parent_id) REFERENCES treenode(id)
Triggers:
    on_edit_treenode BEFORE UPDATE ON treenode FOR EACH ROW EXECUTE PROCEDURE on_edit()
Inherits: location

The double3d composite type is defined as follows: double3d复合类型定义如下:

Composite type "public.double3d"
 Column |       Type       
--------+------------------
 x      | double precision
 y      | double precision
 z      | double precision

The other two tables involved in the join are treenode_class_instance :连接中涉及的另外两个表是treenode_class_instance

                               Table "public.treenode_class_instance"
      Column       |           Type           |                      Modifiers                       
-------------------+--------------------------+------------------------------------------------------
 id                | bigint                   | not null default nextval('concept_id_seq'::regclass)
 user_id           | bigint                   | not null
 creation_time     | timestamp with time zone | not null default now()
 edition_time      | timestamp with time zone | not null default now()
 project_id        | bigint                   | not null
 relation_id       | bigint                   | not null
 treenode_id       | bigint                   | not null
 class_instance_id | bigint                   | not null
Indexes:
    "treenode_class_instance_pkey" PRIMARY KEY, btree (id)
    "treenode_class_instance_id_key" UNIQUE, btree (id)
    "idx_class_instance_id" btree (class_instance_id)
Foreign-key constraints:
    "treenode_class_instance_class_instance_id_fkey" FOREIGN KEY (class_instance_id) REFERENCES class_instance(id) ON DELETE CASCADE
    "treenode_class_instance_relation_id_fkey" FOREIGN KEY (relation_id) REFERENCES relation(id)
    "treenode_class_instance_treenode_id_fkey" FOREIGN KEY (treenode_id) REFERENCES treenode(id) ON DELETE CASCADE
    "treenode_class_instance_user_id_fkey" FOREIGN KEY (user_id) REFERENCES "user"(id)
Triggers:
    on_edit_treenode_class_instance BEFORE UPDATE ON treenode_class_instance FOR EACH ROW EXECUTE PROCEDURE on_edit()
Inherits: relation_instance

... and class_instance : ...和class_instance

                                  Table "public.class_instance"
    Column     |           Type           |                      Modifiers                       
---------------+--------------------------+------------------------------------------------------
 id            | bigint                   | not null default nextval('concept_id_seq'::regclass)
 user_id       | bigint                   | not null
 creation_time | timestamp with time zone | not null default now()
 edition_time  | timestamp with time zone | not null default now()
 project_id    | bigint                   | not null
 class_id      | bigint                   | not null
 name          | character varying(255)   | not null
Indexes:
    "class_instance_pkey" PRIMARY KEY, btree (id)
    "class_instance_id_key" UNIQUE, btree (id)
Foreign-key constraints:
    "class_instance_class_id_fkey" FOREIGN KEY (class_id) REFERENCES class(id)
    "class_instance_user_id_fkey" FOREIGN KEY (user_id) REFERENCES "user"(id)
Referenced by:
    TABLE "class_instance_class_instance" CONSTRAINT "class_instance_class_instance_class_instance_a_fkey" FOREIGN KEY (class_instance_a) REFERENCES class_instance(id) ON DELETE CASCADE
    TABLE "class_instance_class_instance" CONSTRAINT "class_instance_class_instance_class_instance_b_fkey" FOREIGN KEY (class_instance_b) REFERENCES class_instance(id) ON DELETE CASCADE
    TABLE "connector_class_instance" CONSTRAINT "connector_class_instance_class_instance_id_fkey" FOREIGN KEY (class_instance_id) REFERENCES class_instance(id)
    TABLE "treenode_class_instance" CONSTRAINT "treenode_class_instance_class_instance_id_fkey" FOREIGN KEY (class_instance_id) REFERENCES class_instance(id) ON DELETE CASCADE
Triggers:
    on_edit_class_instance BEFORE UPDATE ON class_instance FOR EACH ROW EXECUTE PROCEDURE on_edit()
Inherits: concept

If the query planner makes bad decisions it's mostly one of two things:如果查询规划器做出错误的决定,主要是以下两种情况之一:

1. The statistics are inaccurate. 1.统计不准确。

Do you run ANALYZE enough?你运行ANALYZE了吗? Also popular in it's combined form VACUUM ANALYZE .在它的组合形式VACUUM ANALYZE也很受欢迎。 If autovacuum is on (which is the default in modern-day Postgres), ANALYZE is run automatically.如果autovacuum开启(这是现代 Postgres 的默认设置), ANALYZE将自动运行。 But consider:但请考虑:

(Top two answers still apply for Postgres 12.) (前两个答案仍然适用于 Postgres 12。)

If your table is big and data distribution is irregular , raising the default_statistics_target may help.如果您的表很大且数据分布不规则,则提高default_statistics_target可能会有所帮助。 Or rather, just set the statistics target for relevant columns (those in WHERE or JOIN clauses of your queries, basically):或者更确切地说,只需为相关列设置统计目标(基本上是查询的WHEREJOIN子句中的那些):

ALTER TABLE ... ALTER COLUMN ... SET STATISTICS 400;  -- calibrate number

The target can be set in the range 0 to 10000;目标可以设置在 0 到 10000 的范围内;

Run ANALYZE again after that (on relevant tables).之后(在相关表上)再次运行ANALYZE

2. The cost settings for planner estimates are off. 2. 计划员估算的成本设置已关闭。

Read the chapter Planner Cost Constants in the manual.阅读手册中的计划员成本常数一章。

Look at the chapters default_statistics_target and random_page_cost on this generally helpful PostgreSQL Wiki page .在这个 通常有用的 PostgreSQL Wiki 页面上查看default_statistics_targetrandom_page_cost章节。

There are many other possible reasons, but these are the most common ones by far.还有许多其他可能的原因,但这些是迄今为止最常见的原因。

I'm skeptical that this has anything to do with bad statistics unless you consider the combination of database statistics and your custom data type.我怀疑这与糟糕的统计数据有什么关系,除非您考虑数据库统计数据和自定义数据类型的组合。

My guess is that PostgreSQL is picking a nested loop join because it looks at the predicates (treenode.location).x >= 8000 AND (treenode.location).x <= (8000 + 4736) and does something funky in the arithmetic of your comparison.我的猜测是 PostgreSQL 正在选择一个嵌套循环连接,因为它查看谓词(treenode.location).x >= 8000 AND (treenode.location).x <= (8000 + 4736)并且在算术中做了一些时髦的事情你的比较。 A nested loop is typically going to be used when you have a small amount of data in the inner side of the join.当连接的内侧有少量数据时,通常会使用嵌套循环

But, once you switch the constant to 10736 you get a different plan.但是,一旦您将常数切换到 10736,您就会得到一个不同的计划。 It's always possible that the plan is of sufficiently complexity that the Genetic Query Optimization (GEQO) is kicking in and you're seeing the side effects of non-deterministic plan building .计划总是有可能足够复杂,以至于遗传查询优化 (GEQO)正在发挥作用,并且您会看到非确定性计划构建的副作用。 There are enough discrepancies in the order of evaluation in the queries to make me think that's what's going on.查询中的评估顺序存在足够的差异,让我认为这就是正在发生的事情。

One option would be to examine using a parameterized/prepared statement for this instead of using ad hoc code.一种选择是使用参数化/准备好的语句进行检查,而不是使用临时代码。 Since you're working in a 3-dimensional space, you might also want to considering using PostGIS .由于您在 3 维空间中工作,因此您可能还想考虑使用PostGIS While it might be overkill, it may also be able to provide you with the performance that you need to get these queries running properly.虽然它可能有点矫枉过正,但它也可以为您提供使这些查询正常运行所需的性能。

While forcing planner behavior isn't the best choice, sometimes we do end up making better decisions than the software.虽然强制规划者行为不是最好的选择,但有时我们最终会做出比软件更好的决策。

What Erwin said about the statistics.欧文对统计数据的看法。 Also:还:

ORDER BY parentid DESC, id, z_diff

Sorting on排序

parentid DESC, id, z

might give the optimiser a bit more room to shuffle.可能会给优化器更多的洗牌空间。 (I don't think it will matter much since it is the last term, and the sort is not that expensive, but you could give it a try) (我不认为它会很重要,因为它是最后一个学期,而且种类也不贵,但您可以尝试一下)

I am not positive it is the source of your problem but it looks like there were some changes made in the postgres query planner between versions 8.4.8 and 8.4.9.我不确定它是您问题的根源,但看起来 postgres 查询规划器在 8.4.8 和 8.4.9 版本之间进行了一些更改。 You could try using an older version and see if it makes a difference.您可以尝试使用旧版本,看看它是否有所作为。

http://postgresql.1045698.n5.nabble.com/BUG-6275-Horrible-performance-regression-td4944891.html http://postgresql.1045698.n5.nabble.com/BUG-6275-Horrible-performance-regression-td4944891.html

Don't forget to reanalyze your tables if you change the version.如果您更改版本,请不要忘记重新分析您的表格。

+1 for tuning statistics target & doing ANALYZE . +1 用于调整统计目标和执行ANALYZE And for PostGIS (for OP).而对于 PostGIS(对于 OP)。

But also, not quite related to the original question, but still, if anyone gets here looking for how to deal, in general, with inaccurate planner's row count estimates in complex queries , leading to undesired plans.而且,与原始问题不太相关,但是,如果有人来这里寻找通常如何处理复杂查询中不准确的规划器行数估计,从而导致不想要的计划。 An option might be to wrap a part of the initial query into a function and to set its ROWS option to something more or less expected.一个选项可能是将初始查询的一部分包装到一个函数中,并将其ROWS选项设置为或多或少的预期值。 I've never done that but should work apparently.我从来没有这样做过,但显然应该工作。

Also there are row estimation directives in pg_hint_plan . pg_hint_plan还有行估计指令。 I would not advice planner hinting in general, but adjusting rows estimate is a softer option.我一般不建议规划者暗示,但调整行估计是一个更软的选择。

And finally, to enforce a nested loop scan, sometimes one might do a LATERAL JOIN with LIMIT N or just OFFSET 0 inside the subquery.最后,为了强制执行嵌套循环扫描,有时可能会在子查询中使用LIMIT NOFFSET 0执行LATERAL JOIN That will give you what you want.那会给你你想要的。 But note it's a very rough trick.但请注意,这是一个非常粗略的技巧。 At some point it WILL lead to bad performance IF the conditions change - because of table growth or just a different data distribution.在某些时候,如果条件发生变化,它将导致性能不佳 - 因为表增长或只是不同的数据分布。 Still this might be a good option just to urgently get some relief for a legacy system.尽管如此,这可能是一个不错的选择,只是为了紧急缓解遗留系统的问题。

In case of a bad plan, you can always resort to pg_hint_plan extension.如果计划不好,你总是可以求助于 pg_hint_plan 扩展。 It provides Oracle style hints for PostgreSQL.它为 PostgreSQL 提供了 Oracle 风格的提示。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM