Chỉ mục không gian có thể giúp một phạm vi - sắp xếp theo thứ tự - giới hạn truy vấn


29

Đặt câu hỏi này, đặc biệt cho Postgres, vì nó có siêu năng lực tốt cho các chỉ số R-tree / không gian.

Chúng ta có bảng sau với cấu trúc cây (mô hình Nested Set) của các từ và tần số của chúng:

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

Và truy vấn:

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

Tôi cho rằng một chỉ số bao phủ trên (lset, frequency, word)sẽ hữu ích nhưng tôi cảm thấy nó có thể không hoạt động tốt nếu có quá nhiều lsetgiá trị trong (@High, @Low)phạm vi.

(frequency DESC)Đôi khi một chỉ mục đơn giản cũng có thể đủ, khi một tìm kiếm sử dụng chỉ mục đó mang lại sớm các @Nhàng khớp với điều kiện phạm vi.

Nhưng dường như hiệu suất phụ thuộc rất nhiều vào các giá trị tham số.

Có cách nào để làm cho nó hoạt động nhanh, bất kể phạm vi (@Low, @High)rộng hay hẹp và bất kể các từ tần số cao nhất có may mắn trong phạm vi (hẹp) đã chọn không?

Một chỉ số R-cây / không gian sẽ giúp đỡ?

Thêm chỉ mục, viết lại truy vấn, thiết kế lại bảng, không có giới hạn.


3
Các chỉ số bao phủ được giới thiệu với 9.2 (hiện là beta), btw. Người PostgreQuery nói về quét chỉ mục . Xem câu trả lời liên quan này: dba.stackexchange.com/a/7541/3684trang Wiki
Erwin Brandstetter

Hai câu hỏi: (1) Loại mô hình sử dụng nào bạn mong đợi cho bảng? Có hầu hết các lần đọc hoặc có cập nhật thường xuyên (đặc biệt là các biến được đặt lồng nhau) không? (2) Có bất kỳ kết nối nào giữa các biến số nguyên được đặt lồng nhau và rset và từ biến văn bản không?
jp

@jug: Chủ yếu là đọc. Không có kết nối giữa lset,rsetword.
ypercubeᵀᴹ

3
Nếu bạn có nhiều cập nhật, mô hình tập hợp lồng nhau sẽ là một lựa chọn tồi đối với hiệu suất (nếu bạn có quyền truy cập vào cuốn sách "Nghệ thuật của SQL", hãy xem chương về các mô hình chữ tượng hình). Nhưng dù sao, vấn đề chính của bạn cũng tương tự như việc tìm giá trị tối đa / cao nhất (của một biến độc lập) trên một khoảng, mà rất khó để thiết kế một phương pháp lập chỉ mục. Theo hiểu biết của tôi, trận đấu gần nhất với chỉ số bạn cần là mô đun knngist, nhưng bạn sẽ phải sửa đổi nó để phù hợp với nhu cầu của bạn. Một chỉ số không gian dường như không hữu ích.
jp

Câu trả lời:


30

Bạn có thể đạt được hiệu suất tốt hơn bằng cách tìm kiếm đầu tiên trong các hàng có tần số cao hơn. Điều này có thể đạt được bằng cách 'tạo hạt' tần số và sau đó bước qua chúng theo thủ tục, ví dụ như sau:

- lexikondữ liệu giả và dữ liệu giả:

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 phân tích (chủ yếu để thông tin và điều chỉnh):

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

chức năng quét tần số cao trước tiên:

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

kết quả (có thể lấy thời gian bằng một nhúm muối nhưng mỗi truy vấn được chạy hai lần để chống lại bất kỳ bộ đệm)

đầu tiên sử dụng chức năng chúng tôi đã viết:

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

và sau đó với một quét chỉ mục đơn giản:

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;

Tùy thuộc vào dữ liệu trong thế giới thực của bạn, có thể bạn sẽ muốn thay đổi số lượng hạt và chức năng được sử dụng để đặt các hàng vào chúng. Phân phối tần số thực tế là chìa khóa ở đây, cũng như các giá trị dự kiến ​​cho limitmệnh đề và kích thước của các lsetphạm vi được tìm kiếm.


Tại sao có một khoảng cách bắt đầu từ width_granule=8giữa granulae_startgranulae_endcủa cấp độ trước?
vyegorov

@vyegorov vì không có giá trị 1809 và 1810? Đây là dữ liệu được tạo ngẫu nhiên nên YMMV :)
Jack Douglas

Hừm, dường như nó không liên quan gì đến sự ngẫu nhiên, nhưng thay vào đó frequencylà cách tạo ra: một khoảng cách lớn giữa 1e6 / 2 và 1e6 / 3, số hàng cao hơn trở thành, khoảng cách nhỏ hơn là. Dù sao, cảm ơn bạn cho cách tiếp cận tuyệt vời này !!
vyegorov

