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 cond
daha 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_path
ve 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 DO
ifade 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.
LIMIT
Her 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: LIMIT
Fazlalığı 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:
Formun ham SQL sorgusu:
SELECT *
FROM lexikon
WHERE lset >= 20000
AND lset <= 30000
ORDER BY frequency DESC
LIMIT 5;
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
İş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 lset
ve 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 lset
iş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_freq
performansta yardımcı olabilir. Tatlı noktayı bulmak için test edin. Burada sunulan araçlarla, test edilmesi kolay olmalıdır.