11.9. Varreduras somente de índice e índices de cobertura #

Todos os índices no PostgreSQL são índices secundários, fazendo com que todo índice seja armazenado em separado da área de dados principal da tabela (que é chamada de heap (pilha) da tabela, na terminologia do PostgreSQL), significando que, em uma varredura de índice comum, cada recuperação de linha requer a busca de dados no índice, e no heap. Além disso, enquanto as entradas de índice que correspondem a uma determinada condição WHERE indexável geralmente estão próximas no índice, as linhas da tabela às quais fazem referência podem estar em qualquer lugar no heap. A parte de acesso ao heap da varredura de índice envolve muito acesso aleatório ao heap, o que pode ser lento, principalmente numa mídia rotativa tradicional. (Conforme descrito na Seção 11.5, as varreduras de bitmap tentam reduzir este custo executando acessos ao heap de maneira ordenada, mas não vai mais longe que isto.)

Para resolver este problema de desempenho, o PostgreSQL oferece suporte a varreduras somente de índice, que podem responder consultas a partir de um índice, sem nenhum acesso ao heap. A ideia básica é retornar os valores diretamente de cada entrada do índice, em vez de consultar a entrada no heap associada. Existem duas restrições fundamentais sobre quando este método pode ser usado:

  1. O tipo de índice deve oferecer suporte a varreduras somente de índice. Os índices árvore-B sempre oferecem. Os índices GiST e SP-GiST oferecem suporte a varreduras somente de índice para algumas classes de operadores, mas não para outras. Os outros tipos de índice não oferecem suporte. O requisito subjacente é que o índice deve armazenar fisicamente, ou então conseguir reconstruir, o valor original dos dados para cada entrada do índice. Como contra-exemplo, os índices GIN não podem oferecer suporte a varreduras somente de índice, porque normalmente cada entrada do índice contém apenas parte do valor dos dados originais.

  2. A consulta deve fazer referência apenas às colunas armazenadas no índice. Por exemplo, dado um índice nas colunas x e y de uma tabela, que também tem uma coluna z, essas consultas podem usar varreduras somente de índice:

    SELECT x, y FROM tab WHERE x = 'chave';
    SELECT x FROM tab WHERE x = 'chave' AND y < 42;
    

    mas essas consultas não podem:

    SELECT x, z FROM tab WHERE x = 'chave';
    SELECT x FROM tab WHERE x = 'chave' AND z < 42;
    

    (Índices de expressão e índices parciais complicam esta regra, conforme discutido abaixo.)

Se estes dois requisitos fundamentais forem atendidos, todos os valores dos dados exigidos pela consulta estarão disponíveis no índice, portanto uma varredura somente de índice será fisicamente possível. Mas há um requisito adicional para qualquer varredura de tabela no PostgreSQL: deve ser verificado se cada linha recuperada está visível para o instantâneo MVCC da consulta, conforme discutido no Capítulo 13. As informações de visibilidade não estão armazenadas nas entradas do índice, apenas nas entradas do heap; então, à primeira vista, parece que cada recuperação de linha exige um acesso ao heap de qualquer maneira. E este será realmente o caso, se a linha da tabela foi modificada recentemente. No entanto, para dados que raramente mudam, existe uma maneira de contornar este problema. O PostgreSQL rastreia, para cada página no heap de uma tabela, se todas as linhas armazenadas nessa página são antigas o suficiente para serem visíveis por todas as transações correntes e futuras. Essa informação é armazenada em um bit na tabela visibility map. Uma varredura somente de índice, após encontrar uma entrada de índice candidata, verifica o bit do mapa de visibilidade para a página de heap correspondente. Se o bit estiver definido, a linha está visível, portanto os dados podem ser retornados sem mais trabalho. Se o bit não estiver definido, a entrada do heap deverá ser acessada para descobrir se está visível, portanto nenhuma vantagem de desempenho será obtida sobre uma verificação de índice padrão. Mesmo no caso de sucesso, esta abordagem troca acessos ao mapa de visibilidade por acessos ao heap; mas como o mapa de visibilidade é quatro ordens de magnitude menor do que o heap que ele descreve, muito menos E/S física é necessária para acessá-lo. Na maioria das situações, o mapa de visibilidade permanece armazenado em cache na memória o tempo todo.

Em suma, embora a varredura somente de índice seja possível devido aos dois requisitos fundamentais, será bem sucedida apenas se uma fração significativa das páginas do heap da tabela tiver seus bits definidos no mapa de visibilidade. Porém, tabelas nas quais uma grande fração das linhas não mudam são comuns o suficiente para tornar este tipo de varredura muito útil na prática.

Para fazer uso eficaz do recurso de varredura somente de índice, pode-se optar por criar um índice de cobertura, que é um índice projetado especificamente para incluir colunas necessárias para um tipo específico de consulta executada com frequência. Como as consultas geralmente precisam recuperar colunas além daquelas em que a procura é feita, o PostgreSQL permite criar índices nos quais algumas colunas são apenas carga útil, não fazendo parte da chave de procura. Isso é feito acrescentando uma cláusula INCLUDE listando as colunas extras. Por exemplo, se é habitual executar consultas como

SELECT y FROM tab WHERE x = 'chave';

a abordagem tradicional para acelerar essas consultas seria criar um índice apenas em x. Entretanto, um índice definido como

