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 cond
será 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_path
e 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_freq
serve 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 DO
declaraçã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 LIMIT
na 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 lset
e 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_freq
podem ajudar no desempenho. Teste para encontrar o ponto ideal. Com as ferramentas apresentadas aqui, deve ser fácil testar.