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:
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 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.