36.15. Informações de otimização do operador #

36.15.1. COMMUTATOR
36.15.2. NEGATOR
36.15.3. RESTRICT
36.15.4. JOIN
36.15.5. HASHES
36.15.6. MERGES

Uma definição de operador no PostgreSQL pode incluir várias cláusulas opcionais que informam ao sistema coisas úteis sobre como o operador se comporta. Essas cláusulas devem ser fornecidas sempre que apropriado, porque podem resultar em consideráveis melhorias de desempenho na execução de consultas que usam o operador. Mas se forem fornecidas, deve-se ter certeza de que estão corretas! O uso incorreto de uma cláusula de otimização pode resultar em consultas lentas, saídas sutilmente erradas, ou outras coisas ruins. Sempre pode ser deixada de fora uma cláusula de otimização quando não houver certeza; a única consequência será que as consultas poderão ser executadas mais lentamente do que o necessário.

Poderão ser adicionadas cláusulas de otimização adicionais em versões futuras do PostgreSQL. As cláusulas descritas aqui são todas as que a versão 18.1 entende.

Também é possível anexar ao planejador uma função de suporte à função subjacente ao operador, fornecendo outra maneira de informar o sistema sobre o comportamento do operador. Veja Informações sobre otimização de funções para obter mais informações.

36.15.1. COMMUTATOR #

A cláusula COMMUTATOR, se estiver presente, indica o nome do operador que é o comutador do operador sendo definido. É dito que o operador A é o comutador do operador B, se (x A y) for igual a (y B x) para todos os valores de entrada possíveis de x, y. Note que B também é o comutador de A. Por exemplo, os operadores < e > para um determinado tipo de dados são geralmente os comutadores uns dos outros, e o operador + é geralmente comutativo consigo mesmo. Mas o operador - geralmente não é comutativo com nada.

O tipo de dados do operando esquerdo de um operador comutável, é idêntico ao tipo de dados do operando direito de seu comutador, e vice-versa. Portanto, o nome do operador comutador é tudo o que o PostgreSQL precisa receber para procurar o comutador, e isso é tudo o que precisa ser fornecido na cláusula COMMUTATOR.

É fundamental fornecer informações do comutador para operadores que serão usados em índices e cláusulas de junção, para permitir que o otimizador de consulta inverta essa cláusula para as formas necessárias para diferentes tipos de plano. Por exemplo, considere uma consulta com uma cláusula WHERE como tab1.x = tab2.y, onde tab1.x e tab2.y são de um tipo de dados definido pelo usuário, e suponha que tab2.y esteja indexado. O otimizador não pode gerar uma varredura de índice, a menos que possa determinar como inverter a cláusula para tab2.y = tab1.x, porque o maquinário de varredura de índice espera ver a coluna indexada à esquerda do operador que lhe é dado. O PostgreSQL não vai simplesmente assumir que essa é uma transformação válida — o criador do operador = deve especificar que ela é válida, marcando o operador com informações do comutador.

36.15.2. NEGATOR #

A cláusula NEGATOR, se estiver presente, indica o nome do operador que é o negador do operador sendo definido. É dito que o operador A é o negador do operador B, se ambos retornarem resultados booleanos, e (x A y) for igual a NOT (x B y), para todas as entradas possíveis de x, y. Note que B também é o negador de A. Por exemplo, < e >= são um par negador para a maioria dos tipos de dados. Um operador nunca pode ser validamente seu próprio negador.

Ao contrário dos comutadores, um par de operadores unários pode ser validamente marcado como negador um do outro; isso significaria (A x) igual a NOT (B x) para todo x.

O negador de um operador deve ter operandos esquerdo e/ou direito dos mesmos tipos de dados do operador sendo definido, portanto, assim como em COMMUTATOR, apenas o nome do operador precisa ser informado na cláusula NEGATOR.

Fornecer um negador é muito útil para o otimizador de consulta, porque permite que expressões como NOT (x = y) sejam simplificadas em x <> y. Isso ocorre com mais frequência do que se imagina, porque as operações NOT podem ser inseridas como consequência de outros rearranjos.

36.15.3. RESTRICT #

A cláusula RESTRICT, se estiver presente, indica o nome de uma função de estimativa de seletividade de restrição para o operador. (Note que é o nome de uma função, e não o nome de um operador.) As cláusulas RESTRICT só fazem sentido para operadores binários que retornam o tipo de dados boolean. A ideia por trás de um estimador de seletividade de restrição é adivinhar qual fração das linhas da tabela irá satisfazer a condição de uma cláusula WHERE com a forma

coluna OP constante

para o operador corrente e um valor constante específico. Isso auxilia o otimizador, dando a ele uma ideia de quantas linhas serão eliminadas pelas cláusulas WHERE que possuem essa forma. (O que acontece se a constante estiver à esquerda?, pode-se perguntar. Bem, essa é uma das coisas para as quais o COMMUTATOR serve...)

