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