L'indice spaziale può aiutare una query "intervallo - ordina per - limite"


29

Porre questa domanda, in particolare per Postgres, in quanto ha un buon supporto per gli indici R-tree / spaziali.

Abbiamo la seguente tabella con una struttura ad albero (modello di set nidificato) di parole e le loro frequenze:

lexikon
-------
_id   integer  PRIMARY KEY
word  text
frequency integer
lset  integer  UNIQUE KEY
rset  integer  UNIQUE KEY

E la query:

SELECT word
FROM lexikon
WHERE lset BETWEEN @Low AND @High
ORDER BY frequency DESC
LIMIT @N

Suppongo che un indice di copertura (lset, frequency, word)sia utile, ma penso che potrebbe non funzionare bene se ci sono troppi lsetvalori (@High, @Low)nell'intervallo.

A (frequency DESC)volte può anche essere sufficiente un semplice indice quando una ricerca che utilizza quell'indice produce in anticipo le @Nrighe che corrispondono alla condizione dell'intervallo.

Ma sembra che le prestazioni dipendono molto dai valori dei parametri.

C'è un modo per farlo funzionare velocemente, indipendentemente dal fatto che l'intervallo (@Low, @High)sia ampio o stretto e indipendentemente dal fatto che le parole della frequenza superiore siano fortunatamente nell'intervallo (stretto) selezionato?

Un indice R / albero spaziale sarebbe d'aiuto?

Aggiunta di indici, riscrittura della query, riprogettazione della tabella, non vi sono limiti.


3
Gli indici di copertura sono introdotti con 9.2 (ora beta), tra l'altro. Le persone di PostgreSQL parlano di scansioni solo indice . Vedi questa risposta correlata: dba.stackexchange.com/a/7541/3684 e la pagina Wiki PostgreSQL
Erwin Brandstetter

Due domande: (1) Che tipo di modello di utilizzo ti aspetti per la tabella? Esistono principalmente letture o aggiornamenti frequenti (soprattutto delle variabili set nidificate)? (2) Esiste una connessione tra le variabili intere del set nidificato lset e rset e la parola della variabile di testo?
jp

@jug: legge principalmente. Nessuna connessione tra il lset,rsete word.
ypercubeᵀᴹ

