¿Por qué PostgreSQL elige el orden de unión más caro?


13

PostgreSQL usando valores predeterminados, más

default_statistics_target=1000
random_page_cost=1.5

Versión

PostgreSQL 10.4 on x86_64-pc-linux-musl, compiled by gcc (Alpine 6.4.0) 6.4.0, 64-bit

He aspirado y analizado. La consulta es muy sencilla:

SELECT r.price
FROM account_payer ap
  JOIN account_contract ac ON ap.id = ac.account_payer_id
  JOIN account_schedule "as" ON ac.id = "as".account_contract_id
  JOIN schedule s ON "as".id = s.account_schedule_id
  JOIN rate r ON s.id = r.schedule_id
WHERE ap.account_id = 8

Cada idcolumna es la clave principal, y todo lo que se une es una relación de clave externa, y cada clave externa tiene un índice. Además de un índice para account_payer.account_id.

Se necesitan 3.93s para devolver 76k filas.

Merge Join  (cost=8.06..83114.08 rows=3458267 width=6) (actual time=0.228..3920.472 rows=75548 loops=1)
  Merge Cond: (s.account_schedule_id = "as".id)
  ->  Nested Loop  (cost=0.57..280520.54 rows=6602146 width=14) (actual time=0.163..3756.082 rows=448173 loops=1)
        ->  Index Scan using schedule_account_schedule_id_idx on schedule s  (cost=0.14..10.67 rows=441 width=16) (actual time=0.035..0.211 rows=89 loops=1)
        ->  Index Scan using rate_schedule_id_code_modifier_facility_idx on rate r  (cost=0.43..486.03 rows=15005 width=10) (actual time=0.025..39.903 rows=5036 loops=89)
              Index Cond: (schedule_id = s.id)
  ->  Materialize  (cost=0.43..49.46 rows=55 width=8) (actual time=0.060..12.984 rows=74697 loops=1)
        ->  Nested Loop  (cost=0.43..49.32 rows=55 width=8) (actual time=0.048..1.110 rows=66 loops=1)
              ->  Nested Loop  (cost=0.29..27.46 rows=105 width=16) (actual time=0.030..0.616 rows=105 loops=1)
                    ->  Index Scan using account_schedule_pkey on account_schedule "as"  (cost=0.14..6.22 rows=105 width=16) (actual time=0.014..0.098 rows=105 loops=1)
                    ->  Index Scan using account_contract_pkey on account_contract ac  (cost=0.14..0.20 rows=1 width=16) (actual time=0.003..0.003 rows=1 loops=105)
                          Index Cond: (id = "as".account_contract_id)
              ->  Index Scan using account_payer_pkey on account_payer ap  (cost=0.14..0.21 rows=1 width=8) (actual time=0.003..0.003 rows=1 loops=105)
                    Index Cond: (id = ac.account_payer_id)
                    Filter: (account_id = 8)
                    Rows Removed by Filter: 0
Planning time: 5.843 ms
Execution time: 3929.317 ms

Si configuro join_collapse_limit=1, toma 0.16s, una aceleración de 25x.

Nested Loop  (cost=6.32..147323.97 rows=3458267 width=6) (actual time=8.908..151.860 rows=75548 loops=1)
  ->  Nested Loop  (cost=5.89..390.23 rows=231 width=8) (actual time=8.730..11.655 rows=66 loops=1)
        Join Filter: ("as".id = s.account_schedule_id)
        Rows Removed by Join Filter: 29040
        ->  Index Scan using schedule_pkey on schedule s  (cost=0.27..17.65 rows=441 width=16) (actual time=0.014..0.314 rows=441 loops=1)
        ->  Materialize  (cost=5.62..8.88 rows=55 width=8) (actual time=0.001..0.011 rows=66 loops=441)
              ->  Hash Join  (cost=5.62..8.61 rows=55 width=8) (actual time=0.240..0.309 rows=66 loops=1)
                    Hash Cond: ("as".account_contract_id = ac.id)
                    ->  Seq Scan on account_schedule "as"  (cost=0.00..2.05 rows=105 width=16) (actual time=0.010..0.028 rows=105 loops=1)
                    ->  Hash  (cost=5.02..5.02 rows=48 width=8) (actual time=0.178..0.178 rows=61 loops=1)
                          Buckets: 1024  Batches: 1  Memory Usage: 11kB
                          ->  Hash Join  (cost=1.98..5.02 rows=48 width=8) (actual time=0.082..0.143 rows=61 loops=1)
                                Hash Cond: (ac.account_payer_id = ap.id)
                                ->  Seq Scan on account_contract ac  (cost=0.00..1.91 rows=91 width=16) (actual time=0.007..0.023 rows=91 loops=1)
                                ->  Hash  (cost=1.64..1.64 rows=27 width=8) (actual time=0.048..0.048 rows=27 loops=1)
                                      Buckets: 1024  Batches: 1  Memory Usage: 10kB
                                      ->  Seq Scan on account_payer ap  (cost=0.00..1.64 rows=27 width=8) (actual time=0.009..0.023 rows=27 loops=1)
                                            Filter: (account_id = 8)
                                            Rows Removed by Filter: 24
  ->  Index Scan using rate_schedule_id_code_modifier_facility_idx on rate r  (cost=0.43..486.03 rows=15005 width=10) (actual time=0.018..1.685 rows=1145 loops=66)
        Index Cond: (schedule_id = s.id)
