Храните 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
Не забудьте повторно проанализировать свои таблицы, если вы измените версию.