Храните PostgreSQL от выбора плохого плана запроса

У меня есть странная проблема с производительностью PostgreSQL для запроса, используя PostgreSQL 8.4.9. Этот запрос выбирает набор точек в трехмерном томе, используя LEFT OUTER JOIN, чтобы добавить столбец связанных идентификаторов, в котором существует соответствующий идентификатор. Небольшие изменения в диапазоне 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)

Однако, если я заменил 8000 в условии диапазона x на 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 до медленной версии запроса, это происходит очень быстро, но я понимаю, что с использованием этого параметра в целом - плохая идея.

Могу ли я, например, создать конкретный индекс, чтобы сделать его более вероятным, чтобы планировщик запросов выберет более эффективную стратегию? Может ли кто-нибудь предположить, почему планировщик запросов 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

Составной тип 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

Ответ 1

Если планировщик запросов принимает плохие решения, это в основном одна из двух вещей:

1. статистика отключена.

Значение "неточно", а не "выключено".

Вы выполняете ANALYZE достаточно? Также популярна в нем комбинированная форма VACUUM ANALYZE. Если autovacuum включен (это по умолчанию используется в современных Postgres), ANALYZE запускается автоматически. Но учтите:

(Два последних ответа по-прежнему применяются к Postgres 9.6.)

Если ваша таблица большая, а распределение данных нерегулярно, повышение default_statistics_target может помочь. Вернее, просто установить целевой показатель статистики для соответствующих столбцов (в WHERE или JOIN предложениях ваших запросов в основном):

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

Цель может быть установлена ​​в диапазоне от 0 до 10000;

Запустите ANALYZE снова после этого (в соответствующих таблицах).

2. Настройки стоимости для оценок планировщика отключены.

Прочитайте главу Плановые константы затрат в руководстве.

Посмотрите на главы default_statistics_target и random_page_cost на этой общедоступной странице PostgreSQL Wiki.

Конечно, может быть много других возможных причин, но они наиболее распространены на сегодняшний день.

Ответ 2

Я скептически отношусь к тому, что это имеет какое-либо отношение к плохой статистике, если вы не рассматриваете комбинацию статистики базы данных и своего настраиваемого типа данных.

Я предполагаю, что PostgreSQL выбирает объединение вложенного цикла, потому что он смотрит на предикаты (treenode.location).x >= 8000 AND (treenode.location).x <= (8000 + 4736) и делает что-то фанк в арифметике вашего сравнения. Вложенный цикл, как правило, будет использоваться, когда у вас есть небольшой объем данных на внутренней стороне соединения.

Но, как только вы переключите константу на 10736, вы получите другой план. Всегда возможно, что план достаточно сложный, что Genetic Query Optimization (GEQO) пинает, и вы видите побочные эффекты недетерминированного построения плана. Достаточно расхождений в порядке оценки в запросах, чтобы заставить меня думать, что происходит.

Один из вариантов - изучить с помощью параметризованного/подготовленного оператора для этого вместо использования ad hoc-кода. Поскольку вы работаете в трехмерном пространстве, вы также можете рассмотреть возможность использования PostGIS. Хотя это может быть чрезмерным, оно также может предоставить вам производительность, необходимую для правильного выполнения этих запросов.

При форсировании поведения планировщика это не лучший выбор, иногда мы получаем лучшие решения, чем программное обеспечение.

Ответ 3

Что сказал Эрвин о статистике. Также:

ORDER BY parentid DESC, id, z_diff

Сортировка

parentid DESC, id, z

может дать оптимизатору немного больше места для перетасовки. (Я не думаю, что это будет иметь большое значение, поскольку это последний термин, и сортировка не такая уж дорогая, но вы можете попробовать)

Ответ 4

Я не уверен, что это источник вашей проблемы, но похоже, что были внесены некоторые изменения в планировщик запросов postgres между версиями 8.4.8 и 8.4.9. Вы можете попробовать использовать более старую версию и посмотреть, не имеет ли она значения.

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

Не забудьте повторно проанализировать свои таблицы, если вы измените версию.