【问题标题】:Keep PostgreSQL from sometimes choosing a bad query plan防止 PostgreSQL 有时选择错误的查询计划
【发布时间】:2012-01-03 22:14:30
【问题描述】:

我在使用 PostgreSQL 8.4.9 进行查询时遇到了一个奇怪的 PostgreSQL 性能问题。此查询正在选择 3D 体积内的一组点,使用 LEFT OUTER JOIN 在相关 ID 所在的位置添加相关 ID 列。 x 范围内的微小变化会导致 PostgreSQL 选择不同的查询计划,执行时间从 0.01 秒到 50 秒。这是有问题的查询:

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;

该查询需要将近一分钟,如果我在该查询前面添加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)

但是,如果我将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)

我远不是解析这些查询计划的专家,但明显的区别似乎是,对于一个 x 范围,它使用 Hash Left Join 来表示 LEFT OUTER JOIN(非常快),而使用它使用的另一个范围是Nested Loop Left Join(这似乎很慢)。在这两种情况下,查询都会返回大约 90 行。如果我在查询的慢版本之前执行SET ENABLE_NESTLOOP TO FALSE,它会非常快,但我理解using that setting in general is a bad idea

例如,我是否可以创建一个特定的索引,以使查询规划器更有可能选择明显更有效的策略?谁能建议为什么 PostgreSQL 的查询计划器应该为这些查询之一选择如此糟糕的策略?下面我包含了可能有用的架构的详细信息。


treenode表有90万行,定义如下:

                                     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

double3d 复合类型定义如下:

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

连接中涉及的另外两个表是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

...和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

【问题讨论】:

  • 你有没有想过在dba.stackexchange.com上发布这个?
  • @Mat:谢谢你的建议——事实上,我不知道dba.stackexchange.com。如果我在这里没有任何运气,我会将这个问题标记为迁移,因为(据我所知)我认为这仍然是 Stack Overflow 的主题。
  • 这里是主题,但我认为您可能会在 DBA 获得更好的答案,这是您遇到的一个非常具体的问题,与“编码”/SQL 并没有真正相关,更多关于数据库引擎。
  • 顺便说一句,如果您在查询前加上“EXPLAIN ANALYZE”,您可以查看估计和测量(实际)成本之间的差异。
  • 请注意,FK 约束似乎缺少支持索引:CREATE INDEX treenode_class_instance ( treenode_id) REFERENCES treenode.id; 这将导致哈希连接或嵌套循环。

标签: database performance postgresql sql-execution-plan postgresql-performance


【解决方案1】:

如果查询规划器做出错误的决定,这主要是以下两种情况之一:

1。 统计数据不准确。

你运行ANALYZE 够吗?它的组合形式VACUUM ANALYZE 也很受欢迎。如果autovacuum 开启(这是现代 Postgres 的默认设置),ANALYZE 会自动运行。但请考虑:

(前两个答案仍然适用于 Postgres 12。)

如果您的表并且数据分布不规则,提高default_statistics_target 可能会有所帮助。或者更确切地说,只是set the statistics target 用于相关列(基本上是那些在您的查询的WHEREJOIN 子句中的列):

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

目标可以设置在0到10000的范围内;

之后再次运行ANALYZE(在相关表格上)。

2。计划员估算的费用设置已关闭。

阅读手册中的Planner Cost Constants 章节。

查看 generally helpful PostgreSQL Wiki page 上的 default_statistics_targetrandom_page_cost 章节。

还有许多其他可能的原因,但这些是迄今为止最常见的原因。

【讨论】:

  • 更糟糕的是:{x,y,z} 维度的统计信息/直方图可能不是真正独立的。不过,较大的直方图可能会有所帮助。
  • 感谢您的建议。我最近运行了VACUUM ANALYZE,但我再次尝试,并且在将default_statistics_target 更改为最大值10000 之后。不幸的是,这样做之后仍然选择了错误的查询计划。更改 random_page_cost 也没有效果。感谢您的建议阅读,我会仔细阅读这些页面。
  • @wildplasser:确实,x、y 和 z 值当然不是独立的。不过,这让我意识到,我所见过的这种计划失误的唯一情况是 x 值直方图的极低端......
  • 顺便说一句:这是什么*?一种邻域搜索?半径字段可以用于预过滤候选行吗?
  • 在我的情况下,我只是执行了DROP TABLE,然后执行了CREATE TABLE ... AS TABLE ...——因此在对该表执行(SELECT with JOIN)查询之前创建了包含数据的表。显然,在 SELECT 查询运行之前没有对其进行分析,因此查询速度很慢。当我在CREATE TABLE 之后运行ANALYZE - 性能很好。