CREATE INDEX tab_x_y ON tab(x) INCLUDE (y);

pode lidar com essas consultas como varreduras somente de índice, porque y pode ser obtido do índice sem visitar o heap.

Como a coluna y não faz parte da chave de procura do índice, ela não precisa ser de um tipo de dados que o índice possa tratar; a coluna é meramente armazenada no índice e não é interpretada pelo mecanismo do índice. Além disso, se o índice for um índice de unicidade, como este

CREATE UNIQUE INDEX tab_x_y ON tab(x) INCLUDE (y);

a condição de unicidade se aplicará apenas à coluna x, e não à combinação de x e y. (A cláusula INCLUDE também pode ser escrita nas restrições UNIQUE e PRIMARY KEY, fornecendo uma sintaxe alternativa para definir um índice desse tipo.)

É bom ser conservador ao adicionar colunas de carga útil não chave a um índice, principalmente colunas grandes. Se uma tupla do índice exceder o tamanho máximo permitido para o tipo de índice, a inserção de dados vai falhar. Em todos os casos, as colunas não chave duplicam os dados da tabela do índice e aumentam o tamanho do índice, o que pode tornar as procuras mais lentas. Lembre-se haver pouco sentido em incluir colunas de carga útil em um índice, a menos que a tabela mude lentamente o suficiente para que a varredura somente de índice provavelmente não precise acessar o heap. Se a tupla no heap deve ser acessada de qualquer maneira, não custa nada obter o valor da coluna de lá mesmo. Outras restrições são que atualmente não há suporte a expressões como colunas incluídas, e que apenas os índices árvore-B, GiST e SP-GiST fornecem suporte para colunas incluídas.

Antes do PostgreSQL disponibilizar o recurso INCLUDE, às vezes era feita a cobertura de índices escrevendo as colunas de carga útil como colunas de índice regulares, ou seja, escrevendo

CREATE INDEX tab_x_y ON tab(x, y);

mesmo não havendo intenção de usar y como parte de uma cláusula WHERE. Isso funciona bem, desde que as colunas extras sejam colunas à direita; torná-las colunas principais não é bom pelas razões explicadas na Seção 11.3. No entanto, este método não é compatível com o caso em que se deseja que o índice imponha unicidade na(s) coluna(s)-chave.

O truncamento de sufixo sempre remove as colunas não-chave dos níveis superiores da árvore-B. Assim como as colunas de carga útil, essas colunas nunca são usadas para orientar as varreduras do índice. O processo de truncamento também remove uma ou mais colunas-chave à direita, quando o restante do prefixo da(s) coluna(s)-chave é suficiente para descrever as tuplas da árvore-B de nível mais baixo. Na prática, as coberturas de índices sem uma cláusula INCLUDE evitam geralmente o armazenamento de colunas que são carga útil efetiva nos níveis superiores. No entanto, definir explicitamente colunas de carga útil como colunas não-chave mantém de forma confiável as tuplas nos níveis superiores pequenas.

A princípio, varreduras somente de índice podem ser usadas com índices de expressão. Por exemplo, dado um índice em f(x), onde x é uma coluna de tabela, deve ser possível executar

SELECT f(x) FROM tab WHERE f(x) < 1;

como varredura somente de índice; sendo muito interessante se f() for uma função com custo alto para calcular. Entretanto, o planejador do PostgreSQL atualmente não é muito perspicaz com relação a estes casos. O planejador considera uma consulta potencialmente executável por varredura somente de índice quando todas as colunas necessárias para a consulta estão disponíveis no índice. Nesse exemplo, x não é necessário, exceto no contexto f(x), mas o planejador não percebe isto e conclui que a varredura somente de índice não é possível. Se a varredura somente de índice parecer valer a pena, este problema pode ser contornado adicionando x como uma coluna incluída, por exemplo

CREATE INDEX tab_f_x ON tab (f(x)) INCLUDE (x);

Uma advertência adicional: se o objetivo é evitar recalcular f(x), o planejador não irá necessariamente corresponder os usos de f(x) que não estão em cláusulas WHERE indexáveis com a coluna do índice. Geralmente, o planejador acertará em consultas simples, como mostrado acima, mas não em consultas que envolvem junções. Essas deficiências poderão ser corrigidas em versões futuras do PostgreSQL.

Os índices parciais também têm interações interessantes com varreduras somente de índice. Considere o índice parcial mostrado no Exemplo 11.3:

CREATE UNIQUE INDEX unique_sucesso_teste_idx ON testes (assunto, resultado)
    WHERE sucesso;

A princípio, seria possível fazer a varredura somente de índice nesse índice para satisfazer uma consulta como

SELECT resultado FROM testes WHERE assunto = 'algum-assunto' AND sucesso;

Mas há um problema: a cláusula WHERE faz referência a sucesso, que não está disponível como coluna de resultado do índice. No entanto, a varredura somente de índice é possível, porque o plano não precisa verificar novamente esta parte da cláusula WHERE em tempo de execução: todas as entradas encontradas no índice têm necessariamente sucesso = verdade, então esta condição não precisa ser verificada explicitamente no plano. A versão 9.6 e posteriores do PostgreSQL reconhecem estes casos, e permitem que varreduras somente de índice ocorram, mas as versões mais antigas não permitem.