Mendirikan
Saya sedang membangun pengaturan @ Jack untuk memudahkan orang untuk mengikuti dan membandingkan. Diuji dengan 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
Dari sini saya mengambil rute yang berbeda:
ANALYZE lexikon;
Meja bantu
Solusi ini tidak menambahkan kolom ke tabel asli, hanya membutuhkan tabel pembantu kecil. Saya menempatkannya di skema public
, gunakan skema apa pun pilihan Anda.
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;
Tabelnya terlihat seperti ini:
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
Karena kolom cond
akan digunakan dalam SQL dinamis lebih jauh ke bawah, Anda harus membuat tabel ini aman . Selalu sediakan skema-tabel jika Anda tidak yakin dengan arus yang sesuai search_path
, dan cabut hak istimewa menulis dari public
(dan peran tidak tepercaya lainnya):
REVOKE ALL ON public.lex_freq FROM public;
GRANT SELECT ON public.lex_freq TO public;
Tabel ini lex_freq
memiliki tiga tujuan:
- Buat indeks parsial yang diperlukan secara otomatis.
- Berikan langkah-langkah untuk fungsi berulang.
- Informasi meta untuk penyetelan.
Indeks
DO
Pernyataan ini menciptakan semua indeks yang dibutuhkan:
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
$$
Semua indeks parsial ini bersama-sama merentang tabel sekali. Ukurannya hampir sama dengan satu indeks dasar di seluruh tabel:
SELECT pg_size_pretty(pg_relation_size('lexikon')); -- 50 MB
SELECT pg_size_pretty(pg_total_relation_size('lexikon')); -- 71 MB
Sejauh ini, hanya 21 MB indeks untuk 50 MB tabel.
Saya membuat sebagian besar indeks parsial (lset, frequency DESC)
. Kolom kedua hanya membantu dalam kasus khusus. Tetapi karena kedua kolom yang terlibat adalah tipe integer
, karena kekhususan penyelarasan data dalam kombinasi dengan MAXALIGN di PostgreSQL, kolom kedua tidak membuat indeks lebih besar. Ini adalah kemenangan kecil tanpa biaya.
Tidak ada gunanya melakukan itu untuk indeks parsial yang hanya menjangkau satu frekuensi. Itu baru saja menyala (lset)
. Indeks yang dibuat terlihat seperti ini:
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;
Fungsi
Fungsi ini agak mirip dengan gaya solusi @ 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;
Perbedaan utama:
SQL dinamis dengan RETURN QUERY EXECUTE
.
Saat kami mengulangi langkah-langkah, rencana kueri yang berbeda mungkin penerima. Rencana kueri untuk SQL statis dihasilkan sekali dan kemudian digunakan kembali - yang dapat menghemat biaya tambahan. Tetapi dalam hal ini kueri itu sederhana dan nilainya sangat berbeda. SQL dinamis akan menjadi kemenangan besar.
DinamisLIMIT
untuk setiap langkah kueri.
Ini membantu dalam berbagai cara: Pertama, baris hanya diambil sesuai kebutuhan. Dalam kombinasi dengan SQL dinamis, ini juga dapat menghasilkan rencana kueri yang berbeda untuk memulai. Kedua: Tidak perlu tambahan LIMIT
dalam pemanggilan fungsi untuk memotong surplus.
Tolok ukur
Mendirikan
Saya mengambil empat contoh dan menjalankan tiga tes berbeda dengan masing-masing. Saya mengambil yang terbaik dari lima untuk membandingkan dengan cache hangat:
Kueri SQL mentah dari formulir:
SELECT *
FROM lexikon
WHERE lset >= 20000
AND lset <= 30000
ORDER BY frequency DESC
LIMIT 5;
Hal yang sama setelah membuat indeks ini
CREATE INDEX ON lexikon(lset);
Membutuhkan ruang yang sama dengan semua indeks parsial saya bersama-sama:
SELECT pg_size_pretty(pg_total_relation_size('lexikon')) -- 93 MB
Fungsinya
SELECT * FROM f_search(20000, 30000, 5);
Hasil
SELECT * FROM f_search(20000, 30000, 5);
1: Total runtime: 315.458 ms
2: Total runtime: 36.458 ms
3: Total runtime: 0.330 ms
SELECT * FROM f_search(60000, 65000, 100);
1: Total runtime: 294.819 ms
2: Total runtime: 18.915 ms
3: Total runtime: 1.414 ms
SELECT * FROM f_search(10000, 70000, 100);
1: Total runtime: 426.831 ms
2: Total runtime: 217.874 ms
3: Total runtime: 1.611 ms
SELECT * FROM f_search(1, 1000000, 5);
1: Total runtime: 2458.205 ms
2: Total runtime: 2458.205 ms - untuk rentang lset yang besar, pemindaian seq lebih cepat daripada indeks.
3: Total runtime: 0,266 ms
Kesimpulan
Seperti yang diharapkan, manfaat dari fungsi tumbuh dengan rentang yang lebih besar lset
dan lebih kecil LIMIT
.
Dengan rentang yang sangat kecillset
, kueri mentah dalam kombinasi dengan indeks sebenarnya lebih cepat . Anda ingin menguji dan mungkin bercabang: kueri mentah untuk rentang kecil lset
, atau fungsi lainnya panggil. Anda bahkan bisa membuatnya menjadi fungsi untuk "yang terbaik dari kedua dunia" - itulah yang akan saya lakukan.
Bergantung pada distribusi data Anda dan pertanyaan umum, langkah-langkah lebih dalam lex_freq
dapat membantu kinerja. Tes untuk menemukan sweet spot. Dengan alat yang disajikan di sini, seharusnya mudah untuk menguji.