3
Se avessi molti aggiornamenti, il modello di set nidificato sarebbe una cattiva scelta rispetto alle prestazioni (se hai accesso al libro "L'arte di SQL", dai un'occhiata al capitolo sui modelli gerachici). Tuttavia, il problema principale è simile alla ricerca dei valori massimo / massimo (di una variabile indipendente) su un intervallo, per il quale è difficile progettare un metodo di indicizzazione. Per quanto ne so, la corrispondenza più vicina all'indice di cui hai bisogno è il modulo Knngist, ma dovresti modificarlo per adattarlo alle tue esigenze. È improbabile che un indice spaziale sia utile.
jp

Risposte:


30

Potresti essere in grado di ottenere prestazioni migliori cercando prima nelle righe con frequenze più alte. Ciò può essere ottenuto "granulando" le frequenze e quindi procedendo attraverso di esse proceduralmente, ad esempio come segue:

- lexikondati testati e fittizi:

begin;
set role dba;
create role stack;
grant stack to dba;
create schema authorization stack;
set role stack;
--
create table lexikon( _id serial, 
                      word text, 
                      frequency integer, 
                      lset integer, 
                      width_granule integer);
--
insert into lexikon(word, frequency, lset) 
select word, (1000000/row_number() over(order by random()))::integer as frequency, lset
from (select 'word'||generate_series(1,1000000) word, generate_series(1,1000000) lset) z;
--
update lexikon set width_granule=ln(frequency)::integer;
--
create index on lexikon(width_granule, lset);
create index on lexikon(lset);
-- the second index is not used with the function but is added to make the timings 'fair'

granule analisi (principalmente per informazione e messa a punto):

create table granule as 
select width_granule, count(*) as freq, 
       min(frequency) as granule_start, max(frequency) as granule_end 
from lexikon group by width_granule;
--
select * from granule order by 1;
/*
 width_granule |  freq  | granule_start | granule_end
---------------+--------+---------------+-------------
             0 | 500000 |             1 |           1
             1 | 300000 |             2 |           4
             2 | 123077 |             5 |          12
             3 |  47512 |            13 |          33
             4 |  18422 |            34 |          90
             5 |   6908 |            91 |         244
             6 |   2580 |           245 |         665
             7 |    949 |           666 |        1808
             8 |    349 |          1811 |        4901
             9 |    129 |          4926 |       13333
            10 |     47 |         13513 |       35714
            11 |     17 |         37037 |       90909
            12 |      7 |        100000 |      250000
            13 |      2 |        333333 |      500000
            14 |      1 |       1000000 |     1000000
*/
alter table granule drop column freq;
--

funzione per la scansione delle alte frequenze prima:

create function f(p_lset_low in integer, p_lset_high in integer, p_limit in integer)
       returns setof lexikon language plpgsql set search_path to 'stack' as $$
declare
  m integer;
  n integer := 0;
  r record;
begin 
  for r in (select width_granule from granule order by width_granule desc) loop
    return query( select * 
                  from lexikon 
                  where width_granule=r.width_granule 
                        and lset>=p_lset_low and lset<=p_lset_high );
    get diagnostics m = row_count;
    n = n+m;
    exit when n>=p_limit;
  end loop;
end;$$;

risultati (i tempi dovrebbero probabilmente essere presi con un pizzico di sale ma ogni query viene eseguita due volte per contrastare qualsiasi cache)

prima usando la funzione che abbiamo scritto:

\timing on
--
select * from f(20000, 30000, 5) order by frequency desc limit 5;
/*
 _id |   word    | frequency | lset  | width_granule
-----+-----------+-----------+-------+---------------
 141 | word23237 |      7092 | 23237 |             9
 246 | word25112 |      4065 | 25112 |             8
 275 | word23825 |      3636 | 23825 |             8
 409 | word28660 |      2444 | 28660 |             8
 418 | word29923 |      2392 | 29923 |             8
Time: 80.452 ms
*/
select * from f(20000, 30000, 5) order by frequency desc limit 5;
/*
 _id |   word    | frequency | lset  | width_granule
-----+-----------+-----------+-------+---------------
 141 | word23237 |      7092 | 23237 |             9
 246 | word25112 |      4065 | 25112 |             8
 275 | word23825 |      3636 | 23825 |             8
 409 | word28660 |      2444 | 28660 |             8
 418 | word29923 |      2392 | 29923 |             8
Time: 0.510 ms
*/

e quindi con una semplice scansione dell'indice:

select * from lexikon where lset between 20000 and 30000 order by frequency desc limit 5;
/*
 _id |   word    | frequency | lset  | width_granule
-----+-----------+-----------+-------+---------------
 141 | word23237 |      7092 | 23237 |             9
 246 | word25112 |      4065 | 25112 |             8
 275 | word23825 |      3636 | 23825 |             8
 409 | word28660 |      2444 | 28660 |             8
 418 | word29923 |      2392 | 29923 |             8
Time: 218.897 ms
*/
select * from lexikon where lset between 20000 and 30000 order by frequency desc limit 5;
/*
 _id |   word    | frequency | lset  | width_granule
-----+-----------+-----------+-------+---------------
 141 | word23237 |      7092 | 23237 |             9
 246 | word25112 |      4065 | 25112 |             8
 275 | word23825 |      3636 | 23825 |             8
 409 | word28660 |      2444 | 28660 |             8
 418 | word29923 |      2392 | 29923 |             8
Time: 51.250 ms
*/
\timing off
--
rollback;

A seconda dei dati del mondo reale, probabilmente vorrai variare il numero di granuli e la funzione utilizzata per inserire le righe in essi. La distribuzione effettiva delle frequenze è la chiave qui, così come i valori attesi per la limitclausola e la dimensione degli lsetintervalli cercati.


Perché c'è un gap a partire da width_granule=8tra granulae_starte granulae_enddel livello precedente?
vyegorov,

@vyegorov perché non ci sono valori 1809 e 1810? Questi sono dati generati casualmente quindi YMMV :)
Jack Douglas

Hm, sembra che non abbia nulla a che fare con la casualità, ma piuttosto con il modo in cui frequencyviene generato: un grande divario tra 1e6 / 2 e 1e6 / 3, il numero di riga più alto diventa, il divario più piccolo è. Comunque, grazie per questo fantastico approccio !!
vyegorov,

@vyegorov scusa, si, hai ragione. Assicurati di dare un'occhiata ai miglioramenti di Erwins se non l'hai già fatto!
Jack Douglas,

