空间索引可以帮助“范围-限制范围”查询吗


29

问这个问题,特别是对Postgres,因为它对R树/空间索引有很好的支持。

下表具有单词及其频率的树结构(嵌套集模型):

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

和查询:

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

我认为覆盖索引(lset, frequency, word)会很有用,但如果范围内的lset值过多,我可能会认为效果不佳(@High, @Low)

(frequency DESC)当使用该索引的搜索提早产生@N与范围条件匹配的行时,使用简单的索引有时也足够了。

但是,性能似乎在很大程度上取决于参数值。

有没有一种方法可以使它快速执行,而不管该范围(@Low, @High)是宽还是窄,以及无论哪个高频字都幸运地处于选定的(狭窄)范围内?

R树/空间索引会有所帮助吗?

添加索引,重写查询,重新设计表,没有任何限制。


3
覆盖索引由9.2(现为beta)(顺便说一句)引入。PostgreSQL人们谈论仅索引扫描。请参阅以下相关答案:dba.stackexchange.com/a/7541/3684PostgreSQL Wiki页面
Erwin Brandstetter,

两个问题:(1)您希望该表使用哪种模式?读取次数最多还是经常更新(尤其是嵌套的set变量)?(2)嵌套集合整数变量lset和rset与文本变量word之间是否有任何联系?
2012年

@jug:大多读。lset,rset和之间没有连接word
ypercubeᵀᴹ

3
如果您有很多更新,那么就性能而言,嵌套集模型将是一个糟糕的选择(如果您可以阅读《 SQL的艺术》一书,请参阅有关层次模型的章节)。但是无论如何,您的主要问题类似于在间隔上找到(自变量的)最大值/最大值,因此很难设计索引方法。据我所知,最接近您需要的索引的是knngist模块,但是您必须对其进行修改以适合您的需求。空间索引不太可能有帮助。
2012年

Answers:


30

通过首先在较高频率的行中搜索,您可能能够获得更好的性能。这可以通过“划分”频率,然后按程序逐步执行,例如,如下所示:

--testbed和lexikon虚拟数据:

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 分析(主要用于信息和调整):

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

首先扫描高频的功能:

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

结果(可能应该用少量盐来估计时间,但每个查询运行两次以应对任何缓存)

首先使用我们编写的函数:

\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
*/

然后进行简单的索引扫描:

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;

根据您的实际数据,您可能需要更改颗粒的数量以及用于将行放入其中的功能。频率的实际分布在这里是关键,limit条款的期望值和所lset寻找范围的大小也是关键。


为什么有一个从开始间隙width_granule=8之间granulae_startgranulae_end以前的水平吗?
vyegorov

@vyegorov,因为没有任何值1809和1810?这是随机生成的数据,因此YMMV :)
Jack Douglas

嗯,似乎与随机性无关,而是与frequency生成方式有关:1e6 / 2与1e6 / 3之间的间隙较大,行数越高,间隙越小。无论如何,谢谢您的出色方法!!
vyegorov

@vyegorov对不起,是的,您是对的。如果还没有,请确保看看Erwins的改进
杰克·道格拉斯

23

设定

我建立在@Jack的设置上,以使人们可以更轻松地进行跟踪和比较。使用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

从这里开始,我走了一条不同的路线:

ANALYZE lexikon;

辅助桌

此解决方案不会在原始表中添加列,而只需要一个小的辅助表。我将其放在架构中public,可以使用您选择的任何架构。

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;

表看起来像这样:

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

由于该列cond将在以后的动态SQL中使用,因此必须使此表安全。如果您不确定当前是否合适search_path,请始终对表进行模式限定,并从public(和任何其他不受信任的角色)撤消写特权:

REVOKE ALL ON public.lex_freq FROM public;
GRANT SELECT ON public.lex_freq TO public;

该表lex_freq用于三个目的:

  • 自动创建所需的部分索引
  • 提供迭代功能的步骤。
  • 用于调整的元信息。

指标

DO语句创建所有需要的索引:

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

所有这些部分索引一起跨表一次。它们的大小与整个表中的一个基本索引大小相同:

SELECT pg_size_pretty(pg_relation_size('lexikon'));       -- 50 MB
SELECT pg_size_pretty(pg_total_relation_size('lexikon')); -- 71 MB

到目前为止,只有50 MB表的21 MB索引。

我在创建了大部分的部分索引(lset, frequency DESC)。第二列仅在特殊情况下有帮助。但是由于涉及的两个列都是type integer,由于 PostgreSQL中的MAXALIGN结合使用的数据对齐方式的特殊性,第二列不会使索引变大。这是一个小小的胜利,几乎没有任何代价。

