AskOverflow.Dev

AskOverflow.Dev Logo AskOverflow.Dev Logo

AskOverflow.Dev Navigation

  • Início
  • system&network
  • Ubuntu
  • Unix
  • DBA
  • Computer
  • Coding
  • LangChain

Mobile menu

Close
  • Início
  • system&network
    • Recentes
    • Highest score
    • tags
  • Ubuntu
    • Recentes
    • Highest score
    • tags
  • Unix
    • Recentes
    • tags
  • DBA
    • Recentes
    • tags
  • Computer
    • Recentes
    • tags
  • Coding
    • Recentes
    • tags
Início / dba / Perguntas / 18300
Accepted
ypercubeᵀᴹ
ypercubeᵀᴹ
Asked: 2012-05-23 13:19:38 +0800 CST2012-05-23 13:19:38 +0800 CST 2012-05-23 13:19:38 +0800 CST

O índice espacial pode ajudar uma consulta "range - order by - limit"

  • 772

Fazendo esta pergunta, especificamente para Postgres, pois tem bom suporte para índices R-tree/espaciais.

Temos a seguinte tabela com uma estrutura em árvore (modelo Nested Set) de palavras e suas frequências:

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

E a consulta:

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

Suponho que um índice de cobertura (lset, frequency, word)seria útil, mas sinto que pode não funcionar bem se houver muitos lsetvalores no (@High, @Low)intervalo.

Às vezes, um índice simples (frequency DESC)também pode ser suficiente, quando uma pesquisa usando esse índice produz antecipadamente as @Nlinhas que correspondem à condição de intervalo.

Mas parece que o desempenho depende muito dos valores dos parâmetros.

Existe uma maneira de torná-lo rápido, independentemente de o intervalo (@Low, @High)ser amplo ou estreito e independentemente de as palavras de frequência superior estarem felizmente no intervalo (estreito) selecionado?

Um índice R-tree/espacial ajudaria?

Adicionar índices, reescrever a consulta, redesenhar a tabela, não há limitação.

database-design postgresql
  • 4 4 respostas
  • 5079 Views

