31.13. Informações de otimização do operador

PostgreSQL 14.5: Informações de otimização do operador

No PostgreSQL a definição do operador pode incluir diversas cláusulas opcionais contendo informações sobre como o operador se comporta, úteis ao sistema. Estas cláusulas devem ser fornecidas sempre que for apropriado, porque podem acelerar muito os comandos que utilizam o operador. Mas se forem fornecidas, é preciso haver certeza que estão corretas! A utilização incorreta de uma cláusula de otimização pode resultar na queda do servidor, uma saída sutilmente errada, e outras coisas ruins. A cláusula de otimização sempre pode ser deixada de fora quando não há certeza sobre a mesma; a única conseqüência pode ser o comando demorar mais tempo para executar que o necessário.

Poderão ser adicionadas cláusulas de otimização nas versões futuras do PostgreSQL. As cláusulas aqui descritas são as que a versão 8.0.0 compreende.

31.13.1. COMMUTATOR

Se for fornecida a cláusula COMMUTATOR, esta informa o nome do operador que é o operador de comutação do operador sendo definido. Se diz que o operador A é o operador de comutação do operador B, se (x A y) for igual a (y B x), para todas as entradas possíveis x, y. Deve ser observado que B também é o operador de comutação de A. Por exemplo, para um determinado tipo de dado os operadores < e > geralmente são o operador de comutação um do outro, e o operador + geralmente é o operador de comutação dele mesmo. Porém, o operador - geralmente não é o operador de comutação de nenhum outro.

O tipo do operando esquerdo de um operador comutável é idêntico ao tipo do operando direito de seu operador de comutação, e vice-versa. Portanto, o nome do operador de comutação é tudo que precisa ser informado ao PostgreSQL para que este procure pelo operador de comutação, e é tudo que precisa ser informado na cláusula COMMUTATOR.

É crítico fornecer a informação sobre o operador de comutação para os operadores a serem utilizados em cláusulas de índice e de junção, porque isto permite ao otimizador de comandos "girar" a cláusula para que esta atenda a uma das formas requeridas por algum dos diferentes tipos de plano. Por exemplo, considere uma consulta com uma cláusula WHERE do tipo tab1.x = tab2.y, onde tab1.x e tab2.y são de um tipo definido pelo usuário, e suponha que tab2.y seja indexada. O otimizador não poderá gerar uma varredura de índice a menos que possa determinar como girar a cláusula para que se torne tab2.y = tab1.x, porque a maquinaria de varredura de índice espera encontrar a coluna indexada à esquerda do operador fornecido. O PostgreSQL não vai simplesmente assumir que esta é uma transformação válida — quem cria o operador = deve especificar que isto é válido, marcando o operador com a informação sobre o operador de comutação.

Quando se está definindo um operador autocomutativo é simples e direto. Agora, quando se está definindo um par de operadores de comutação a situação fica mais complicada: como é possível o primeiro operador definido fazer referência ao segundo que ainda não foi definido? Existem duas soluções para este problema:

31.13.2. NEGATOR

A cláusula NEGATOR, se estiver presente, declara o nome de um operador que é o operador de negação do operador sendo definido. Se diz que o operador A é o operador de negação do operador B se ambos retornam resultados booleanos, e (x A y) é igual a NÃO (x B y) para todas as entradas possíveis de x, y. Deve ser observado que B também é o operador de negação de A. Por exemplo, < e >= é um par de operadores de negação para a maior parte dos tipos de dado. Nunca é válido um operador ser seu próprio operador de negação.

Ao contrário dos operadores de comutação, pode ser válido existir um par de operadores unários marcados como operadores de negação um do outro; isto significa que (A x) é igual a NÃO (B x) para todo x, ou o equivalente para os operadores unários direito.

O operador de negação do operador deve possuir o mesmo tipo de dado do operando esquerdo e/ou direito do operador sendo definido. Portanto, da mesma forma que em COMMUTATOR, somente é necessário especificar o nome do operador na cláusula NEGATOR.

Fornecer o operador de negação é muito útil para o otimizador de comandos, por permitir que expressões como NOT (x = y) sejam simplificadas como x <> y. Isto ocorre com mais freqüência do que se imagina, porque podem ser inseridas operações NOT como conseqüência de outras rearrumações.

Os pares de operadores de negação podem ser definidos utilizando os mesmos métodos explicados acima para os pares de operadores de comutação.

31.13.3. RESTRICT

A cláusula RESTRICT, se estiver presente, declara o nome de uma função estimadora de seletividade de restrição para o operador (Deve ser observado que é declarado o nome da função, e não o nome do operador). A cláusula RESTRICT só faz sentido em operador binário que retorna o tipo boolean. A idéia por trás do estimador de seletividade de restrição é adivinhar a fração de linhas da tabela que satisfazem a condição da cláusula WHERE na forma

coluna OP constante