23

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:

  1. La query SQL non elaborata del modulo:

    SELECT * 
    FROM   lexikon 
    WHERE  lset >= 20000
    AND    lset <= 30000
    ORDER  BY frequency DESC
    LIMIT  5;
  2. 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
  3. 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.


1

Non vedo alcun motivo per includere la colonna di parole nell'indice. Quindi questo indice

CREATE INDEX lexikon_lset_frequency ON lexicon (lset, frequency DESC)

renderà veloce la tua query.

UPD

Attualmente non ci sono modi per creare un indice di copertura in PostgreSQL. C'è stata una discussione su questa funzione nella mailing list di PostgreSQL http://archives.postgresql.org/pgsql-performance/2012-06/msg00114.php


1
È stato incluso per rendere l'indice "coprente".
ypercubeᵀᴹ

Ma non cercando quel termine nell'albero decisionale della query, sei sicuro che l'indice di copertura stia aiutando qui?
jcolebrand

Va bene, vedo ora. Attualmente non ci sono modi per creare un indice di copertura in PostgreSQL. C'è stata una discussione su questa funzione nella mailing list archives.postgresql.org/pgsql-performance/2012-06/msg00114.php .
grayhemp

A proposito di "Copertura degli indici" in PostgreSQL vedi anche il commento di Erwin Brandstetter alla domanda.
jp

1

Utilizzando un indice GIST

C'è un modo per farlo funzionare velocemente, indipendentemente dal fatto che l'intervallo (@Low, @High) sia ampio o stretto e indipendentemente dal fatto che le parole della frequenza superiore siano fortunatamente nell'intervallo (stretto) selezionato?

Dipende da cosa intendi quando digiuni: ovviamente devi visitare ogni riga dell'intervallo perché la tua query lo è ORDER freq DESC. Timido che il pianificatore di query copre già questo se capisco la domanda,

Qui creiamo una tabella con 10k righe di (5::int,random()::double precision)

CREATE EXTENSION IF NOT EXISTS btree_gin;
CREATE TABLE t AS
  SELECT 5::int AS foo, random() AS bar
  FROM generate_series(1,1e4) AS gs(x);

Lo indicizziamo,

CREATE INDEX ON t USING gist (foo, bar);
ANALYZE t;

Lo interroghiamo,

EXPLAIN ANALYZE
SELECT *
FROM t
WHERE foo BETWEEN 1 AND 6
ORDER BY bar DESC
FETCH FIRST ROW ONLY;

Abbiamo un Seq Scan on t. Questo semplicemente perché le nostre stime di selettività consentono a pg di concludere che l'accesso all'heap è più veloce rispetto alla scansione di un indice e al nuovo controllo. Quindi lo rendiamo più succoso inserendo 1.000.000 di righe in più (42::int,random()::double precision)che non rientrano nella nostra "gamma".

INSERT INTO t(foo,bar)
SELECT 42::int, x
FROM generate_series(1,1e6) AS gs(x);

VACUUM ANALYZE t;

E quindi richiediamo,

EXPLAIN ANALYZE
SELECT *
FROM t
WHERE foo BETWEEN 1 AND 6
ORDER BY bar DESC
FETCH FIRST ROW ONLY;

Puoi vedere qui che completiamo in 4.6 MS con una scansione solo indice ,

                                                                 QUERY PLAN                                                                  
---------------------------------------------------------------------------------------------------------------------------------------------
 Limit  (cost=617.64..617.64 rows=1 width=12) (actual time=4.652..4.652 rows=1 loops=1)
   ->  Sort  (cost=617.64..642.97 rows=10134 width=12) (actual time=4.651..4.651 rows=1 loops=1)
         Sort Key: bar DESC
         Sort Method: top-N heapsort  Memory: 25kB
         ->  Index Only Scan using t_foo_bar_idx on t  (cost=0.29..566.97 rows=10134 width=12) (actual time=0.123..3.623 rows=10000 loops=1)
               Index Cond: ((foo >= 1) AND (foo <= 6))
               Heap Fetches: 0
 Planning time: 0.144 ms
 Execution time: 4.678 ms
(9 rows)

L'espansione dell'intervallo per includere l'intera tabella, produce un'altra scansione seq - logicamente, e aumentarla con un altro miliardo di righe produrrebbe un'altra scansione dell'indice.

Quindi in sintesi,

  • Funzionerà rapidamente, per la quantità di dati.
  • Veloce è relativo all'alternativa, se l'intervallo non è abbastanza selettivo una scansione sequenziale potrebbe essere la più veloce possibile.
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.