@vyegorov xin lỗi, vâng, bạn nói đúng. Hãy chắc chắn xem qua các cải tiến của Erwins nếu bạn chưa có!
Jack Douglas

23

Thiết lập

Tôi đang xây dựng trên thiết lập của @ Jack để giúp mọi người theo dõi và so sánh dễ dàng hơn. Đã thử nghiệm với 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

Từ đây tôi đi một con đường khác:

ANALYZE lexikon;

Bảng phụ

Giải pháp này không thêm các cột vào bảng gốc, nó chỉ cần một bảng trợ giúp nhỏ. Tôi đặt nó trong lược đồ public, sử dụng bất kỳ lược đồ nào bạn chọn.

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;

Bảng trông như thế này:

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

Vì cột condsẽ được sử dụng trong SQL động hơn nữa, bạn phải làm cho bảng này an toàn . Luôn lập sơ đồ đủ điều kiện cho bảng nếu bạn không thể chắc chắn về dòng điện thích hợp search_pathvà thu hồi các đặc quyền ghi từ public(và bất kỳ vai trò không đáng tin cậy nào khác):

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

Bảng lex_freqphục vụ ba mục đích:

  • Tạo các chỉ mục một phần cần thiết tự động.
  • Cung cấp các bước cho chức năng lặp.
  • Thông tin meta để điều chỉnh.

Chỉ mục

DOTuyên bố này tạo ra tất cả các chỉ mục cần thiết:

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

Tất cả các chỉ mục một phần này cùng nhau trải rộng bảng một lần. Chúng có cùng kích thước với một chỉ số cơ bản trên toàn bộ bảng:

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

Chỉ có 21 MB chỉ mục cho bảng 50 MB cho đến nay.

Tôi tạo ra hầu hết các chỉ mục một phần trên (lset, frequency DESC). Cột thứ hai chỉ giúp trong trường hợp đặc biệt. Nhưng vì cả hai cột có liên quan đều thuộc loại integer, do đặc thù của việc căn chỉnh dữ liệu kết hợp với MAXALIGN trong , cột thứ hai không làm cho chỉ mục lớn hơn. Đó là một chiến thắng nhỏ cho hầu như không có chi phí.

Không có điểm nào trong việc làm điều đó đối với các chỉ mục một phần chỉ kéo dài một tần số duy nhất. Đó chỉ là trên (lset). Các chỉ mục được tạo trông như thế này:

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;

Chức năng

Chức năng này có phần giống phong cách với giải pháp của @ 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;

Sự khác biệt chính:

  • SQL động với RETURN QUERY EXECUTE.
    Khi chúng tôi lặp lại các bước, một kế hoạch truy vấn khác nhau có thể được hưởng lợi. Kế hoạch truy vấn cho SQL tĩnh được tạo một lần và sau đó được sử dụng lại - có thể tiết kiệm một số chi phí. Nhưng trong trường hợp này, truy vấn rất đơn giản và các giá trị rất khác nhau. SQL động sẽ là một chiến thắng lớn.

  • Năng độngLIMIT cho mọi bước truy vấn.
    Điều này giúp theo nhiều cách: Đầu tiên, các hàng chỉ được tìm nạp khi cần thiết. Kết hợp với SQL động, điều này cũng có thể tạo ra các kế hoạch truy vấn khác nhau để bắt đầu. Thứ hai: Không cần bổ sung LIMITtrong lệnh gọi hàm để cắt giảm thặng dư.

Điểm chuẩn

Thiết lập

Tôi chọn bốn ví dụ và thực hiện ba bài kiểm tra khác nhau với mỗi bài kiểm tra. Tôi lấy thứ tốt nhất trong năm để so sánh với bộ đệm ấm:

  1. Truy vấn SQL thô có dạng:

    SELECT * 
    FROM   lexikon 
    WHERE  lset >= 20000
    AND    lset <= 30000
    ORDER  BY frequency DESC
    LIMIT  5;
  2. Tương tự sau khi tạo chỉ mục này

    CREATE INDEX ON lexikon(lset);

    Cần về cùng một không gian với tất cả các chỉ mục một phần của tôi với nhau:

    SELECT pg_size_pretty(pg_total_relation_size('lexikon')) -- 93 MB
  3. Chức năng

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

Các kết quả

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

1: Tổng thời gian chạy: 315.458 ms
2: Tổng thời gian chạy: 36.458 ms
3: Tổng thời gian chạy: 0.330 ms

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

1: Tổng thời gian chạy: 294.819 ms
2: Tổng thời gian chạy: 18.915 ms
3: Tổng thời gian chạy: 1.414 ms

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

1: Tổng thời gian chạy: 426.831 ms
2: Tổng thời gian chạy: 217.874 ms
3: Tổng thời gian chạy: 1.611 ms

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