para o operador corrente e para um determinado valor constante. Isto ajuda o otimizador dando uma noção de quantas linhas serão eliminadas pela cláusula WHERE que possui esta forma (Você pode estar se perguntando o que acontece quando a constante está do lado esquerdo. Bem, é para isto que o COMMUTATOR existe...).

A escrita de funções estimadoras de seletividade de restrição está muito acima do escopo deste capítulo, mas felizmente geralmente é possível utilizar uma das funções estimadoras padrão do sistema em operadores definidos pelo usuário. Estas são as funções estimadoras de restrição padrão:

eqsel para =
neqsel para <>
scalarltsel para < ou <=
scalargtsel para > ou >=
Pode parecer um pouco estranho que estas sejam as categorias, mas faz sentido quando se pensa sobre isto. Geralmente = aceita apenas uma pequena fração das linhas da tabela; Geralmente <> rejeita somente uma pequena fração. < aceita uma fração que depende da constante fornecida estar na faixa de valores da coluna da tabela (que, caso seja verdade, a informação coletada pelo comando ANALYZE será utilizada pela função estimadora de seletividade). <= aceita uma fração um pouco maior que < para a comparação com a mesma constante, mas as frações são próximas o suficiente para não valer a pena fazer distinção entre as duas, principalmente porque não se está fazendo nada melhor que uma estimativa grosseira. Comentários semelhantes se aplicam a > e >=.

Freqüentemente pode-se utilizar eqsel ou neqsel para o caso de operadores que possuam uma seletividade muito alta ou muito baixa, mesmo que não sejam realmente uma igualdade ou desigualdade. Por exemplo, os operadores geométricos de igualdade aproximada usam eqsel assumindo que irão corresponder apenas a uma pequena fração das entradas na tabela.

Podem ser utilizados scalarltsel e scalargtsel para fazer comparação em tipos de dado onde a conversão em escalares numéricos para fazer comparação de intervalo faça sentido. Se for possível, o tipo de dado deve ser adicionado aos compreendidos pela função convert_to_scalar() no arquivo src/backend/utils/adt/selfuncs.c (Um dia esta função será substituída por funções por-tipo-de-dado identificadas através de uma coluna do catálogo do sistema pg_type; mas isto ainda não foi feito). Se não for utilizado os comandos ainda assim vão funcionar, mas as estimativas do otimizador não serão tão boas quanto poderiam ser.

Existem funções estimadoras de seletividade adicionais projetadas para operadores geométricos no arquivo src/backend/utils/adt/geo_selfuncs.c: areasel, positionsel, e contsel. Quando este texto foi escrito estas funções eram apenas stubs, mas podem ser utilizadas assim mesmo (ou melhor ainda, melhoradas).

31.13.4. JOIN

A cláusula JOIN, se estiver presente, declara o nome de uma função estimadora de seletividade de junção para o operador (Deve ser observado que é declarado o nome de uma função, e não o nome de operador). A cláusula JOIN só faz sentido em operador binário que retorna o tipo boolean. A idéia por trás do estimador de seletividade de junção é adivinhar a fração de linhas de um par de tabelas que satisfazem a condição da cláusula WHERE na forma

tabela1.coluna1 OP tabela2.coluna2

para o operador corrente. Assim como na cláusula RESTRICT, ajuda o otimizador permitindo que descubra entre várias seqüências de junção possíveis qual a que deve dar menos trabalho para ser realizada.

Da mesma maneira que antes, este capítulo não tenta explicar como escrever uma função estimadora de seletividade de junção, e apenas sugere que seja utilizada uma das funções estimadoras existentes, caso uma delas se aplique:

eqjoinsel para =
neqjoinsel para <>
scalarltjoinsel para < ou <=
scalargtjoinsel para > ou >=
areajoinsel para comparações baseadas em área 2D
positionjoinsel para comparações baseadas em posição 2D
contjoinsel para operações baseadas em contém 2D

31.13.5. HASHES

A cláusula HASHES, se estiver presente, informa ao sistema que é permitido utilizar o método de junção por hash em uma junção baseada neste operador. A cláusula HASHES só faz sentido em operador binário que retorna o tipo boolean e, na prática, o operador deve representar igualdade para algum tipo de dado.

A suposição subjacente à junção por hash é que o operador de junção só retorna verdade para pares de valores à esquerda e à direita que resultam no mesmo código de hash. Se dois valores forem colocados em receptáculos de hash diferentes, a junção nunca vai compará-los, assumindo implicitamente que o resultado do operador de junção é falso. Portanto, nunca faz sentido especificar HASHES para operadores que não representam igualdade.

Para ser marcado como HASHES o operador de junção deve estar presente em uma classe de operadores de índice hash. Isto não é exigido quando se cria o operador, uma vez que a classe de operadores que faz referência não pode existir ainda. Mas a tentativa de utilizar o operador em junções hash falham em tempo de execução quando a classe de operadores não existe. O sistema precisa da classe de operadores para encontrar a função de hash específica do tipo de dado, para o tipo de dado de entrada do operador. Obviamente, antes de ser possível criar a classe de operadores é necessário criar uma função de hash adequada.

