14.3. Controle do planejador usando cláusulas JOIN explícitas #

É possível controlar o planejador de consultas até certo ponto usando a sintaxe explícita JOIN. Para ver por que isto importa, primeiro precisamos de algum conhecimento.

Em uma consulta de junção simples, como:

SELECT *
FROM a, b, c
WHERE a.id = b.id
  AND b.ref = c.id;

o planejador é livre para juntar as tabelas em qualquer ordem. Por exemplo, o planejador pode gerar um plano de consulta que une A a B, usando a condição WHERE a.id = b.id e, em seguida, unir C a esta tabela unida, usando a outra condição WHERE. Ou poderia juntar B a C e depois juntar A a este resultado. Ou poderia juntar A a C e depois juntá-los com B — mas isto seria ineficiente, porque o produto cartesiano completo de A e C teria que ser formado, não havendo condição aplicável na cláusula WHERE para permitir a otimização da junção. (Todas as junções no executor do PostgreSQL acontecem entre duas tabelas de entrada, então é necessário construir o resultado em uma ou outra dessas formas.) O ponto importante é que estas diferentes possibilidades de junção fornecem resultados semanticamente equivalentes, mas podem ter custos de execução muito diferentes. Portanto, o planejador irá explorar todos eles para tentar encontrar o plano de consulta mais eficiente.

Quando uma consulta envolve apenas duas ou três tabelas, não há muitos pedidos de junção com os quais se preocupar. Mas o número de pedidos de junção possíveis cresce exponencialmente à medida que o número de tabelas se expande. Além de dez ou mais tabelas de entrada, não é mais prático fazer uma busca exaustiva de todas as possibilidades, e mesmo para seis ou sete tabelas o planejamento pode levar um tempo irritantemente longo. Quando houver muitas tabelas de entrada, o planejador do PostgreSQL mudará da procura exaustiva para uma procura probabilística genética através de um número limitado de possibilidades. (O limite de alternância é definido pelo parâmetro de tempo de execução geqo_threshold.) A busca genética levará menos tempo, mas não encontrará necessariamente o melhor plano possível.

Quando a consulta envolve junções externas, o planejador tem menos liberdade do que para junções simples (internas). Por exemplo, considere:

SELECT *
FROM a
  LEFT JOIN (b JOIN c ON (b.ref = c.id))
  ON (a.id = b.id);

Embora as restrições dessa consulta sejam superficialmente semelhantes ao exemplo anterior, a semântica é diferente, porque uma linha deve ser emitida para cada linha de A que não possui linha correspondente na junção de B e C. Portanto, o planejador não tem escolha de ordem de junção aqui: deve unir B a C e então unir A a este resultado. Assim, esta consulta leva menos tempo para planejar do que a consulta anterior. Em outros casos, o planejador pode determinar que mais de uma ordem de associação é segura. Por exemplo, dado:

SELECT *
FROM a
  LEFT JOIN b ON (a.bid = b.id)
  LEFT JOIN c ON (a.cid = c.id);

é válido juntar A a B ou C primeiro. No momento, apenas FULL JOIN restringe completamente a ordem de junção. A maioria dos casos práticos envolvendo LEFT JOIN ou RIGHT JOIN podem ser reorganizados até certo ponto.

A sintaxe de junção interna explícita (INNER JOIN, CROSS JOIN, ou JOIN sem adornos), é semanticamente idêntica a listar as relações de entrada em FROM, portanto, não restringe a ordem de junção.

Mesmo que a maioria dos tipos de JOIN não restrinja completamente a ordem de junção, é possível instruir o planejador de consulta do PostgreSQL para tratar todas as cláusulas JOIN como restringindo a ordem de junção de qualquer maneira. Por exemplo, estas três consultas são logicamente equivalentes:

SELECT *
FROM a, b, c
WHERE a.id = b.id
  AND b.ref = c.id;

SELECT *
FROM a CROSS JOIN b CROSS JOIN c
WHERE a.id = b.id AND b.ref = c.id;

SELECT *
FROM a JOIN (b JOIN c ON (b.ref = c.id))
  ON (a.id = b.id);

Mas se dissermos ao planejador para honrar a ordem de JOIN, o segundo e o terceiro levarão menos tempo para planejar do que o primeiro. Não vale a pena se preocupar com este efeito para apenas três tabelas, mas pode ser um salva-vidas com muitas tabelas.

Para forçar o planejador a seguir a ordem de junção estabelecida por JOINs explícitos, deve ser definido o parâmetro de tempo de execução join_collapse_limit como 1. (Outros valores possíveis são discutidos abaixo.)

Não é necessário restringir completamente a ordem de junção para reduzir o tempo de procura, por não haver problema em usar operadores JOIN dentro dos itens de uma lista FROM simples. Por exemplo, considere:

SELECT *
FROM a
CROSS JOIN b, c, d, e WHERE ...;

Com join_collapse_limit = 1, isto força o planejador a unir A a B antes de uni-los a outras tabelas, mas não restringe suas escolhas de outra forma. Neste exemplo, o número de pedidos de junção possíveis é reduzido por um fator de 5.

Restringir a procura do planejador dessa maneira é uma técnica útil tanto para reduzir o tempo de planejamento, quanto para direcionar o planejador para um bom plano de consulta. Se o planejador escolher uma ordem de junção incorreta por padrão, poderá ser forçado a escolher uma ordem melhor por meio da sintaxe JOIN — ou seja, supondo que se conheça uma ordem melhor. A experimentação é recomendada.

Um problema intimamente relacionado que afeta o tempo de planejamento é o colapso da subconsulta em sua consulta mãe. Por exemplo, considere:

SELECT *
FROM x, y,
    (SELECT * FROM a, b, c WHERE algumacoisa) AS ss
WHERE algumaoutracoisa;

Esta situação pode surgir do uso de uma visão contendo uma junção; a regra SELECT da visão será inserida no lugar da referência da visão, produzindo uma consulta muito parecida com a vista acima. Normalmente, o planejador tentará colapsar a subconsulta na consulta mãe, produzindo:

SELECT *
FROM x, y, a, b, c
WHERE algumacoisa AND algumaoutracoisa;

Isto geralmente fornece um plano melhor do que planejar a subconsulta em separado. (Por exemplo, as condições externas WHERE podem ser tais que unir X a A primeiro elimina muitas linhas de A, evitando assim a necessidade de gerar a saída lógica completa da subconsulta.) Mas, ao mesmo tempo, aumenta o tempo de planejamento; aqui temos um problema de junção de cinco tabelas, substituindo dois problemas de junção de três tabelas separados. Devido ao aumento exponencial do número de possibilidades, algumaoutracoisa faz uma grande diferença. O planejador tenta evitar ficar preso a problemas de procura em muitas junções, não colapsando a subconsulta se surgirem mais de from_collapse_limit itens na cláusula FROM da consulta mãe. Pode-se compensar o tempo de planejamento pela qualidade do plano, ajustando este parâmetro de tempo de execução para cima ou para baixo.

from_collapse_limit e join_collapse_limit têm nomes semelhantes, porque fazem quase a mesma coisa: um controla quando o planejador irá achatar as subconsultas, e o outro controla quando ele irá achatar as junções explícitas. Normalmente se define join_collapse_limit igual a from_collapse_limit (para que junções e subconsultas explícitas atuem de maneira semelhante), ou se define join_collapse_limit igual a 1 (se for desejado controlar a ordem de junção com junções explícitas). Mas podem ser definidos de forma diferente, quando se tenta ajustar o equilíbrio entre o tempo de planejamento e o tempo de execução.