Impostare
Sto costruendo sulla configurazione di @ Jack per rendere le persone più facili da seguire e confrontare. Testato con 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
Da qui prendo un percorso diverso:
ANALYZE lexikon;
Tabella ausiliaria
Questa soluzione non aggiunge colonne alla tabella originale, ma solo una piccola tabella di supporto. L'ho inserito nello schema public, utilizza qualsiasi schema di tua scelta.
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;
La tabella è simile alla seguente:
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
Poiché la colonna condverrà utilizzata in SQL dinamico più in basso, è necessario rendere sicura questa tabella . Qualificare sempre la tabella in base allo schema se non si è certi della corrente appropriata search_pathe revocare i privilegi di scrittura da public(e qualsiasi altro ruolo non attendibile):
REVOKE ALL ON public.lex_freq FROM public;
GRANT SELECT ON public.lex_freq TO public;
La tabella ha lex_freqtre scopi:
- Crea automaticamente gli indici parziali necessari .
- Fornire passaggi per la funzione iterativa.
- Meta informazioni per la messa a punto.
indici
Questa DOaffermazione crea tutti gli indici necessari:
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
$$
Tutti questi indici parziali insieme abbracciano la tabella una volta. Hanno circa le stesse dimensioni di un indice di base sull'intera tabella:
SELECT pg_size_pretty(pg_relation_size('lexikon')); -- 50 MB
SELECT pg_size_pretty(pg_total_relation_size('lexikon')); -- 71 MB
Finora solo 21 MB di indici per una tabella di 50 MB.
Creo la maggior parte degli indici parziali su (lset, frequency DESC). La seconda colonna aiuta solo in casi speciali. Ma poiché entrambe le colonne coinvolte sono di tipo integer, a causa delle specifiche dell'allineamento dei dati in combinazione con MAXALIGN in PostgreSQL, la seconda colonna non ingrandisce l'indice. È una piccola vittoria per quasi nessun costo.
Non ha senso farlo per indici parziali che si estendono su una sola frequenza. Quelli sono appena accesi (lset). Gli indici creati si presentano così:
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;
Funzione
La funzione è in qualche modo simile alla soluzione di @ 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;
Differenze chiave:
SQL dinamico con RETURN QUERY EXECUTE.
Mentre eseguiamo i passaggi, è possibile che un piano di query diverso sia un beneficiario. Il piano di query per SQL statico viene generato una volta e quindi riutilizzato, il che può salvare un certo sovraccarico. Ma in questo caso la query è semplice e i valori sono molto diversi. Dynamic SQL sarà una grande vittoria.
DinamicoLIMIT per ogni passaggio della query.
Questo aiuta in molti modi: in primo luogo, le righe vengono recuperate solo se necessario. In combinazione con SQL dinamico, ciò può anche generare diversi piani di query. Secondo: non è necessario un ulteriore LIMITnella chiamata di funzione per tagliare l'eccedenza.
segno di riferimento
Impostare
Ho scelto quattro esempi e ho eseguito tre diversi test con ciascuno. Ho preso il meglio dei cinque per confrontare con warm cache:
La query SQL non elaborata del modulo:
SELECT *
FROM lexikon
WHERE lset >= 20000
AND lset <= 30000
ORDER BY frequency DESC
LIMIT 5;
Lo stesso dopo aver creato questo indice
CREATE INDEX ON lexikon(lset);
Ha bisogno dello stesso spazio di tutti i miei indici parziali insieme:
SELECT pg_size_pretty(pg_total_relation_size('lexikon')) -- 93 MB
La funzione
SELECT * FROM f_search(20000, 30000, 5);
risultati
SELECT * FROM f_search(20000, 30000, 5);
1: durata totale: 315.458 ms
2: durata totale: 36.458 ms
3: durata totale: 0.330 ms
SELECT * FROM f_search(60000, 65000, 100);
1: durata totale: 294.819 ms
2: durata totale: 18.915 ms
3: durata totale: 1.414 ms
SELECT * FROM f_search(10000, 70000, 100);
1: durata totale: 426.831 ms
2: durata totale: 217.874 ms
3: durata totale: 1.611 ms
SELECT * FROM f_search(1, 1000000, 5);
1: Runtime totale: 2458.205 ms
2: Runtime totale: 2458.205 ms - per ampie gamme di lset, la scansione seq è più veloce dell'indice.
3: durata totale: 0,266 ms
Conclusione
Come previsto, il vantaggio della funzione aumenta con intervalli maggiori lsete minori LIMIT.
Con intervalli molto piccoli dilset , la query non elaborata in combinazione con l'indice è effettivamente più veloce . Avrai voglia di testare e forse diramare: query non elaborata per piccoli intervalli di lset, altrimenti chiamata di funzione. Potresti anche solo incorporarlo nella funzione per un "migliore dei due mondi" - questo è quello che vorrei fare.
A seconda della distribuzione dei dati e delle query tipiche, ulteriori passaggi lex_freqpossono migliorare le prestazioni. Prova per trovare il punto giusto. Con gli strumenti qui presentati, dovrebbe essere facile testare.