Escrever novas funções de estimativa de seletividade de restrição está muito além do escopo desse capítulo, mas felizmente pode-se usar apenas um dos estimadores padrão do sistema para muitos de seus próprios operadores. Esses são os estimadores de restrição padrão:

eqsel para =
neqsel para <>
scalarltsel para <
scalarlesel para <=
scalargtsel para >
scalargesel para >=

Frequentemente, pode ser usada a função eqsel ou neqsel para operadores que possuem seletividade muito alta ou muito baixa, mesmo que, na verdade, não sejam igualdade ou desigualdade. Por exemplo, os operadores geométricos de igualdade aproximada usam eqsel na suposição de que eles normalmente corresponderão apenas a uma pequena fração das entradas em uma tabela.

Pode-se usar scalarltsel, scalarlesel, scalargtsel e scalargesel para comparações em tipos de dados que têm alguns meios práticos de serem convertidos em escalares numéricos para comparações de intervalo. Se possível, o tipo de dados deve ser adicionado aos compreendidos pela função convert_to_scalar() no arquivo src/backend/utils/adt/selfuncs.c. (Algum dia, essa função deverá ser substituída por funções por tipo de dados identificadas através de uma coluna do catálogo do sistema pg_type; mas isso ainda não aconteceu.) Se isso não for feito, ainda assim vai funcionar, mas as estimativas do otimizador não serão tão boas quanto poderiam ser.

Outra função integrada útil de estimativa de seletividade é a matchingsel, que funcionará para quase qualquer operador binário, se as estatísticas padrão de MCV e/ou histograma forem coletadas para o(s) tipo(s) de dados de entrada. Sua estimativa padrão é definida como duas vezes a estimativa padrão usada em eqsel, tornando-a mais adequada para operadores de comparação que são um pouco menos rígidos que a igualdade. (Ou pode ser chamada a função generic_restriction_selectivity subjacente, fornecendo uma estimativa padrão diferente.)

Existem funções de estimativa de seletividade adicionais projetadas para operadores geométricos no arquivo src/backend/utils/adt/geo_selfuncs.c da distribuição do código-fonte do PostgreSQL: areasel, positionsel, e contsel. No presente momento, são apenas esboços, mas pode-se querer usá-las (ou melhor ainda, melhorá-las) de qualquer maneira.

36.15.4. JOIN #

A cláusula JOIN, se estiver presente, indica o nome de uma função de estimativa de seletividade de junção para o operador. (Note que é o nome de uma função, e não o nome de um operador.) As cláusulas JOIN só fazem sentido para operadores binários que retornam o tipo de dados boolean. A ideia por trás de um estimador de seletividade de junção é adivinhar qual fração das linhas em um par de tabelas satisfará uma condição da cláusula WHERE com a forma

tabela1.coluna1 OP tabela2.coluna2

para o operador corrente. Assim como a cláusula RESTRICT, ajuda substancialmente o otimizador, permitindo que ele descubra qual das várias sequências de junção possíveis provavelmente exigirá menos trabalho.

Como antes, esse capítulo não tentará explicar como escrever uma função estimadora de seletividade de junção, sugerindo apenas que se use um dos estimadores padrão, se aplicável:

eqjoinsel para =
neqjoinsel para <>
scalarltjoinsel para <
scalarlejoinsel para <=
scalargtjoinsel para >
scalargejoinsel para >=
matchingjoinsel para operadores de correspondência genéricos
areajoinsel para comparações baseadas em área 2D
positionjoinsel para comparações baseadas em posição 2D
contjoinsel para comparações baseadas em contém 2D

36.15.5. HASHES #

A cláusula HASHES, se estiver presente, informa ao sistema que é permitido usar o método de junção por hash para uma junção baseada nesse operador. A cláusula HASHES só faz sentido para um operador binário que retorna o tipo de dados boolean e, na prática, o operador deve representar a igualdade para algum tipo de dados, ou par de tipos de dados.

A suposição subjacente à junção por hash é que o operador de junção só pode retornar verdade para pares de valores esquerdo e direito que fazem hash para o mesmo código hash. Se dois valores forem colocados em blocos de hash diferentes, a junção nunca os irá comparar, assumindo implicitamente que o resultado do operador de junção deve ser falso. Portanto, nunca faz sentido especificar HASHES para operadores que não representam alguma forma de igualdade. Geralmente, é prático apenas oferecer suporte a hashing para operadores que usam o mesmo tipo de dados nos dois lados. No entanto, às vezes é possível projetar funções de hash compatíveis para dois ou mais tipos de dados; ou seja, funções que vão gerar os mesmos códigos de hash para valores iguais, mesmo que os valores tenham representações diferentes. Por exemplo, é bem simples organizar essa propriedade ao fazer hash de números inteiros de comprimentos diferentes.

