Не позволяйте PostgreSQL иногда выбирать неверный план запроса - PullRequest
22 голосов
/ 22 ноября 2011

У меня странная проблема с производительностью 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

Ответы [ 4 ]

44 голосов
/ 22 ноября 2011

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

1.Статистика неточна.

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

(Лучшие два ответа все еще применяются для Postgres 9.6.)

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

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

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

После этого снова запустить ANALYZE (для соответствующих таблиц).

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

Прочтите главу Константы стоимости планировщика в руководстве.

Посмотрите главы default_statistics_target и random_page_cost на этой , как правило, полезной странице Вики PostgreSQL .

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

6 голосов
/ 23 ноября 2011

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

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

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

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

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

1 голос
/ 28 ноября 2011

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

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

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

1 голос
/ 22 ноября 2011

Что сказал Эрвин о статистике. Кроме того:

ORDER BY parentid DESC, id, z_diff

Сортировка по

parentid DESC, id, z

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

...