Чи може просторовий індекс допомогти запиту "діапазон - порядок за межами"


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 (зараз бета-версія), btw. Люди PostgreSQL говорять про сканування лише для індексів . Дивіться відповідну відповідь: dba.stackexchange.com/a/7541/3684 та Вікі-сторінку PostgreSQL Wiki
Ервін Брандштеттер

Два питання: (1) Яку схему використання ви очікуєте для таблиці? Чи є здебільшого читання чи часті оновлення (особливо вкладених змінних наборів)? (2) Чи існує зв’язок між вкладеними цілими змінними lset та rset та словом текстової змінної?
jp

@jug: В основному читає. Немає зв'язку між lset,rsetта word.
ypercubeᵀᴹ

3
Якщо у вас було багато оновлень, вкладена модель набору була б поганим вибором щодо продуктивності (якщо у вас є доступ до книги "Мистецтво SQL", подивіться розділ про ієрархічні моделі). Але в будь-якому випадку, ваша основна проблема схожа на пошук максимальних / найвищих значень (незалежної змінної) на інтервалі, для якого важко розробити метод індексації. Наскільки мені відомо, найбільш близьким до потрібного вам індексу є модуль knngist, але вам доведеться змінити його, щоб відповідати вашим потребам. Просторовий індекс навряд чи буде корисним.
jp

Відповіді:


30

Можливо, ви зможете досягти кращих показників, шукаючи спочатку рядки з більш високими частотами. Цього можна досягти, «гранулюючи» частоти, а потім переглянувши їх процедурно, наприклад:

- дані про тестування та 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_startі granulae_endпопереднього рівня?
vyegorov

@vyegorov, оскільки немає жодних значень 1809 та 1810? Це випадково генеровані дані, тому YMMV :)
Джек Дуглас

Гм, схоже, це не має нічого спільного з випадковістю, а скоріше із способом, frequencyякий генерується: великий розрив між 1e6 / 2 та 1e6 / 3, чим більше число рядків стає, тим менший проміжок. У будь-якому випадку, дякую за цей дивовижний підхід !!
vyegorov

@vyegorov вибачте, так, ви маєте рацію. Не забудьте поглянути на вдосконалення Ервінса, якщо ви цього ще не зробили!
Джек Дуглас

23

Налаштування

Я будую на установці @ Джека, щоб людям було легше наслідувати та порівняти. Тестовано з 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

Поки що лише 21 МБ індексів для таблиці 50 Мб.

Я створюю більшість часткових індексів на (lset, frequency DESC). Другий стовпець допомагає лише в особливих випадках. Але оскільки обидва задіяні стовпці мають тип integer, через специфіку вирівнювання даних у поєднанні з MAXALIGN у PostgreSQL, другий стовпець не робить індекс більшим. Це невеликий виграш за майже будь-які витрати.

Немає сенсу робити це для часткових індексів, які охоплюють лише одну частоту. Це тільки що (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;

Функція

Функція дещо схожа на рішення @ рішення Джека:

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у виклику функції, щоб зменшити надлишки.

Орієнтир

Налаштування

Я вибрав чотири приклади і провів три різні тести з кожним. Я взяв найкращу з п'яти для порівняння з теплим кешем:

  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 мс
2: Загальний час виконання: 18,915 мс
3: Загальний час виконання: 1,414 мс

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 мс
2: Загальний час виконання: 2458,205 мс - для великих діапазонів lset сканування послідовності швидше, ніж індекс.
3: Загальний час виконання: 0,266 мс

Висновок

Як і очікувалося, вигода від функції зростає з більшими діапазонами lsetта меншими LIMIT.

З дуже малим діапазономlset , необроблений запит у поєднанні з індексом насправді швидше . Ви хочете перевірити і, можливо, відгалуження: необроблений запит для невеликих діапазонів lset, виклик функції else Ви могли навіть просто вбудувати це у функцію для "кращого з обох світів" - ось що я би робив.

Залежно від вашого розповсюдження даних та типових запитів, ефективність 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 .
сірий коноп

Про "Покриття індексів" у PostgreSQL див. Також коментар Ервіна Брандстеттера до цього питання.
jp

1

Використання індексу GIST

Чи є спосіб зробити його швидким, незалежно від того, широкий або вузький діапазон (@ низький, @ високий) і незалежно від того, чи є найвищі слова частоти на щастя в (вузькому) вибраному діапазоні?

Це залежить від того, що ви маєте на увазі під час посту: вам очевидно доведеться відвідувати кожен рядок у діапазоні, оскільки ваш запит є 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 зробити висновок про доступ до купи швидше, ніж сканування індексу та повторна перевірка. Таким чином, ми робимо його більш соковитим, вставляючи ще 1 000 000 рядків (42::int,random()::double precision), які не відповідають нашому "діапазону".

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 МС із скануванням лише з індексом ,

                                                                 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)

Розширення діапазону, щоб включити всю таблицю, спричиняє ще одне послідовне сканування - логічно, а збільшення його ще одним мільярдом рядків призведе до іншого сканування індексів.

Отже, підсумовуючи,

  • Він буде працювати швидко, для кількості даних.
  • Швидкість відносно альтернативи, якщо діапазон недостатньо вибірковий, послідовне сканування може бути настільки ж швидким, як ви можете отримати.
Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.