Configuração
Estou desenvolvendo a configuração do @ Jack para facilitar o acompanhamento e a comparação das pessoas. Testado com PostgreSQL 9.1.4 .
CREATE TABLE lexikon (
lex_id serial PRIMARY KEY
, word text
, frequency int NOT NULL -- we'd need to do more if NULL was allowed
, lset int
);
INSERT INTO lexikon(word, frequency, lset)
SELECT 'w' || g -- shorter with just 'w'
, (1000000 / row_number() OVER (ORDER BY random()))::int
, g
FROM generate_series(1,1000000) g
A partir daqui, tomo uma rota diferente:
ANALYZE lexikon;
Mesa auxiliar
Esta solução não adiciona colunas à tabela original, apenas precisa de uma pequena tabela auxiliar. Coloquei-o no esquema public, use qualquer esquema de sua escolha.
CREATE TABLE public.lex_freq AS
WITH x AS (
SELECT DISTINCT ON (f.row_min)
f.row_min, c.row_ct, c.frequency
FROM (
SELECT frequency, sum(count(*)) OVER (ORDER BY frequency DESC) AS row_ct
FROM lexikon
GROUP BY 1
) c
JOIN ( -- list of steps in recursive search
VALUES (400),(1600),(6400),(25000),(100000),(200000),(400000),(600000),(800000)
) f(row_min) ON c.row_ct >= f.row_min -- match next greater number
ORDER BY f.row_min, c.row_ct, c.frequency DESC
)
, y AS (
SELECT DISTINCT ON (frequency)
row_min, row_ct, frequency AS freq_min
, lag(frequency) OVER (ORDER BY row_min) AS freq_max
FROM x
ORDER BY frequency, row_min
-- if one frequency spans multiple ranges, pick the lowest row_min
)
SELECT row_min, row_ct, freq_min
, CASE freq_min <= freq_max
WHEN TRUE THEN 'frequency >= ' || freq_min || ' AND frequency < ' || freq_max
WHEN FALSE THEN 'frequency = ' || freq_min
ELSE 'frequency >= ' || freq_min
END AS cond
FROM y
ORDER BY row_min;
A tabela fica assim:
row_min | row_ct | freq_min | cond
--------+---------+----------+-------------
400 | 400 | 2500 | frequency >= 2500
1600 | 1600 | 625 | frequency >= 625 AND frequency < 2500
6400 | 6410 | 156 | frequency >= 156 AND frequency < 625
25000 | 25000 | 40 | frequency >= 40 AND frequency < 156
100000 | 100000 | 10 | frequency >= 10 AND frequency < 40
200000 | 200000 | 5 | frequency >= 5 AND frequency < 10
400000 | 500000 | 2 | frequency >= 2 AND frequency < 5
600000 | 1000000 | 1 | frequency = 1
Como a coluna condserá usada no SQL dinâmico mais adiante, é necessário tornar essa tabela segura . Sempre qualifique a tabela com esquema se você não puder ter certeza de uma corrente apropriada search_pathe revogue os privilégios de gravação de public(e de qualquer outra função não confiável):
REVOKE ALL ON public.lex_freq FROM public;
GRANT SELECT ON public.lex_freq TO public;
A tabela lex_freqserve para três propósitos:
- Crie índices parciais necessários automaticamente.
- Forneça etapas para a função iterativa.
- Meta informações para ajuste.
Índices
Esta DOdeclaração cria todos os índices necessários:
DO
$$
DECLARE
_cond text;
BEGIN
FOR _cond IN
SELECT cond FROM public.lex_freq
LOOP
IF _cond LIKE 'frequency =%' THEN
EXECUTE 'CREATE INDEX ON lexikon(lset) WHERE ' || _cond;
ELSE
EXECUTE 'CREATE INDEX ON lexikon(lset, frequency DESC) WHERE ' || _cond;
END IF;
END LOOP;
END
$$
Todos esses índices parciais juntos abrangem a tabela uma vez. Eles têm o mesmo tamanho de um índice básico em toda a tabela:
SELECT pg_size_pretty(pg_relation_size('lexikon')); -- 50 MB
SELECT pg_size_pretty(pg_total_relation_size('lexikon')); -- 71 MB
Apenas 21 MB de índices para a tabela de 50 MB até agora.
Eu crio a maioria dos índices parciais (lset, frequency DESC). A segunda coluna ajuda apenas em casos especiais. Porém, como as duas colunas envolvidas são do tipo integer, 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. Aqueles estão apenas começando (lset). Os índices criados são assim:
CREATE INDEX ON lexikon(lset, frequency DESC) WHERE frequency >= 2500;
CREATE INDEX ON lexikon(lset, frequency DESC) WHERE frequency >= 625 AND frequency < 2500;
-- ...
CREATE INDEX ON lexikon(lset, frequency DESC) WHERE frequency >= 2 AND frequency < 5;
CREATE INDEX ON lexikon(lset) WHERE freqency = 1;
Função
A função é um pouco semelhante à solução @ Jack:
CREATE OR REPLACE FUNCTION f_search(_lset_min int, _lset_max int, _limit int)
RETURNS SETOF lexikon
$func$
DECLARE
_n int;
_rest int := _limit; -- init with _limit param
_cond text;
BEGIN
FOR _cond IN
SELECT l.cond FROM public.lex_freq l ORDER BY l.row_min
LOOP
-- RAISE NOTICE '_cond: %, _limit: %', _cond, _rest; -- for debugging
RETURN QUERY EXECUTE '
SELECT *
FROM public.lexikon
WHERE ' || _cond || '
AND lset >= $1
AND lset <= $2
ORDER BY frequency DESC
LIMIT $3'
USING _lset_min, _lset_max, _rest;
GET DIAGNOSTICS _n = ROW_COUNT;
_rest := _rest - _n;
EXIT WHEN _rest < 1;
END LOOP;
END
$func$ LANGUAGE plpgsql STABLE;
Principais diferenças:
SQL dinâmico com RETURN QUERY EXECUTE.
À medida que percorremos as etapas, um plano de consulta diferente pode ser beneficiário. O plano de consulta para SQL estático é gerado uma vez e depois reutilizado - o que pode economizar um pouco de sobrecarga. Mas, neste caso, a consulta é simples e os valores são muito diferentes. SQL dinâmico será uma grande vitória.
DinâmicoLIMIT para cada etapa da consulta.
Isso ajuda de várias maneiras: Primeiro, as linhas são buscadas apenas conforme necessário. Em combinação com o SQL dinâmico, isso também pode gerar planos de consulta diferentes para começar. Segundo: Não é necessário um adicional LIMITna chamada de função para reduzir o excedente.
Referência
Configuração
Escolhi quatro exemplos e fiz três testes diferentes com cada um. Tirei o melhor de cinco para comparar com o cache quente:
A consulta SQL bruta do formulário:
SELECT *
FROM lexikon
WHERE lset >= 20000
AND lset <= 30000
ORDER BY frequency DESC
LIMIT 5;
O mesmo depois de criar este índice
CREATE INDEX ON lexikon(lset);
Precisa aproximadamente do mesmo espaço que todos os meus índices parciais juntos:
SELECT pg_size_pretty(pg_total_relation_size('lexikon')) -- 93 MB
A função
SELECT * FROM f_search(20000, 30000, 5);
Resultados
SELECT * FROM f_search(20000, 30000, 5);
1: Tempo de execução total: 315,458 ms
2: Tempo de execução total: 36,458 ms
3: Tempo de execução total: 0,330 ms
SELECT * FROM f_search(60000, 65000, 100);
1: Tempo de execução total: 294,819 ms
2: Tempo de execução total: 18,915 ms
3: Tempo de execução total: 1,414 ms
SELECT * FROM f_search(10000, 70000, 100);
1: Tempo de execução total: 426,831 ms
2: Tempo de execução total: 217,874 ms
3: Tempo de execução total: 1,611 ms
SELECT * FROM f_search(1, 1000000, 5);
1: Tempo de execução total: 2458,205 ms
2: Tempo de execução total: 2458,205 ms - para grandes faixas de lset, a verificação seq é mais rápida que o índice.
3: Tempo de execução total: 0,266 ms
Conclusão
Como esperado, o benefício da função cresce com intervalos maiores lsete menores LIMIT.
Com intervalos muito pequenos delset , a consulta bruta em combinação com o índice é realmente mais rápida . Você deseja testar e talvez ramificar: consulta bruta para pequenos intervalos de lset, ou outra chamada de função. Você pode até incorporar isso na função para um "melhor dos dois mundos" - é o que eu faria.
Dependendo da distribuição dos dados e das consultas típicas, mais etapas lex_freqpodem ajudar no desempenho. Teste para encontrar o ponto ideal. Com as ferramentas apresentadas aqui, deve ser fácil testar.