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 cond
verrà 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_path
e 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_freq
tre scopi:
- Crea automaticamente gli indici parziali necessari .
- Fornire passaggi per la funzione iterativa.
- Meta informazioni per la messa a punto.
indici
Questa DO
affermazione 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 LIMIT
nella 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 lset
e 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_freq
possono migliorare le prestazioni. Prova per trovare il punto giusto. Con gli strumenti qui presentati, dovrebbe essere facile testare.