1: Tổng thời gian chạy: 2458.205 ms
2: Tổng thời gian chạy: 2458.205 ms - đối với phạm vi lớn của lset, quét seq nhanh hơn chỉ mục.
3: Tổng thời gian chạy: 0,266 ms

Phần kết luận

Như mong đợi, lợi ích từ chức năng tăng lên với phạm vi lớn hơn lsetvà nhỏ hơnLIMIT .

Với phạm vi rất nhỏlset , truy vấn thô kết hợp với chỉ mục thực sự nhanh hơn . Bạn sẽ muốn kiểm tra và có thể phân nhánh: truy vấn thô cho các phạm vi nhỏ lset, gọi hàm khác. Bạn thậm chí có thể xây dựng nó thành chức năng cho một "thế giới tốt nhất" - đó là những gì tôi sẽ làm.

Tùy thuộc vào phân phối dữ liệu của bạn và các truy vấn thông thường, nhiều bước hơn lex_freqcó thể giúp thực hiện. Thử để tìm điểm ngọt. Với các công cụ được trình bày ở đây, nó sẽ dễ dàng để kiểm tra.


1

Tôi không thấy bất kỳ lý do để bao gồm cột từ trong chỉ mục. Vì vậy, chỉ số này

CREATE INDEX lexikon_lset_frequency ON lexicon (lset, frequency DESC)

sẽ làm cho truy vấn của bạn để thực hiện nhanh chóng.

CẬP NHẬT

Hiện tại không có cách nào để tạo một chỉ mục bao trùm trong PostgreSQL. Đã có một cuộc thảo luận về tính năng này trong danh sách gửi thư của PostgreSQL http://archives.postgresql.org/pgsql-performance/2012-06/msg00114.php


1
Nó được bao gồm để làm cho chỉ số "bao phủ".
ypercubeᵀᴹ

Nhưng bằng cách không tìm kiếm cụm từ đó trong cây quyết định truy vấn, bạn có chắc chắn rằng chỉ số bao phủ đang giúp đỡ ở đây không?
jcolebrand

Được rồi, tôi thấy bây giờ. Hiện tại không có cách nào để tạo một chỉ mục bao trùm trong PostgreSQL. Đã có một cuộc thảo luận về tính năng này trong danh sách gửi thư lưu trữ.postgresql.org/pgsql-performance/2012-06/msg00114.php .
Grayhemp

Về "Bao gồm các chỉ mục" trong PostgreSQL cũng xem bình luận của Erwin Brandstetter cho câu hỏi.
jp

1

Sử dụng chỉ số GIST

Có cách nào để làm cho nó hoạt động nhanh, bất kể phạm vi (@Low, @High) rộng hay hẹp và bất kể các từ tần số cao nhất có may mắn trong phạm vi (hẹp) được chọn không?

Nó phụ thuộc vào ý của bạn khi bạn nhanh: rõ ràng bạn phải truy cập mọi hàng trong phạm vi vì truy vấn của bạn là ORDER freq DESC. Nhút nhát rằng kế hoạch truy vấn đã bao gồm điều này nếu tôi hiểu câu hỏi,

Ở đây chúng tôi tạo một bảng với 10k hàng (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);

Chúng tôi lập chỉ mục nó,

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

Chúng tôi truy vấn nó,

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

Chúng tôi nhận được một Seq Scan on t. Điều này chỉ đơn giản là vì ước tính chọn lọc của chúng tôi cho phép pg kết luận truy cập heap nhanh hơn quét chỉ mục và kiểm tra lại. Vì vậy, chúng tôi làm cho nó trở nên ngon ngọt hơn bằng cách chèn thêm 1.000.000 hàng (42::int,random()::double precision)không phù hợp với "phạm vi" của chúng tôi.

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

VACUUM ANALYZE t;

Và sau đó chúng tôi yêu cầu,

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

Bạn có thể thấy ở đây chúng tôi hoàn thành trong 4.6 MS với Quét chỉ mục ,

                                                                 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)

Mở rộng phạm vi để bao gồm toàn bộ bảng, tạo ra một lần quét seq khác - một cách hợp lý và phát triển nó với một tỷ hàng khác sẽ tạo ra một Quét chỉ mục khác.

Vì vậy, tóm lại,

  • Nó sẽ thực hiện nhanh chóng, cho số lượng dữ liệu.
  • Nhanh là tương đối với giải pháp thay thế, nếu phạm vi không đủ chọn lọc, việc quét tuần tự thể nhanh như bạn có thể nhận được.
Khi sử dụng trang web của chúng tôi, bạn xác nhận rằng bạn đã đọc và hiểu Chính sách cookieChính sách bảo mật của chúng tôi.
Licensed under cc by-sa 3.0 with attribution required.