【解决方案2】:

除非您考虑数据库统计信息和自定义数据类型的组合,否则我怀疑这与错误的统计信息有任何关系。

我的猜测是 PostgreSQL 选择了一个 嵌套循环 连接,因为它查看谓词 (treenode.location).x &gt;= 8000 AND (treenode.location).x &lt;= (8000 + 4736) 并在比较的算术中做了一些时髦的事情。当连接的内侧有少量数据时,通常会使用 嵌套循环

但是,一旦将常量切换到 10736,您就会得到不同的方案。计划非常复杂,基因查询优化 (GEQO) 开始发挥作用,而您看到非确定性计划构建的副作用总是可能的。查询中的评估顺序有足够的差异,让我认为这就是正在发生的事情。

一种选择是为此使用参数化/准备语句来检查,而不是使用临时代码。由于您在 3 维空间中工作,您可能还需要考虑使用 PostGIS。虽然它可能有点矫枉过正,但它也可以为您提供使这些查询正常运行所需的性能。

虽然强制规划者行为不是最佳选择,但有时我们最终会做出比软件更好的决策。

【讨论】:

  • 我们数据库课的教授总是说查询规划器不能很好地处理算术表达式,但我认为我们没有看到这个例子。很有趣。
【解决方案3】:

Erwin 对统计数据的看法。另外:

ORDER BY parentid DESC, id, z_diff

排序

parentid DESC, id, z

可能会给优化器更多的洗牌空间。 (我觉得没关系,因为是最后一学期,而且那种也不是很贵,不过你可以试一试)

【讨论】:

  • 感谢您的建议,但恐怕删除它并没有什么不同 - 也没有完全删除 ORDER BY 子句。
  • 是的,看起来排序是在最后完成的,并且只排序了一行,所以在这个测试中不会有真正的区别。
【解决方案4】:

我不确定这是您问题的根源,但似乎在 8.4.8 和 8.4.9 版本之间的 postgres 查询规划器中进行了一些更改。您可以尝试使用旧版本,看看是否有所不同。

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

如果您更改版本,请不要忘记重新分析您的表格。

【讨论】:

    【解决方案5】:

    +1 用于调整统计目标和执行ANALYZE。对于 PostGIS(对于 OP)。

    但是,与原始问题不太相关,但是,如果有人到这里来寻找如何处理,通常情况下,复杂查询中的规划器行数估计不准确,导致不受欢迎的计划。一个选项可能是将初始查询的一部分包装到一个函数中,并将其ROWS 选项设置为或多或少预期的值。我从来没有这样做过,但显然应该可以工作。

    pg_hint_plan 中还有行估计指令。我一般不会建议规划师提示,但调整行估计是一个更柔和的选择。

    最后,为了强制执行嵌套循环扫描,有时可能会在子查询中使用LIMIT N 或仅使用OFFSET 0 执行LATERAL JOIN。那会给你你想要的。但请注意,这是一个非常粗糙的技巧。在某些时候,如果条件发生变化,它会导致性能下降 - 因为表增长或只是不同的数据分布。不过,这可能是一个不错的选择,只是为了紧急缓解遗留系统的问题。

    【讨论】:

      【解决方案6】:

      如果计划不好,您总是可以求助于 pg_hint_plan 扩展。它为 PostgreSQL 提供 Oracle 风格的提示。

      【讨论】:

        猜你喜欢
        • 2018-03-20
        • 2014-04-15
        • 1970-01-01
        • 1970-01-01
        • 2015-01-18
        • 1970-01-01
        • 2019-07-27
        • 2017-07-03
        • 1970-01-01
        相关资源
        最近更新 更多