Uzamsal dizin bir "aralık-sırayla-sınırla" sorgusuna yardımcı olabilir


29

Bu soruyu sormak, özellikle Postgres için, R-tree / uzaysal dizinleri desteklemesi nedeniyle.

Kelimelerin ağaç yapılı (Nested Set model) ve sıklıklarının bulunduğu aşağıdaki tabloya sahibiz:

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

Ve sorgu:

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

Bir örtü endeksinin (lset, frequency, word)faydalı olacağını varsayalım, ancak aralıkta çok fazla lsetdeğer varsa iyi performans göstermeyeceğini düşünüyorum (@High, @Low).

Basit bir dizin açık (frequency DESC), bazen, bu dizini kullanan bir arama @N, aralık koşuluyla eşleşen satırları erken verdiğinde de yeterli olabilir .

Ancak, performansın parametre değerlerine çok bağlı olduğu görülmektedir.

Aralığın (@Low, @High)geniş veya dar olmasına bakılmaksızın ve en sık kullanılan kelimelerin neyse ki (dar) seçilen aralıkta olup olmadıklarına bakılmaksızın, hızlı performans göstermenin bir yolu var mı ?

Bir R-ağacı / mekansal indeksi yardımcı olur mu?

Dizin ekleme, sorguyu yeniden yazma, tabloyu yeniden tasarlama, sınırlama yoktur.


3
Kaplama indeksleri 9.2 (şimdi beta), btw. PostgreSQL kullanıcıları yalnızca Dizin taramalarından bahseder . Bu ilgili cevaba bakınız: dba.stackexchange.com/a/7541/3684 ve PostgreSQL Wiki sayfası
Erwin Brandstetter

İki soru: (1) Masa için ne tür kullanım şekli bekliyorsunuz? Çoğunlukla okuma var mı, yoksa sık güncellemeler mi var (özellikle iç içe geçmiş değişkenlerin)? (2) İç içe geçmiş tamsayı değişkenleri lset ve rset ile metin değişkeni kelimesi arasında bir bağlantı var mı?
jp

@jug: Çoğunlukla okur. Arasında hiçbir bağlantı lset,rsetve word.
ypercubeᵀᴹ

3
Pek çok güncellemeniz olsaydı, iç içe geçmiş küme modeli performans açısından kötü bir seçim olurdu ("SQL sanatı" kitabına erişiminiz varsa, hierachic modelleri ile ilgili bölüme bakın). Ama yine de, asıl probleminiz bir endeksleme metodu tasarlamanın zor olduğu bir aralıkta maksimum / en yüksek değerleri (bağımsız bir değişkenin) bulmakla benzerdir. Bildiğim kadarıyla, ihtiyacınız olan endekse en yakın eşleşme knngist modülüdür, ancak ihtiyaçlarınızı karşılayacak şekilde değiştirmeniz gerekecektir. Uzamsal bir dizinin yardımcı olması muhtemel değildir.
jp

Yanıtlar:


30

Önce daha yüksek frekanslara sahip satırlarda arayarak daha iyi performans elde edebilirsiniz. Bu, frekansları 'küçülterek' ve ardından prosedürleri kullanarak, örneğin şu şekilde adım adım ilerleterek başarılabilir:

- test edilmiş ve lexikonyapay veriler:

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 analiz (çoğunlukla bilgi ve ayar için):

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

önce yüksek frekansları taramak için işlev:

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

sonuçlar (zamanlamalar muhtemelen bir tutam tuzla alınmalıdır, ancak her sorgu, herhangi bir önbelleğe alma işlemine karşı koymak için iki kez çalıştırılır)

İlk önce yazdığımız fonksiyonu kullanarak:

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

ve sonra basit bir indeks taraması ile:

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;

Gerçek dünyadaki verilerinize bağlı olarak, büyük olasılıkla granül sayısını ve bunlara satır koymak için kullanılan işlevi değiştirmek isteyeceksiniz. Frekansların fiili dağılımı, limitmadde ve beklenen lsetaralıkların büyüklüğü için beklenen değerler gibi anahtardır .


Neden başlayarak bir boşluk var width_granule=8aralarında granulae_startve granulae_endönceki düzeyin?
vyegorov

@vyegorov 1809 ve 1810 diye bir değer olmadığı için? Bu rastgele oluşturulmuş bir veridir, yani YMMV :)
Jack Douglas