Planning time: 4.692 ms
Execution time: 160.585 ms

Estas salidas tienen poco sentido para mí. El primero tiene un costo (muy alto) de 280.500 para la unión de bucle anidado para la programación y los índices de tasas. ¿Por qué PostgreSQL elige intencionalmente esa unión tan costosa primero?

Información adicional solicitada a través de comentarios

¿Es rate_schedule_id_code_modifier_facility_idxun índice compuesto?

Es, con schedule_idser la primera columna. Lo hice un índice dedicado, y lo eligió el planificador de consultas, pero no afecta el rendimiento ni afecta el plan.


¿Se puede cambiar la configuración default_statistics_targety random_page_costvolver a sus valores predeterminados? ¿Qué sucede cuando subes default_statistics_targetaún más? ¿Puedes hacer un DB Fiddle (en dbfiddle.uk) e intentar reproducir el problema allí?
Colin 't Hart

3
¿Puede inspeccionar las estadísticas reales para ver si hay algo sesgado / extraño en sus datos? postgresql.org/docs/10/static/planner-stats.html
Colin 't Hart

¿Cuál es el valor actual para el parámetro work_mem? ¿Cambiarlo da diferentes horarios?
eppesuig

Respuestas:


1

Parece que sus estadísticas no son precisas (ejecute el análisis de vacío para actualizarlas) o que tenga columnas correlacionadas en su modelo (por lo que deberá realizar create statistics para informar al planificador de ese hecho).

los join_collapse parámetro permite al planificador reorganizar las uniones para que realice primero el que obtiene menos datos. Pero, para el rendimiento, no podemos dejar que el planificador haga eso en una consulta con muchas uniones. Por defecto, se establece en 8 uniones como máximo. Al establecerlo en 1, simplemente deshabilita esa habilidad.

Entonces, ¿cómo prevé postgres cuántas filas debe buscar esta consulta? Utiliza estadísticas para estimar el número de filas.

Lo que podemos ver en sus planes explicativos es que hay varias estimaciones de número de filas inexactas (el primer valor es estimado, el segundo es real).

Por ejemplo, aquí:

Materialize  (cost=0.43..49.46 rows=55 width=8) (actual time=0.060..12.984 rows=74697 loops=1)

El planificador estimó obtener 55 filas cuando en realidad obtuvo 74697.

Lo que haría (si estuviera en tu lugar) es:

  • analyze las cinco tablas involucradas para actualizar las estadísticas
  • Repetición explain analyze
  • Mire la diferencia entre los números de fila estimados y los números de fila reales
  • Si los números estimados de las filas son correctos, tal vez el plan cambió y es más eficiente. Si todo está bien, puede considerar cambiar la configuración de vacío automático para analizar (y aspirar) con mayor frecuencia
  • Si los números de fila estimados siguen siendo incorrectos, parece que tiene datos correlacionados en su tabla (tercera infracción de forma normal). Puede considerar declararlo con CREATE STATISTICS(documentación aquí )

Si necesita más información sobre las estimaciones de las filas y sus cálculos, encontrará todo lo que necesita en la charla confidencial de Tomás Vondra "Crear estadísticas: ¿para qué sirve?" (diapositivas aquí )

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.