SELECCIONAR DISTINCT EN subconsulta utiliza plan ineficiente

8

Tengo una tabla progresses(contiene del orden de cientos de miles de registros actualmente):

    Column     |            Type             |                        Modifiers                        
---------------+-----------------------------+---------------------------------------------------------
 id            | integer                     | not null default nextval('progresses_id_seq'::regclass)
 lesson_id     | integer                     | 
 user_id       | integer                     | 
 created_at    | timestamp without time zone | 
 deleted_at    | timestamp without time zone | 
Indexes:
    "progresses_pkey" PRIMARY KEY, btree (id)
    "index_progresses_on_deleted_at" btree (deleted_at)
    "index_progresses_on_lesson_id" btree (lesson_id)
    "index_progresses_on_user_id" btree (user_id)

y una vista v_latest_progressesque consulta los más recientes progressde user_idy lesson_id:

SELECT DISTINCT ON (progresses.user_id, progresses.lesson_id)
  progresses.id AS progress_id,
  progresses.lesson_id,
  progresses.user_id,
  progresses.created_at,
  progresses.deleted_at
 FROM progresses
WHERE progresses.deleted_at IS NULL
ORDER BY progresses.user_id, progresses.lesson_id, progresses.created_at DESC;

Un usuario puede tener muchos progresos para cualquier lección dada, pero a menudo queremos consultar un conjunto de los progresos creados más recientemente para un conjunto determinado de usuarios o lecciones (o una combinación de los dos).

La vista v_latest_progresseshace esto muy bien e incluso funciona cuando especifico un conjunto de user_ids:

# EXPLAIN SELECT "v_latest_progresses".* FROM "v_latest_progresses" WHERE "v_latest_progresses"."user_id" IN ([the same list of ids given by the subquery in the second example below]);
                                                                               QUERY PLAN                                                                                                                                         
----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------
 Unique  (cost=526.68..528.66 rows=36 width=57)
   ->  Sort  (cost=526.68..527.34 rows=265 width=57)
         Sort Key: progresses.user_id, progresses.lesson_id, progresses.created_at
         ->  Index Scan using index_progresses_on_user_id on progresses  (cost=0.47..516.01 rows=265 width=57)
               Index Cond: (user_id = ANY ('{ [the above list of user ids] }'::integer[]))
               Filter: (deleted_at IS NULL)
(6 rows)

Sin embargo, si trato de hacer la misma consulta reemplazando el conjunto de user_ids con una subconsulta, se vuelve muy ineficiente:

# EXPLAIN SELECT "v_latest_progresses".* FROM "v_latest_progresses" WHERE "v_latest_progresses"."user_id" IN (SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44);
                                             QUERY PLAN                                              
-----------------------------------------------------------------------------------------------------
 Merge Semi Join  (cost=69879.08..72636.12 rows=19984 width=57)
   Merge Cond: (progresses.user_id = users.id)
   ->  Unique  (cost=69843.45..72100.80 rows=39969 width=57)
         ->  Sort  (cost=69843.45..70595.90 rows=300980 width=57)
               Sort Key: progresses.user_id, progresses.lesson_id, progresses.created_at
               ->  Seq Scan on progresses  (cost=0.00..31136.31 rows=300980 width=57)
                     Filter: (deleted_at IS NULL)
   ->  Sort  (cost=35.63..35.66 rows=10 width=4)
         Sort Key: users.id
         ->  Index Scan using index_users_on_company_id on users  (cost=0.42..35.46 rows=10 width=4)
               Index Cond: (company_id = 44)
(11 rows)

Lo que estoy tratando de entender es por qué PostgreSQL quiere realizar la DISTINCTconsulta en toda la progressestabla antes de que se filtre por subconsulta en el segundo ejemplo.

¿Alguien tendría algún consejo sobre cómo mejorar esta consulta?

Aaron
fuente

Respuestas:

11

Aaron

En mi trabajo reciente, he estado buscando algunas preguntas similares con PostgreSQL. PostgreSQL casi siempre es bastante bueno para generar el plan de consulta correcto, pero no siempre es perfecto.

Algunas sugerencias simples serían asegurarse de ejecutar un archivo ANALYZEen su progressestabla para asegurarse de tener estadísticas actualizadas, ¡pero esto no garantiza que solucione sus problemas!

Por razones que probablemente son demasiado largas para esta publicación, he encontrado algunos comportamientos extraños en la recopilación de estadísticas ANALYZEy el planificador de consultas que pueden necesitar resolverse a largo plazo. A corto plazo, el truco es reescribir su consulta para intentar hackear el plan de consulta que desee.

Sin tener acceso a sus datos para realizar pruebas, haré las siguientes dos sugerencias posibles.

1) uso ARRAY()

PostgreSQL trata las matrices y los conjuntos de registros de manera diferente en su planificador de consultas. A veces terminarás con un plan de consulta idéntico. En este caso, como en muchos de mis casos, no lo haces.

En su consulta original tenía:

EXPLAIN SELECT "v_latest_progresses".* FROM "v_latest_progresses" 
WHERE "v_latest_progresses"."user_id" 
IN (SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44);

Como primer paso para tratar de arreglarlo, intente

EXPLAIN SELECT "v_latest_progresses".* FROM "v_latest_progresses" 
WHERE "v_latest_progresses"."user_id" =
ANY(ARRAY(SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44));

Tenga en cuenta el cambio de la subconsulta de INa =ANY(ARRAY()).

2) Use CTE

Otro truco es forzar optimizaciones separadas, si mi primera sugerencia no funciona. Sé que muchas personas usan este truco, porque las consultas dentro de un CTE se optimizan y materializan por separado de la consulta principal.

EXPLAIN 
WITH user_selection AS(
  SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44
)
SELECT "v_latest_progresses".* FROM "v_latest_progresses" 
WHERE "v_latest_progresses"."user_id" =
ANY(ARRAY(SELECT "id" FROM user_selection));

Esencialmente, al crear el CTE user_selectionusando la WITHcláusula, le está pidiendo a PostgreSQL que realice una optimización por separado en la subconsulta

SELECT "users"."id" FROM "users" WHERE "users"."company_id"=44

y luego materializando esos resultados. Luego, una vez más, uso la =ANY(ARRAY())expresión para tratar de manipular manualmente el plan.

En estos casos, probablemente no pueda confiar solo en los resultados EXPLAIN, porque ya pensó que encontró la solución menos costosa. Asegúrese de ejecutar un EXPLAIN (ANALYZE,BUFFERS)...para averiguar lo que realmente cuesta en términos de tiempo y lecturas de página.

Chris
fuente
Como resultado, su primera sugerencia funciona de maravilla. El costo de esa consulta es 144.07..144.6, ¡MUCHO por debajo de los 70,000 que he estado recibiendo! Muchas gracias.
Aaron
1
¡Decir ah! Me alegro de poder ayudar. Me cuesta mucho resolver estos problemas de "pirateo de planes de consulta"; Es un poco de arte por encima de la ciencia.
Chris
He estado aprendiendo trucos a lo largo de los años para conseguir que las bases de datos hagan lo que quiero y debo decir que esta ha sido una de las situaciones más extrañas con las que he lidiado. Realmente es un arte. ¡Realmente aprecio tu explicación bien pensada!
Aaron