设定
我建立在@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;
主要区别:
动态SQL与RETURN QUERY EXECUTE。
在我们逐步执行这些步骤时,可能会受益于其他查询计划。静态SQL的查询计划生成一次,然后重新使用-这样可以节省一些开销。但是在这种情况下,查询很简单,值也非常不同。动态SQL将是一个巨大的胜利。
 
LIMIT每个查询步骤都是动态的。
这有多种帮助:首先,仅根据需要获取行。结合动态SQL,这可能还会生成不同的查询计划。第二:不需要LIMIT在函数调用中进行额外的调整以减少剩余量。
 
基准测试
设定
我选择了四个示例,并分别进行了三个不同的测试。我选择了五个中最好的一个与温暖的缓存进行比较:
原始SQL查询的形式为:
SELECT * 
FROM   lexikon 
WHERE  lset >= 20000
AND    lset <= 30000
ORDER  BY frequency DESC
LIMIT  5;
 
创建此索引后相同
CREATE INDEX ON lexikon(lset);
与我所有的部分索引在一起所需的空间大约相同:
SELECT pg_size_pretty(pg_total_relation_size('lexikon')) -- 93 MB
 
功能
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可能会提高性能。测试以找到最佳位置。使用此处介绍的工具,它应该很容易测试。