4 respostas

  • Voted
  1. Best Answer
    Jack Douglas
    2012-08-11T06:21:45+08:002012-08-11T06:21:45+08:00

    Você pode conseguir um melhor desempenho pesquisando primeiro nas linhas com frequências mais altas. Isso pode ser obtido "granulando" as frequências e, em seguida, percorrendo-as processualmente, por exemplo, da seguinte maneira:

    --testbed e lexikondados fictícios:

    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'
    

    granuleanálise (principalmente para informação e ajuste):

    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;
    --
    

    função para escanear altas frequências primeiro:

    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;$$;
    

    resultados (tempos provavelmente devem ser tomados com uma pitada de sal, mas cada consulta é executada duas vezes para combater qualquer cache)

    primeiro usando a função que escrevemos:

    \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, em seguida, com uma simples varredura de índice:

    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;
    

    Dependendo de seus dados do mundo real, você provavelmente desejará variar o número de grânulos e a função usada para colocar linhas neles. A distribuição real de frequências é fundamental aqui, assim como os valores esperados para a limitcláusula e o tamanho dos lsetintervalos procurados.

    • 31
  2. Erwin Brandstetter
    2012-08-15T19:36:39+08:002012-08-15T19:36:39+08:00

    Configurar

    Estou desenvolvendo a configuração de @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 sigo um caminho diferente:

    ANALYZE lexikon;
    

    mesa auxiliar

    Esta solução não adiciona colunas à tabela original, apenas precisa de uma pequena tabela auxiliar. Coloquei no schema public, use qualquer schema de sua preferência.

    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 abaixo, você deve tornar esta tabela segura . Sempre qualifique o esquema da tabela se não puder ter certeza de um current apropriado search_pathe revogue os privilégios de gravação de public(e 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 a três propósitos:

    • Crie índices parciais necessários automaticamente.
    • Forneça etapas para a função iterativa.
    • Metainformações para ajuste.

    Índices

    Esta DOinstruçã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 aproximadamente 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 tabela de 50 MB até agora.

    Eu crio a maioria dos índices parciais em arquivos (lset, frequency DESC). A segunda coluna só ajuda em casos especiais. Mas 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. Esses são apenas (lset). Os índices criados têm a seguinte aparência:

    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 em estilo à solução de @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 o beneficiário. O plano de consulta para SQL estático é gerado uma vez e reutilizado - o que pode economizar alguma sobrecarga. Mas neste caso a consulta é simples e os valores são bem diferentes. O SQL dinâmico será uma grande vitória.

    • Dynamic LIMIT for every query step.
      This helps in multiple ways: First, rows are only fetched as needed. In combination with dynamic SQL this may also generate different query plans to begin with. Second: No need for an additional LIMIT in the function call to trim the surplus.

    Benchmark

    Setup

    I picked four examples and ran three different tests with each. I took the best of five to compare with warm cache:

    1. The raw SQL query of the form:

      SELECT * 
      FROM   lexikon 
      WHERE  lset >= 20000
      AND    lset <= 30000
      ORDER  BY frequency DESC
      LIMIT  5;
      
    2. The same after creating this index

      CREATE INDEX ON lexikon(lset);
      

      Needs about the same space as all my partial indexes together:

      SELECT pg_size_pretty(pg_total_relation_size('lexikon')) -- 93 MB
      
    3. The function

      SELECT * FROM f_search(20000, 30000, 5);
      

    Results

    SELECT * FROM f_search(20000, 30000, 5);

    1: Total runtime: 315.458 ms
    2: Total runtime: 36.458 ms
    3: Total runtime: 0.330 ms

    SELECT * FROM f_search(60000, 65000, 100);

    1: Total runtime: 294.819 ms
    2: Total runtime: 18.915 ms
    3: Total runtime: 1.414 ms

    SELECT * FROM f_search(10000, 70000, 100);

    1: Total runtime: 426.831 ms
    2: Total runtime: 217.874 ms
    3: Total runtime: 1.611 ms

    SELECT * FROM f_search(1, 1000000, 5);

    1: Total runtime: 2458.205 ms
    2: Total runtime: 2458.205 ms -- for large ranges of lset, seq scan is faster than index.
    3: Total runtime: 0.266 ms

    Conclusion

    As expected, the benefit from the function grows with bigger ranges of lset and smaller LIMIT.

    With very small ranges of lset, the raw query in combination with the index is actually faster. You'll want to test and maybe branch: raw query for small ranges of lset, else function call. You could even just build that into the function for a "best of both worlds" - that's what I would do.

    Depending on your data distribution and typical queries, more steps in lex_freq may help performance. Test to find the sweet spot. With the tools presented here, it should be easy to test.

    • 23
  3. grayhemp
    2012-08-08T02:59:11+08:002012-08-08T02:59:11+08:00

    Não vejo razão para incluir a palavra coluna no índice. Então este índice

    CREATE INDEX lexikon_lset_frequency ON lexicon (lset, frequency DESC)
    

    fará com que sua consulta seja executada rapidamente.

    UPD

    Atualmente não há maneiras de criar um índice de cobertura no PostgreSQL. Houve uma discussão sobre esse recurso na lista de discussão do PostgreSQL http://archives.postgresql.org/pgsql-performance/2012-06/msg00114.php

    • 1
  4. Evan Carroll
    2018-06-13T10:40:40+08:002018-06-13T10:40:40+08:00

    Using a GIST index

    Is there a way to make it perform fast, regardless of whether the range (@Low, @High) is wide or narrow and regardless of whether the top frequency words are luckily in the (narrow) selected range?

    It depends on what you mean when you fast: you obviously have to visit every row in the range because your query is ORDER freq DESC. Shy of that the query planner already covers this if I understand the question,

    Here we create a table with 10k rows of (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);
    

    We index it,

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

    We query it,

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

    We get a Seq Scan on t. This is simply because our selectivity estimates let pg conclude heap access is faster than scanning an index and rechecking. So we make it more juicy by inserting 1,000,000 more rows of (42::int,random()::double precision) that don't fit our "range".

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

    And then we requery,

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

    You can see here we complete in 4.6 MS with an Index Only Scan,

                                                                     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)
    

    Expanding the range to include the whole table, produces another seq scan -- logically, and growing it with another billion rows would produce another Index Scan.

    So in summary,

    • It'll perform fast, for the quantity of data.
    • Fast is relative to the alternative, if the range isn't selective enough a sequential scan may be as fast as you can get.
    • 1