Hm, rastgelelikle ilgisi yok gibi görünüyor, ama yöntem frequencyşu şekilde üretiliyor: 1e6 / 2 ve 1e6 / 3 arasındaki büyük boşluk, daha yüksek satır sayısı, daha küçük boşluk olur. Neyse, bu harika yaklaşım için teşekkür ederiz!
vyegorov

@vyegorov üzgünüm, evet haklısın. Henüz yapmadıysanız Erwins'in iyileştirmelerine bir göz atmayı unutmayın!
Jack Douglas

23

Kurmak

Üzerinde inşa ediyorum Jack'in kurulum @ daha kolay insanlar takip etmek ve karşılaştırmak için yapmak. PostgreSQL 9.1.4 ile test edilmiştir .

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

Bundan sonra farklı bir rota izlerim:

ANALYZE lexikon;

Yardımcı masa

Bu çözüm orijinal tabloya sütun eklemiyor, sadece küçük bir yardımcı masaya ihtiyacı var. Şemaya yerleştirdim public, seçtiğiniz herhangi bir şema kullandım.

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;

Tablo şöyle görünüyor:

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

Sütun conddaha aşağı dinamik SQL'de kullanılacaksa, bu tabloyu güvenli hale getirmelisiniz . Uygun bir akımdan emin olamıyorsanız tabloyu daima şema haline getirin search_pathve yazma ayrıcalıklarını public(ve güvenilmeyen herhangi bir rolden) iptal edin :

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

Tablo lex_freqüç amaca hizmet eder:

  • Otomatik olarak gerekli kısmi indeksleri oluşturun .
  • Yinelemeli işlev için adımlar sağlayın.
  • Ayarlama için meta bilgi.

endeksleri

Bu DOifade gerekli tüm dizinleri oluşturur :

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

Bu kısmi indekslerin hepsi birlikte bir kez tabloyu kaplar. Tüm tablodaki bir temel indeksle aynı büyüklüktedirler:

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

Şimdiye kadar 50 MB'lık tablo için yalnızca 21 MB'lık endeks.

Üzerinde kısmi dizinlerin çoğunu oluşturuyorum (lset, frequency DESC). İkinci sütun sadece özel durumlarda yardımcı olur. Ancak, her iki ilgili sütun da tür olduğundan , PostgreSQL'de MAXALIGN ile birlikteinteger veri hizalamasının özellikleri nedeniyle , ikinci sütun dizini daha büyük yapmaz. Neredeyse hiçbir maliyet için küçük bir kazanç.

Bunu sadece tek bir frekansı kapsayan kısmi indeksler için yapmanın bir anlamı yoktur. Bunlar sadece açık (lset). Oluşturulan dizinler şuna benzer:

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;

fonksiyon

İşlev, @ Jack'in çözümüne tarzla biraz benzer:

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;

Anahtar farklılıklar:

  • dinamik SQL ile RETURN QUERY EXECUTE.
    Adımlardan geçerken, farklı bir sorgu planı yararlanıcı olabilir. Statik SQL için sorgu planı bir kez üretilir ve daha sonra yeniden kullanılır - bu da yükü azaltabilir. Ancak bu durumda sorgu basittir ve değerler çok farklıdır. Dinamik SQL büyük bir kazanç olacak.

  • LIMITHer sorgu adımı için dinamik .
    Bu, çeşitli şekillerde yardımcı olur: İlk olarak, satırlar yalnızca gerektiği gibi alınır. Dinamik SQL ile birlikte bu, başlamak için farklı sorgu planları da oluşturabilir. İkincisi: LIMITFazlalığı düzeltmek için işlev çağrısında bir eke gerek yok .

Karşılaştırma

Kurmak

Dört örnek seçtim ve her biriyle üç farklı test yaptım. Sıcak önbellekle karşılaştırmak için beşin en iyisini aldım:

  1. Formun ham SQL sorgusu:

    SELECT * 
    FROM   lexikon 
    WHERE  lset >= 20000
    AND    lset <= 30000
    ORDER  BY frequency DESC
    LIMIT  5;
  2. Bu dizini oluşturduktan sonra aynı

    CREATE INDEX ON lexikon(lset);

    Tüm kısmi indekslerimle birlikte aynı alana ihtiyaç duyuyor:

    SELECT pg_size_pretty(pg_total_relation_size('lexikon')) -- 93 MB
  3. İşlev

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

Sonuçlar

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

1: Toplam çalışma zamanı: 315.458 ms
2: Toplam çalışma zamanı: 36.458 ms
3: Toplam çalışma zamanı: 0.330 ms

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

1: Toplam çalışma zamanı: 294.819 ms
2: Toplam çalışma zamanı: 18.915 ms
3: Toplam çalışma zamanı: 1.414 ms

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