Deve ser tomado cuidado ao preparar a função de hash, porque existem formas dependentes de máquina pelas quais a função pode deixar de funcionar corretamente. Por exemplo, quando o tipo de dado é uma estrutura onde existem bits de preenchimento que não interessam, não se pode simplesmente passar toda a estrutura para a função 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 são as máquinas que seguem o padrão IEEE para valores de ponto flutuante. Nestas máquinas zero negativo e zero positivo são valores diferentes (padrões de bit diferentes), mas definidos como sendo iguais na comparação. Se o valor de ponto flutuante puder conter zero negativo, então são necessários passos adicionais para garantir que este gera o mesmo valor de hash que o zero positivo.

Nota: A função subjacente ao operador juntável por hash deve ser marcada como imutável ou estável. Se for volátil, o sistema nunca vai tentar utilizar o operador para uma junção por hash.

Nota: Se o operador juntável por hash possuir uma função subjacente marcada como estrita, a função também deve ser completa, ou seja, a função deve retornar verdade ou falso, e nunca nulo, para quaisquer duas entradas não nulas. Se esta regra não for seguida, a otimização de hash nas operações IN podem gerar resultados errados (Especificamente, IN deve retornar falso onde a resposta correta de acordo com o padrão seria nulo; ou pode gerar um erro reclamando que não foi preparada para resultado nulo).

31.13.6. MERGES (SORT1, SORT2, LTCMP, GTCMP)

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

A junção por mesclagem se baseia na idéia de ordenar as tabelas da esquerda e da direita primeiro, para depois varre-las em paralelo. Portanto, os dois tipos de dado devem ser capaz de ser totalmente ordenados, e o operador de junção deve ser um que somente seja bem sucedido para pares de valores que "caiam no mesmo lugar" na ordem de classificação. Na prática isto significa que o operador de junção deve se comportar como igualdade, mas diferentemente da junção por hash, onde devem ser o mesmo (ou pelo menos equivalente bit a bit), é possível fazer a junção por mesclagem de dois tipos de dado distintos, desde que sejam binariamente compatíveis. Por exemplo, o operador de igualdade smallint-versus-integer é juntável por mesclagem. Somente se necessita de operadores de classificação que coloquem os dois tipos de dado em uma seqüência logicamente compatível.

A execução de uma junção por mesclagem requer que o sistema seja capaz de identificar quatro operadores relacionados com o operador de junção por mesclagem: comparação menor-que para o tipo de dado do operando à esquerda, comparação menor-que para o tipo de dado do operando à direita, comparação menor-que entre os dois tipos de dado, e comparação maior-que entre os dois tipos de dado (Na verdade são quatro operadores distintos quando o operador juntável por mesclagem possui tipos de dado dos operandos diferentes, mas quando os tipos de dado dos operandos são o mesmo os três operadores menor-que são o mesmo). É possível especificar estes operadores individualmente por nome, pelas opções SORT1, SORT2, LTCMP e GTCMP, respectivamente. O sistema preenche os nomes padrão <, <, <, >, respectivamente, quando um destes é omitido ao se especificar MERGES. Também, MERGES é assumido como implicado quando alguma destas quatro opções de operador aparece, portanto é possível especificar apenas algumas delas e deixar o sistema preencher o restante.

Os tipos de dado dos operandos dos quatro operadores de comparação podem ser deduzidos a partir dos tipos dos operandos do operador juntável por mesclagem e, portanto, da mesma maneira que em COMMUTATOR, somente é necessário fornecer o nome do operador nestas cláusulas. A menos que se esteja utilizando escolhas peculiares para os nomes dos operadores, basta escrever MERGES e deixar o sistema preencher os detalhes (Da mesma forma que em COMMUTATOR e NEGATOR, o sistema tem condição de criar entradas de operador fictícias se for definido o operador de igualdade antes dos demais).

Existem restrições adicionais para os operadores marcados como juntáveis por mesclagem. No momento estas restrições não são verificadas pelo comando CREATE OPERATOR, mas podem ocorrer erros ao se utilizar o operador quando alguma destas restrições não é atendida:

Nota: A junção subjacente ao operador juntável por mesclagem deve ser marcada como imutável ou estável. Se for volátil, o sistema nunca tentará utilizar o operador em uma junção por mesclagem.

Nota: Nas versões do PostgreSQL anteriores a 7.3, MERGES não estava disponível: para construir um operador juntável por mesclagem era necessário escrever SORT1 e SORT2 explicitamente. E, também, as opções LTCMP e GTCMP não existiam; os nomes destes operadores eram estabelecidos como < e >, respectivamente.

SourceForge.net Logo CSS válido!