保持PostgreSQL有时选择错误的查询计划

Mar*_*air 25 database postgresql performance sql-execution-plan postgresql-performance

使用PostgreSQL 8.4.9,我对查询的PostgreSQL性能有一个奇怪的问题.此查询正在选择3D卷中的一组点,使用a 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;
Run Code Online (Sandbox Code Playgroud)

该查询需要将近一分钟,如果我添加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)
Run Code Online (Sandbox Code Playgroud)

但是,如果我8000x范围条件中替换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)
Run Code Online (Sandbox Code Playgroud)

我远在解析这些查询计划的专家,但明显的差别似乎是一个x范围,它使用一Hash Left JoinLEFT OUTER JOIN(这是非常快的),而与其他范围内它采用了Nested Loop Left Join(这似乎是很慢).在这两种情况下,查询返回大约90行.如果我SET ENABLE_NESTLOOP TO FALSE在查询的慢速版本之前执行,它会非常快,但我知道使用该设置通常是一个坏主意.

例如,我可以创建一个特定的索引,以使查询规划者更有可能选择明显更有效的策略吗?有人可以建议为什么PostgreSQL的查询规划器应该为这些查询之一选择这么差的策略吗?下面我列出了可能有用的架构的详细信息.


treenode表有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
Run Code Online (Sandbox Code Playgroud)

double3d复合型定义如下:

Composite type "public.double3d"
 Column |       Type       
--------+------------------
 x      | double precision
 y      | double precision
 z      | double precision
Run Code Online (Sandbox Code Playgroud)

联接中涉及的另外两个表是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
Run Code Online (Sandbox Code Playgroud)

......和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
Run Code Online (Sandbox Code Playgroud)

Erw*_*ter 46

如果查询规划器做出错误的决定,那么它主要是两件事之一:

1. 统计数据不准确.

你跑得ANALYZE够吗?它也是流行的组合形式VACUUM ANALYZE.如果启用了autovacuum(这是现代Postgres中的默认值),ANALYZE则会自动运行.但考虑一下:

(前两个答案仍然适用于Postgres 9.6.)

如果您的表很大并且数据分布不规则,那么提高default_statistics_target可能会有所帮助.或者更确切地说,只需设置相关列的统计目标(基本上是查询的子句WHEREJOIN子句):

ALTER TABLE ... ALTER COLUMN ... SET STATISTICS 400;  -- calibrate number
Run Code Online (Sandbox Code Playgroud)

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

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

2. 计划员估算的成本设置已关闭.

阅读手册中的Planner Cost Constants一章.

查看这个通常有用的PostgreSQL Wiki页面上的章节default_statistics_targetrandom_page_cost.

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

  • 谢谢你的建议.我最近运行了`VACUUM ANALYZE`,但我再次尝试,并且在将`default_statistics_target`更改为最大值10000之后.不幸的是,在执行此操作后仍然选择了错误的查询计划.改变`random_page_cost`也没有效果.感谢您推荐的阅读,我会仔细阅读这些页面. (2认同)

Jer*_*hka 6

除非您考虑数据库统计信息和自定义数据类型的组合,否则我怀疑这与坏的统计信息有关.

我的猜测是PostgreSQL正在选择嵌套循环连接,因为它会查看谓词(treenode.location).x >= 8000 AND (treenode.location).x <= (8000 + 4736)并在比较算法中做一些时髦的事情.甲嵌套循环通常要当你在联接的内侧的数据量小的情况下使用.

但是,一旦你将常数切换到10736,你就会得到一个不同的计划.该计划总是有可能足够复杂,以至于基因查询优化(GEQO)正在进行,您将看到非确定性计划构建的副作用.查询中的评估顺序存在足够的差异,使我认为这是正在发生的事情.

一种选择是使用参数化/预准备语句来检查,而不是使用ad hoc代码.由于您在三维空间中工作,您可能还想考虑使用PostGIS.虽然它可能过度,但它也可能为您提供使这些查询正常运行所需的性能.

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