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