1: Toplam çalışma zamanı: 426.831 ms
2: Toplam çalışma zamanı: 217.874 ms
3: Toplam çalışma zamanı: 1.611 ms

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

1: Toplam çalışma zamanı: 2458.205 ms
2: Toplam çalışma zamanı: 2458.205 ms - büyük setler için, seq tarama dizinden hızlıdır.
3: Toplam çalışma zamanı: 0.266 ms

Sonuç

Beklendiği gibi, fonksiyondan elde edilen fayda daha büyük lsetve daha küçük aralıklarla büyür LIMIT.

İle çok küçük aralıklarlset , endeks ile birlikte ham sorgu aslında daha hızlı . Test etmek ve belki de dalmak isteyeceksiniz: küçük lsetişlev aralıkları için raw sorgusu , else function call. Bunu sadece fonksiyona dahil edebilirsiniz. "iki dünyanın da en iyisi" - ben de öyle yapardım.

Veri dağıtımınıza ve tipik sorgularınıza bağlı olarak, atılacak daha fazla adım lex_freqperformansta yardımcı olabilir. Tatlı noktayı bulmak için test edin. Burada sunulan araçlarla, test edilmesi kolay olmalıdır.


1

Sütun kelimesini dizine eklemek için herhangi bir sebep görmüyorum. Yani bu indeks

CREATE INDEX lexikon_lset_frequency ON lexicon (lset, frequency DESC)

Sorgunuzu hızlı gerçekleştirecek.

UPD

Şu anda PostgreSQL'de bir kapak endeksi yapmanın bir yolu yoktur. Bu özellik hakkında PostgreSQL posta listesinde http://archives.postgresql.org/pgsql-performance/2012-06/msg00114.php adresinde bir tartışma yapıldı.


1
"Kaplama" endeksini yapmak için dahil edildi.
ypercubeᵀᴹ

Ancak, sorgu karar ağacında bu terimi aramayarak, kapsam endeksinin burada yardımcı olduğundan emin misiniz?
jcolebrand

Tamam, şimdi anlıyorum. Şu anda PostgreSQL'de bir kapak endeksi yapmanın bir yolu yoktur. Bu özellik hakkında mail listemizde bir tartışma vardı . Archives.postgresql.org/pgsql-performance/2012-06/msg00114.php .
grayhemp

PostgreSQL'deki "Dizinleri kapsayan" hakkında, ayrıca Erwin Brandstetter'ın bu soruya yorumunu da görebilirsiniz.
jp

1

GIST endeksi kullanma

Aralığın (@Düşük, @Yüksek) geniş veya dar olmasına ve en sık kullanılan kelimelerin neyse ki (dar) seçilen aralıkta olmasına bakılmaksızın, hızlı performans göstermenin bir yolu var mı?

Bu, oruç tuttuğunuzda neyi kastettiğinize bağlı: açıkça aralıktaki her satırı ziyaret etmeniz gerekiyor, çünkü sorgunuz ORDER freq DESC. Utangaçlık, sorgu planlayıcısının bunu zaten anladığıma göre kapsıyor.

Burada 10k satırlık bir tablo oluşturuyoruz. (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);

Dizine ekliyoruz

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

Sorgularız

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

Bir anladık Seq Scan on t. Bunun nedeni, seçicilik tahminlerimizin, pg'nin yığın erişiminin bir dizini taramak ve yeniden denetlemekten daha hızlı olduğu sonucuna varmasıdır. Böylece 1.000.000 daha fazla satır ekleyerek (42::int,random()::double precision)"ürün yelpazemize" uygun şekilde daha sulu hale getiriyoruz .

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

VACUUM ANALYZE t;

Ve sonra talep ediyoruz

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

Sen biz ile 4.6 MS tamamlamak burada görebilirsiniz Sadece Index Tarama ,

                                                                 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)

Tüm tabloyu kapsayacak şekilde aralığı genişletmek, mantıksal olarak başka bir sıra taraması üretir ve onu başka bir milyar satırla büyütmek başka bir Dizin Taraması oluşturur.

Yani özet olarak,

  • Veri miktarı için hızlı çalışır.
  • Hızlı aralık seçici yeterli değilse, alternatif görelidir sıralı tarama olabilir kadar hızlı olarak alabilirsiniz ol.
Sitemizi kullandığınızda şunları okuyup anladığınızı kabul etmiş olursunuz: Çerez Politikası ve Gizlilik Politikası.
Licensed under cc by-sa 3.0 with attribution required.