Fazendo esta pergunta, especificamente para Postgres, pois tem bom suporte para índices R-tree/espaciais.
Temos a seguinte tabela com uma estrutura em árvore (modelo Nested Set) de palavras e suas frequências:
lexikon
-------
_id integer PRIMARY KEY
word text
frequency integer
lset integer UNIQUE KEY
rset integer UNIQUE KEY
E a consulta:
SELECT word
FROM lexikon
WHERE lset BETWEEN @Low AND @High
ORDER BY frequency DESC
LIMIT @N
Suponho que um índice de cobertura (lset, frequency, word)
seria útil, mas sinto que pode não funcionar bem se houver muitos lset
valores no (@High, @Low)
intervalo.
Às vezes, um índice simples (frequency DESC)
também pode ser suficiente, quando uma pesquisa usando esse índice produz antecipadamente as @N
linhas que correspondem à condição de intervalo.
Mas parece que o desempenho depende muito dos valores dos parâmetros.
Existe uma maneira de torná-lo rápido, independentemente de o intervalo (@Low, @High)
ser amplo ou estreito e independentemente de as palavras de frequência superior estarem felizmente no intervalo (estreito) selecionado?
Um índice R-tree/espacial ajudaria?
Adicionar índices, reescrever a consulta, redesenhar a tabela, não há limitação.
Você pode conseguir um melhor desempenho pesquisando primeiro nas linhas com frequências mais altas. Isso pode ser obtido "granulando" as frequências e, em seguida, percorrendo-as processualmente, por exemplo, da seguinte maneira:
--testbed e
lexikon
dados fictícios:granule
análise (principalmente para informação e ajuste):função para escanear altas frequências primeiro:
resultados (tempos provavelmente devem ser tomados com uma pitada de sal, mas cada consulta é executada duas vezes para combater qualquer cache)
primeiro usando a função que escrevemos:
e, em seguida, com uma simples varredura de índice:
Dependendo de seus dados do mundo real, você provavelmente desejará variar o número de grânulos e a função usada para colocar linhas neles. A distribuição real de frequências é fundamental aqui, assim como os valores esperados para a
limit
cláusula e o tamanho doslset
intervalos procurados.Configurar
Estou desenvolvendo a configuração de @Jack para facilitar o acompanhamento e a comparação das pessoas. Testado com PostgreSQL 9.1.4 .
A partir daqui sigo um caminho diferente:
mesa auxiliar
Esta solução não adiciona colunas à tabela original, apenas precisa de uma pequena tabela auxiliar. Coloquei no schema
public
, use qualquer schema de sua preferência.A tabela fica assim:
Como a coluna
cond
será usada no SQL dinâmico mais abaixo, você deve tornar esta tabela segura . Sempre qualifique o esquema da tabela se não puder ter certeza de um current apropriadosearch_path
e revogue os privilégios de gravação depublic
(e qualquer outra função não confiável):A tabela
lex_freq
serve a três propósitos:Índices
Esta
DO
instrução cria todos os índices necessários:Todos esses índices parciais juntos abrangem a tabela uma vez. Eles têm aproximadamente o mesmo tamanho de um índice básico em toda a tabela:
Apenas 21 MB de índices para tabela de 50 MB até agora.
Eu crio a maioria dos índices parciais em arquivos
(lset, frequency DESC)
. A segunda coluna só ajuda em casos especiais. Mas como as duas colunas envolvidas são do tipointeger
, devido às especificidades do alinhamento de dados em combinação com MAXALIGN no PostgreSQL, a segunda coluna não aumenta o índice. É uma pequena vitória por quase nenhum custo.Não faz sentido fazer isso para índices parciais que abrangem apenas uma única frequência. Esses são apenas
(lset)
. Os índices criados têm a seguinte aparência:Função
A função é um pouco semelhante em estilo à solução de @Jack:
Principais diferenças:
SQL dinâmico com
RETURN QUERY EXECUTE
.À medida que percorremos as etapas, um plano de consulta diferente pode ser o beneficiário. O plano de consulta para SQL estático é gerado uma vez e reutilizado - o que pode economizar alguma sobrecarga. Mas neste caso a consulta é simples e os valores são bem diferentes. O SQL dinâmico será uma grande vitória.
Dynamic
LIMIT
for every query step.This helps in multiple ways: First, rows are only fetched as needed. In combination with dynamic SQL this may also generate different query plans to begin with. Second: No need for an additional
LIMIT
in the function call to trim the surplus.Benchmark
Setup
I picked four examples and ran three different tests with each. I took the best of five to compare with warm cache:
The raw SQL query of the form:
The same after creating this index
Needs about the same space as all my partial indexes together:
The function
Results
1: Total runtime: 315.458 ms
2: Total runtime: 36.458 ms
3: Total runtime: 0.330 ms
1: Total runtime: 294.819 ms
2: Total runtime: 18.915 ms
3: Total runtime: 1.414 ms
1: Total runtime: 426.831 ms
2: Total runtime: 217.874 ms
3: Total runtime: 1.611 ms
1: Total runtime: 2458.205 ms
2: Total runtime: 2458.205 ms -- for large ranges of lset, seq scan is faster than index.
3: Total runtime: 0.266 ms
Conclusion
As expected, the benefit from the function grows with bigger ranges of
lset
and smallerLIMIT
.With very small ranges of
lset
, the raw query in combination with the index is actually faster. You'll want to test and maybe branch: raw query for small ranges oflset
, else function call. You could even just build that into the function for a "best of both worlds" - that's what I would do.Depending on your data distribution and typical queries, more steps in
lex_freq
may help performance. Test to find the sweet spot. With the tools presented here, it should be easy to test.Não vejo razão para incluir a palavra coluna no índice. Então este índice
fará com que sua consulta seja executada rapidamente.
UPD
Atualmente não há maneiras de criar um índice de cobertura no PostgreSQL. Houve uma discussão sobre esse recurso na lista de discussão do PostgreSQL http://archives.postgresql.org/pgsql-performance/2012-06/msg00114.php
Using a GIST index
It depends on what you mean when you fast: you obviously have to visit every row in the range because your query is
ORDER freq DESC
. Shy of that the query planner already covers this if I understand the question,Here we create a table with 10k rows of
(5::int,random()::double precision)
We index it,
We query it,
We get a
Seq Scan on t
. This is simply because our selectivity estimates let pg conclude heap access is faster than scanning an index and rechecking. So we make it more juicy by inserting 1,000,000 more rows of(42::int,random()::double precision)
that don't fit our "range".And then we requery,
You can see here we complete in 4.6 MS with an Index Only Scan,
Expanding the range to include the whole table, produces another seq scan -- logically, and growing it with another billion rows would produce another Index Scan.
So in summary,