postgresql sakila database
Mantenga PostgreSQL a veces eligiendo un plan de consulta incorrecto (4)
Tengo un problema extraño con el rendimiento de PostgreSQL para una consulta, usando PostgreSQL 8.4.9. Esta consulta consiste en seleccionar un conjunto de puntos dentro de un volumen 3D, utilizando un LEFT OUTER JOIN
para agregar una columna de ID relacionada donde exista ese ID relacionado. Pequeños cambios en el rango x
pueden hacer que PostgreSQL elija un plan de consulta diferente, lo que lleva el tiempo de ejecución de 0.01 segundos a 50 segundos. Esta es la consulta en cuestión:
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;
Esa consulta toma casi un minuto y, si agrego EXPLAIN
al frente de esa consulta, parece estar usando el siguiente plan de consulta:
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)
Sin embargo, si reemplazo el 8000
en la condición de rango x
con 10644
, la consulta se realiza en una fracción de segundo y usa este plan de consulta:
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)
Estoy lejos de ser un experto en analizar estos planes de consulta, pero la clara diferencia parece ser que con un rango x
usa un Hash Left Join
para LEFT OUTER JOIN
(que es muy rápido), mientras que con el otro rango usa un Nested Loop Left Join
(que parece ser muy lento). En ambos casos, las consultas devuelven aproximadamente 90 filas. Si SET ENABLE_NESTLOOP TO FALSE
antes de la versión lenta de la consulta, va muy rápido, pero entiendo que usar esa configuración en general es una mala idea .
¿Puedo, por ejemplo, crear un índice particular para hacer más probable que el planificador de consultas elija la estrategia claramente más eficiente? ¿Podría alguien sugerir por qué el planificador de consultas de PostgreSQL debería elegir una estrategia tan deficiente para una de estas consultas? A continuación he incluido detalles del esquema que pueden ser útiles.
La tabla treenode tiene 900,000 filas, y se define de la siguiente manera:
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
El tipo compuesto double3d
se define de la siguiente manera:
Composite type "public.double3d"
Column | Type
--------+------------------
x | double precision
y | double precision
z | double precision
Las otras dos tablas involucradas en la unión son 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
... y 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
Lo que dijo Erwin sobre las estadísticas. También:
ORDER BY parentid DESC, id, z_diff
Ordenando
parentid DESC, id, z
podría darle al optimizador un poco más de espacio para mezclar. (No creo que importe mucho ya que es el último trimestre, y el tipo no es tan caro, pero podrías probarlo)
No estoy seguro de que sea la fuente de su problema, pero parece que se realizaron algunos cambios en el planificador de consultas postgres entre las versiones 8.4.8 y 8.4.9. Podría intentar usar una versión anterior y ver si hace la diferencia.
http://postgresql.1045698.n5.nabble.com/BUG-6275-Horrible-performance-regression-td4944891.html
No olvide volver a analizar sus tablas si cambia la versión.
Soy escéptico de que esto tenga algo que ver con malas estadísticas a menos que considere la combinación de estadísticas de base de datos y su tipo de datos personalizados.
Mi suposición es que PostgreSQL está escogiendo una unión de bucle anidado porque mira los predicados (treenode.location).x >= 8000 AND (treenode.location).x <= (8000 + 4736)
y hace algo funky en la aritmética de tu comparación Normalmente, se utilizará un bucle anidado cuando tenga una pequeña cantidad de datos en el lado interno de la unión.
Pero, una vez que cambias la constante a 10736, obtienes un plan diferente. Siempre es posible que el plan sea suficientemente complejo como para que la Genetic Query Optimization (GEQO) esté funcionando y vea los efectos colaterales de la construcción de un plan no determinista . Hay suficientes discrepancias en el orden de evaluación en las consultas para hacerme pensar que eso es lo que está sucediendo.
Una opción sería examinar el uso de una declaración parametrizada / preparada para esto en lugar de usar un código ad hoc. Dado que está trabajando en un espacio tridimensional, es posible que desee considerar el uso de PostGIS . Si bien puede ser excesivo, también puede proporcionarle el rendimiento que necesita para que estas consultas se ejecuten correctamente.
Si bien forzar el comportamiento del planificador no es la mejor opción, a veces terminamos tomando mejores decisiones que el software.
Si el planificador de consultas toma malas decisiones, es principalmente una de dos cosas:
1. Las estadísticas están apagadas.
Significado "inexacto", no "apagado".
¿Corres ANALYZE
suficiente? También popular en su forma combinada VACUUM ANALYZE
. Si el autovacuum está activado (que es el predeterminado en Postgres de hoy en día), ANALYZE
se ejecuta automáticamente. Pero considera:
(Las dos respuestas más importantes aún se aplican a Postgres 9.6).
Si su tabla es grande y la distribución de datos es irregular , puede ser útil aumentar el valor default_statistics_target
. O más bien, simplemente configure el objetivo de estadísticas para las columnas relevantes (aquellas en las cláusulas WHERE
o JOIN
de sus consultas, básicamente):
ALTER TABLE ... ALTER COLUMN ... SET STATISTICS 1234; -- calibrate number
El objetivo se puede establecer en el rango de 0 a 10000;
Ejecute ANALYZE
nuevamente después de eso (en tablas relevantes).
2. La configuración de costo para las estimaciones del planificador está desactivada.
Lea el capítulo Constantes de costos del planificador en el manual.
Mire los capítulos default_statistics_target y random_page_cost en esta página Wiki de PostgreSQL generalmente útil .
Por supuesto, puede haber muchas otras razones posibles, pero estas son las más comunes de lejos.