对于仅跨越单个频率的部分索引,这样做是没有意义的。那些只是在(lset)。创建的索引如下所示:

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;

功能

该函数的风格与@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;

主要区别:

  • 动态SQLRETURN QUERY EXECUTE
    在我们逐步执行这些步骤时,可能会受益于其他查询计划。静态SQL的查询计划生成一次,然后重新使用-这样可以节省一些开销。但是在这种情况下,查询很简单,值也非常不同。动态SQL将是一个巨大的胜利。

  • LIMIT每个查询步骤都是动态的。
    这有多种帮助:首先,仅根据需要获取行。结合动态SQL,这可能还会生成不同的查询计划。第二:不需要LIMIT在函数调用中进行额外的调整以减少剩余量。

基准测试

设定

我选择了四个示例,并分别进行了三个不同的测试。我选择了五个中最好的一个与温暖的缓存进行比较:

  1. 原始SQL查询的形式为:

    SELECT * 
    FROM   lexikon 
    WHERE  lset >= 20000
    AND    lset <= 30000
    ORDER  BY frequency DESC
    LIMIT  5;
  2. 创建此索引后相同

    CREATE INDEX ON lexikon(lset);

    与我所有的部分索引在一起所需的空间大约相同:

    SELECT pg_size_pretty(pg_total_relation_size('lexikon')) -- 93 MB
  3. 功能

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

结果

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

1:总运行时间:315.458毫秒
2:总运行时间:36.458毫秒
3:总运行时间:0.330毫秒

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

1:总运行时间:294.819 ms
2:总运行时间:18.915 ms
3:总运行时间:1.414 ms

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

1:总运行时间:426.831毫秒
2:总运行时间:217.874毫秒
3:总运行时间:1.611毫秒

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

1:总运行时间:2458.205 ms
2:总运行时间:2458.205 ms-对于较大范围的lset,seq扫描比索引快。
3:总运行时间:0.266毫秒

结论

正如预期的那样,该功能的好处随着的范围的增大lset和的减小而增长LIMIT

如果范围很小lset,则原始查询与索引的组合实际上会更快。您可能需要测试,甚至可能会分支:小范围的原始查询lset,否则调用函数。您甚至可以将其内置到 “两全其美” 的函数中,这就是我要做的。

根据您的数据分布和典型查询,更多的步骤lex_freq可能会提高性能。测试以找到最佳位置。使用此处介绍的工具,它应该很容易测试。


1

我认为没有任何理由在索引中包含单词列。所以这个指数

CREATE INDEX lexikon_lset_frequency ON lexicon (lset, frequency DESC)

将使您的查询快速执行。

UPD

当前,没有办法在PostgreSQL中建立覆盖索引。PostgreSQL邮件列表http://archives.postgresql.org/pgsql-performance/2012-06/msg00114.php中对此功能进行了讨论。


1
它被包括在内以使索引“覆盖”。
ypercubeᵀᴹ

但是通过不在查询决策树中搜索该术语,您确定覆盖索引在这里有所帮助吗?
jcolebrand

好吧,我现在知道了。当前,没有办法在PostgreSQL中建立覆盖索引。邮件列表archives.postgresql.org/pgsql-performance/2012-06/msg00114.php中对此功能进行了讨论。
grayhemp 2012年

关于PostgreSQL中的“覆盖索引”,另请参见Erwin Brandstetter对问题的评论。
2012年

1

使用GIST索引

有没有一种方法可以使它快速执行,而不管范围(@ Low,@ High)宽还是窄,以及最高频的词是否幸运地处于选定的(窄)范围内?

它取决于您斋戒时的意思:显然,您必须访问范围中的每一行,因为查询为ORDER freq DESC。如果我理解这个问题,那么查询计划员就已经涵盖了这一点,

在这里,我们创建了一个包含10k行的表格 (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);

我们索引它,

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

我们查询它

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

我们得到一个Seq Scan on t。这仅仅是因为我们的选择性估计使pg得出结论,访问堆比扫描索引和重新检查要快。因此,我们通过插入(42::int,random()::double precision)不符合我们的“范围”的100万行来使其更加多汁。

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

VACUUM ANALYZE t;

然后我们重新查询,

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

您可以在此处看到我们在4.6 MS中完成了“仅索引扫描”

                                                                 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)

扩大范围以包括整个表,再进行一次seq扫描-从逻辑上讲,再增加10亿行,将进行另一次Index Scan。

总而言之,

  • 对于大量数据,它将快速执行。
  • 快速是相对于替代方法的结果,如果范围选择不够充分,则顺序扫描可能会尽可能快。
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.