Para ser marcado como HASHES, o operador de junção deve aparecer em uma família de operador de índice de hash. Isso não é obrigatório quando se cria o operador, porque é claro que a família de operador de referência ainda não pode existir. Mas as tentativas de usar o operador em junções de hash vão falhar em tempo de execução, se essa família de operador não existir. O sistema precisa que a família do operador encontre as funções de hash específicas do tipo de dados para o(s) tipo(s) de dados de entrada do operador. Obviamente, também se deve criar funções de hash adequadas antes de criar a família de operador.

Deve-se ter cuidado ao preparar uma função de hash, porque existem circunstâncias dependentes de máquina nas quais ela pode não fazer a coisa certa. Por exemplo, se o tipo de dados for uma estrutura onde pode haver bits de preenchimento sem interesse, não se pode simplesmente passar toda a estrutura para hash_any. (A menos que se escreva outros operadores e funções para garantir que os bits não utilizados sejam sempre zero, que é a estratégia recomendada.) Outro exemplo é que, em máquinas que atendem ao padrão de ponto flutuante do IEEE, zero negativo e zero positivo são valores diferentes (diferentes padrões de bits), mas são definidos para serem comparados iguais. Se um valor de ponto flutuante puder conter zero negativo, serão necessárias etapas extras para garantir que ele gere o mesmo valor de hash que o zero positivo.

Um operador que pode ser usado em junção por hash, deve ter um comutador (ele mesmo, se os tipos de dados dos dois operandos forem iguais, ou um operador de igualdade relacionado, se forem diferentes) que aparece na mesma família de operador. Se não for esse o caso, podem ocorrer erros do planejador quando o operador for usado. Além disso, é uma boa ideia (mas não estritamente necessária), que uma família de operador de hash, que suporte vários tipos de dados, forneça operadores de igualdade para cada combinação dos tipos de dados; isso permite uma melhor otimização.

Nota

A função subjacente a um operador que pode ser usado em junção por hash deve ser marcada como imutável ou estável. Se for volátil, o sistema nunca tentará usar o operador para uma junção por hash.

Nota

Se um operador que pode ser usado em junção por hash tiver uma função subjacente marcada como estrita, a função também deverá ser completa: ou seja, deve retornar verdade ou falso, e nunca nulo, para quaisquer duas entradas não nulas. Se essa regra não for seguida, a otimização do hash das operações IN pode gerar resultados incorretos. (Especificamente, IN pode retornar falso onde a resposta correta, segundo o padrão, seria nulo; ou pode gerar um erro reclamando que não foi preparado para um resultado nulo.)

36.15.6. MERGES #

A cláusula MERGES, se estiver presente, informa ao sistema que é permitido usar o método de junção por merge para uma junção baseada nesse operador. A cláusula MERGES só faz sentido para um operador binário que retorna o tipo de dados boolean e, na prática, o operador deve representar a igualdade para algum tipo de dados, ou par de tipos de dados.

A junção por mesclagem baseia-se na ideia de classificar as tabelas da esquerda e da direita em ordem e, em seguida, varrê-las em paralelo. Portanto, os dois tipos de dados devem poder ser inteiramente classificados, e o operador de junção deve ser aquele que só pode ser bem-sucedido para pares de valores que caem no mesmo lugar na ordem de classificação. Na prática, isso significa que o operador de junção deve se comportar como igualdade. Mas é possível realizar a junção por mesclagem de dois tipos de dados distintos, desde que sejam logicamente compatíveis. Por exemplo, o operador de igualdade smallint-versus-integer permite a junção por mesclagem. São necessários apenas os operadores de ordenação que trarão os dois tipos de dados em uma sequência logicamente compatível.

Para ser marcado como MERGES, o operador de junção deve aparecer como membro de igualdade de uma família de operador de índice btree. Isso não é obrigatório quando se cria o operador, porque é claro que a família de operador de referência ainda não pode existir. Mas, na verdade, o operador não será usado para junções por mesclagem, a menos que uma família de operador correspondente possa ser encontrada. O sinalizador MERGES atua como uma dica para o planejador, dizendo que vale a pena procurar uma família de operador correspondente.

Um operador que pode ser usado em junção por merge deve ter um comutador (ele mesmo, se os tipos de dados dos dois operandos forem iguais, ou um operador de igualdade relacionado, se forem diferentes) que apareça na mesma família de operador. Se não for esse o caso, podem ocorrer erros do planejador quando o operador for usado. Além disso, é uma boa ideia (mas não estritamente necessária), que uma família de operador btree, que suporte vários tipos de dados, forneça operadores de igualdade para cada combinação dos tipos de dados; isso permite uma melhor otimização.

Nota

A função subjacente a um operador que pode ser usado em junção por merge deve ser marcada como imutável ou estável. Se for volátil, o sistema nunca tentará usar o operador para uma junção por merge.