A subconsulta SELECT DISTINCT ON usa plano ineficiente


8

Eu tenho uma tabela progresses(contém atualmente na ordem de centenas de milhares de registros):

    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)

e uma vista v_latest_progressesque consulta o mais recente progresspor user_ide 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;

Um usuário pode ter muitos progressos para qualquer lição, mas geralmente queremos consultar um conjunto dos progressos criados mais recentemente para um determinado conjunto de usuários ou lições (ou uma combinação dos dois).

A visualização v_latest_progressesfaz isso muito bem e tem até desempenho quando especifico um 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)

No entanto, se eu tentar fazer a mesma consulta substituindo o conjunto de user_ids por uma subconsulta, ela se tornará muito 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)

O que estou tentando descobrir é por que o PostgreSQL deseja executar a DISTINCTconsulta em toda a progressestabela antes de filtrar pela subconsulta no segundo exemplo.

Alguém teria algum conselho sobre como melhorar esta consulta?

Respostas:


11

Aaron,

No meu trabalho recente, estive analisando algumas questões semelhantes com o PostgreSQL. O PostgreSQL quase sempre é muito bom em gerar o plano de consulta correto, mas nem sempre é perfeito.

Algumas sugestões simples seriam: certifique-se de executar uma tabela ANALYZEem sua progressestabela para garantir que você atualize as estatísticas, mas isso não garante os problemas!

Por razões que provavelmente são muito demoradas para esta postagem, eu encontrei alguns comportamentos estranhos na coleta de estatísticas ANALYZEe no planejador de consultas que podem precisar ser resolvidos a longo prazo. A curto prazo, o truque é reescrever sua consulta para tentar hackear o plano de consulta que você deseja.

Sem ter acesso aos seus dados para teste, farei as duas sugestões possíveis a seguir.

1) Use ARRAY()

O PostgreSQL trata matrizes e conjuntos de registros de maneira diferente em seu planejador de consultas. Às vezes, você acaba com um plano de consulta idêntico. Nesse caso, como em muitos dos meus casos, você não.

Na sua consulta original, você tinha:

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 primeira tentativa de consertar, tente

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));

Observe a alteração da subconsulta de INpara =ANY(ARRAY()).

2) Use CTEs

Outro truque é forçar otimizações separadas, se minha primeira sugestão não funcionar. Sei que muitas pessoas usam esse truque, porque as consultas dentro de um CTE são otimizadas e materializadas separadamente da 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));

Essencialmente, ao criar o CTE user_selectionusando a WITHcláusula, você está solicitando ao PostgreSQL que realize uma otimização separada na subconsulta

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

e depois materializando esses resultados. Então, mais uma vez, uso a =ANY(ARRAY())expressão para tentar manipular manualmente o plano.

Nesses casos, você provavelmente não pode confiar apenas nos resultados EXPLAIN, porque já pensou ter encontrado a solução menos dispendiosa. Certifique-se de executar um EXPLAIN (ANALYZE,BUFFERS)...para descobrir o que realmente custa em termos de tempo e leitura de página.


Como se vê, sua primeira sugestão faz maravilhas. O custo dessa consulta é 144.07..144.6MUITO abaixo dos 70.000 que recebi! Muito obrigado.
Aaron

11
Ha! Ainda bem que pude ajudar. Eu luto muito com essas questões do "plano de consulta de hackers"; é um pouco de arte no topo da ciência.
Chris

Eu tenho aprendido truques de esquerda e direita ao longo dos anos para conseguir que os bancos de dados façam o que eu quero e devo dizer que essa foi uma das situações mais estranhas com as quais eu lidei. É realmente uma arte. Eu realmente aprecio sua explicação bem pensada!
Aaron
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.