relate perguntas

  • Práticas recomendadas para executar a replicação atrasada do deslocamento de tempo

  • Os procedimentos armazenados impedem a injeção de SQL?

  • Quais são algumas maneiras de implementar um relacionamento muitos-para-muitos em um data warehouse?

  • Sequências Biológicas do UniProt no PostgreSQL

  • Qual é a diferença entre a replicação do PostgreSQL 9.0 e o Slony-I?

Sidebar

Stats

  • Perguntas 205573
  • respostas 270741
  • best respostas 135370
  • utilizador 68524
  • Highest score
  • respostas
  • Marko Smith

    Como ver a lista de bancos de dados no Oracle?

    • 8 respostas
  • Marko Smith

    Quão grande deve ser o mysql innodb_buffer_pool_size?

    • 4 respostas
  • Marko Smith

    Listar todas as colunas de uma tabela especificada

    • 5 respostas
  • Marko Smith

    restaurar a tabela do arquivo .frm e .ibd?

    • 10 respostas
  • Marko Smith

    Como usar o sqlplus para se conectar a um banco de dados Oracle localizado em outro host sem modificar meu próprio tnsnames.ora

    • 4 respostas
  • Marko Smith

    Como você mysqldump tabela (s) específica (s)?

    • 4 respostas
  • Marko Smith

    Como selecionar a primeira linha de cada grupo?

    • 6 respostas
  • Marko Smith

    Listar os privilégios do banco de dados usando o psql

    • 10 respostas
  • Marko Smith

    Como inserir valores em uma tabela de uma consulta de seleção no PostgreSQL?

    • 4 respostas
  • Marko Smith

    Como faço para listar todos os bancos de dados e tabelas usando o psql?

    • 7 respostas
  • Martin Hope
    Mike Walsh Por que o log de transações continua crescendo ou fica sem espaço? 2012-12-05 18:11:22 +0800 CST
  • Martin Hope
    Stephane Rolland Listar todas as colunas de uma tabela especificada 2012-08-14 04:44:44 +0800 CST
  • Martin Hope
    haxney O MySQL pode realizar consultas razoavelmente em bilhões de linhas? 2012-07-03 11:36:13 +0800 CST
  • Martin Hope
    qazwsx Como posso monitorar o andamento de uma importação de um arquivo .sql grande? 2012-05-03 08:54:41 +0800 CST
  • Martin Hope
    markdorison Como você mysqldump tabela (s) específica (s)? 2011-12-17 12:39:37 +0800 CST
  • Martin Hope
    pedrosanta Listar os privilégios do banco de dados usando o psql 2011-08-04 11:01:21 +0800 CST
  • Martin Hope
    Jonas Como posso cronometrar consultas SQL usando psql? 2011-06-04 02:22:54 +0800 CST
  • Martin Hope
    Jonas Como inserir valores em uma tabela de uma consulta de seleção no PostgreSQL? 2011-05-28 00:33:05 +0800 CST
  • Martin Hope
    Jonas Como faço para listar todos os bancos de dados e tabelas usando o psql? 2011-02-18 00:45:49 +0800 CST
  • Martin Hope
    bernd_k Quando devo usar uma restrição exclusiva em vez de um índice exclusivo? 2011-01-05 02:32:27 +0800 CST

Hot tag

sql-server mysql postgresql sql-server-2014 sql-server-2016 oracle sql-server-2008 database-design query-performance sql-server-2017

Explore

  • Início
  • Perguntas
    • Recentes
    • Highest score
  • tag
  • help

Footer

AskOverflow.Dev

About Us

  • About Us
  • Contact Us

Legal Stuff

  • Privacy Policy

Language

  • Pt
  • Server
  • Unix

© 2023 AskOverflow.DEV All Rights Reserve